This guide helps you migrate from the old numbered color system to the new semantic color levels in Frontile v0.18+.
The semantic color system has been redesigned to use named levels instead of numbered scales. This provides:
| Old Name | New Name | Notes |
|---|---|---|
default-* |
neutral-* |
Renamed to better indicate non-semantic UI elements |
There is no inverse color category. Content on an inverted surface is handled
one of two ways.
For a single element sitting on a filled background, use the automatic contrast color for that background level:
<div class="bg-neutral-strong text-on-neutral-strong">
Content with guaranteed contrast
</div>
For a whole region that should read as the opposite theme — a dark panel in light
mode, or a light one in dark mode — add the theme-inverse class. Every semantic
token inside it resolves to its other-theme value, so ordinary classes keep
working:
<div class="theme-inverse bg-surface-canvas p-6">
<h3 class="text-neutral-bolder">Reads as dark in light mode</h3>
<p class="text-neutral-strong">And as light in dark mode.</p>
</div>
theme-inverse is a selector the theme defines, not a color. It is unrelated to
the inverse category that appeared briefly during the 0.18 alpha cycle and was
removed before release.
The surface system (introduced during the v0.18 alpha cycle) has been simplified before its first stable release:
| Removed | Replacement | Notes |
|---|---|---|
surface-solid-0 … surface-solid-11 |
(removed) | The 12-step scale is gone. Use a surface role (app, canvas, card, modal, input) instead |
text-surface-solid-* |
text-neutral-* or text-on-surface-modal |
Use the neutral text scale for body copy, or the auto-generated on-color for content on surface-modal |
surface-popover |
surface-modal |
Popovers, dropdowns, modals, and drawers now share one "highest elevation" surface |
surface-overlay-content |
surface-modal |
Same merge as above — this was already identical in value to surface-popover |
surface-panel |
(removed) | Had no real consumer; use surface-card for elevated sidebar/panel containers |
surface-inset |
(removed) | Had no real consumer; use surface-overlay-subtle/soft for recessed translucent wells |
surface-overlay-inverse-* |
surface-overlay-strong (for backdrops) |
Had no real consumer. If you need a heavy backdrop tint, use strong; for a normal darken/lighten step, use subtle/soft/mild/firm |
(new) surface-input |
— | Dedicated surface for text inputs, checkboxes, and radios (previously surface-solid-0) |
surface-card also changed value: it's now translucent (white @ 90% in light mode) instead of fully opaque, and resolves to base-800 in dark mode instead of base-700.
// Before
<div class="bg-surface-popover rounded-md shadow-lg">Menu</div>
<div class="bg-surface-overlay-content rounded-lg">Modal</div>
<input class="bg-surface-solid-0" />
// After
<div class="bg-surface-modal rounded-md shadow-lg">Menu</div>
<div class="bg-surface-modal rounded-lg">Modal</div>
<input class="bg-surface-input" />
See the Surfaces guide for the full, current token set.
The translucent overlay levels were renamed to line up with the named levels used
by every other color category (subtle → soft → mild → firm → strong).
The values did not change — only the names:
| Old | New | Notes |
|---|---|---|
surface-overlay-medium |
surface-overlay-mild |
Same value, renamed to match the shared level names |
surface-overlay-strong |
surface-overlay-firm |
Same value, shifted down one name |
surface-overlay-scrim |
surface-overlay-strong |
The heavy modal/drawer backdrop is now just the top level |
// Before
<div class="bg-surface-overlay-medium" />
<div class="border-surface-overlay-strong" />
<div class="fixed inset-0 bg-surface-overlay-scrim" />
// After
<div class="bg-surface-overlay-mild" />
<div class="border-surface-overlay-firm" />
<div class="fixed inset-0 bg-surface-overlay-strong" />
surface-lift-* is a new translucent family alongside surface-overlay-*, with
the same five levels. Overlay darkens in light mode and lightens in dark mode,
pressing an element into the page; lift does the opposite, so the element floats
above what it covers.
// Recedes — hover states, section fills
<div class="bg-surface-overlay-soft" />
// Floats — frosted panels, sticky headers, captions over media
<header class="bg-surface-lift-firm backdrop-blur-md" />
Nothing is deprecated by this: existing surface-overlay-* usage is unaffected.
See the
Surfaces
guide for the level
table and usage guidance.
The numbered scale (50, 100, 200, ..., 950) has been replaced with named levels:
| Old Pattern | New Pattern | Description |
|---|---|---|
{color}-{number} |
{color}-{level} |
Named emphasis levels |
{color}-foreground |
on-{color}-{level} |
Automatic contrasting colors (black/white) |
Each semantic color is organized into two bands that share one emphasis vocabulary but are consumed by different CSS properties. Names describe emphasis rank, never brightness — a level may be dark in light mode and light in dark mode, since interaction direction inverts between the two schemes.
Surface band — fills for backgrounds and decorative borders (low → high emphasis):
subtle — Faintest tint, for hairline backgrounds and tonal restsmuted — Light tint, for hover on tonal surfacessoft — Soft fill, the hover step for solid fillsDEFAULT — Resting fill, the bare bg-{color} tokenfirm — Most emphatic fill, for pressed/active backgroundsInk band — legible foregrounds for text and outlined borders (low → high emphasis):
strong — Default legible foreground, for body text and outlined-control text/bordersbolder — Highest-emphasis foreground, for headings and hover/active texton-{color}-{level} — Automatic contrasting color (black or white)
calculated for optimal WCAG contrast on the specified background level.
| Old Class | New Class | Context |
|---|---|---|
bg-default-50 |
bg-neutral-subtle |
Very light backgrounds |
bg-default-100 |
bg-neutral-subtle |
Light backgrounds, borders |
bg-default-200 |
bg-neutral-subtle |
Light surfaces |
bg-default-300 |
bg-neutral-soft |
Moderate backgrounds |
bg-default-400 |
bg-neutral-soft |
Borders, dividers |
bg-default-500 |
bg-neutral-soft or bg-neutral |
Medium emphasis |
bg-default-600 |
bg-neutral |
Standard emphasis |
bg-default-700 |
bg-neutral or bg-neutral-strong |
Strong emphasis |
bg-default-800 |
bg-neutral-strong |
Maximum emphasis, buttons |
bg-default-900 |
bg-neutral-strong |
Darkest backgrounds |
bg-default-950 |
bg-neutral-strong |
Maximum contrast |
text-default |
text-neutral-strong or text-neutral-bolder |
Body text |
text-default-foreground |
text-on-neutral |
Contrasting text on bg |
border-default |
border-neutral-soft |
Standard borders |
| Old Class | New Class | Context |
|---|---|---|
bg-primary-500 |
bg-primary-soft or bg-primary |
Hover states |
bg-primary-600 |
bg-primary |
Default buttons |
bg-primary-700 |
bg-primary or bg-primary-strong |
Strong emphasis |
bg-primary-800 |
bg-primary-strong |
Active/pressed states |
text-primary-foreground |
text-on-primary |
Contrasting text on bg |
ring-primary-500 |
ring-primary-soft |
Focus rings |
| Old Class | New Class | Context |
|---|---|---|
bg-success-100 |
bg-success-subtle |
Alert backgrounds |
bg-success-500 |
bg-success-soft or bg-success |
Moderate emphasis |
bg-success-600 |
bg-success or bg-success-strong |
Buttons, hover |
bg-success-800 |
bg-success-strong |
Active states |
text-success-foreground |
text-on-success |
Contrasting text on bg |
ring-success-500 |
ring-success-soft |
Focus rings |
| Old Class | New Class | Context |
|---|---|---|
bg-warning-100 |
bg-warning-subtle |
Alert backgrounds |
bg-warning-500 |
bg-warning-soft or bg-warning |
Moderate emphasis |
bg-warning-600 |
bg-warning or bg-warning-strong |
Buttons, hover |
bg-warning-800 |
bg-warning-strong |
Active states |
text-warning-foreground |
text-on-warning |
Contrasting text on bg |
| Old Class | New Class | Context |
|---|---|---|
bg-danger-100 |
bg-danger-subtle |
Alert backgrounds |
bg-danger-500 |
bg-danger-soft or bg-danger |
Moderate emphasis |
bg-danger-600 |
bg-danger or bg-danger-strong |
Buttons, hover |
bg-danger-800 |
bg-danger-strong |
Active states |
text-danger-foreground |
text-on-danger |
Contrasting text on bg |
// Before
<button class="bg-default-800 text-default-100 hover:bg-default-800/80">
Click me
</button>
// After
<button class="bg-neutral-strong text-on-neutral-strong hover:bg-neutral">
Click me
</button>
// Before
<button class="bg-primary text-primary-foreground hover:bg-primary-500">
Submit
</button>
// After
<button class="bg-primary text-on-primary hover:bg-primary-soft">
Submit
</button>
// Before
<button class="border-primary text-primary hover:bg-primary hover:text-primary-foreground">
Cancel
</button>
// After
<button class="border-primary text-primary hover:bg-primary hover:text-on-primary">
Cancel
</button>
// Before
<div class="bg-success-100 border-success-500 text-success-900">
<Icon class="text-success" />
<p>Success message</p>
</div>
// After
<div class="bg-success-subtle border-success-soft text-on-success-subtle">
<Icon class="text-success" />
<p>Success message</p>
</div>
// Before
<div class="bg-danger-100 border-danger text-danger-foreground">
Error occurred
</div>
// After
<div class="bg-danger-subtle border-danger text-on-danger-subtle">
Error occurred
</div>
// Before
<input class="border-default-700 focus:border-primary text-default-900" />
// After
<input class="border-neutral-soft focus:border-primary text-neutral-strong" />
// Before
<h1 class="text-default-900">Heading</h1>
<p class="text-default-700">Body text</p>
<span class="text-default-500">Caption</span>
// After
<h1 class="text-neutral-bolder">Heading</h1>
<p class="text-neutral-strong">Body text</p>
<span class="text-neutral-firm">Caption</span>
// Before
<span class="bg-success text-success-foreground">
Active
</span>
// After
<span class="bg-success-strong text-on-success-strong">
Active
</span>
The new semantic colors automatically adapt to dark mode. Key changes:
strong becomes the lightest shade (inverted from light mode)subtle uses darker base colors with lower alphaon-{color}-{level} automatically maintains WCAG accessibility standardsNo changes needed — your dark mode classes will work automatically:
// Both light and dark modes handled
<div class="bg-neutral-subtle text-neutral-strong">
Content
</div>
Search your codebase for old color patterns:
# Find numbered color classes
grep -r "default-[0-9]" --include="*.{tsx,ts,gts,gjs,md}"
grep -r "primary-[0-9]" --include="*.{tsx,ts,gts,gjs,md}"
# Find foreground classes
grep -r "foreground" --include="*.{tsx,ts,gts,gjs,md}"
If you've customized Frontile's theme, update your color definitions:
// Before
import { themeColors } from '@frontile/theme';
colors: {
default: themeColors.light.default,
primary: themeColors.light.primary,
}
// After
import semanticColors from '@frontile/theme/colors/semantic';
colors: {
neutral: semanticColors.light.neutral,
primary: semanticColors.light.primary,
}
Replace old color classes with new semantic levels:
The tables above map each old number to a resting level. They deliberately do not try to guess hover, pressed, or disabled variants, because the old numbered scale encoded those by stepping the number and the new one has dedicated levels:
| Old | New | Level |
|---|---|---|
hover:bg-primary-500 on a bg-primary-600 element |
hover:bg-primary-soft |
soft is the hover step for solid fills |
active:bg-primary-800 |
active:bg-primary-firm |
firm is the most emphatic fill |
hover:bg-default-200 on a tonal surface |
hover:bg-neutral-muted |
muted is the hover step for tonal surfaces |
| A fill between resting and firm | bg-primary-mild |
mild |
If you had a three-state solid button — bg-primary-600, hover:bg-primary-500,
active:bg-primary-700 — it becomes bg-primary, hover:bg-primary-soft,
active:bg-primary-firm.
There is no supported codemod for this. A find-and-replace can do the
unambiguous half — the category rename and the foreground suffix — but it
cannot pick emphasis levels, and it cannot tell a resting fill from a hover one:
# The mechanical part only. Review everything it touches.
# GNU sed: drop the '' after -i
find . -type f \( -name "*.hbs" -o -name "*.gts" -o -name "*.gjs" -o -name "*.ts" \) \
-exec sed -i '' \
-e 's/\bbg-default-/bg-neutral-/g' \
-e 's/\btext-default-/text-neutral-/g' \
-e 's/\bborder-default-/border-neutral-/g' \
-e 's/\bbg-background\b/bg-surface-canvas/g' \
{} +
Then find what's left by hand — anything still carrying a number needs a level chosen for it:
grep -rnE '(bg|text|border|ring|from|to|via)-(neutral|primary|secondary|tertiary|success|warning|danger)-[0-9]{2,3}' .
That grep is worth keeping in CI for a release or two. A leftover numbered class produces no CSS and no error, so it will not show up any other way.
When choosing between levels, ask:
{color}-strong{color} (DEFAULT){color}-soft{color}-subtleon-{color}-{level} (automatically white or black based on WCAG contrast)neutral-bolder (heading), neutral-strong (body), neutral-firm (caption){color} or {color}-softneutral-soft or neutral-subtle