Frontile

Frontile v0.18

Ten months, 27 prereleases, and roughly two hundred merged pull requests after v0.17.1. This is the largest release Frontile has had, and the one where the library's shape settles.

Why this took a while

The gap was deliberate.

Frontile had accumulated a set of changes that could not be made gently: the color system, the way themes are configured, the package layout, the naming of component arguments, and the DOM that components render. Every one of them was breaking. Spread across five releases, they would have been five migrations, five rounds of upgrade work. Batched into one, they are a single migration you do once.

So v0.18 took the whole cost up front. What comes out the other side is a design system with a coherent vocabulary, a single package instead of eight, and component APIs that are named consistently enough that you can guess them.

None of it was developed in the dark. Those 27 prereleases — 21 alphas and 6 betas — go back to 0.18.0-alpha.0 in February 2026, each published and installable.

And it has been running in production. Multiple applications at Underline have tracked the prerelease line for several months, so these APIs have already survived real use.

New components

Fourteen of them. Every demo below is live.

DatePicker

A date field combining a text trigger with a Calendar popover, for single dates or ranges. Docs →

import { DatePicker } from 'frontile';

<template>
  <div class='demo-stack'>
    <DatePicker @label='Start date' @placeholder='Pick a date' />
  </div>
</template>

Calendar

The month grid on its own: keyboard navigable, localized through Intl, and composable into your own pickers. Docs →

September 2026
September 2026
Sun Mon Tue Wed Thu Fri Sat
import { Calendar } from 'frontile';

const today = new Date();

<template><Calendar @defaultValue={{today}} /></template>

Command

A searchable, relevance-ranked command palette. Use it inline, as below, or as CommandDialog for the ⌘K treatment. Docs →

  • Profile
  • Billing
  • Settings
import { Command } from 'frontile';

const commands = [
  { key: 'profile', label: 'Profile' },
  { key: 'billing', label: 'Billing' },
  { key: 'settings', label: 'Settings' }
];

<template>
  <Command @items={{commands}} @isBordered={{true}} @placeholder='Type a command…' as |c|>
    <c.Input />
    <c.List>
      <:item as |ctx|>
        <ctx.Item @key={{ctx.key}}>{{ctx.label}}</ctx.Item>
      </:item>
    </c.List>
  </Command>
</template>

Autocomplete

A text input that filters and ranks as you type. It matches substrings and acronyms both. Type nz and New Zealand comes up. Docs →

import { Autocomplete } from 'frontile';

const countries = [
  'Brazil',
  'Canada',
  'Japan',
  'Netherlands',
  'New Zealand',
  'United Kingdom'
];

<template>
  <div class='demo-stack'>
    <Autocomplete @placeholder='Search countries' @items={{countries}} />
  </div>
</template>

Accordion

Collapsible sections for progressive disclosure. Docs →

Standard shipping arrives in three to five business days. Express arrives the next business day.

Unopened items can be returned within thirty days for a full refund.
import { Accordion } from 'frontile';

<template>
  <div class='demo-stack'>
    <Accordion as |a|>
      <a.Item @title='What are your shipping options?'>
        Standard shipping arrives in three to five business days. Express
        arrives the next business day.
      </a.Item>
      <a.Item @title='What is your return policy?'>
        Unopened items can be returned within thirty days for a full refund.
      </a.Item>
    </Accordion>
  </div>
</template>

Tabs

Panels shown one at a time, with an indicator that slides between them. Docs →

Update your name, email, and photo.
import { Tabs } from 'frontile';

<template>
  <div class='demo-stack items-center'>
    <Tabs @defaultValue='account' as |t|>
      <t.List @label='Settings'>
        <t.Tab @value='account'>Account</t.Tab>
        <t.Tab @value='security'>Security</t.Tab>
        <t.Tab @value='billing'>Billing</t.Tab>
      </t.List>

      <t.Panel @value='account'>Update your name, email, and photo.</t.Panel>
      <t.Panel @value='security'>Manage passwords and two-factor auth.</t.Panel>
      <t.Panel @value='billing'>View invoices and update your plan.</t.Panel>
    </Tabs>
  </div>
</template>

TabNav

Tabs' appearance, but built for real navigation: genuine links, aria-current, each one individually tabbable. Docs →

import { TabNav } from 'frontile';

<template>
  <div class='demo-stack items-center'>
    <TabNav @label='Settings' @orientation='vertical' as |nav|>
      <nav.Item @href='#account' @isActive={{true}}>Account</nav.Item>
      <nav.Item @href='#security'>Security</nav.Item>
      <nav.Item @href='#billing'>Billing</nav.Item>
    </TabNav>
  </div>
</template>

Tooltip

A short hint on hover or focus, built on a reworked Popover hover foundation. Docs →

import { Tooltip, Button } from 'frontile';

<template>
  <Tooltip @content='Add to library' as |t|>
    <Button {{t.trigger}}>Add</Button>
  </Tooltip>
</template>

Alert

A persistent, inline message, rather than a notification that comes and goes. Also available as a full-width banner layout. Docs →

Update available
A new version is ready to install.
import { Alert } from 'frontile';

<template>
  <div class='demo-stack'>
    <Alert
      @title='Update available'
      @description='A new version is ready to install.'
    />
  </div>
</template>

Where the current page sits in a hierarchy. Docs →

import { Breadcrumbs } from 'frontile';

<template>
  <Breadcrumbs as |b|>
    <b.Item @href='/'>Home</b.Item>
    <b.Item @href='/library'>Library</b.Item>
    <b.Item>Data</b.Item>
  </Breadcrumbs>
</template>

Pagination

Page chips, previous/next, an optional summary. The simplest case needs only @total. Docs →

import { Pagination } from 'frontile';

<template><Pagination @total={{120}} /></template>

SegmentedControl

Mutually exclusive options with a sliding indicator, for small fixed choices. Docs →

import { SegmentedControl } from 'frontile';

<template>
  <SegmentedControl @defaultValue='week' aria-label='Date range' as |Ctl|>
    <Ctl.Item @value='day'>Day</Ctl.Item>
    <Ctl.Item @value='week'>Week</Ctl.Item>
    <Ctl.Item @value='month'>Month</Ctl.Item>
  </SegmentedControl>
</template>

Skeleton

A placeholder that hints at the shape of what's loading. Docs →

import { Skeleton } from 'frontile';

<template>
  <div class='demo-stack items-center'>
    <div class='not-prose w-80 space-y-3'>
      <Skeleton @shape='rounded' @class='h-32' />
      <Skeleton @size='sm' />
      <Skeleton @size='sm' @class='w-2/3' />
    </div>
  </div>
</template>

A link that marks itself as external: icon, rel="noopener noreferrer", and hidden text announcing the new tab. Docs →

import { ExternalLink } from 'frontile';

<template>
  <div class='not-prose text-neutral-strong p-2'>
    <ExternalLink @href='https://emberjs.com'>Ember.js</ExternalLink>
  </div>
</template>

Components that got better

Dropdown gained nested submenus, with the keyboard and pointer behavior that implies.

Drawer was restyled and learned to drag closed. Grab it and throw it off the edge.

Select renders multiple selections as chips. They used to be a comma-joined string inside the trigger; now each is a removable Chip, so a user can drop one without reopening the dropdown. Opt back out with @selectedItemsDisplay='text'.

Listbox gained groups, and filtering is ranked by relevance. Autocomplete and filterable Select used to filter with a case-insensitive "contains" check and render survivors in source order. They now score each option and put the closest match first, and match acronyms. Nothing that matched before stops matching. See Filter Ranking .

Table moved its keyboard navigation onto the shared rovingFocus utility, gained opt-in skeleton rows via @skeletonRows, and now yields its rendered columns and cell components to body blocks.

Button has @isLoading.

Notifications were redesigned: opaque surfaces, proper stacking, and promise support, so a toast can follow an async operation through pending, resolved and rejected without you wiring it.

Overlays that render already open now animate, after first paint, instead of appearing fully formed.

Divider gained a sketch variant, a hand-drawn rule that recolors with the standard bg-* utilities.

import { Divider } from 'frontile';

<template>
  <div class='demo-stack'>
    <Divider @variant='sketch' />
    <Divider @variant='sketch' @class='bg-primary' />
    <Divider @variant='sketch' @class='bg-danger' />
  </div>
</template>

The design system

The visual foundation was rebuilt.

Colors are semantic, not numbered. The old primary-500 style scale is gone. Colors are now categories — neutral, primary, secondary, tertiary, success, warning, danger, and the surface-* family — each with named levels that run from subtle through muted, soft, mild, DEFAULT, firm, strong, to bolder. bg-primary is the resting fill; bg-primary-firm is the emphatic one. You pick by intent rather than by memorizing which number meant "hover".

Contrast text is automatic. text-on-primary-firm resolves to whichever of black or white meets WCAG contrast on that background, generated rather than hand-maintained.

Colors moved to OKLCH, which makes the levels perceptually even in a way HSL never managed. This is invisible in your code: you may notice small differences in rendered color, but there is nothing to change.

theme-inverse flips a subtree. Every token is emitted for both schemes, so adding .theme-inverse to an element re-resolves the entire palette to the opposite scheme inside it. Dark panels on light pages stop being a hand-built special case.

Radius derives from one knob. The whole border-radius scale is computed from a single --radius variable.

The typography scale was corrected. The --text-body-* tokens had been mapped to the wrong steps of the modular scale: md was shipping at 20.74px where the spec said 16px. They now match, and the scale gained 4xs, 5xs, 2xl and 3xl to fill it out.

Theming and developer experience

One package. The seven @frontile/* component packages — buttons, collections, forms, overlays, notifications, status, utilities — are now a single frontile package, organized internally by category. The old packages still re-export everything and only log a deprecation warning, so your existing imports keep working throughout 0.18.x.

CSS-first configuration. Theme configuration moved to Tailwind v4's CSS-native approach. Frontile's styles now arrive through @import "@frontile/theme" in your app.css.

A stable DOM anatomy. Every component renders data-component and data-part attributes, giving you a documented, stable way to target internals from your own CSS or tests. The ad-hoc data-fr-* attributes are gone, and the data-test-id attributes that existed only as anatomy selectors went with them.

Consistent argument naming. @appearance became @variant, with one shared vocabulary across components: solid, soft, subtle, outline, ghost, plain. @intent became @color, or @status on Alert, NotificationCard and FormFeedback, the three where the value also picks an icon, an ARIA role, or whether a message is announced assertively. default became neutral throughout. The old names still work through 0.18.x with a deprecation warning.

Glint v2. The whole repo moved to @glint/ember-tsc.

Documentation, and the tool that builds it

The docs site was rebuilt, and a meaningful share of that work went into Docfy — the static site generator behind frontile.dev — rather than into Frontile.

Three upstream Docfy releases landed capabilities these docs needed:

  • v0.12.0 — static Markdown export. Every page now has a .md mirror, alongside generated llms.txt and llms-full.txt. The documentation is readable without running JavaScript, which matters for agents and for anyone reading from a terminal.
  • v0.14.0 — a Shiki code-block pipeline. Real glimmer-js and glimmer-ts TextMate grammars, so .gts and .gjs fences highlight properly instead of being approximated. It replaced three separate in-repo highlighters, and brought copy buttons, titles, collapsing and line highlighting with it.
  • v0.14.2 — richer llms.txt. Per-entry descriptions, project framing, and topic-scoped bundles, so an agent fetching the index gets something navigable rather than a flat list of URLs.

Each was built and verified against a real build of these docs before shipping.

On top of that, the site keeps a few local Docfy plugins for things only Frontile needs: one resolves <Signature /> tags into real argument tables in the exported Markdown, another maps a maintained component inventory onto page descriptions, and a hand-written preamble supplies the framing an automated export cannot generate: what Frontile is for, and which older argument names still silently resolve. The Copy Page controls on every docs page are local UI built on the exported URLs.

None of it required forking Docfy. The local pieces stayed thin and the heavy lifting went upstream, where other projects get it too.

For AI agents

Frontile now ships an installable agent skill. In a project that depends on Frontile, npx skills add josemarluedke/frontile gives a coding agent the component vocabulary, the semantic color system, and the @intent/@appearance rename, so it writes current Frontile rather than what it remembers from training data.

Breaking changes

There are a lot. The v0.18 migration guide covers each one and orders the work so that each step is verifiable before the next one begins.

Several of these changes fail silently. A Tailwind class that no longer resolves produces no error, no warning, and no CSS; the element renders without the style you asked for. Frontile's own documentation shipped thirteen demos with unresolvable background classes during this cycle, and nobody noticed until a linter went looking. Budget time for looking at the running app, not only at the diff.

What's next: v0.19

v0.19 is a cleanup release, and it is coming soon.

Its purpose is to remove what v0.18 deprecated, so the project stops carrying two of everything. It will remove:

  • @frontile/forms-legacy and @frontile/changeset-form, removed entirely rather than re-exported
  • the @frontile/* wrapper packages (move your imports to frontile)
  • the old @intent and @appearance argument names

It may carry new features and bug fixes too, but the cleanup drives its timing.

This shortens the runway on the optional migrations. The v0.18 guide marks package consolidation and the move off forms-legacy as "optional, any time before 0.19", which is true, but "before 0.19" is sooner than it sounds. If you are upgrading to v0.18 now, doing those two while you are already in the code is the cheaper order.

Full changelog

The complete list of merged pull requests, categorized, is in the v0.18.0 release on GitHub.

Released under MIT License - Created by Josemar Luedke