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.

Default
default
Clean neutral palette. Works well for most products.
Minimal
minimal
High contrast, reduced color. Great for documentation and developer tools.
Sleek
sleek
Refined tones with subtle depth. Suited for premium or SaaS applications.
/* 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.

TokenPurpose
--backgroundPage background color
--foregroundDefault text color
--primaryPrimary action color
--primary-foregroundText on primary backgrounds
--secondarySecondary action color
--secondary-foregroundText on secondary backgrounds
--mutedMuted / subtle background
--muted-foregroundMuted text (placeholders, hints)
--accentHover and focus highlight
--accent-foregroundText on accent backgrounds
--destructiveDestructive / error color
--borderBorder and divider color
--inputInput field border color
--ringFocus ring color
--radiusDefault 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>
  )
}

Related