Theming
Everything is driven by CSS variables in OKLCH. Surfaces, actions, status colors, a brand accent and motion curves — all in one file.
Playground
Pick a hue and a radius, then copy the variables into your theme file.
:root {
--radius: 0.625rem;
--brand: oklch(0.56 0.2 330);
--ring: oklch(0.56 0.2 330 / 45%);
}
.dark {
--brand: oklch(0.7 0.17 330);
--ring: oklch(0.7 0.17 330 / 50%);
}Tokens
Each token comes in pairs where it makes sense: --primary is the fill, --primary-foreground is the text that sits on it. Tailwind utilities like bg-primary and text-muted-foreground map to them automatically.
Surfaces
--backgroundPage background
--cardRaised surfaces
--popoverMenus, popovers, dialogs
--mutedSubtle fills
--accentHover and highlight
Actions
--primaryMain buttons, selected states
--secondarySecondary buttons
--brandYour accent color, used sparingly
--destructiveDangerous actions
Status
--successPositive outcomes
--warningNeeds attention
--infoNeutral information
Lines
--borderDividers and outlines
--inputForm control borders
--ringFocus rings
Dark mode
Dark tokens live under a .dark class. Use next-themes (or anything that toggles the class on html):
pnpm add next-themes"use client"
import { ThemeProvider as NextThemesProvider } from "next-themes"
export function ThemeProvider({ children }: { children: React.ReactNode }) {
return (
<NextThemesProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
{children}
</NextThemesProvider>
)
}background → card → popover each get a touch brighter, and borders are translucent white so they adapt to any surface.Adding tokens
Declare the variable for both themes, then expose it to Tailwind with @theme inline.
:root { --highlight: oklch(0.9 0.12 95); }
.dark { --highlight: oklch(0.4 0.1 95); }
@theme inline {
--color-highlight: var(--highlight);
}
/* bg-highlight, text-highlight, border-highlight… are now available */