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.
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.
Fourteen of them. Every demo below is live.
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>
The month grid on its own: keyboard navigable, localized through Intl, and
composable into your own pickers.
Docs →
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
import { Calendar } from 'frontile';
const today = new Date();
<template><Calendar @defaultValue={{today}} /></template>
A searchable, relevance-ranked command palette. Use it inline, as below, or as
CommandDialog for the ⌘K treatment.
Docs →
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>
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>
Collapsible sections for progressive disclosure. Docs →
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>
Panels shown one at a time, with an indicator that slides between them. Docs →
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>
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>
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>
A persistent, inline message, rather than a notification that comes and goes. Also available as a full-width banner layout. Docs →
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>
Page chips, previous/next, an optional summary. The simplest case needs only
@total. Docs →
import { Pagination } from 'frontile';
<template><Pagination @total={{120}} /></template>
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>
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>
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 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.
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.
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:
.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.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.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.
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.
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.
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@frontile/* wrapper packages (move your imports to frontile)@intent and @appearance argument namesIt 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.
The complete list of merged pull requests, categorized, is in the v0.18.0 release on GitHub.