Theming
.skin uses CSS custom properties for theming. Switch between built-in themes, enable dark mode, or override any token to match your brand — all without touching component source code.
How theming works
Every component references a fixed set of CSS custom properties (design tokens) declared on the :root element. Changing a token value updates every component that uses it simultaneously — no per-component overrides needed.
Dark mode is applied by adding the dark class to the <html> element. A matching set of token values under .dark in your CSS swaps the palette automatically.
Theme selection (default / minimal / sleek) is handled by the data-theme attribute on <html>. The ThemeProvider manages both attributes reactively and persists the user's preference to localStorage.
Built-in themes
Three themes are included out of the box. You can preview them on the Themes page.
/* Applied as a data attribute on <html> */ <html data-theme="minimal"> <!-- Minimal theme --> <html data-theme="sleek"> <!-- Sleek theme --> <html> <!-- Default theme (no attribute) -->
CSS design tokens
These are the tokens every component references. Override any of them in your global CSS to customize the look without touching component files.
| Token | Purpose |
|---|---|
| --background | Page background color |
| --foreground | Default text color |
| --primary | Primary action color |
| --primary-foreground | Text on primary backgrounds |
| --secondary | Secondary action color |
| --secondary-foreground | Text on secondary backgrounds |
| --muted | Muted / subtle background |
| --muted-foreground | Muted text (placeholders, hints) |
| --accent | Hover and focus highlight |
| --accent-foreground | Text on accent backgrounds |
| --destructive | Destructive / error color |
| --border | Border and divider color |
| --input | Input field border color |
| --ring | Focus ring color |
| --radius | Default border radius |
Creating a custom theme
To define your own theme, add a data-theme selector to your global CSS and override any tokens you want to change:
/* globals.css */
/* Your custom theme */
[data-theme="brand"] {
--background: 0 0% 100%;
--foreground: 222 47% 11%;
--primary: 217 91% 60%; /* Blue */
--primary-foreground: 0 0% 100%;
--secondary: 214 32% 91%;
--secondary-foreground: 222 47% 11%;
--accent: 214 32% 91%;
--accent-foreground: 222 47% 11%;
--muted: 214 32% 91%;
--muted-foreground: 215 16% 47%;
--border: 213 27% 84%;
--ring: 217 91% 60%;
--radius: 0.5rem;
}
/* Dark variant of your custom theme */
.dark[data-theme="brand"] {
--background: 222 47% 11%;
--foreground: 213 31% 91%;
--primary: 217 91% 60%;
--primary-foreground: 222 47% 11%;
/* ... other dark tokens */
}Token values use HSL components without the hsl() wrapper, which lets Tailwind compose opacity utilities like bg-primary/50.
Dark mode
Dark mode is toggled by adding the dark class to the <html> element. Use the DarkModeToggle component (included in all tiers) to let users switch, or call setDarkMode from the useTheme hook directly:
import { DarkModeToggle } from "@/components/ui/dark-mode-toggle"
// Drop-in toggle — reads and writes the global dark state
<DarkModeToggle />
// Or control it programmatically
import { useTheme } from "@/lib/theme-provider"
function MyButton() {
const { darkMode, setDarkMode } = useTheme()
return (
<button onClick={() => setDarkMode(!darkMode)}>
{darkMode ? "Light mode" : "Dark mode"}
</button>
)
}The user's preference is persisted to localStorage and restored on the next visit. A blocking inline script in the document head reads the preference before the first paint, preventing a flash of unstyled content.
Using the useTheme hook
Any client component inside ThemeProvider can read and set the current theme:
"use client"
import { useTheme } from "@/lib/theme-provider"
export function ThemeSwitcher() {
const { theme, setTheme, darkMode, setDarkMode } = useTheme()
return (
<div>
<p>Current theme: {theme}</p>
<p>Dark mode: {darkMode ? "on" : "off"}</p>
<button onClick={() => setTheme("minimal")}>Minimal</button>
<button onClick={() => setTheme("sleek")}>Sleek</button>
<button onClick={() => setDarkMode(!darkMode)}>
Toggle dark mode
</button>
</div>
)
}