Theming
Semantic color tokens, custom themes, and customization guide.
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>Primary Colors (root, root-100, root-200, primary, secondary, accent)
| Token | Usage |
|---|---|
| {color} | Base color |
| {color}-contrast | Guaranteed-contrast text on {color} bg |
| {color}-subtle | Softer version for muted text |
| {color}-contrast-subtle | Softer contrast text on {color} bg |
| {color}-ghost | Ghost variant background (semi-transparent) |
| {color}-ghost-hover | Ghost variant hover background |
| {color}-hover | Solid variant hover background |
State Colors (info, success, warning, danger)
| Token | Usage |
|---|---|
| {color} | State background |
| {color}-contrast | Text on {color} bg |
| {color}-ghost | Ghost variant background |
| {color}-ghost-hover | Ghost variant hover background |
| {color}-hover | Solid variant hover background |
Utility Tokens
| Token | Usage |
|---|---|
| divider | Lines, borders, separators |
| overlay | Semi-opaque dark layer (80%) for modals |
- 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
-contrastcompanion. - 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.
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;
}