Prototyper UI

Theming

Customize colors, surfaces, and shadows using OKLCH tokens and CSS custom properties.

Prototyper UI's design system is built on CSS custom properties using the OKLCH color space. Every color, surface, shadow, and easing curve is a token you can override.

OKLCH Color System

All colors use the OKLCH color space — a perceptually uniform model where lightness, chroma, and hue are independent axes.

/* OKLCH values: lightness chroma hue */
--primary: 39.11% 0.084 240.8;

Why OKLCH?

  • Perceptually uniform — a 10% lightness shift looks like 10% regardless of the base hue. HSL lightness is wildly inconsistent across colors.
  • Predictable color-mix() — derived states (hover, soft) use color-mix(in oklab) and produce consistent results across the entire hue range.
  • Wide gamut — OKLCH can represent P3 and Rec.2020 colors that HSL cannot.

How Derived Colors Work

Hover states, soft variants, and surface tiers are computed automatically using color-mix():

/* Hover: 90% base + 10% paired foreground */
--primary-hover: color-mix(in oklab, oklch(var(--primary)) 90%, oklch(var(--primary-foreground)) 10%);

/* Soft variant: 15% base on transparent */
--primary-soft: color-mix(in oklab, oklch(var(--primary)) 15%, transparent);

/* Surface tiers: progressive foreground mixing */
--surface-secondary: color-mix(in oklab, oklch(var(--surface)) 94%, oklch(var(--surface-foreground)) 6%);
--surface-tertiary: color-mix(in oklab, oklch(var(--surface)) 88%, oklch(var(--surface-foreground)) 12%);

You only need to define the base color and its foreground — all interaction states derive automatically.

Token Reference

Base Colors

TokenDescriptionLight Default
--backgroundPage background100% 0 0 (white)
--foregroundDefault text color14.05% 0.004 285.8
--radiusBase border radius0.5rem

Primary Ramp

TokenDescriptionLight Default
--primaryPrimary brand color, buttons39.11% 0.084 240.8
--primary-foregroundText on primary backgrounds98.48% 0 0
--primary-lightGradient light stop75.84% 0.137 231.6
--primary-middleGradient middle stop49.96% 0.118 242.2
--primary-darkGradient dark stop39.20% 0.084 240.8

Semantic Colors

TokenDescription
--secondary / --secondary-foregroundSecondary actions, subtle buttons
--muted / --muted-foregroundMuted backgrounds, placeholder text
--accent / --accent-foregroundAccent highlights, hover backgrounds
--destructive / --destructive-foregroundDestructive actions, error states
--success / --success-foregroundSuccess states
--warning / --warning-foregroundWarning states
--info / --info-foregroundInformational states

Surfaces

TokenDescription
--surface / --surface-foregroundCards, panels, tabs
--surface-secondaryNested containers (derived via color-mix)
--surface-tertiaryDeeper nesting (derived via color-mix)
--overlay / --overlay-foregroundDialogs, popovers, menus
--card / --card-foregroundCard backgrounds
--popover / --popover-foregroundPopover backgrounds

Fields

TokenDescription
--field-backgroundInput, select, combobox backgrounds
--field-borderField border color
--field-border-hoverField border on hover (derived)
--field-border-invalidField border on validation error (derived)

Borders

TokenDescription
--borderDefault border
--border-lightSubtle border (dividers, separators)
--border-darkEmphasized border
--inputInput border (matches --border)
--ringFocus ring color (matches --primary)

Shadows

TokenLight ModeDark Mode
--shadow-surfaceMulti-layer subtle shadownone
--shadow-fieldSubtle shadow + 1px edgenone
--shadow-overlayHeavy multi-layer shadow1px white inset glow + deep shadow

Easings

TokenValueUsage
--ease-smoothcubic-bezier(0.4, 0, 0.2, 1)General transitions
--ease-out-fluidcubic-bezier(0.32, 0.72, 0, 1)Signature deceleration curve
--ease-out-quadcubic-bezier(0.25, 0.46, 0.45, 0.94)Subtle ease-out
--ease-out-quartcubic-bezier(0.165, 0.84, 0.44, 1)Pronounced ease-out
--ease-in-quadcubic-bezier(0.55, 0.085, 0.68, 0.53)Subtle ease-in
--ease-in-quartcubic-bezier(0.895, 0.03, 0.685, 0.22)Pronounced ease-in
--ease-in-out-quadcubic-bezier(0.455, 0.03, 0.515, 0.955)Symmetric ease

Derived Interaction Colors

These are auto-computed from base tokens — you typically do not override them:

TokenFormula
--primary-hover90% --primary + 10% --primary-foreground
--destructive-hover90% --destructive + 10% --destructive-foreground
--success-hover90% --success + 10% --success-foreground
--warning-hover90% --warning + 10% --warning-foreground
--accent-hover92% --accent + 8% --accent-foreground
--primary-soft15% --primary on transparent
--primary-soft-hover20% --primary on transparent
--destructive-soft15% --destructive on transparent
--destructive-soft-hover20% --destructive on transparent

Customize Colors

Override any token in your globals.css to change the look of all components at once. Tokens use raw OKLCH values (lightness, chroma, hue) without the oklch() wrapper:

:root {
  /* Change primary to a teal */
  --primary: 55% 0.15 180;
  --primary-foreground: 98% 0 0;
  --primary-light: 75% 0.12 175;
  --primary-middle: 60% 0.14 178;
  --primary-dark: 45% 0.13 182;

  /* Update the focus ring to match */
  --ring: 55% 0.15 180;
}

.dark {
  --primary: 40% 0.10 180;
  --primary-foreground: 95% 0.02 178;
  --primary-light: 75% 0.12 175;
  --primary-middle: 60% 0.14 178;
  --primary-dark: 45% 0.13 182;

  --ring: 70% 0.08 178;
}

All derived tokens (--primary-hover, --primary-soft, etc.) will auto-adapt because they use color-mix() with your new base values.

Create a Custom Theme

Here is a full example of a custom theme with both light and dark modes. Copy this into your globals.css and adjust the values:

:root {
  /* Base */
  --background: 100% 0 0;
  --foreground: 14.05% 0.004 285.8;
  --radius: 0.5rem;

  /* Primary — violet example */
  --primary: 50% 0.2 280;
  --primary-foreground: 98% 0 0;
  --primary-light: 75% 0.15 275;
  --primary-middle: 58% 0.18 278;
  --primary-dark: 42% 0.19 282;

  /* Secondary */
  --secondary: 96% 0.01 280;
  --secondary-foreground: 25% 0.03 280;

  /* Muted */
  --muted: 96% 0 0;
  --muted-foreground: 55% 0.01 285;

  /* Accent */
  --accent: 96% 0 0;
  --accent-foreground: 21% 0.006 285;

  /* Destructive */
  --destructive: 63% 0.21 25;
  --destructive-foreground: 98% 0 0;

  /* Borders */
  --border: 92% 0.004 286;
  --border-light: 96% 0 0;
  --border-dark: 80% 0.01 286;
  --input: 92% 0.004 286;
  --ring: 50% 0.2 280;

  /* Surfaces */
  --surface: 98% 0 0;
  --surface-foreground: 14% 0.004 285;
  --surface-secondary: color-mix(in oklab, oklch(var(--surface)) 94%, oklch(var(--surface-foreground)) 6%);
  --surface-tertiary: color-mix(in oklab, oklch(var(--surface)) 88%, oklch(var(--surface-foreground)) 12%);
  --overlay: 100% 0 0;
  --overlay-foreground: 14% 0.004 285;

  /* Cards & Popovers */
  --card: 98% 0 0;
  --card-foreground: 14% 0.004 285;
  --popover: 100% 0 0;
  --popover-foreground: 14% 0.004 285;

  /* Fields */
  --field-background: 100% 0 0;
  --field-border: 92% 0.004 286;
  --field-border-hover: color-mix(in oklab, oklch(var(--field-border)) 70%, oklch(var(--foreground)) 30%);
  --field-border-invalid: oklch(var(--destructive));

  /* Derived hover states — auto-adapt */
  --primary-hover: color-mix(in oklab, oklch(var(--primary)) 90%, oklch(var(--primary-foreground)) 10%);
  --destructive-hover: color-mix(in oklab, oklch(var(--destructive)) 90%, oklch(var(--destructive-foreground)) 10%);
  --accent-hover: color-mix(in oklab, oklch(var(--accent)) 92%, oklch(var(--accent-foreground)) 8%);

  /* Derived soft variants */
  --primary-soft: color-mix(in oklab, oklch(var(--primary)) 15%, transparent);
  --primary-soft-hover: color-mix(in oklab, oklch(var(--primary)) 20%, transparent);
  --destructive-soft: color-mix(in oklab, oklch(var(--destructive)) 15%, transparent);
  --destructive-soft-hover: color-mix(in oklab, oklch(var(--destructive)) 20%, transparent);
}

.dark {
  /* Base */
  --background: 14% 0.004 285;
  --foreground: 98% 0 0;

  /* Primary — violet dark mode */
  --primary: 35% 0.14 282;
  --primary-foreground: 95% 0.02 278;
  --primary-light: 75% 0.15 275;
  --primary-middle: 58% 0.18 278;
  --primary-dark: 42% 0.19 282;

  /* Secondary */
  --secondary: 28% 0.03 280;
  --secondary-foreground: 98% 0 0;

  /* Muted */
  --muted: 27% 0.006 286;
  --muted-foreground: 71% 0.013 286;

  /* Accent */
  --accent: 27% 0.006 286;
  --accent-foreground: 98% 0 0;

  /* Destructive */
  --destructive: 40% 0.13 26;
  --destructive-foreground: 98% 0 0;

  /* Borders */
  --border: 32% 0.007 286;
  --border-light: 37% 0.008 286;
  --border-dark: 23% 0.004 286;
  --input: 27% 0.006 286;
  --ring: 70% 0.1 278;

  /* Surfaces */
  --surface: 16% 0.006 285;
  --surface-foreground: 98% 0 0;
  --surface-secondary: color-mix(in oklab, oklch(var(--surface)) 94%, oklch(var(--surface-foreground)) 6%);
  --surface-tertiary: color-mix(in oklab, oklch(var(--surface)) 88%, oklch(var(--surface-foreground)) 12%);
  --overlay: 20% 0.008 285;
  --overlay-foreground: 98% 0 0;

  /* Cards & Popovers */
  --card: 16% 0.006 285;
  --card-foreground: 98% 0 0;
  --popover: 14% 0.004 285;
  --popover-foreground: 98% 0 0;

  /* Fields */
  --field-background: 16% 0.006 285;
  --field-border: 32% 0.007 286;
  --field-border-hover: color-mix(in oklab, oklch(var(--field-border)) 70%, oklch(var(--foreground)) 30%);
  --field-border-invalid: oklch(var(--destructive));

  /* Derived — same formulas, auto-adapt to dark values */
  --primary-hover: color-mix(in oklab, oklch(var(--primary)) 90%, oklch(var(--primary-foreground)) 10%);
  --destructive-hover: color-mix(in oklab, oklch(var(--destructive)) 90%, oklch(var(--destructive-foreground)) 10%);
  --accent-hover: color-mix(in oklab, oklch(var(--accent)) 92%, oklch(var(--accent-foreground)) 8%);
  --primary-soft: color-mix(in oklab, oklch(var(--primary)) 15%, transparent);
  --primary-soft-hover: color-mix(in oklab, oklch(var(--primary)) 20%, transparent);
  --destructive-soft: color-mix(in oklab, oklch(var(--destructive)) 15%, transparent);
  --destructive-soft-hover: color-mix(in oklab, oklch(var(--destructive)) 20%, transparent);

  /* Shadows — tonal contrast replaces shadows */
  --shadow-surface: none;
  --shadow-field: none;
  --shadow-overlay: 0 0 0 1px oklch(100% 0 0 / 0.08), 0 8px 30px oklch(0% 0 0 / 0.35);
}

What to Define vs. What to Derive

Define manuallyDerived via color-mix()
Base colors (--primary, --destructive, etc.)Hover states (90% base + 10% foreground)
Foreground colors (--primary-foreground, etc.)Soft variants (15% base + transparent)
Color ramps (--primary-light/middle/dark)Surface tiers (secondary, tertiary)
Surface/overlay/field backgroundsBorder hover progressions

Foreground colors cannot be auto-derived — green needs dark text in both modes, while blue needs light text in both modes. Ramp derivation (light/middle/dark stops) collapses for light colors and washes chroma for dark colors. Define where it matters, derive where it's safe.

Runtime Theme Switching

Toggle Dark Mode

Add or remove the .dark class on the <html> element:

// Toggle dark mode
document.documentElement.classList.toggle("dark")

// Set explicitly
document.documentElement.classList.add("dark")    // dark
document.documentElement.classList.remove("dark")  // light

With next-themes, use the useTheme hook:

import { useTheme } from "next-themes"

function ThemeToggle() {
  const { theme, setTheme } = useTheme()

  return (
    <button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
      Toggle theme
    </button>
  )
}

data-theme Attribute

You can also switch themes using a data-theme attribute, which is useful when supporting multiple named themes beyond light/dark:

<html data-theme="dark">
[data-theme="dark"] {
  --background: 14.05% 0.004 285.8;
  --foreground: 98.48% 0 0;
  /* ... all dark mode overrides */
}

Swap Color Themes at Runtime

To switch between color themes (e.g., different primary colors) at runtime, apply CSS custom property overrides via a class or attribute:

[data-theme="violet"] {
  --primary: 50% 0.2 280;
  --primary-foreground: 98% 0 0;
  --primary-light: 75% 0.15 275;
  --primary-middle: 58% 0.18 278;
  --primary-dark: 42% 0.19 282;
  --ring: 50% 0.2 280;
}

[data-theme="violet"].dark {
  --primary: 35% 0.14 282;
  --primary-foreground: 95% 0.02 278;
  --ring: 70% 0.1 278;
}
// Switch color theme
document.documentElement.setAttribute("data-theme", "violet")

Theme Builder

Use the interactive Theme Builder to preview color combinations and generate the CSS tokens for your custom theme.

On this page