Frontile's semantic color system provides meaningful, accessible colors that adapt automatically between light and dark themes.
Instead of using arbitrary gray scales like bg-gray-300, Frontile's semantic colors use meaningful names that describe their purpose and visual weight. Each color automatically adapts between light and dark themes, ensuring consistent contrast and accessibility.
Every category exposes the same set of levels, organized into two bands. The bands share one emphasis vocabulary but are consumed by different CSS properties — the band a level belongs to tells you what it's for.
| Band | Levels (low → high emphasis) | Use for |
|---|---|---|
| Surface | subtle · muted · soft · mild · DEFAULT · firm |
Fills — bg-* and decorative border-* |
| Ink | strong · bolder |
Legible foregrounds — text-*, outlined borders |
DEFAULT is the resting fill. It has no suffix, so the bare class works:
bg-primary is the DEFAULT fill, text-primary the DEFAULT-level text.mild sits between soft and DEFAULT — a lower-emphasis fill for
cases where soft reads too faint but the full resting fill is too strong.firm is the most emphatic fill (e.g. a pressed background). It sits at
the top of the surface band, below the ink band it never competes with —
firm is a background, strong/bolder are text, so they live in different
property slots even though they share the ladder.on-{category}-{level} is the auto-generated black/white text that meets
WCAG contrast on that fill (e.g. text-on-primary on bg-primary).A level name describes emphasis rank, never a specific lightness. The same token can be dark in light mode and light in dark mode, because interaction direction inverts between schemes (light mode lightens on hover and darkens on press; dark mode does the reverse). A brightness word like "deep" or "light" would be correct in one theme and wrong in the other, so the vocabulary stays abstract.
Tailwind maps one token name to exactly one color value, reused across every
property (bg-primary, text-primary, and border-primary are always the
same color). A fill and the text that must stay legible on top of it are
different colors, so they can't share a single rung — they need two names. The
band split makes that boundary explicit instead of hiding it inside a flat
50…900-style scale.
Default interface colors for text, backgrounds, and borders
Usage Examples:
Text colors:
text-neutral-firm - Secondary text for descriptions and metadatatext-neutral-strong - Default text color for body contenttext-neutral-bolder - Maximum emphasis for headingsBackground colors with automatic text colors:
bg-neutral-subtle with text-on-neutral-subtle - Subtle background for hover statesbg-neutral-soft with text-on-neutral-soft - Soft background for cardsbg-neutral with text-on-neutral - Medium emphasis backgroundbg-neutral-strong with text-on-neutral-strong - Strong emphasis backgroundPrimary brand colors for important actions and brand elements
Usage Examples:
<template>
<div class='flex gap-4 flex-col'>
{{! Filled Button }}
<button
class='bg-primary text-on-primary hover:bg-primary-soft px-4 py-2 rounded'
>
Filled Button
</button>
{{! Outlined Button }}
<button
class='border border-primary text-primary hover:bg-primary-subtle bg-transparent px-4 py-2 rounded'
>
Outlined Button
</button>
{{! Tonal Button }}
<button
class='bg-primary-subtle text-primary-strong hover:bg-primary-soft/20 px-4 py-2 rounded'
>
Tonal Button
</button>
{{! Text Button }}
<button
class='text-primary hover:bg-primary-subtle bg-transparent px-4 py-2 rounded'
>
Text Button
</button>
</div>
</template>
Banners:
<template>
<div class='flex gap-4 flex-col'>
{{! Filled Banner }}
<div class='bg-primary text-on-primary p-4 rounded'>
<div class='font-semibold'>Filled Banner</div>
<div class='text-sm opacity-90'>High emphasis notification with brand
colors</div>
</div>
{{! Tonal Banner }}
<div class='bg-primary-subtle border border-primary-soft p-4 rounded'>
<div class='text-primary-strong font-semibold'>Tonal Banner</div>
<div class='text-primary-strong text-sm'>Subtle notification with brand
colors</div>
</div>
</div>
</template>
Secondary brand colors for supporting actions and highlights
Tertiary brand colors for accents and decorative emphasis
Usage Examples:
<template>
<div class='flex gap-4 flex-col'>
{{! Accent Alert }}
<div class='bg-secondary-subtle border border-secondary-soft p-4 rounded'>
<div class='text-secondary-strong'>
<div class='font-semibold'>New Feature!</div>
<div class='text-sm'>Check out our latest updates and improvements.</div>
</div>
</div>
{{! Accent Button }}
<button
class='bg-secondary text-on-secondary hover:bg-secondary-soft px-4 py-2 rounded'
>
Explore Features
</button>
</div>
</template>
Colors for positive states, confirmations, and successful actions
Usage Examples:
<template>
<div class='flex gap-4 flex-col'>
{{! Success Alert }}
<div class='bg-success-subtle border border-success-soft p-4 rounded'>
<div class='text-success-strong'>
<div class='font-semibold'>Success!</div>
<div class='text-sm'>Your changes have been saved successfully.</div>
</div>
</div>
{{! Success Button }}
<button
class='bg-success text-on-success hover:bg-success-soft px-4 py-2 rounded'
>
Confirm Action
</button>
</div>
</template>
Colors for errors, warnings, and destructive actions
Usage Examples:
<template>
<div class='flex gap-4 flex-col'>
{{! Error Alert }}
<div class='bg-danger-subtle border border-danger-soft p-4 rounded'>
<div class='text-danger-strong'>
<div class='font-semibold'>Error</div>
<div class='text-sm'>Failed to save changes. Please try again.</div>
</div>
</div>
{{! Destructive Button }}
<button
class='bg-danger text-on-danger hover:bg-danger-soft px-4 py-2 rounded'
>
Delete Item
</button>
</div>
</template>
Colors for cautions and important notices
Usage Examples:
<template>
<div class='flex gap-4 flex-col'>
{{! Warning Alert }}
<div class='bg-warning-subtle border border-warning-soft p-4 rounded'>
<div class='text-warning-strong'>
<div class='font-semibold'>Warning</div>
<div class='text-sm'>This action cannot be undone.</div>
</div>
</div>
{{! Warning Button }}
<button class='bg-warning text-on-warning hover:bg-warning-soft rounded'>
Proceed with Caution
</button>
</div>
</template>
Pick from the band that matches the property you're setting.
subtle: Hover states, selected rows, tonal/hairline backgroundsmuted: Hover on tonal fills, chip and close-button backgroundssoft: Card backgrounds, secondary surfaces, the hover step for solid fillsDEFAULT (bare bg-{color}): Primary buttons, badges, tags, resting bordersfirm: Pressed/active backgroundsstrong: Default body text, readable content, outlined-control text and bordersbolder: Headings, hover/active text, highest-emphasis foregroundson-{color}-{level}: Automatic contrasting text on a colored fill (e.g., text-on-primary on bg-primary)Note: Surface-band levels (
subtle…firm) are fills, not text colors. In dark mode they map to values that are illegible as text on dark surfaces. For visible text use an ink-band level (strong/bolder) or the matchingon-{color}token.
All color combinations in Frontile meet WCAG 2.1 Level AA contrast requirements (minimum 4.5:1 contrast ratio).
Frontile automatically generates optimal contrasting text colors for every background color using WCAG contrast calculations. Simply use the on- prefix with the same color level:
<!-- Automatically generates optimal text color (black or white) -->
<button class="bg-primary text-on-primary">
<div class="bg-success-soft text-on-success-soft">
<div class="bg-neutral-strong text-on-neutral-strong"></div>
</div>
</button>
How it works:
on-{color}-{level} class automaticallyBenefits:
While auto-generated on-colors work for most cases, you can override them for brand consistency or specific design requirements:
const { frontile } = require('@frontile/theme/plugin');
module.exports = frontile({
themes: {
light: {
colors: {
primary: {
subtle: '#eff6ff',
soft: '#93c5fd',
DEFAULT: '#3b82f6',
strong: '#1e40af'
},
// Override specific on-colors
'on-primary': {
DEFAULT: '#ffffff', // Force white text on the bare primary fill
strong: '#e0f2fe' // Use light blue instead of auto-generated white
}
// on-primary-subtle and on-primary-soft are still auto-generated
}
}
}
});
Partial overrides are supported — only define the levels you want to customize, and the rest will be auto-generated as usual. This works for all semantic color categories: on-neutral, on-primary, on-secondary, on-tertiary, on-success, on-warning, on-danger, and on-surface-modal.
Note: If a color value is a CSS variable reference (e.g.,
var(--my-color)), auto-generation is skipped for that color since contrast cannot be calculated at build time.
If you're upgrading from an older version of Frontile, see the Semantic Colors v2 Migration Guide for detailed instructions on updating your code.