# Theme System
Source: /docs/theming/overview.md
# Theme System
Frontile's theme system is an accessible design foundation built on semantic colors, a surface system, and automatic dark mode support.
## Installation
Install the theme package using Ember CLI:
```bash
ember install @frontile/theme
```
## Configuration
### Using Default Theme
Add this to your `app/styles/app.css`:
```css title="app/styles/app.css"
@import 'tailwindcss' source('../../');
@plugin "@frontile/theme/plugin/default";
@import "@frontile/theme";
```
### Custom Theme Configuration
Create `frontile.js` in your project root:
```js title="frontile.js"
const { frontile } = require('@frontile/theme/plugin');
module.exports = frontile({
/* your configuration */
});
```
Then update `app/styles/app.css`:
```css title="app/styles/app.css"
@import 'tailwindcss' source('../../');
@plugin "./../../frontile.js";
@import "@frontile/theme";
```
## Key Concepts
### Semantic Colors
Meaningful color categories (neutral, primary, success, danger, warning) with intuitive levels (subtle, soft, firm, strong) that adapt between light and dark themes.
### Surface System
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.
### Dark Mode
Automatic theme adaptation with color inversion. Toggle the `dark` class to switch between light and dark modes.
## Quick Example
Frontile components resolve semantic colors automatically:
```gts preview
import { Button } from 'frontile';
```
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:
```gts preview
Your own element, same tokens
```
## Theme Switching
Toggle between light and dark themes by adding or removing the `dark` class on a parent element (typically `` or `
`):
```html
```
Remove the `dark` class to revert to light theme. You can also explicitly use the `light` class if needed.
### How Theme Classes Work
Frontile uses custom CSS variants that 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 descendants
The theme system uses CSS selectors to ensure proper inheritance:
```css
/* 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.
## Theme Inverse
Create sections with inverted theme colors using the `theme-inverse` utility class. In light mode, these sections display dark theme colors, and vice versa.
```gts preview
import { Button } from 'frontile';
Regular Theme
Inverted Theme
```
[Learn more about theme-inverse →](configuration/theme-switching.md)
## Color System
Frontile provides six semantic color categories:
- **Neutral** - Default interface colors
- **Primary** - Primary brand colors (blue)
- **Accent** - Visual emphasis and highlights (violet)
- **Success** - Positive states and actions (green)
- **Danger** - Errors and destructive actions (red)
- **Warning** - Warnings and cautions (orange)
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.
### How Colors Adapt to Themes
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:
```css
/* 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.
### Using Colors in Your CSS
You can use semantic colors in custom CSS using the Tailwind `theme()` function or CSS variables:
```css
/* 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);
}
```
### Customizing Colors
You can customize semantic colors using the JavaScript plugin configuration:
```js
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 →](design-tokens/colors.md)
## Surface System
The Surface system provides two types of backgrounds:
- **Surface Solid** - Opaque base layers (0-11 scale)
- **Surface Overlay** - Translucent layers that recede into the page (subtle, soft, mild, firm, strong)
- **Surface Lift** - Translucent layers that float above the page (subtle, soft, mild, firm, strong)
[Learn more about the surface system →](design-tokens/surfaces.md)
## Basic Customization
### Customizing Colors
Override semantic colors in your configuration:
```js
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
```
### Setting Default Theme
Specify which theme loads by default:
```js
module.exports = frontile({
defaultTheme: 'light', // or 'dark'
themes: {
// ... your themes
}
});
```
## Advanced Configuration
### Customizing with CSS Variables
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.
#### Override Theme Defaults
Add your customizations in your `app/styles/app.css` after importing the theme:
```css title="app/styles/app.css"
@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;
}
```
#### Theme-Specific Customization
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:
```css
@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)
- This ensures your customizations apply consistently across both regular theme usage and theme-inverse sections
> **Note:** Variables in the `@theme` block automatically generate Tailwind utilities (e.g., `--radius-xl` creates `rounded-xl` class). Variables in `:root` or 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.**
### JavaScript Configuration
You can also configure the theme using JavaScript by creating a `frontile.js` file in your project root:
```js title="frontile.js" collapsible
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:
```css
@import 'tailwindcss' source('../../');
@plugin "./../../frontile.js";
@import "@frontile/theme";
```
## Best Practices
### Use Semantic Colors
Always prefer semantic color classes over generic Tailwind utilities:
```hbs
{{! Good - uses semantic colors }}
{{! Avoid - hard-coded colors don't adapt to theme }}
```
### Use Surface System for Backgrounds
Use the surface system for component backgrounds:
```hbs
{{! Good - uses a surface role for the base }}
{{! Good - uses overlay for elevated surface }}
Card content
```
### Test in Both Modes
Always verify your UI in both light and dark modes:
```html
```
---
# Component Styles
Source: /docs/theming/component-styles.md
# Customizing Component Styles
Frontile components can be customized with Tailwind Variants, either globally or per-instance. This guide covers both approaches.
## Overview
Frontile uses [Tailwind Variants](https://www.tailwind-variants.org/) to manage and customize component styles. Tailwind Variants provide a structured way to define different visual representations (or variants) for components while keeping your styles consistent and maintainable.
A **slot** represents a specific part of a component that can be styled or customized separately. Slots allow you to apply different styles to different parts of a complex component, providing more granular control.
When customizing styles in Frontile, you can either apply styles globally using `registerCustomStyles` or customize individual components using class arguments. You can override default component styles by passing your own class names to the `class` or `classes` argument, depending on whether the component has slots.
## DOM Anatomy: `data-component` and `data-part`
Every Frontile component renders a stable, documented DOM anatomy, independent
of its CSS classes. Two attributes carry it:
- **`data-component=""`** — the kebab-cased `tv()` config name (e.g. the
`notificationCard` config produces `notification-card`), written on the
component's **outermost rendered element only**. This is the name the
component is registered under, not its filename — `commandDialog` →
`command-dialog`.
- **`data-part=""`** — the kebab-cased `tv()` slot key, written on
**every** element that renders a slot (`startContent` → `start-content`).
Slot names are exactly the keys you already pass to `@classes`, so if you
can style a part with `@classes={{hash startContent='...'}}`, you can also
select it with `[data-part="start-content"]`.
The root element's part is usually `base`, but not always — it carries
whichever slot it actually renders. `Table`'s root is its wrapper `
`, so
that element is `data-part="wrapper"`; `SimpleTable` used on its own roots on
the `
` element, so that one is `data-part="table"`. `ProgressBar`'s
outer `
` renders no slot at all, so it has `data-component="progress-bar"`
and no `data-part`. Check the component's page if you need the exact shape.
Both attributes can be overridden: a `data-component` or `data-part` you pass
to a component wins over the one it would render itself.
A nested component's root legitimately carries **both** its own
`data-component` and a `data-part` belonging to its parent. `CloseButton`
rendered inside `Alert`, for instance, is simultaneously
`data-component="close-button"` (its own root) and `data-part="close-button"`
(the part it fills in `Alert`'s anatomy).
Two components can also render the same `data-component` value: `TabNav` is a
second renderer of the `tabs` config, alongside `Tabs` itself.
### Selecting by anatomy
Scope a selector to one component's own parts with:
```css
[data-component="modal"] [data-part="header"] {
/* ... */
}
```
### Known limitation: a nested component can share a part name
`[data-component="x"] [data-part="y"]` is a plain CSS descendant combinator —
it matches **any** descendant, not just `y` parts that belong directly to
`x`. That's ambiguous whenever a nested component happens to have a part
with the same name. `Alert` renders a `CloseButton`, and `CloseButton` has
its own `icon` part (its SVG); `Alert` also has its own `icon` part (the
alert's leading icon). `[data-component="alert"] [data-part="icon"]` matches
both — the alert's own icon *and* the close button's icon glyph, because the
close button is a descendant of the alert's root.
What you usually mean is "the parts whose nearest `[data-component]` ancestor
is `x`". CSS has no "nearest enclosing" combinator, so in a stylesheet you
narrow the selector yourself, using child combinators down the path you want:
```css
/* The alert's own icon. The close button's icon is nested one level
deeper, inside the close button, so it doesn't match. */
[data-component="alert"] > [data-part="inner"] > [data-part="icon"] {
/* ... */
}
```
In tests, use the `ownParts(root, part)` helper from `frontile/test-support`,
which returns only the parts belonging to `root` itself:
```ts
import { ownParts } from 'frontile/test-support';
// Only Alert's own `icon` part, not CloseButton's.
const icons = ownParts(alertRootElement, 'icon');
```
### Not `data-slot`
If you're coming from shadcn/ui, Nuxt UI, or HeroUI, this convention will look
familiar but the attribute name differs — those use `data-slot`, Frontile uses
`data-part`. In Ember, "slot" already means a named block (`{{yield to="title"}}`),
which is a different thing from a styleable piece of the DOM.
`@frontile/forms-legacy` predates this convention and does not carry these
attributes.
## Customizing Styles
### Setting Up Global Customization
To apply global styles to your Frontile components, it's recommended to create a separate file for your theme settings. You can create a file named `app/theme.js` to register your global custom styles, and then import it in `app/app.js` to ensure the styles are applied across your application.
#### Example: Setting Up Global Styles
1. **Create a Theme File**: Create a new file called `app/theme.js` and add your global customizations.
```javascript
// app/theme.js
import { useStyles, registerCustomStyles, tv } from '@frontile/theme';
const components = useStyles();
registerCustomStyles({
// stuff ...
});
```
2. **Import the Theme File**: Import the theme file in `app/app.js` to ensure the global styles are applied.
```javascript
// app/app.js
import Application from '@ember/application';
// stuff...
import './theme'; // Import your theme file here
export default class App extends Application {
// stuff...
}
loadInitializers(App, config.modulePrefix);
```
### Global Customization with `registerCustomStyles`
The `registerCustomStyles` function allows you to override the default styles of Frontile components globally. This means that every instance of a specific component throughout your application will inherit the styles defined in `registerCustomStyles`.
#### Example: Global Customization
To globally change the styles of the Drawer, Modal, and Button components:
```javascript
import { useStyles, registerCustomStyles, tv } from '@frontile/theme';
const components = useStyles();
registerCustomStyles({
modal: tv({
extend: components.modal,
slots: {
header: 'font-header text-2xl pt-12 pl-6 pb-6',
body: 'pt-4 pb-4 pl-6 pr-12',
footer: 'bg-transparent py-4 px-12 pb-8'
},
variants: {
size: {
lg: 'max-w-[48rem]',
xl: 'max-w-[64rem]'
}
}
}),
drawer: tv({
extend: components.drawer,
slots: {
header: 'text-2xl px-4 py-6 bg-black text-white dark:bg-white dark:text-black rounded-none',
footer: 'px-4 py-6'
},
variants: {
size: {
lg: 'max-w-[48rem]',
xl: 'max-w-[64rem]'
}
}
}),
button: tv({
...components.button,
base: ['font-header text-xl'],
variants: {
...components.button.variants,
appearance: {
...components.button.variants.appearance,
default: 'shadow-elevation-2'
}
},
compoundVariants: [
...components.button.compoundVariants,
{
appearance: 'default',
intent: 'default',
class: 'bg-black text-white hover:bg-black/80 dark:bg-white dark:text-black dark:hover:bg-white/80'
}
]
})
});
```
In this example:
- The `modal` component is customized globally, including changes to the header, body, and footer slots, and additional size variants.
- The `drawer` component is updated with new styles for the header and footer slots, as well as size variants.
- The `button` component's base styles and compound variants are customized, adding new default appearances.
Use global customization when you want consistency across your entire application for a particular component.
### Local Customization with Component Arguments
If you only need to customize the styles of a specific component instance, use arguments like `class` or `classes`, depending on the component. Frontile uses Tailwind Variants with `tw-merge` to merge Tailwind classes, so any class passed locally overwrites the default styles.
#### Example: Local Customization with Drawer Component
```hbs
```
In this example, the `Drawer` component is provided with specific styles for the `header`, `body`, and `footer` slots. The `header` and `footer` have a dark background (`bg-neutral-strong`) with automatically contrasting text (`text-on-neutral-strong`), while the `body` has a light background (`bg-neutral-subtle`). This approach allows for context-specific customizations without impacting other instances of the Drawer component.
---
# Design Tokens Overview
Source: /docs/theming/design-tokens/overview.md
# Design Tokens
Frontile extends Tailwind CSS with additional design tokens for building consistent, accessible user interfaces. These tokens automatically generate Tailwind utility classes.
## What are Design Tokens?
Design tokens are named entities that store visual design attributes. They provide a consistent language for design decisions across your application. Frontile's tokens use Tailwind v4's `@theme` directive to define CSS variables that automatically generate utility classes.
For example, defining `--radius: 8px` in your theme automatically generates the `rounded` utility class.
## Token Categories
Frontile adds these design token categories to Tailwind:
### [Colors](colors.md)
Semantic color system with five categories (neutral, primary, success, danger, warning) that automatically adapt between light and dark themes.
**Generated utilities:** `bg-primary`, `text-danger-strong`, `border-success-soft`
### [Typography](typography.md)
Text styles with semantic sizing across six categories (marquee, header, body, code, caption, label) built on a modular scale.
**Generated utilities:** `text-header-lg`, `text-body-md`, `text-label-sm`
### [Icons](icons.md)
Consistent icon sizing scale that pairs harmoniously with typography.
**Generated utilities:** `size-icon-sm`, `size-icon-md`, `size-icon-lg`
### [Borders](borders.md)
Border widths and radius tokens for consistent UI styling.
**Generated utilities:** `border-thin`, `border-heavy`, `rounded-xl`, `rounded-pill`
### [Elevation](elevation.md)
Shadow system for creating visual depth with six elevation levels (0-5).
**Generated utilities:** `shadow-elevation-1`, `shadow-elevation-3`
### [Surfaces](surfaces.md)
Background color system for creating distinct surface layers in your UI.
**Generated utilities:** `bg-surface-app`, `bg-surface-modal`, `bg-surface-overlay-soft`, `bg-surface-lift-soft`
## How Tokens Work
Frontile uses Tailwind v4's `@theme` directive to define tokens as CSS variables:
```css
@theme {
--radius: 8px; /* Generates: rounded */
--radius-xl: 12px; /* Generates: rounded-xl */
--size-icon-md: 16px; /* Generates: size-icon-md */
}
```
These variables:
- Automatically generate Tailwind utility classes
- Can be customized globally or per-theme
- Maintain consistency across your application
- Support light and dark theme variations
## Standard Tailwind Utilities
For layout, spacing, flexbox, grid, and other standard utilities, refer to the [Tailwind CSS documentation](https://tailwindcss.com/docs). Frontile uses Tailwind as-is for these utilities and only documents the design tokens it adds.
---
# Colors
Source: /docs/theming/design-tokens/colors.md
# Semantic Colors
Frontile's semantic color system provides meaningful, accessible colors that adapt automatically between light and dark themes.
## Overview
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.
## Color Levels
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`).
### Names are ranks, not brightness
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.
### Why two bands instead of one numbered scale
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.
## Color Categories
### Neutral
**Usage Examples:**
Text colors:
- `text-neutral-firm` - Secondary text for descriptions and metadata
- `text-neutral-strong` - Default text color for body content
- `text-neutral-bolder` - Maximum emphasis for headings
Background colors with automatic text colors:
- `bg-neutral-subtle` with `text-on-neutral-subtle` - Subtle background for hover states
- `bg-neutral-soft` with `text-on-neutral-soft` - Soft background for cards
- `bg-neutral` with `text-on-neutral` - Medium emphasis background
- `bg-neutral-strong` with `text-on-neutral-strong` - Strong emphasis background
### Primary
**Usage Examples:**
```gts preview collapsible
```
## Choosing the Right Color Level
Pick from the band that matches the property you're setting.
### Backgrounds & decorative borders (surface band)
- **`subtle`**: Hover states, selected rows, tonal/hairline backgrounds
- **`muted`**: Hover on tonal fills, chip and close-button backgrounds
- **`soft`**: Card backgrounds, secondary surfaces, the hover step for solid fills
- **`DEFAULT`** (bare `bg-{color}`): Primary buttons, badges, tags, resting borders
- **`firm`**: Pressed/active backgrounds
### Text & outlined borders (ink band)
- **`strong`**: Default body text, readable content, outlined-control text and borders
- **`bolder`**: Headings, hover/active text, highest-emphasis foregrounds
- **`on-{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 matching
> `on-{color}` token.
## Accessibility
All color combinations in Frontile meet WCAG 2.1 Level AA contrast requirements (minimum 4.5:1 contrast ratio).
### Automatic "On-" Colors
Frontile automatically generates optimal contrasting text colors for every background color using WCAG contrast calculations. Use the `on-` prefix with the same color level:
```html
```
**How it works:**
1. Frontile calculates the relative luminance of each background color
2. Determines contrast ratio with both black and white text
3. Selects the color with better contrast (≥4.5:1 ratio)
4. Generates the appropriate `on-{color}-{level}` class automatically
**Benefits:**
- No need to manually determine text colors
- Always meets WCAG AA standards
- Automatically adapts to theme changes (light/dark mode)
- Works with custom theme colors
### Customizing On-Colors
While auto-generated on-colors work for most cases, you can override them for brand consistency or specific design requirements:
```js
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.
## Migrating from Old Colors
If you're upgrading from an older version of Frontile, see the [Semantic Colors v2 Migration Guide](../../migrations/v0.18/semantic-colors.md) for detailed instructions on updating your code.
---
# Typography
Source: /docs/theming/design-tokens/typography.md
# Typography System
Frontile's typography system is a scalable type system built on semantic text styles and a modular scale. It uses Tailwind v4's `@theme` directive for base tokens and `@layer utilities` for explicit utility classes.
## Overview
The typography system includes:
- **7 text style categories**: Marquee, Header, Strong, Body, Code, Caption, and Label
- **Multiple size variants** for each category
- **Modular scale** based on a mathematical ratio for harmonic sizing
- **Composite utility classes** that bundle all typography properties together
- **Full customization** through CSS variables
## Text Style Categories
### Marquee
Large, semibold serif display text (Domine) for hero sections and prominent headings.
```gts preview
```
**Available sizes:** `4xs`, `3xs`, `2xs`, `xs`, `sm`, `md`, `lg`, `xl`, `2xl`, `3xl`
**Best for:** Content headings (h1-h6), section titles, hierarchical structure
### Strong
Emphasis text for numerics, prices, and metrics. Shares the Header size, tracking, and leading scale but is a distinct role so it can be styled independently.
```gts preview
$1,299
2 Gbps
99.99% uptime
12 included
```
**Available sizes:** `4xs`, `3xs`, `2xs`, `xs`, `sm`, `md`, `lg`, `xl`, `2xl`, `3xl`
**Font:** Open Sans · **Weight:** bold · **Best for:** Prices, counts, metrics, any numeric emphasis
### Body
Standard body text with spacious line height optimized for readability.
```gts preview collapsible
Body 3XL: The quick brown fox jumps over the lazy dog. This is 3XL body text with spacious line height for comfortable reading.
Body 2XL: The quick brown fox jumps over the lazy dog. This is 2XL body text with spacious line height for comfortable reading.
Body XL: The quick brown fox jumps over the lazy dog. This is extra large body text with spacious line height for comfortable reading.
Body LG: The quick brown fox jumps over the lazy dog. This is large body text with spacious line height for comfortable reading.
Body MD: The quick brown fox jumps over the lazy dog. This is default body text with spacious line height for comfortable reading.
Body SM: The quick brown fox jumps over the lazy dog. This is small body text with spacious line height for comfortable reading.
Body XS: The quick brown fox jumps over the lazy dog. This is extra small body text.
Body 2XS: The quick brown fox jumps over the lazy dog. This is 2XS body text.
Body 3XS: The quick brown fox jumps over the lazy dog. This is 3XS body text.
Body 4XS: The quick brown fox jumps over the lazy dog. This is 4XS body text.
Body 5XS: The quick brown fox jumps over the lazy dog. This is 5XS body text.
```
**Available sizes:** `5xs`, `4xs`, `3xs`, `2xs`, `xs`, `sm`, `md`, `lg`, `xl`, `2xl`, `3xl`
**Best for:** Paragraphs, article content, descriptions, general text
### Code
Monospace text for code snippets and technical content.
```gts preview
```
**Available sizes:** `sm`, `md`
**Best for:** Code blocks, inline code, technical identifiers
### Caption
Secondary descriptive text with relaxed letter spacing.
```gts preview
Example Image
Caption MD: Figure 1 - Example image caption with medium size
Caption SM: Small caption text for supplementary information
```
**Available sizes:** `sm`, `md`
**Best for:** Image captions, figure labels, supplementary text
### Label
UI labels and form labels with semibold weight and tight (100%) leading.
```gts preview
Label 3XL: Extra Large Label
Label 2XL: Double Extra Large Label
Label XL: Extra Large Label
Label LG: Large Label
Label MD: Medium Label
Label SM: Small Label
Label XS: Extra Small Label
Label 2XS: Double Extra Small Label
Label Micro: Micro Label
Label Nano: Nano Label
```
**Available sizes:** `nano`, `micro`, `2xs`, `xs`, `sm`, `md`, `lg`, `xl`, `2xl`, `3xl`
**Best for:** Form labels, UI labels, buttons, navigation items, badges
## Available Utilities
All text style utilities require both a font-family utility and a sized text style utility:
```html
...
...
...
......
```
## Combining with Other Utilities
Typography utilities work with other Tailwind utilities:
```gts preview
{{! Colored header }}
Colored Header
{{! Truncated body text }}
This is a very long paragraph that will be truncated with an ellipsis when it exceeds the maximum width.
```
## How It Works
The typography system uses Tailwind v4's `@theme` directive to automatically generate utilities from CSS variables:
1. **Base tokens** defined in `@theme` automatically generate individual utilities:
- `--font-header` → `font-header` utility
- `--font-size-14` → `text-14` utility
- `--font-weight-bold` → `font-bold` utility
- `--letter-spacing-tight` → `tracking-tight` utility
- `--font-weight-semibold` → `font-semibold` utility
2. **Composite text style tokens** generate size/weight/spacing utilities (but NOT font-family):
- `--text-header-md` → `text-header-md` utility (sets size, weight, spacing, line-height)
- `--text-body-lg` → `text-body-lg` utility (sets size, weight, spacing, line-height)
**Usage pattern:** Combine font-family with text style for complete typography:
```html
Heading Text
Body text with larger size
Small label
Heading Text
```
The separation of font-family from text styles provides flexibility to mix font families with different text styles while maintaining consistent sizing, weight, spacing, and line-height.
## Customization
### Using CSS Variables
Customize typography by overriding CSS variables in your app's CSS file:
```css
@import "@frontile/theme";
/* Override font families */
@theme {
--font-header: "Inter", system-ui, sans-serif;
--font-body: "Inter", system-ui, sans-serif;
--font-code: "JetBrains Mono", monospace;
}
```
### Theme-Specific Typography
Customize for light and dark themes using theme selectors:
```css
/* Light theme customization */
.light,
.dark .theme-inverse {
--font-body: "Inter", system-ui, sans-serif;
}
/* Dark theme customization */
.dark,
.light .theme-inverse {
--font-body: "Geist", system-ui, sans-serif;
}
```
## Using Custom Fonts
Add custom fonts by declaring them with `@font-face` and overriding the font family variables:
```css
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
}
@theme {
--font-header: 'Inter', system-ui, sans-serif;
--font-body: 'Inter', system-ui, sans-serif;
--font-label: 'Inter', system-ui, sans-serif;
}
```
## Best Practices
### Use Semantic Text Styles
Always prefer semantic text style utilities over combining individual properties:
```gts preview
{{! Good - semantic text style }}
✓ Semantic: Easy to maintain
{{! Avoid - manual combination }}
✗ Manual: Harder to maintain
```
### Maintain Visual Hierarchy
Use size variants to create clear content hierarchy:
```gts preview
Article Title
Published on December 7, 2024
Major Section
This is the main body content with comfortable line height for easy reading. The text flows naturally and maintains good readability.
Subsection
Subsection content continues the hierarchy with appropriately sized headers.
Figure 1: Caption providing additional context
```
### Responsive Typography
Scale typography for different screen sizes:
```gts preview
Responsive Heading
Body text that scales up on larger screens for better readability across all devices.
```
---
# Icon Sizes
Source: /docs/theming/design-tokens/icons.md
# Icon Sizes
Frontile provides an icon sizing scale that pairs with typography for consistent visual alignment.
## Overview
Icon sizes ensure visual consistency between icons and text throughout your interface. They use the same modular scale as typography, making it easy to pair icons with text styles.
The scale ranges from `pico` (smallest) to `mega` (largest), providing 14 distinct sizes for different UI contexts.
## Using Icon Sizes
Apply icon sizes using the `size-icon-*` utilities, which set both width and height:
```gts preview
import { StarIcon } from 'site/components/icons';
```
## Available Sizes
| Size | Utility | Usage Context |
|------|---------|---------------|
| Pico | `size-icon-pico` | Extremely small UI elements |
| Nano | `size-icon-nano` | Tiny decorative icons |
| Micro | `size-icon-micro` | Very small inline icons |
| 3XS | `size-icon-3xs` | Extra extra small icons |
| 2XS | `size-icon-2xs` | Extra small icons |
| XS | `size-icon-xs` | Small inline icons |
| SM | `size-icon-sm` | Small UI icons |
| MD | `size-icon-md` | Default icon size |
| LG | `size-icon-lg` | Large prominent icons |
| XL | `size-icon-xl` | Extra large icons |
| 2XL | `size-icon-2xl` | Double extra large icons |
| 3XL | `size-icon-3xl` | Triple extra large icons |
| Kilo | `size-icon-kilo` | Very large decorative icons |
| Mega | `size-icon-mega` | Largest decorative icons |
## All Icon Sizes
Here are all available icon sizes with visual examples:
```gts preview collapsible
import { CheckIcon } from 'site/components/icons';
Pico
size-icon-pico
Nano
size-icon-nano
Micro
size-icon-micro
3XS
size-icon-3xs
2XS
size-icon-2xs
XS
size-icon-xs
SM
size-icon-sm
MD
size-icon-md
LG
size-icon-lg
XL
size-icon-xl
2XL
size-icon-2xl
3XL
size-icon-3xl
Kilo
size-icon-kilo
Mega
size-icon-mega
```
## Pairing with Typography
Icon sizes are designed to pair harmoniously with text styles. Use these recommended pairings for visual alignment:
```gts preview
import { CheckIcon, ViewIcon, StarIcon } from 'site/components/icons';
{{! Small icon pairing }}
Small text with size-icon-xs
{{! Medium icon pairing }}
Default text with size-icon-md
{{! Large icon pairing }}
Larger text with size-icon-lg
{{! Extra large icon pairing }}
Header with size-icon-xl
```
### Recommended Pairings
| Text Style | Recommended Icon Size |
|------------|----------------------|
| `text-body-sm` | `size-icon-xs` or `size-icon-sm` |
| `text-body-md` | `size-icon-sm` or `size-icon-md` |
| `text-body-lg` | `size-icon-md` or `size-icon-lg` |
| `text-header-sm` | `size-icon-md` |
| `text-header-md` | `size-icon-md` or `size-icon-lg` |
| `text-header-lg` | `size-icon-lg` or `size-icon-xl` |
| `text-header-xl` | `size-icon-xl` or `size-icon-2xl` |
## Customization
Override icon sizes using CSS variables in your `@theme` block:
```css
@import "@frontile/theme";
@theme {
/* Adjust specific sizes */
--size-icon-sm: 14px;
--size-icon-md: 18px;
--size-icon-lg: 22px;
}
```
For more customization options, see the [Configuration Guide](../configuration/customization.md).
---
# Borders & Radius
Source: /docs/theming/design-tokens/borders.md
# Borders & Radius
Frontile provides border width and radius tokens for consistent UI styling across your application.
## Border Widths
Control border thickness with semantic width tokens. These tokens provide meaningful names that describe the visual weight of borders.
### Available Widths
```gts preview
{{! Standard input }}
{{! Selected item with heavy border }}
Selected Item
Uses heavy border to show selection
```
## Border Radius
Control corner rounding with radius tokens that range from sharp edges to fully rounded elements.
Every step is a multiple of a single `--radius` base (`0.5rem` by default), so
the scale is proportional rather than arbitrary. Changing `--radius` alone
re-rounds the entire library — see [Scaling the whole system](#scaling-the-whole-system) below.
### Available Radii
```gts preview collapsible
None
XS
SM
MD
Default
LG
XL
2XL
3XL
4XL
Default (Custom)
Pill
```
| Radius | Utility | Multiplier | Default | Common Uses |
|--------|---------|-----------|---------|-------------|
| None | `rounded-none` | — | 0px | Sharp corners, technical interfaces, no rounding |
| XS | `rounded-xs` | 0.25x | 2px | Hairline rounding on very small marks |
| SM | `rounded-sm` | 0.5x | 4px | Keyboard shortcuts, checkboxes, tight layouts |
| MD | `rounded-md` | 0.75x | 6px | Compact controls, inline chips |
| Default | `rounded` | 1x | 8px | The base unit itself |
| LG | `rounded-lg` | 1x | 8px | Menu items nested inside a popover |
| XL | `rounded-xl` | 1.5x | 12px | Inputs, selects, popovers, dropdowns, notifications |
| 2XL | `rounded-2xl` | 2x | 16px | Modals, drawers |
| Default (Custom) | `rounded-default` | 2.5x | 20px | Tables, skeletons, soft friendly corners |
| 3XL | `rounded-3xl` | 3x | 24px | Oversized surfaces |
| 4XL | `rounded-4xl` | 4x | 32px | Hero-scale surfaces |
| Pill | `rounded-pill` | — | 9999px | Pills, badges, fully rounded buttons |
`rounded-none` and `rounded-pill` are absolutes: they stay 0 and 9999px no
matter what `--radius` is set to.
## Combining Borders and Radius
Use border widths and radius together to create various UI patterns:
```gts preview
{{! Alert with thin border and soft corners }}
Success
Your changes have been saved successfully.
{{! Warning with heavy border and default corners }}
Warning
This action cannot be undone.
{{! Error with aggressive border and sharp corners }}
Error
Failed to save changes. Please try again.
```
## Best Practices
### Maintain Consistency
Use consistent border styles for similar UI elements throughout your application:
- **Forms**: Use default border width with default or small radius
- **Cards**: Use thin borders with medium to large radius
- **Alerts**: Use thin to heavy borders with appropriate radius for the context
- **Buttons**: Use default radius or pill for interactive elements
### Visual Hierarchy
Use border weights to establish hierarchy:
- **Thin**: Background elements, subtle divisions
- **Default**: Primary UI elements
- **Heavy**: Active states, selections, focus indicators
- **Aggressive**: Critical states, errors, important boundaries
### Radius and Component Size
Pair border radius with component size:
- **Small components** (badges, small buttons): Use `rounded` or `rounded-sm`
- **Medium components** (buttons, inputs): Use `rounded` or `rounded-md`
- **Large components** (cards, panels): Use `rounded-lg` or `rounded-xl`
- **Extra large components** (heroes, features): Use `rounded-xl` or `rounded-2xl`
## Customization
Override border widths and radius using CSS variables:
```css
@import "@frontile/theme";
@theme {
/* Customize border widths */
--border-width-thin: 1px;
--border-width-default: 1.5px;
--border-width-heavy: 3px;
/* Customize border radius — one knob for the whole scale */
--radius: 0.75rem;
}
```
### Scaling the whole system
Because every step is `calc(var(--radius) * n)`, one value controls how round
the entire library looks. Nothing else has to change:
| `--radius` | Menus / inputs | Menu items | Modals |
|-----------|----------------|-----------|--------|
| `0` | 0px | 0px | 0px |
| `0.25rem` | 6px | 4px | 8px |
| `0.5rem` (default) | 12px | 8px | 16px |
| `0.75rem` | 18px | 12px | 24px |
| `1rem` | 24px | 16px | 32px |
The same knob is available from the JavaScript plugin config, where it can also
differ per theme:
```js
frontile({
layout: {
radius: { DEFAULT: '0.75rem' }
}
});
```
Individual steps remain overridable when you need to break one out of the
proportional scale:
```css
@theme {
--radius: 0.5rem;
--radius-2xl: 28px; /* modals rounder than the scale would give */
}
```
For more customization options, see the [Configuration Guide](../configuration/customization.md).
---
# Surfaces
Source: /docs/theming/design-tokens/surfaces.md
# Surface System
The Surface system creates depth and hierarchy in your interfaces using opaque roles and two families of translucent veils.
## Overview
The Surface system consists of three types:
- **Surface Roles** - Semantic, opaque surface tokens for specific UI contexts (app, canvas, card, table, modal, input)
- **Surface Overlay** (subtle, soft, mild, firm, strong) - Translucent layers that push an element _into_ its background
- **Surface Lift** (subtle, soft, mild, firm, strong) - Translucent layers that pull an element _up off_ its background
## Surface Roles
Surface roles provide semantic meaning to your surfaces based on their context in the UI hierarchy. Instead of choosing arbitrary scale values, use these semantic tokens that automatically adapt to the active theme.
### Available Roles
#### App (`bg-surface-app`)
Root application background layer (hierarchy level 0).
**When to use:**
- App shell root
- Full-viewport base layer
- Root layout behind navigation
```gts preview
Content area with canvas background
```
#### Canvas (`bg-surface-canvas`)
Component contrast baseline (hierarchy level 1).
**When to use:**
- Main content areas
- Page containers that need contrast from app background
- Component baseline surfaces
```gts preview
Content Area
Canvas provides contrast against the app
background
```
#### Card (`bg-surface-card`)
Elevated card container surface (hierarchy level 1). Opaque white in light mode;
in dark mode it is a translucent veil (white @ 7%, the same value as
`overlay-subtle`), so whatever sits behind it shows through faintly. Where a
surface has to stay opaque, use `surface-table` — the same colour without the
transparency.
**When to use:**
- Product cards
- Article previews
- Content blocks
```gts preview
Product Title
This card is elevated off the canvas
background
```
#### Table (`bg-surface-table`)
Data table surface (hierarchy level 1). Usually the same color as a card, but must be opaque.
Tables need that guarantee. A sticky header, a frozen row, or a pinned column
paints over the rows and columns scrolling beneath it, and anything translucent
lets them show through.
**When to use:**
- Data table containers, and the sticky parts inside them
```gts preview
Sticky header — rows pass under it
Row one
Row two
Row three
Row four
Row five
```
#### Modal (`bg-surface-modal`)
Modal, drawer, and popover container surface, highest elevation (hierarchy level 3).
**When to use:**
- Modal dialogs
- Drawers
- Popovers and dropdown menus
- Confirmation prompts
Pair it with `bg-surface-overlay-strong dark:bg-surface-lift-strong` for the backdrop behind modals and drawers.
```gts preview
Confirm Action
Are you sure you want to continue?
```
#### Input (`bg-surface-input`)
Form control surface for inputs, checkboxes, radios, and similar controls (hierarchy level -1).
**When to use:**
- Text inputs
- Checkboxes and radios
- Any native form control background
```gts preview
```
### Hierarchy Levels
Surface roles are designed to create visual depth through a hierarchy system:
| Level | Role | Purpose |
| ----- | ------------------- | -------------------------------------------------------- |
| -1 | Input | Recessed below canvas |
| 0 | App | Root application background |
| 1 | Canvas, Card, Table | Component contrast baseline and first level of elevation |
| 3 | Modal | Highest elevation (modals, drawers, popovers, dropdowns) |
In **light mode**, elevated surfaces appear brighter (white) against gray backgrounds following the elevation-luminance principle.
In **dark mode**, elevated surfaces appear progressively lighter against near-black backgrounds.
## Surface Overlay (subtle, soft, mild, firm, strong)
Translucent overlays with alpha channel that stack on top of solid surfaces. These add depth through transparency rather than changing the base color.
### When to Use
- **Sections within components**: Modal footers, card headers, dropdown sections
- **Cards/panels on pages**: Elevated surfaces that float on top of the page background
- **Hover states**: Subtle translucent effect on hover
- **Progress bars, selection highlights**: Semi-transparent UI elements
- **`strong`**: The heaviest step (75% in light, 95% in dark), far beyond a hover or elevation hint. In light mode this is the backdrop behind modals and drawers; in dark mode the black scrim is `lift-strong` instead, since the families mirror each other — see [Surface Lift](#surface-lift-subtle-soft-mild-firm-strong)
### Example: Modal with Overlay Footer
```hbs
{{! Modal base uses the modal surface role }}
<:default>
Modal content on opaque base
<:footer>
Footer with subtle overlay effect
```
### Example: Card on Page
```hbs
{{! Page has a solid app background }}
{{! Card floats on top with translucent overlay }}
Card Title
Card content with depth
```
### Stacking Overlays
Overlays can stack on top of each other to create progressive depth:
```hbs
Layer 1 (subtle)
Layer 2 (soft)
Layer 3 (mild)
Layer 4 (firm)
```
### Example: Modal/Drawer Backdrop
```hbs
Modal content
```
## Surface Lift (subtle, soft, mild, firm, strong)
Lift is the mirror of overlay. Overlay darkens in light mode and lightens in dark mode, pressing an element _into_ the page. Lift does the opposite — a white veil on a light page, a black veil on a dark one — so the element reads as floating _above_ whatever it covers.
Both families share the same level names, and both adapt to the active theme automatically:
| Level | Light mode | Dark mode |
| -------- | ----------- | ----------- |
| `subtle` | white @ 30% | black @ 10% |
| `soft` | white @ 50% | black @ 20% |
| `mild` | white @ 70% | black @ 30% |
| `firm` | white @ 90% | black @ 40% |
| `strong` | white @ 95% | black @ 75% |
Every lift level flips with the scheme, `strong` included.
The two families are exact mirrors, which has one consequence worth knowing: the
heavy black veil used as a modal backdrop is `overlay-strong` in light mode but
`lift-strong` in dark mode. A component that wants a dark scrim in both schemes
pairs them — `bg-surface-overlay-strong dark:bg-surface-lift-strong` — which is
what Overlay's backdrop does.
### When to Use
- **Frosted/glass panels**: Pair with `backdrop-blur-*` for a translucent panel that stays legible over busy content
- **Sticky headers and toolbars**: Content scrolls under them without washing them out
- **Floating controls over imagery**: Media captions, hero overlays, map controls
- **Anything that must read as _above_ the page** rather than recessed into it
Reach for `surface-overlay-*` instead when the element belongs to the surface it sits on — hover states, table stripes, section fills.
### Example: Frosted Sticky Header
```hbs
```
### Example: Caption Over an Image
```hbs
Floats above the image in either theme
```
### Example: Overlay vs Lift
The same nesting, one family each — overlay recedes, lift advances:
```hbs
{{! Recedes into the page }}
Overlay
{{! Floats above the page }}
Lift
```
## Choosing the Right Surface
Follow this decision flow:
1. **Does this match a specific UI context?**
- If yes → Use `surface-{role}` (app, canvas, card, table, modal, input)
- Surface roles provide semantic meaning and automatically adapt to themes.
2. **Is this the base container and no role matches?**
- Use `surface-app` for most cases.
3. **Does it need to be translucent?**
- If yes → Continue to next step
- If no → Use a surface role
4. **Should it read as recessed into the surface, or floating above it?**
- Recessed (hover states, section fills, table stripes) → Use `surface-overlay-*`
- Floating (frosted panels, sticky headers, captions over media) → Use `surface-lift-*`
- If it's a modal/drawer backdrop → Use `surface-overlay-strong dark:surface-lift-strong`
## Common Patterns
### Pattern 1: Page Layout with Cards
```gts preview
Card 1
Content on elevated card surface
Card 2
Another elevated card
```
### Pattern 2: Sidebar Layout
```gts preview
Main Content
Content area on canvas
```
### Pattern 3: Form with Inputs
```gts preview
```
## Migrating from content1-4
If you're upgrading from an older version that used `content1-4`, here's the migration mapping:
| Old | New | Reason |
| ------------------------------ | ----------------------------------- | --------------------------------- |
| `bg-content1` | `bg-surface-app` | Base opaque surface |
| `bg-content3 dark:bg-content2` | `bg-surface-overlay-soft` | Single value works in both themes |
| `selection:bg-content3` | `selection:bg-surface-overlay-soft` | Translucent selection highlight |
## Best Practices
### Do
- **Prefer surface roles** (`surface-canvas`, `surface-card`, etc.) over ad hoc colors when they match your UI context
- Stack overlays on a surface role for depth
- Use `overlay-strong` with `dark:lift-strong` for modal/drawer backdrops
- Test in both light and dark modes
- Follow the hierarchy levels for visual consistency
### Don't
- Don't use overlay without a solid role beneath it
- Don't mix surface system with old content1-4
- Don't use too many overlay layers (max 3-4)
- Don't forget to check contrast ratios
---
# Elevation
Source: /docs/theming/design-tokens/elevation.md
# Elevation
Frontile provides an elevation system using shadows to create visual depth and hierarchy in your interface.
## Overview
Elevation helps establish spatial relationships and visual hierarchy by making elements appear "closer" or "further" from the user. Frontile provides six elevation levels (0-5), each designed for specific UI contexts.
Higher elevations indicate elements that are more prominent or interactive, such as floating menus, modals, and tooltips.
## Elevation Levels
```gts preview collapsible
Level 0
No shadow
Flat surface
Level 1
Subtle lift
Hover states
Level 2
Raised surface
Cards, panels
Level 3
Floating element
Dropdowns
Level 4
Elevated overlay
Modals, dialogs
Level 5
Maximum depth
Top layer
```
## When to Use Each Level
| Level | Utility | Use Cases | Examples |
| ----- | -------------------- | --------------------------------- | --------------------------------------------------------- |
| 0 | `shadow-elevation-0` | Flat elements, no depth needed | Inline content, flat buttons, embedded content |
| 1 | `shadow-elevation-1` | Subtle separation from background | Hover states, subtle cards, list items |
| 2 | `shadow-elevation-2` | Standard raised surfaces | Cards, panels, tiles, navigation bars |
| 3 | `shadow-elevation-3` | Floating elements above content | Dropdowns, tooltips, popovers, date pickers |
| 4 | `shadow-elevation-4` | Modal overlays | Modals, dialogs, drawers, sheets |
| 5 | `shadow-elevation-5` | Maximum emphasis | Critical alerts, system notifications, top-layer elements |
## Common Use Cases
### Cards and Panels
```gts preview collapsible
{{! Basic card with level 2 elevation }}
Product Card
Standard card with level 2 elevation provides subtle depth.
{{! Feature card with no shadow }}
Flat Card
Uses border instead of shadow for a flatter appearance.
```
### Interactive States
```gts preview
{{! Button that lifts on hover }}
{{! Card that lifts on hover }}
Interactive Card
Elevation increases on hover
```
## Best Practices
### Establish Clear Hierarchy
Use elevation consistently to indicate the z-axis position of elements:
- **Base layer** (level 0-1): Page content, static elements
- **Raised layer** (level 2): Cards, panels, navigation
- **Floating layer** (level 3): Dropdowns, popovers, tooltips
- **Overlay layer** (level 4-5): Modals, critical alerts
### Avoid Elevation Confusion
Don't use high elevation for non-interactive or less important elements. Reserve levels 4-5 for elements that need to command attention.
### Transition Elevations
Animate elevation changes for smooth interactions:
```html
Smooth elevation transition
```
### Consider Dark Mode
Shadows may be less visible in dark mode. Consider combining elevation with subtle borders:
```html
Enhanced depth with border
```
## Accessibility
### Don't Rely on Shadows Alone
Never use shadows as the only indicator of important information. Always provide additional visual cues:
- Use borders for critical boundaries
- Use semantic colors for status
- Provide clear text labels
- Ensure proper focus indicators
### Test in Different Lighting
Shadows may be difficult to perceive:
- On certain displays (high brightness, poor contrast)
- For users with visual impairments
- In different lighting conditions
Always provide alternative visual cues beyond elevation.
## Customization
Override elevation shadows using CSS variables:
```css
@import '@frontile/theme';
@theme {
/* Customize specific elevation levels */
--shadow-elevation-1: 0px 2px 4px rgba(0, 0, 0, 0.05);
--shadow-elevation-2: 0px 4px 12px rgba(0, 0, 0, 0.1);
--shadow-elevation-3: 0px 8px 24px rgba(0, 0, 0, 0.15);
--shadow-elevation-4: 0px 16px 48px rgba(0, 0, 0, 0.2);
}
```
### Theme-Specific Shadows
Adjust shadows for light and dark themes:
```css
/* Light theme */
.light,
.dark .theme-inverse {
--shadow-elevation-2: 0px 4px 12px rgba(0, 0, 0, 0.1);
}
/* Dark theme */
.dark,
.light .theme-inverse {
--shadow-elevation-2: 0px 4px 12px rgba(0, 0, 0, 0.4);
}
```
For more customization options, see the [Configuration Guide](../configuration/customization.md).
---
# Configuration Overview
Source: /docs/theming/configuration/overview.md
# Configuration
Learn how to configure and customize Frontile's theme system to match your application's design requirements.
## Overview
Frontile provides two primary methods for configuring your theme:
1. **CSS Variables** - Recommended for most customizations
2. **JavaScript Configuration** - For complex theme setups and color customization
## CSS Variables (Recommended)
The most straightforward way to customize Frontile is using CSS variables in your stylesheet. This approach uses Tailwind v4's `@theme` directive for simple, performant customization.
### Quick Start
Add customizations after importing the theme in your `app/styles/app.css`:
```css title="app/styles/app.css"
@import 'tailwindcss' source('../../');
@plugin "@frontile/theme/plugin/default";
@import "@frontile/theme";
@theme {
/* Customize design tokens */
--radius: 12px;
--border-width-default: 2px;
--size-icon-md: 20px;
--opacity-hover: .85;
}
```
### What You Can Customize
Use CSS variables to customize:
- **Border radius** - `--radius` (one knob; the whole `rounded-*` scale is derived from it), plus `--radius-pill` and individual steps as escape hatches
- **Border widths** - `--border-width-thin`, `--border-width-heavy`
- **Icon sizes** - `--size-icon-sm`, `--size-icon-md`, `--size-icon-lg`
- **Shadows** - `--shadow-elevation-1`, `--shadow-elevation-2`
- **Opacity** - `--opacity-hover`, `--opacity-disabled`
- **Typography** - Font families, sizes, and text styles
- **Component sizes** - Modal and drawer sizes
### Benefits of CSS Variables
- **Simple** - No JavaScript configuration needed
- **Fast** - Changes are applied at CSS level
- **Flexible** - Can be changed at runtime
- **Theme-specific** - Easy to customize per theme (light/dark)
### When to Use CSS Variables
Choose CSS variables when you need to:
- Override specific design token values
- Adjust border radius, spacing, or sizing
- Customize per-theme values (different in light vs dark)
- Make runtime changes via JavaScript
## JavaScript Configuration
For more complex customization, especially colors, use JavaScript configuration. This method provides programmatic control and type safety.
### Quick Start
Create `frontile.js` in your project root:
```js title="frontile.js"
const { frontile } = require('@frontile/theme/plugin');
module.exports = frontile({
defaultTheme: 'light',
themes: {
light: {
colors: {
primary: {
subtle: '#eff6ff',
soft: '#93c5fd',
DEFAULT: '#3b82f6',
strong: '#1e40af'
}
}
},
dark: {
colors: {
primary: {
subtle: '#1e3a8a',
soft: '#3b82f6',
DEFAULT: '#60a5fa',
strong: '#dbeafe'
}
}
}
}
});
```
Then reference it in your CSS:
```css
@import 'tailwindcss' source('../../');
@plugin "./../../frontile.js";
@import "@frontile/theme";
```
### What You Can Customize
Use JavaScript configuration for:
- **Semantic colors** - Primary, success, danger, warning, neutral
- **Default theme** - Which theme loads by default
- **Theme variants** - Create multiple theme variations
- **Complex configurations** - Programmatic theme generation
### When to Use JavaScript Configuration
Choose JavaScript configuration when you need to:
- Customize semantic colors (primary, success, danger, etc.)
- Set the default theme (light or dark)
- Create multiple theme variants
- Generate themes programmatically
- Share configuration across projects
## Comparison
| Feature | CSS Variables | JavaScript Config |
|---------|---------------|-------------------|
| **Border radius** | ✅ Recommended | ❌ Not needed |
| **Semantic colors** | ❌ Use JS instead | ✅ Recommended |
| **Icon sizes** | ✅ Recommended | ❌ Not needed |
| **Elevation shadows** | ✅ Recommended | ❌ Not needed |
| **Typography** | ✅ Recommended | ❌ Not needed |
| **Default theme** | ❌ Not possible | ✅ Use JS |
| **Runtime changes** | ✅ Easy | ❌ Requires rebuild |
| **Per-theme values** | ✅ Simple | ⚠️ More complex |
## Common Patterns
### Pattern 1: Simple Adjustments
For minor adjustments to existing tokens, use CSS variables:
```css
@theme {
--radius: 6px; /* scales every rounded-* step down proportionally */
--opacity-hover: .9;
}
```
### Pattern 2: Custom Primary Colors
For primary color customization, use JavaScript:
```js
module.exports = frontile({
themes: {
light: {
colors: {
primary: {
subtle: '#f0f9ff',
soft: '#7dd3fc',
DEFAULT: '#0ea5e9',
strong: '#0c4a6e'
}
}
}
}
});
```
### Pattern 3: Mixed Approach
Combine both methods for comprehensive customization:
```js
// frontile.js - Customize colors
module.exports = frontile({
themes: {
light: {
colors: { /* custom colors */ }
}
}
});
```
```css
/* app.css - Customize design tokens */
@theme {
--radius: 8px;
--size-icon-md: 18px;
}
```
### Pattern 4: Theme-Specific Overrides
Use CSS for theme-specific token values:
```css
/* Light theme */
.light,
.dark .theme-inverse {
--opacity-hover: .85;
--shadow-elevation-2: 0px 4px 12px rgba(0, 0, 0, 0.1);
}
/* Dark theme */
.dark,
.light .theme-inverse {
--opacity-hover: .75;
--shadow-elevation-2: 0px 4px 12px rgba(0, 0, 0, 0.3);
}
```
## Next Steps
- [CSS Variables Reference](css-variables.md) - Complete list of available variables
- [Theme Switching](theme-switching.md) - Implement light/dark mode
- [Customization Guide](customization.md) - Detailed customization examples
---
# CSS Variables
Source: /docs/theming/configuration/css-variables.md
# CSS Variables Reference
Complete reference of CSS variables available in Frontile's theme system for customization.
## How CSS Variables Work
Frontile uses CSS variables in two ways:
### 1. Theme Variables (`@theme` block)
Variables defined in the `@theme` block automatically generate Tailwind utility classes:
```css
@theme {
--radius: 8px; /* Generates: rounded utility */
--radius-xl: 12px; /* Generates: rounded-xl utility */
}
```
These are the primary customization points for design tokens.
### 2. Component Variables (`:root` or theme selectors)
Variables defined in `:root` or theme-specific selectors are for component-specific values:
```css
:root {
--modal-lg: 32rem; /* Used by Modal component */
--drawer-md: 28rem; /* Used by Drawer component */
}
```
## Typography Variables
### Font Families
```css
@theme {
--font-header: system-ui, sans-serif;
--font-body: system-ui, sans-serif;
--font-code: 'Courier New', monospace;
--font-label: system-ui, sans-serif;
--font-caption: system-ui, sans-serif;
--font-marquee: system-ui, sans-serif;
}
```
### Text Styles
Frontile provides text style variables. Each text style category has multiple size variants:
- **Marquee**: `--text-marquee-5xs` through `--text-marquee-3xl`
- **Header**: `--text-header-4xs` through `--text-header-3xl`
- **Strong**: `--text-strong-4xs` through `--text-strong-3xl`
- **Body**: `--text-body-5xs` through `--text-body-3xl`
- **Code**: `--text-code-sm`, `--text-code-md`
- **Caption**: `--text-caption-sm`, `--text-caption-md`
- **Label**: `--text-label-nano` through `--text-label-3xl`
Each text style includes font-size, font-family, font-weight, letter-spacing, and line-height properties.
See [Typography documentation](../design-tokens/typography.md) for detailed information.
## Layout Variables
### Border Width
```css
@theme {
--border-width-thin: 0.5px;
--border-width-default: 1px;
--border-width-heavy: 2px;
--border-width-aggressive: 4px;
}
```
**Generated utilities:** `border-thin`, `border`, `border-heavy`, `border-aggressive`
### Border Radius
Every step is derived from a single `--radius` base, so you can dial the
roundness of the whole library with one value:
```css
@theme {
--radius: 0.5rem; /* 8px — the one knob */
--radius-none: 0px;
--radius-xs: calc(var(--radius) * 0.25); /* 2px */
--radius-sm: calc(var(--radius) * 0.5); /* 4px */
--radius-md: calc(var(--radius) * 0.75); /* 6px */
--radius-lg: calc(var(--radius) * 1); /* 8px */
--radius-xl: calc(var(--radius) * 1.5); /* 12px */
--radius-2xl: calc(var(--radius) * 2); /* 16px */
--radius-3xl: calc(var(--radius) * 3); /* 24px */
--radius-4xl: calc(var(--radius) * 4); /* 32px */
--radius-default: calc(var(--radius) * 2.5); /* 20px */
--radius-pill: 9999px;
}
```
**Generated utilities:** `rounded-none`, `rounded-xs`, `rounded-sm`, `rounded-md`, `rounded`, `rounded-lg`, `rounded-xl`, `rounded-2xl`, `rounded-3xl`, `rounded-4xl`, `rounded-default`, `rounded-pill`
To make every component softer or sharper, override `--radius` alone — the rest
of the scale follows:
```css
@theme {
--radius: 0.75rem; /* menus 18px, list items 12px, modals 24px */
}
```
```css
@theme {
--radius: 0; /* square everything off */
}
```
`--radius-none` and `--radius-pill` are absolutes and deliberately do not scale,
so `rounded-full` buttons stay pills at any base value.
### Icon Sizes
```css
@theme {
--size-icon-pico: /* ... */;
--size-icon-nano: /* ... */;
--size-icon-micro: /* ... */;
--size-icon-3xs: /* ... */;
--size-icon-2xs: /* ... */;
--size-icon-xs: /* ... */;
--size-icon-sm: /* ... */;
--size-icon-md: /* ... */;
--size-icon-lg: /* ... */;
--size-icon-xl: /* ... */;
--size-icon-2xl: /* ... */;
--size-icon-3xl: /* ... */;
--size-icon-kilo: /* ... */;
--size-icon-mega: /* ... */;
}
```
**Generated utilities:** `size-icon-pico` through `size-icon-mega`
See [Icon Sizes documentation](../design-tokens/icons.md) for usage guidelines.
### Elevation Shadows
```css
@theme {
--shadow-elevation-0: /* ... */;
--shadow-elevation-1: /* ... */;
--shadow-elevation-2: /* ... */;
--shadow-elevation-3: /* ... */;
--shadow-elevation-4: /* ... */;
--shadow-elevation-5: /* ... */;
}
```
**Generated utilities:** `shadow-elevation-0` through `shadow-elevation-5`
See [Elevation documentation](../design-tokens/elevation.md) for usage guidelines.
### Opacity
```css
@theme {
--opacity-hover: .8;
--opacity-disabled: .5;
}
```
**Generated utilities:** `opacity-hover`, `opacity-disabled`
## Component Variables
These variables are used by specific Frontile components and should be defined in `:root` or theme-specific selectors (not in `@theme` block):
### Modal Sizes
```css
:root {
--modal-xs: 22rem;
--modal-sm: 30rem;
--modal-md: 35rem;
--modal-lg: 48rem;
--modal-xl: 60rem;
--modal-full: 100%;
}
```
### Drawer Sizes
```css
:root {
--drawer-xs: 22rem;
--drawer-sm: 30rem;
--drawer-md: 48rem;
--drawer-lg: 64rem;
--drawer-xl: 80rem;
--drawer-full: 100%;
}
```
## Color Variables
**Important:** Color variables should be customized using JavaScript configuration, not CSS variables directly. See [Color Customization](customization.md#colors) for details.
Frontile's semantic colors use a system that automatically calculates:
- Light and dark theme variations
- Contrasting text colors (`on-{color}` classes)
- Colors in OKLCH format for perceptual uniformity
## Overriding Variables
### Global Overrides
Override in the `@theme` block for design tokens:
```css
@import "@frontile/theme";
@theme {
--radius: 12px;
--border-width-default: 2px;
--opacity-hover: .9;
--size-icon-md: 20px;
}
```
### Theme-Specific Overrides
Use theme selectors for per-theme customization:
```css
/* Light theme - targets .light AND .theme-inverse within .dark */
.light,
.dark .theme-inverse {
--opacity-hover: .85;
--shadow-elevation-2: 0px 4px 12px rgba(0, 0, 0, 0.1);
}
/* Dark theme - targets .dark AND .theme-inverse within .light */
.dark,
.light .theme-inverse {
--opacity-hover: .75;
--shadow-elevation-2: 0px 4px 12px rgba(0, 0, 0, 0.3);
}
```
**Why both selectors?** This ensures customizations apply to:
- Regular theme usage (`.light` or `.dark`)
- Theme-inverse sections (`.theme-inverse` within opposite theme)
### Component-Specific Overrides
Override component variables in `:root`:
```css
:root {
--modal-lg: 48rem;
--drawer-md: 32rem;
}
```
Or per-theme:
```css
.dark {
--modal-lg: 56rem;
}
```
## Usage Examples
### Example 1: Customize Border Radius
```css
@theme {
/* Softer corners throughout the app */
--radius: 12px;
--radius-xl: 20px;
--radius-pill: 9999px;
}
```
### Example 2: Adjust Icon Sizes
```css
@theme {
/* Slightly larger icons */
--size-icon-sm: 15px;
--size-icon-md: 19px;
--size-icon-lg: 23px;
}
```
### Example 3: Theme-Specific Shadows
```css
/* Subtler shadows in light mode */
.light,
.dark .theme-inverse {
--shadow-elevation-1: 0px 1px 3px rgba(0, 0, 0, 0.05);
--shadow-elevation-2: 0px 2px 8px rgba(0, 0, 0, 0.08);
}
/* Stronger shadows in dark mode */
.dark,
.light .theme-inverse {
--shadow-elevation-1: 0px 2px 4px rgba(0, 0, 0, 0.3);
--shadow-elevation-2: 0px 4px 12px rgba(0, 0, 0, 0.4);
}
```
### Example 4: Custom Font Families
```css
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-weight: 100 900;
}
@theme {
--font-header: 'Inter', system-ui, sans-serif;
--font-body: 'Inter', system-ui, sans-serif;
--font-label: 'Inter', system-ui, sans-serif;
}
```
### Example 5: Adjust Component Sizes
```css
:root {
/* Larger modals for desktop */
--modal-md: 36rem;
--modal-lg: 48rem;
--modal-xl: 64rem;
}
/* Smaller on mobile */
@media (max-width: 768px) {
:root {
--modal-md: 90vw;
--modal-lg: 90vw;
}
}
```
## Best Practices
### Use @theme for Design Tokens
Always use `@theme` block for variables that should generate utility classes:
```css
/* ✓ Good */
@theme {
--radius: 10px;
}
/* ✗ Avoid */
:root {
--radius: 10px; /* Won't generate rounded utility */
}
```
### Use :root for Component Values
Use `:root` for component-specific values that don't need utilities:
```css
/* ✓ Good */
:root {
--modal-lg: 48rem;
--drawer-md: 32rem;
}
```
### Keep Theme Selectors Consistent
Always use both selectors for theme-specific overrides:
```css
/* ✓ Good - covers all cases */
.light,
.dark .theme-inverse {
--opacity-hover: .85;
}
/* ✗ Incomplete - misses .theme-inverse */
.light {
--opacity-hover: .85;
}
```
### Document Your Customizations
Add comments to explain custom values:
```css
@theme {
/* Brand requires 16px minimum for icon clarity */
--size-icon-md: 20px;
/* Softer corners match brand guidelines */
--radius: 12px;
}
```
## Runtime Customization
CSS variables can be changed at runtime using JavaScript:
```js
// Change globally
document.documentElement.style.setProperty('--radius', '16px');
// Change for specific element
element.style.setProperty('--opacity-hover', '0.9');
// Read current value
const radius = getComputedStyle(document.documentElement)
.getPropertyValue('--radius');
```
This is useful for user preferences, theme builders, or dynamic customization features.
## Next Steps
- [Customization Guide](customization.md) - Detailed customization examples
- [Theme Switching](theme-switching.md) - Light/dark mode implementation
- [Design Tokens](../design-tokens/overview.md) - Understanding design tokens
---
# Theme Switching
Source: /docs/theming/configuration/theme-switching.md
# Theme Switching
Frontile supports light and dark themes, and theme inversion for creating visual contrast within your application.
## Basic Light/Dark Mode
Toggle between light and dark themes by adding or removing the `dark` class on a parent element (typically `` or ``):
```html
```
The `light` class is optional since light mode is the default. However, it's useful when you want to be explicit.
## Implementing Theme Switching
### Client-Side Toggle
Here's a basic implementation. It stays a static snippet rather than a live demo
because it writes to `localStorage` and swaps classes on ``, which would
fight this site's own theme switcher.
```gts
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { Button } from 'frontile';
export default class ThemeToggle extends Component {
@tracked isDark = false;
constructor(owner: unknown, args: Record) {
super(owner as never, args);
// Saved choice wins; fall back to the system preference.
const saved = localStorage.getItem('theme');
this.isDark = saved
? saved === 'dark'
: window.matchMedia('(prefers-color-scheme: dark)').matches;
this.applyTheme();
}
@action
toggleTheme(): void {
this.isDark = !this.isDark;
this.applyTheme();
localStorage.setItem('theme', this.isDark ? 'dark' : 'light');
}
applyTheme(): void {
const html = document.documentElement;
// Both classes are meaningful — `light` is what makes `theme-inverse`
// work inside a dark region — so set one and clear the other.
html.classList.toggle('dark', this.isDark);
html.classList.toggle('light', !this.isDark);
}
}
```
Frontile's own documentation site does exactly this — see
[`docfy-theme-switcher.gts`](https://github.com/josemarluedke/frontile/blob/main/site/app/components/docfy/docfy-theme-switcher.gts)
for a version that also reacts to the system preference changing while the page
is open, and guards against running during server-side rendering.
### Respecting System Preference
Detect and respond to the user's system theme preference:
```js
// Check system preference on load
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
// Listen for system preference changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
const isDark = e.matches;
// Update theme
document.documentElement.classList.toggle('dark', isDark);
document.documentElement.classList.toggle('light', !isDark);
});
```
### Persisting Theme Choice
Store the user's theme preference:
```js
// Save preference
localStorage.setItem('theme', 'dark');
// Load preference
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
document.documentElement.classList.add(savedTheme);
}
// Or use system preference if no saved preference
if (!savedTheme) {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.classList.add(prefersDark ? 'dark' : 'light');
}
```
## How Theme Classes Work
Frontile uses CSS selectors that apply theme-specific styles:
```css
/* Light theme styles apply to: */
.light *, /* Elements in light mode */
.dark .theme-inverse * /* Theme-inverse within dark mode */
/* Dark theme styles apply to: */
.dark *, /* Elements in dark mode */
.light .theme-inverse * /* Theme-inverse within light mode */
```
This means themes properly cascade and can be nested anywhere in your application.
## Theme Inverse
Create sections with inverted theme colors using the `theme-inverse` utility. In light mode, these sections display dark theme colors, and vice versa.
### Basic Usage
```gts preview
import { Button } from 'frontile';
{{! Regular theme section }}
Regular Theme
This section uses the current theme colors.
{{! Inverted theme section }}
Inverted Theme
This section uses the opposite theme colors automatically.
```
### Use Cases for Theme Inverse
Theme inverse is perfect for creating visual contrast and emphasis:
- **Hero sections** - Dark hero on light page, or vice versa
- **Feature highlights** - Draw attention to specific sections
- **Sidebars/panels** - Create visual separation
- **Marketing sections** - Alternate theme for variety
### Component Examples
All Frontile components work automatically with theme-inverse:
```gts preview
import { Button, Chip, ProgressBar } from 'frontile';
{{! Cards with status chips }}
Project Status
ActiveIn ReviewDraft
```
## Important: Modals and Overlays
### Default Behavior
By default, Modal and Drawer components render at the top level of the DOM using portals. This means they inherit the theme from `` or ``, **not** from `theme-inverse` sections:
```gts
import { Modal, Button, toggleState } from 'frontile';
const state = toggleState(false);
This section uses inverted theme, but the modal uses root theme.
{{! Modal renders at top level - uses root theme }}
Modal Header
This modal inherits from html/body, not the theme-inverse parent.
```
### Rendering in Place
To make modals inherit `theme-inverse`, render them in place:
```gts
import { Modal, Button, toggleState } from 'frontile';
const state = toggleState(false);
{{! Modal renders in place - inherits theme-inverse }}
Modal Header
This modal inherits the theme-inverse from its parent.
```
## Best Practices
### Always Set Base Backgrounds
Surface overlay colors are semi-transparent. Always provide a solid base background:
```html
Content
Content (might be invisible!)
```
### Use Semantic Colors
Theme-inverse works automatically with semantic colors:
```html
Heading
Body text
```
### Test Both Themes
Always test your UI in both light and dark modes:
```html
```
### Avoid Hard-Coded Colors
Don't use hard-coded Tailwind colors that don't adapt to themes:
```html
Content
Content
```
## Dark/Light Variants
Tailwind's `dark:` and `light:` variants work correctly with `theme-inverse`:
```html
Content
```
The variants understand theme-inverse contexts:
- `dark:` applies in dark mode AND in `.light .theme-inverse`
- `light:` applies in light mode AND in `.dark .theme-inverse`
## How Theme Inverse Works
Under the hood:
- Theme-inverse only works with base themes (light and dark)
- In light mode, `.light .theme-inverse` applies dark theme colors
- In dark mode, `.dark .theme-inverse` applies light theme colors
- All CSS variables are automatically swapped
- Components work without modification
## Advanced: Multiple Theme Levels
You can nest theme-inverse multiple times, though it's rarely needed:
```html
```
## Next Steps
- [CSS Variables Reference](css-variables.md) - Customize theme tokens
- [Customization Guide](customization.md) - Theme customization examples
- [Colors Documentation](../design-tokens/colors.md) - Understanding semantic colors
---
# Customization
Source: /docs/theming/configuration/customization.md
# Customization Guide
Complete guide to customizing Frontile's theme system, from simple token adjustments to comprehensive brand theming.
## Quick Start
The fastest way to customize Frontile is using CSS variables in your `app/styles/app.css`:
```css title="app/styles/app.css"
@import 'tailwindcss' source('../../');
@plugin "@frontile/theme/plugin/default";
@import '@frontile/theme';
@theme {
--radius: 12px;
--size-icon-md: 20px;
--opacity-hover: 0.9;
}
```
## Customizing Colors
### Using JavaScript Configuration
Colors should be customized using JavaScript configuration for best results. Create `frontile.js` in your project root:
```js title="frontile.js" collapsible
const { frontile } = require('@frontile/theme/plugin');
module.exports = frontile({
themes: {
light: {
colors: {
primary: {
subtle: '#f0f9ff',
soft: '#7dd3fc',
DEFAULT: '#0ea5e9',
strong: '#0c4a6e'
},
success: {
subtle: '#f0fdf4',
soft: '#86efac',
DEFAULT: '#22c55e',
strong: '#14532d'
},
danger: {
subtle: '#fef2f2',
soft: '#fca5a5',
DEFAULT: '#ef4444',
strong: '#7f1d1d'
},
warning: {
subtle: '#fffbeb',
soft: '#fde68a',
DEFAULT: '#f59e0b',
strong: '#78350f'
}
}
},
dark: {
colors: {
primary: {
subtle: '#0c4a6e',
soft: '#0ea5e9',
DEFAULT: '#38bdf8',
strong: '#e0f2fe'
}
// ... other colors
}
}
}
});
```
Then reference it in your CSS:
```css
@import 'tailwindcss' source('../../');
@plugin "./../../frontile.js";
@import '@frontile/theme';
```
### How Color Customization Works
Frontile automatically:
- Generates contrasting text colors (`on-{color}-{level}`)
- Converts your colors to OKLCH format for perceptual uniformity
- Accepts any color format as input (hex, rgb, hsl, oklch, CSS variables)
- Creates theme-aware CSS variables
- Ensures proper contrast ratios
### Customizing On-Colors
By default, Frontile auto-generates optimal `on-{color}-{level}` text colors (black or white) based on WCAG contrast calculations. If you need specific on-colors for brand consistency, you can override them in your theme configuration:
```js
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 white
}
// on-primary-subtle and on-primary-soft will still be auto-generated
}
}
}
});
```
**Key behavior:**
- Partial overrides work — define only the levels you want to customize, and the rest will be auto-generated
- Works for: `on-neutral`, `on-primary`, `on-secondary`, `on-tertiary`, `on-success`, `on-warning`, `on-danger`, `on-surface-modal`
- CSS variable references (e.g., `var(--my-color)`) are passed through as-is — auto-generation is skipped since contrast can't be calculated
## Customizing Typography
### Font Families
Override font families for different text categories:
```css
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
}
@font-face {
font-family: 'JetBrains Mono';
src: url('/fonts/jetbrains-mono.woff2') format('woff2');
font-display: swap;
}
@theme {
--font-header: 'Inter', system-ui, sans-serif;
--font-body: 'Inter', system-ui, sans-serif;
--font-label: 'Inter', system-ui, sans-serif;
--font-code: 'JetBrains Mono', monospace;
}
```
### Theme-Specific Fonts
Use different fonts for light and dark themes:
```css
.light,
.dark .theme-inverse {
--font-body: 'Inter', system-ui, sans-serif;
}
.dark,
.light .theme-inverse {
--font-body: 'Geist', system-ui, sans-serif;
}
```
## Customizing Border Radius
### Global Radius
Adjust border radius throughout your application:
```css
@theme {
/* Softer, more rounded design */
--radius: 12px;
--radius-xl: 20px;
--radius-2xl: 28px;
--radius-pill: 9999px;
}
```
### Sharp Design
Create a more angular design:
```css
@theme {
/* Sharp, geometric design */
--radius: 2px;
--radius-sm: 1px;
--radius-md: 3px;
--radius-xl: 6px;
--radius-2xl: 8px;
}
```
## Customizing Icon Sizes
Adjust icon sizes to match your design:
```css
@theme {
/* Slightly larger icons */
--size-icon-xs: 13px;
--size-icon-sm: 15px;
--size-icon-md: 19px;
--size-icon-lg: 23px;
--size-icon-xl: 27px;
}
```
## Customizing Borders
### Border Widths
Adjust border thickness:
```css
@theme {
/* Thicker borders throughout */
--border-width-thin: 1px;
--border-width-default: 2px;
--border-width-heavy: 3px;
--border-width-aggressive: 5px;
}
```
## Customizing Elevation
### Shadow Styles
Adjust shadow intensity and style:
```css
@theme {
/* Subtler shadows */
--shadow-elevation-1: 0px 1px 2px rgba(0, 0, 0, 0.04);
--shadow-elevation-2: 0px 2px 8px rgba(0, 0, 0, 0.06);
--shadow-elevation-3: 0px 8px 16px rgba(0, 0, 0, 0.08);
--shadow-elevation-4: 0px 16px 32px rgba(0, 0, 0, 0.12);
}
```
### Theme-Specific Shadows
Different shadows for light and dark modes:
```css
/* Light theme - softer shadows */
.light,
.dark .theme-inverse {
--shadow-elevation-2: 0px 2px 8px rgba(0, 0, 0, 0.08);
--shadow-elevation-3: 0px 8px 20px rgba(0, 0, 0, 0.12);
}
/* Dark theme - stronger shadows for contrast */
.dark,
.light .theme-inverse {
--shadow-elevation-2: 0px 4px 12px rgba(0, 0, 0, 0.4);
--shadow-elevation-3: 0px 12px 28px rgba(0, 0, 0, 0.5);
}
```
## Customizing Opacity
Adjust hover and disabled opacity:
```css
@theme {
--opacity-hover: 0.9;
--opacity-disabled: 0.4;
}
```
## Customizing Component Sizes
### Modal Sizes
Adjust modal dimensions:
```css
:root {
--modal-xs: 18rem;
--modal-sm: 22rem;
--modal-md: 32rem;
--modal-lg: 48rem;
--modal-xl: 64rem;
}
/* Responsive sizing */
@media (max-width: 768px) {
:root {
--modal-md: 90vw;
--modal-lg: 90vw;
--modal-xl: 95vw;
}
}
```
### Drawer Sizes
```css
:root {
--drawer-sm: 20rem;
--drawer-md: 32rem;
--drawer-lg: 48rem;
--drawer-xl: 64rem;
}
```
## Setting Default Theme
Specify which theme loads by default:
```js
// frontile.js
module.exports = frontile({
defaultTheme: 'dark', // or 'light'
themes: {
light: {/* ... */},
dark: {/* ... */}
}
});
```
## Complete Brand Example
Here's a complete example of customizing Frontile for a brand:
### frontile.js
```js
const { frontile } = require('@frontile/theme/plugin');
module.exports = frontile({
defaultTheme: 'light',
themes: {
light: {
colors: {
// Purple brand color
primary: {
subtle: '#faf5ff',
soft: '#c084fc',
DEFAULT: '#9333ea',
strong: '#581c87'
},
// Success remains green
success: {
subtle: '#f0fdf4',
soft: '#86efac',
DEFAULT: '#22c55e',
strong: '#14532d'
},
// Custom orange warning
warning: {
subtle: '#fff7ed',
soft: '#fdba74',
DEFAULT: '#f97316',
strong: '#7c2d12'
}
}
},
dark: {
colors: {
primary: {
subtle: '#581c87',
soft: '#9333ea',
DEFAULT: '#a855f7',
strong: '#f3e8ff'
}
// ... other colors
}
}
}
});
```
### app/styles/app.css
```css
@import 'tailwindcss' source('../../');
@plugin "./../../frontile.js";
@import '@frontile/theme';
/* Load custom fonts */
@font-face {
font-family: 'Poppins';
src: url('/fonts/poppins-var.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
}
@theme {
/* Typography */
--font-header: 'Poppins', system-ui, sans-serif;
--font-body: 'Poppins', system-ui, sans-serif;
--font-label: 'Poppins', system-ui, sans-serif;
/* Rounded design */
--radius: 12px;
--radius-xl: 20px;
--radius-2xl: 24px;
/* Slightly larger icons */
--size-icon-sm: 16px;
--size-icon-md: 20px;
--size-icon-lg: 24px;
/* Subtle opacity changes */
--opacity-hover: 0.9;
--opacity-disabled: 0.5;
}
/* Component customization */
:root {
/* Wider modals */
--modal-md: 36rem;
--modal-lg: 52rem;
}
```
## Common Customization Patterns
### Pattern 1: Minimal/Sharp Design
```css
@theme {
--radius: 2px;
--radius-xl: 4px;
--radius-2xl: 6px;
--border-width-default: 1px;
--shadow-elevation-2: 0px 1px 3px rgba(0, 0, 0, 0.06);
}
```
### Pattern 2: Soft/Rounded Design
```css
@theme {
--radius: 16px;
--radius-xl: 24px;
--radius-2xl: 32px;
--shadow-elevation-2: 0px 8px 24px rgba(0, 0, 0, 0.08);
}
```
### Pattern 3: Bold/High Contrast
```css
@theme {
--border-width-default: 2px;
--border-width-heavy: 3px;
--opacity-hover: 0.95;
}
.light,
.dark .theme-inverse {
--shadow-elevation-2: 0px 4px 12px rgba(0, 0, 0, 0.15);
}
```
### Pattern 4: Subtle/Minimal
```css
@theme {
--border-width-thin: 0.5px;
--border-width-default: 1px;
--opacity-hover: 0.85;
--shadow-elevation-2: 0px 2px 4px rgba(0, 0, 0, 0.04);
}
```
## Advanced: Per-Component Customization
Override styles for specific components using Tailwind variants:
```css
/* Larger buttons */
.btn-lg {
@apply px-6 py-3 text-label-lg;
}
/* Card with custom shadow */
.card {
@apply rounded-2xl shadow-elevation-3;
}
/* Custom modal backdrop */
.modal-backdrop {
@apply bg-surface-overlay-strong dark:bg-surface-lift-strong backdrop-blur-md;
}
```
## Testing Your Customizations
### Visual Regression Testing
Test your theme in different scenarios:
```html
```
### Browser DevTools
Use browser DevTools to inspect CSS variables:
```js
// Check current value
getComputedStyle(document.documentElement).getPropertyValue('--radius');
// Test changes
document.documentElement.style.setProperty('--radius', '16px');
```
## Best Practices
### Document Your Customizations
Add comments explaining why values were chosen:
```css
@theme {
/* Brand guideline requires 16px minimum border radius */
--radius: 16px;
/* Larger icons for better visibility on mobile */
--size-icon-md: 22px;
}
```
### Use Consistent Scales
Maintain proportional relationships when customizing:
```css
/* Good - maintains scale relationships */
@theme {
--radius-sm: 4px;
--radius: 8px;
--radius-xl: 16px;
--radius-2xl: 24px;
}
/* Avoid - breaks scale relationships */
@theme {
--radius-sm: 2px;
--radius: 20px;
--radius-xl: 15px;
--radius-2xl: 8px;
}
```
### Test Accessibility
Ensure customizations maintain accessibility:
- Maintain sufficient color contrast (WCAG AA minimum 4.5:1)
- Don't reduce opacity below readable levels
- Test with screen readers
- Verify keyboard navigation remains clear
### Performance Considerations
- CSS variable changes are performant
- Avoid excessive theme switching
- Use `prefers-reduced-motion` for users who need it:
```css
@media (prefers-reduced-motion: reduce) {
* {
transition-duration: 0.01ms !important;
}
}
```
## Troubleshooting
### Colors Not Updating
**Problem**: Changed color values but UI doesn't update
**Solution**: Colors must be configured via JavaScript, not CSS variables:
```js
// Correct
module.exports = frontile({
themes: {
light: {
colors: {
primary: {/* ... */}
}
}
}
});
```
### Utilities Not Generated
**Problem**: Custom CSS variable doesn't generate utility class
**Solution**: Make sure the variable is in the `@theme` block:
```css
/* Generates utilities */
@theme {
--radius-custom: 10px;
}
/* Does NOT generate utilities */
:root {
--radius-custom: 10px;
}
```
### Theme-Specific Values Not Applying
**Problem**: Theme-specific overrides don't work
**Solution**: Use both selectors for proper theme-inverse support:
```css
/* Correct */
.light,
.dark .theme-inverse {
--opacity-hover: 0.85;
}
/* Incomplete */
.light {
--opacity-hover: 0.85;
}
```
## Next Steps
- [CSS Variables Reference](css-variables.md) - Complete variable list
- [Design Tokens](../design-tokens/overview.md) - Understanding tokens
- [Theme Switching](theme-switching.md) - Implement theme toggling