GitHub

Theme System

Frontile's theme system provides a comprehensive, accessible design foundation built on semantic colors, flexible surfaces, and automatic dark mode support.

Installation

Install the theme package using Ember CLI:

ember install @frontile/theme

Configuration

Using Default Theme

Add this to your 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:

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";

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 intelligent color inversion. Simply toggle the dark class to switch between light and dark modes.

Quick Example

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:

Your own element, same tokens
<template>
  <div class='bg-primary text-on-primary rounded px-4 py-2'>
    Your own element, same tokens
  </div>
</template>

Theme Switching

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.

How Theme Classes Work

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 descendants

The 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.

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.

Regular Theme

Inverted Theme

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 →

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.

Primary brand colors for important actions and brand elements

Colors for positive states, confirmations, and successful actions

Colors for errors, warnings, and destructive actions

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:

/* 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:

/* 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:

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 →

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)

Surface Overlay (on an app base)

These overlays are translucent and stack on top of a solid base background.

surface-overlay-subtle Translucent layer
surface-overlay-soft Translucent layer
surface-overlay-mild Translucent layer
surface-overlay-firm Translucent layer
surface-overlay-strong Heaviest step — the modal/drawer backdrop in light mode
Stacking Example

Overlays can stack on top of each other:

Layer 1 (soft)
Layer 2 (mild)
Layer 3 (firm)

Learn more about the surface system →

Basic Customization

Customizing Colors

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

Setting Default Theme

Specify which theme loads by default:

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:

@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:

@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:

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";

Best Practices

Use Semantic Colors

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 Surface System for Backgrounds

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>

Test in Both Modes

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>
Released under MIT License - Created by Josemar Luedke