Frontile's theme system provides a comprehensive, accessible design foundation built on semantic colors, flexible surfaces, and automatic dark mode support.
Install the theme package using Ember CLI:
ember install @frontile/theme
Add this to your app/styles/app.css:
@import 'tailwindcss' source('../../');
@plugin "@frontile/theme/plugin/default";
@import "@frontile/theme";
Create frontile.js in your project root:
const { frontile } = require('@frontile/theme/plugin');
module.exports = frontile({
/* your configuration */
});
Then update app/styles/app.css:
@import 'tailwindcss' source('../../');
@plugin "./../../frontile.js";
@import "@frontile/theme";
Meaningful color categories (neutral, primary, success, danger, warning) with intuitive levels (subtle, soft, firm, strong) that adapt between light and dark themes.
Flexible depth system with opaque surface roles and two families of translucent veils — overlay (recedes) and lift (advances), each with subtle, soft, mild, firm, strong.
Automatic theme adaptation with intelligent color inversion. Simply toggle the dark class to switch between light and dark modes.
Here's how easy it is to create a themed button with Frontile:
import { Button } from 'frontile';
<template>
<Button @intent='primary'>Click me</Button>
</template>
The Button already resolves its own semantic colors, so it adapts to light and dark themes without additional code. Under the hood the primary color provides the emphasis, while on-primary calculates the optimal contrasting text color (black or white) for accessibility — the same tokens you'd reach for on your own markup:
<template>
<div class='bg-primary text-on-primary rounded px-4 py-2'>
Your own element, same tokens
</div>
</template>
Toggle between light and dark themes by adding or removing the dark class on a parent element (typically <html> or <body>):
<html class="dark">
<!-- Your content adapts to dark mode -->
</html>
Remove the dark class to revert to light theme. You can also explicitly use the light class if needed.
Frontile uses custom CSS variants that intelligently apply theme-specific styles:
.dark - When present on a parent element, all descendant elements use dark theme colors.light - When present on a parent element, all descendant elements use light theme colors (default).theme-inverse - Inverts the current theme for a section and its descendantsThe theme system uses sophisticated CSS selectors to ensure proper inheritance:
/* Light theme applies to .light elements and .theme-inverse within .dark */
.light *, .dark .theme-inverse *
/* Dark theme applies to .dark elements and .theme-inverse within .light */
.dark *, .light .theme-inverse *
This means you can nest themes and use theme-inverse sections anywhere in your application, and the colors will automatically adapt.
Create sections with inverted theme colors using the theme-inverse utility class. In light mode, these sections display dark theme colors, and vice versa.
import { Button } from 'frontile';
<template>
<div class='space-y-8 p-8'>
<div class='p-6 bg-surface-overlay-subtle rounded-lg'>
<h3 class='text-lg font-bold text-neutral-strong mb-4'>Regular Theme</h3>
<Button @intent='primary'>Primary Button</Button>
</div>
<div class='theme-inverse p-6 bg-surface-canvas rounded-lg'>
<div class='p-6 bg-surface-overlay-subtle rounded-lg'>
<h3 class='text-lg font-bold text-neutral-strong mb-4'>Inverted Theme</h3>
<Button @intent='primary'>Primary Button</Button>
</div>
</div>
</div>
</template>
Learn more about theme-inverse →
Frontile provides six semantic color categories:
Each category spans two bands: a surface band of fills (subtle, muted, soft, mild, DEFAULT, firm) and an ink band of legible foregrounds (strong, bolder). The on-{color}-{level} prefix automatically provides optimal contrasting text colors (black or white) for accessibility.
Primary brand colors for important actions and brand elements
Colors for positive states, confirmations, and successful actions
Colors for errors, warnings, and destructive actions
Colors automatically adapt based on the current theme (.dark, .light, or .theme-inverse). Frontile uses CSS variables that change their values based on the theme context:
/* Example: primary adapts to the theme */
.light {
--color-primary: oklch(55.86% 0.2094 263.84); /* Vivid teal in light mode */
}
.dark {
--color-primary: oklch(69.83% 0.1526 252.37); /* Lighter teal in dark mode */
}
When you use bg-primary, the actual color value comes from the CSS variable, which automatically switches based on whether the element is in a .light or .dark context.
You can use semantic colors in custom CSS using the Tailwind theme() function or CSS variables:
/* Using Tailwind theme function */
.my-component {
background-color: theme(colors.primary.DEFAULT);
color: theme(colors.on-primary.DEFAULT);
}
/* Using CSS variables directly */
.my-component {
background-color: var(--color-primary);
color: var(--color-on-primary);
}
You can customize semantic colors using the JavaScript plugin configuration:
const { frontile } = require('@frontile/theme/plugin');
module.exports = frontile({
themes: {
light: {
colors: {
primary: {
subtle: '#eff6ff',
soft: '#93c5fd',
DEFAULT: '#3b82f6',
strong: '#1e40af'
}
}
},
dark: {
colors: {
primary: {
subtle: '#1e3a8a',
soft: '#3b82f6',
DEFAULT: '#60a5fa',
strong: '#dbeafe'
}
}
}
}
});
Learn more about semantic colors →
The Surface system provides two types of backgrounds:
These overlays are translucent and stack on top of a solid base background.
Learn more about the surface system →
Override semantic colors in your configuration:
const { frontile } = require('@frontile/theme/plugin');
module.exports = frontile({
themes: {
light: {
colors: {
primary: {
subtle: '#eff6ff',
soft: '#93c5fd',
DEFAULT: '#3b82f6',
strong: '#1e40af'
}
}
},
dark: {
colors: {
primary: {
subtle: '#1e3a8a',
soft: '#3b82f6',
DEFAULT: '#60a5fa',
strong: '#dbeafe'
}
}
}
}
});
// Note: on-{color}-{level} classes are automatically generated
// based on the background colors you define above
Specify which theme loads by default:
module.exports = frontile({
defaultTheme: 'light', // or 'dark'
themes: {
// ... your themes
}
});
The recommended approach for Tailwind v4 is to customize theme values directly using CSS variables. This provides fine-grained control without JavaScript configuration and better performance.
Add your customizations in your app/styles/app.css after importing the theme:
@import 'tailwindcss' source('../../');
@plugin "@frontile/theme/plugin/default";
@import "@frontile/theme";
/* Override Tailwind theme utilities */
@theme {
/* Custom border radius values */
--radius: 0.75rem; /* One knob: scales the whole rounded-* scale */
--radius-pill: 2rem; /* Custom pill radius (absolute, does not scale) */
/* Custom opacity values */
--opacity-hover: .9;
--opacity-disabled: .4;
}
/* Override component-specific values */
:root {
/* Modal sizes */
--modal-md: 32rem;
--modal-lg: 48rem;
/* Drawer sizes */
--drawer-md: 32rem;
--drawer-lg: 48rem;
}
Customize layout values for specific themes using the .dark and .light classes. Important: To properly target all instances of a theme including .theme-inverse sections, use both selectors:
@import "@frontile/theme";
/* Light theme customization - targets .light AND .theme-inverse within .dark */
.light,
.dark .theme-inverse {
--opacity-hover: .85;
}
/* Dark theme customization - targets .dark AND .theme-inverse within .light */
.dark,
.light .theme-inverse {
--opacity-hover: .75;
--modal-lg: 56rem;
}
Why both selectors?
.light targets elements in light mode.dark .theme-inverse targets inverted sections within dark mode (which display light theme)Note: Variables in the
@themeblock automatically generate Tailwind utilities (e.g.,--radius-xlcreatesrounded-xlclass). Variables in:rootor theme-specific selectors are for component-specific values that don't need utility classes. Colors should be customized using the JavaScript plugin configuration, not CSS variables.
You can also configure the theme using JavaScript by creating a frontile.js file in your project root:
const { frontile } = require('@frontile/theme/plugin');
module.exports = frontile({
defaultTheme: 'dark',
themes: {
light: {
colors: {
// Customize semantic colors
primary: {
subtle: '#eff6ff',
soft: '#93c5fd',
DEFAULT: '#3b82f6',
strong: '#1e40af'
}
},
layout: {
// Customize layout values
opacity: {
hover: .85,
disabled: .4
},
radius: {
DEFAULT: '0.75rem',
pill: '2rem'
}
}
},
dark: {
colors: {
primary: {
subtle: '#1e3a8a',
soft: '#3b82f6',
DEFAULT: '#60a5fa',
strong: '#dbeafe'
}
}
}
}
});
Then reference it in your CSS:
@import 'tailwindcss' source('../../');
@plugin "./../../frontile.js";
@import "@frontile/theme";
Always prefer semantic color classes over generic Tailwind utilities:
{{! Good - uses semantic colors }}
<button class='bg-primary text-on-primary hover:bg-primary-soft'>
Submit
</button>
{{! Avoid - hard-coded colors don't adapt to theme }}
<button class='bg-blue-500 text-white hover:bg-blue-400'>
Submit
</button>
Use the surface system for component backgrounds:
{{! Good - uses a surface role for the base }}
<div class='bg-surface-app'>
{{! Good - uses overlay for elevated surface }}
<div class='bg-surface-overlay-soft rounded-lg p-4'>
Card content
</div>
</div>
Always verify your UI in both light and dark modes:
<!-- Test light mode -->
<html class="light">
<!-- Your app -->
</html>
<!-- Test dark mode -->
<html class="dark">
<!-- Your app -->
</html>