Theming

Semantic color tokens, custom themes, and customization guide.

Colors

Root Scale

root
Aa 123Subtle
root-100
Aa 123Subtle
root-200
Aa 123Subtle
root-contrast
Aa 123Subtle

Brand

primary
Aa 123Subtle
secondary
Aa 123Subtle
accent
Aa 123Subtle

States

info
Aa 123
progress
Aa 123
success
Aa 123
warning
Aa 123
danger
Aa 123

Utilities

divider
overlay

80% opacity

Base Palette

The default light theme defined in @theme. Every token is a Tailwind utility automatically.

:root {
  --light: oklch(0.97 0.008 85);
  --dark: oklch(0.18 0.012 60);
}

@theme {
  --color-*: initial;

  /* ── Root ── */
  --color-root: var(--light);
  --color-root-contrast: var(--dark);
  --color-root-subtle: oklch(0.82 0.008 85);
  --color-root-contrast-subtle: oklch(0.53 0.01 75);
  --color-root-ghost: oklch(from var(--color-root-contrast) l c h / 0.05);
  --color-root-ghost-hover: oklch(from var(--color-root-contrast) l c h / 0.11);
  --color-root-hover: oklch(from var(--color-root-contrast) l c h / 0.88);

  /* ── Root-100 ── */
  --color-root-100: oklch(0.92 0.015 80);
  --color-root-100-contrast: var(--dark);
  --color-root-100-subtle: oklch(0.78 0.012 80);
  --color-root-100-contrast-subtle: oklch(0.5 0.01 75);
  --color-root-100-ghost: oklch(from var(--color-root-100-contrast) l c h / 0.09);
  --color-root-100-ghost-hover: oklch(from var(--color-root-100-contrast) l c h / 0.17);
  --color-root-100-hover: oklch(from var(--color-root-100-contrast) l c h / 0.88);

  /* ── Root-200 ── */
  --color-root-200: oklch(0.82 0.018 75);
  --color-root-200-contrast: var(--dark);
  --color-root-200-subtle: oklch(0.68 0.014 75);
  --color-root-200-contrast-subtle: oklch(0.43 0.01 75);
  --color-root-200-ghost: oklch(from var(--color-root-200-contrast) l c h / 0.13);
  --color-root-200-ghost-hover: oklch(from var(--color-root-200-contrast) l c h / 0.23);
  --color-root-200-hover: oklch(from var(--color-root-200-contrast) l c h / 0.88);

  /* ── Primary ── */
  --color-primary: oklch(0.46 0.09 55);
  --color-primary-contrast: var(--light);
  --color-primary-subtle: oklch(0.58 0.05 55);
  --color-primary-contrast-subtle: oklch(0.85 0.03 55);
  --color-primary-ghost: oklch(from var(--color-primary) l c h / 0.08);
  --color-primary-ghost-hover: oklch(from var(--color-primary) l c h / 0.14);
  --color-primary-hover: oklch(from var(--color-primary) l c h / 0.88);

  /* ── Secondary ── */
  --color-secondary: oklch(0.46 0.04 139);
  --color-secondary-contrast: var(--light);
  --color-secondary-subtle: oklch(0.58 0.025 139);
  --color-secondary-contrast-subtle: oklch(0.88 0.02 139);
  --color-secondary-ghost: oklch(from var(--color-secondary) l c h / 0.08);
  --color-secondary-ghost-hover: oklch(from var(--color-secondary) l c h / 0.14);
  --color-secondary-hover: oklch(from var(--color-secondary) l c h / 0.88);

  /* ── Accent ── */
  --color-accent: oklch(0.54 0.21 41);
  --color-accent-contrast: var(--light);
  --color-accent-subtle: oklch(0.48 0.12 41);
  --color-accent-contrast-subtle: oklch(0.92 0.06 41);
  --color-accent-ghost: oklch(from var(--color-accent) l c h / 0.08);
  --color-accent-ghost-hover: oklch(from var(--color-accent) l c h / 0.14);
  --color-accent-hover: oklch(from var(--color-accent) l c h / 0.88);

  /* ── States ── */
  --color-info: oklch(0.92 0.005 75);
  --color-info-contrast: oklch(0.42 0.01 75);
  --color-info-ghost: oklch(from var(--color-info-contrast) l c h / 0.08);
  --color-info-ghost-hover: oklch(from var(--color-info-contrast) l c h / 0.14);
  --color-info-hover: oklch(from var(--color-info) l c h / 0.88);

  --color-success: oklch(0.82 0.04 145);
  --color-success-contrast: oklch(0.28 0.14 145);
  --color-success-ghost: oklch(from var(--color-success-contrast) l c h / 0.08);
  --color-success-ghost-hover: oklch(from var(--color-success-contrast) l c h / 0.14);
  --color-success-hover: oklch(from var(--color-success) l c h / 0.88);

  --color-warning: oklch(0.84 0.05 63);
  --color-warning-contrast: oklch(0.34 0.16 63);
  --color-warning-ghost: oklch(from var(--color-warning-contrast) l c h / 0.08);
  --color-warning-ghost-hover: oklch(from var(--color-warning-contrast) l c h / 0.14);
  --color-warning-hover: oklch(from var(--color-warning) l c h / 0.88);

  --color-danger: oklch(0.82 0.04 25);
  --color-danger-contrast: oklch(0.3 0.18 25);
  --color-danger-ghost: oklch(from var(--color-danger-contrast) l c h / 0.08);
  --color-danger-ghost-hover: oklch(from var(--color-danger-contrast) l c h / 0.14);
  --color-danger-hover: oklch(from var(--color-danger) l c h / 0.88);

  /* ── Utilities ── */
  --color-divider: oklch(from var(--color-root-contrast) l c h / 0.1);
  --color-overlay: oklch(from var(--dark) l c h / 0.8);

  --font-display: var(--font-display);
  --font-body: var(--font-inter);
}

Custom Themes

Create any theme by adding a class that overrides tokens. Only override what you need — everything else inherits from the base palette.

Example: Forest Theme

.forest {
  --color-root: oklch(0.95 0.015 100);
  --color-root-contrast: oklch(0.18 0.015 100);
  --color-root-subtle: oklch(0.8 0.012 100);
  --color-root-contrast-subtle: oklch(0.52 0.012 100);

  --color-primary: oklch(0.4 0.1 145);
  --color-primary-contrast: oklch(0.95 0.015 100);
  --color-primary-subtle: oklch(0.52 0.05 145);
  --color-primary-contrast-subtle: oklch(0.82 0.03 145);

  --color-secondary: oklch(0.45 0.06 85);
  --color-secondary-contrast: oklch(0.95 0.015 100);

  /* Nest .dark for automatic dark mode */
  &.dark, & .dark {
    --color-root: oklch(0.13 0.01 100);
    --color-root-contrast: oklch(0.92 0.015 100);
    --color-primary: oklch(0.65 0.12 145);
    --color-primary-contrast: oklch(0.13 0.01 100);
  }
}

Applying a Theme

// Scoped to a section
<section className="forest">
  <Button color="primary">Forest Button</Button>
</section>

// Or via Container
<Container theme="forest">
  ...
</Container>

Token Reference

Every color family follows a consistent token pattern.

Primary Colors (root, root-100, root-200, primary, secondary, accent)

TokenUsage
{color}Base color
{color}-contrastGuaranteed-contrast text on {color} bg
{color}-subtleSofter version for muted text
{color}-contrast-subtleSofter contrast text on {color} bg
{color}-ghostGhost variant background (semi-transparent)
{color}-ghost-hoverGhost variant hover background
{color}-hoverSolid variant hover background

State Colors (info, success, warning, danger)

TokenUsage
{color}State background
{color}-contrastText on {color} bg
{color}-ghostGhost variant background
{color}-ghost-hoverGhost variant hover background
{color}-hoverSolid variant hover background

Utility Tokens

TokenUsage
dividerLines, borders, separators
overlaySemi-opaque dark layer (80%) for modals

Color Rules

  • All Tailwind built-in colors are disabled — only theme tokens are available.
  • Never use raw hex values — define all colors as tokens in global.css.
  • Pair every background token with its -contrast companion.
  • Use Tailwind opacity modifier for transparency: bg-root-contrast/50.
  • Never set color via inline style — all color must go through Tailwind.
  • Ghost/ghost-hover/hover tokens use relative color syntax — they auto-adapt to dark mode with no extra overrides.

Full Source

The complete global.css file — copy this into your project as a starting point.

src/app/global.css
@import "tailwindcss";

@custom-variant dark (&:is(.dark *));

[id] {
  scroll-margin-top: calc(var(--spacing-navbar) + 16px);
}

@utility container {
  width: 100%;
  max-width: none;
  @media (width >= theme(--breakpoint-2xl)) {
    max-width: theme(--breakpoint-2xl);
  }
}

/* Palette — raw values, not available as Tailwind utilities (defined outside @theme).
   Referenced by semantic tokens below. Swap these when changing the project's base palette. */
:root {
  --light: oklch(0.97 0.008 85);
  --dark: oklch(0.18 0.012 60);
}

@theme {
  --color-*: initial;

  /* ── Root ──────────────────────────────────────────────────────────── */
  --color-root: var(--light);
  --color-root-contrast: var(--dark);
  --color-root-subtle: oklch(0.82 0.008 85);
  --color-root-contrast-subtle: oklch(0.53 0.01 75);
  --color-root-ghost: oklch(from var(--color-root-contrast) l c h / 0.05);
  --color-root-ghost-hover: oklch(from var(--color-root-contrast) l c h / 0.11);
  --color-root-hover: oklch(from var(--color-root-contrast) l c h / 0.88);

  /* ── Root-100 ─────────────────────────────────────────────────────── */
  --color-root-100: oklch(0.92 0.015 80);
  --color-root-100-contrast: var(--dark);
  --color-root-100-subtle: oklch(0.78 0.012 80);
  --color-root-100-contrast-subtle: oklch(0.5 0.01 75);
  --color-root-100-ghost: oklch(
    from var(--color-root-100-contrast) l c h / 0.09
  );
  --color-root-100-ghost-hover: oklch(
    from var(--color-root-100-contrast) l c h / 0.17
  );
  --color-root-100-hover: oklch(
    from var(--color-root-100-contrast) l c h / 0.88
  );

  /* ── Root-200 ─────────────────────────────────────────────────────── */
  --color-root-200: oklch(0.82 0.018 75);
  --color-root-200-contrast: var(--dark);
  --color-root-200-subtle: oklch(0.68 0.014 75);
  --color-root-200-contrast-subtle: oklch(0.43 0.01 75);
  --color-root-200-ghost: oklch(
    from var(--color-root-200-contrast) l c h / 0.13
  );
  --color-root-200-ghost-hover: oklch(
    from var(--color-root-200-contrast) l c h / 0.23
  );
  --color-root-200-hover: oklch(
    from var(--color-root-200-contrast) l c h / 0.88
  );

  /* ── Primary ──────────────────────────────────────────────────────── */
  --color-primary: oklch(0.46 0.09 55);
  --color-primary-contrast: var(--light);
  --color-primary-subtle: oklch(0.58 0.05 55);
  --color-primary-contrast-subtle: oklch(0.85 0.03 55);
  --color-primary-ghost: oklch(from var(--color-primary) l c h / 0.08);
  --color-primary-ghost-hover: oklch(from var(--color-primary) l c h / 0.14);
  --color-primary-hover: oklch(from var(--color-primary) l c h / 0.88);

  /* ── Secondary ────────────────────────────────────────────────────── */
  --color-secondary: oklch(0.46 0.04 139);
  --color-secondary-contrast: var(--light);
  --color-secondary-subtle: oklch(0.58 0.025 139);
  --color-secondary-contrast-subtle: oklch(0.88 0.02 139);
  --color-secondary-ghost: oklch(from var(--color-secondary) l c h / 0.08);
  --color-secondary-ghost-hover: oklch(
    from var(--color-secondary) l c h / 0.14
  );
  --color-secondary-hover: oklch(from var(--color-secondary) l c h / 0.88);

  /* ── Accent ────────────────────────────────────────────────────────── */
  --color-accent: oklch(0.54 0.21 41);
  --color-accent-contrast: var(--light);
  --color-accent-subtle: oklch(0.48 0.12 41);
  --color-accent-contrast-subtle: oklch(0.92 0.06 41);
  --color-accent-ghost: oklch(from var(--color-accent) l c h / 0.08);
  --color-accent-ghost-hover: oklch(from var(--color-accent) l c h / 0.14);
  --color-accent-hover: oklch(from var(--color-accent) l c h / 0.88);

  /* ── States ───────────────────────────────────────────────────────── */
  --color-info: oklch(0.92 0.005 75);
  --color-info-contrast: oklch(0.42 0.01 75);
  --color-info-ghost: oklch(from var(--color-info-contrast) l c h / 0.08);
  --color-info-ghost-hover: oklch(from var(--color-info-contrast) l c h / 0.14);
  --color-info-hover: oklch(from var(--color-info) l c h / 0.88);

  --color-progress: oklch(0.88 0.04 250);
  --color-progress-contrast: oklch(0.3 0.14 250);
  --color-progress-ghost: oklch(from var(--color-progress-contrast) l c h / 0.08);
  --color-progress-ghost-hover: oklch(from var(--color-progress-contrast) l c h / 0.14);
  --color-progress-hover: oklch(from var(--color-progress) l c h / 0.88);

  --color-success: oklch(0.82 0.04 145);
  --color-success-contrast: oklch(0.28 0.14 145);
  --color-success-ghost: oklch(from var(--color-success-contrast) l c h / 0.08);
  --color-success-ghost-hover: oklch(
    from var(--color-success-contrast) l c h / 0.14
  );
  --color-success-hover: oklch(from var(--color-success) l c h / 0.88);

  --color-warning: oklch(0.84 0.05 63);
  --color-warning-contrast: oklch(0.34 0.16 63);
  --color-warning-ghost: oklch(from var(--color-warning-contrast) l c h / 0.08);
  --color-warning-ghost-hover: oklch(
    from var(--color-warning-contrast) l c h / 0.14
  );
  --color-warning-hover: oklch(from var(--color-warning) l c h / 0.88);

  --color-danger: oklch(0.82 0.04 25);
  --color-danger-contrast: oklch(0.3 0.18 25);
  --color-danger-ghost: oklch(from var(--color-danger-contrast) l c h / 0.08);
  --color-danger-ghost-hover: oklch(
    from var(--color-danger-contrast) l c h / 0.14
  );
  --color-danger-hover: oklch(from var(--color-danger) l c h / 0.88);

  /* ── Utilities ────────────────────────────────────────────────────── */
  --color-divider: oklch(from var(--color-root-contrast) l c h / 0.1);
  --color-overlay: oklch(from var(--dark) l c h / 0.8);

  --font-display: var(--font-display);
  --font-body: var(--font-inter);
}

/* Dark mode — add .dark to <html> or any ancestor to activate */
.dark {
  --color-root: var(--dark);
  --color-root-contrast: var(--light);
  --color-root-subtle: oklch(0.35 0.008 65);
  --color-root-contrast-subtle: oklch(0.68 0.01 72);
  --color-root-ghost: oklch(from var(--color-root-contrast) l c h / 0.08);
  --color-root-ghost-hover: oklch(from var(--color-root-contrast) l c h / 0.14);

  --color-root-100: oklch(0.22 0.01 65);
  --color-root-100-contrast: var(--light);
  --color-root-100-subtle: oklch(0.38 0.01 65);
  --color-root-100-contrast-subtle: oklch(0.7 0.01 72);

  --color-root-200: oklch(0.3 0.01 65);
  --color-root-200-contrast: var(--light);
  --color-root-200-subtle: oklch(0.45 0.012 65);
  --color-root-200-contrast-subtle: oklch(0.74 0.01 72);

  --color-primary: oklch(0.85 0.07 55);
  --color-primary-contrast: var(--dark);
  --color-primary-subtle: oklch(0.7 0.04 55);
  --color-primary-contrast-subtle: oklch(0.38 0.03 55);

  --color-secondary: oklch(0.85 0.05 139);
  --color-secondary-contrast: var(--dark);
  --color-secondary-subtle: oklch(0.7 0.03 139);
  --color-secondary-contrast-subtle: oklch(0.38 0.02 139);

  --color-info: oklch(0.24 0.03 75);
  --color-info-contrast: oklch(0.78 0.06 75);
  --color-progress: oklch(0.24 0.06 250);
  --color-progress-contrast: oklch(0.78 0.14 250);
  --color-success: oklch(0.24 0.06 145);
  --color-success-contrast: oklch(0.8 0.16 145);
  --color-warning: oklch(0.26 0.07 63);
  --color-warning-contrast: oklch(0.88 0.14 63);
  --color-danger: oklch(0.24 0.07 25);
  --color-danger-contrast: oklch(0.78 0.18 25);
}

/* Forest theme — olive-green palette with light and dark modes */
.forest {
  --color-root: oklch(0.95 0.015 100);
  --color-root-contrast: oklch(0.18 0.015 100);
  --color-root-subtle: oklch(0.8 0.012 100);
  --color-root-contrast-subtle: oklch(0.52 0.012 100);

  --color-root-100: oklch(0.9 0.02 100);
  --color-root-100-contrast: oklch(0.18 0.015 100);
  --color-root-100-subtle: oklch(0.76 0.016 100);
  --color-root-100-contrast-subtle: oklch(0.49 0.012 100);

  --color-root-200: oklch(0.84 0.025 100);
  --color-root-200-contrast: oklch(0.18 0.015 100);
  --color-root-200-subtle: oklch(0.7 0.018 100);
  --color-root-200-contrast-subtle: oklch(0.44 0.012 100);

  --color-primary: oklch(0.4 0.1 145);
  --color-primary-contrast: oklch(0.95 0.015 100);
  --color-primary-subtle: oklch(0.52 0.05 145);
  --color-primary-contrast-subtle: oklch(0.82 0.03 145);

  --color-secondary: oklch(0.45 0.06 85);
  --color-secondary-contrast: oklch(0.95 0.015 100);
  --color-secondary-subtle: oklch(0.53 0.035 85);
  --color-secondary-contrast-subtle: oklch(0.84 0.02 85);

  --color-info: oklch(0.92 0.02 145);
  --color-info-contrast: oklch(0.35 0.08 145);
  --color-progress: oklch(0.88 0.04 250);
  --color-progress-contrast: oklch(0.3 0.12 250);
  --color-success: oklch(0.88 0.06 145);
  --color-success-contrast: oklch(0.28 0.14 145);
  --color-warning: oklch(0.88 0.06 85);
  --color-warning-contrast: oklch(0.34 0.14 85);
  --color-danger: oklch(0.88 0.04 25);
  --color-danger-contrast: oklch(0.32 0.16 25);

  &.dark,
  & .dark {
    --color-root: oklch(0.13 0.01 100);
    --color-root-contrast: oklch(0.92 0.015 100);
    --color-root-subtle: oklch(0.3 0.008 100);
    --color-root-contrast-subtle: oklch(0.6 0.01 100);

    --color-root-100: oklch(0.17 0.015 100);
    --color-root-100-contrast: oklch(0.92 0.015 100);
    --color-root-100-subtle: oklch(0.34 0.01 100);
    --color-root-100-contrast-subtle: oklch(0.6 0.01 100);

    --color-root-200: oklch(0.22 0.018 100);
    --color-root-200-contrast: oklch(0.92 0.015 100);
    --color-root-200-subtle: oklch(0.38 0.012 100);
    --color-root-200-contrast-subtle: oklch(0.66 0.01 100);

    --color-primary: oklch(0.65 0.12 145);
    --color-primary-contrast: oklch(0.13 0.01 100);
    --color-primary-subtle: oklch(0.62 0.04 145);
    --color-primary-contrast-subtle: oklch(0.28 0.03 145);

    --color-secondary: oklch(0.72 0.08 85);
    --color-secondary-contrast: oklch(0.13 0.01 100);
    --color-secondary-subtle: oklch(0.62 0.03 85);
    --color-secondary-contrast-subtle: oklch(0.35 0.02 85);

    --color-info: oklch(0.22 0.03 145);
    --color-info-contrast: oklch(0.78 0.08 145);
    --color-progress: oklch(0.24 0.06 250);
    --color-progress-contrast: oklch(0.78 0.14 250);
    --color-success: oklch(0.24 0.06 145);
    --color-success-contrast: oklch(0.8 0.16 145);
    --color-warning: oklch(0.26 0.07 85);
    --color-warning-contrast: oklch(0.88 0.12 85);
    --color-danger: oklch(0.24 0.07 25);
    --color-danger-contrast: oklch(0.78 0.18 25);
  }
}

body {
  font-family: var(--font-inter), sans-serif;
}