After this lesson, you will be able to: Define and use CSS custom properties (variables) to build a small design-token system, swap themes (including dark mode) in five lines, and recognize when tokens belong in a project vs when they don't.
Every professional design system runs on tokens — named values for color, spacing, typography, and radii that the whole UI references instead of hardcoding raw values. Tokens live as CSS custom properties (a feature shipped to every browser since 2017, no preprocessing required). This lesson takes you from never having seen `--main-color: ...` to building a real token system and theming an entire app with a single attribute swap.
A CSS custom property is a name starting with `--` that holds a value. You set it inside a selector (most often `:root`, which applies it globally), and you read it elsewhere with `var(--name)`. Unlike Sass variables, CSS variables exist at runtime — you can change them with JavaScript, override them inside a component scope, or swap them based on an attribute. That's what makes them the foundation of every modern theming system.
Three things to notice: the `--` prefix, the global scope on `:root`, and the `var(--name, fallback)` syntax for safe fallbacks.
:root {--color-brand: #FF4848;--color-text: #0f172a;--color-bg: #ffffff;--space-md: 1rem;--radius-lg: 0.5rem;}.button {background: var(--color-brand);color: var(--color-bg);padding: var(--space-md);border-radius: var(--radius-lg);/* Fallback if the var is unset */border: 1px solid var(--color-border, transparent);}
Design tokens solve three problems hardcoded values can't. First, **consistency** — every "primary" button in the app uses `--color-brand` and updates simultaneously when the brand changes. Second, **handoff** — designers define tokens in Figma, engineers consume the same names in code, and the meaning doesn't drift. Third, **theming** — dark mode, accessibility high-contrast mode, and per-customer brand colors all become an attribute swap instead of a CSS rewrite. Tokens aren't for every value. They're for the cross-cutting decisions a design system needs to enforce: colors, spacing scale, typography scale, radius scale, shadow scale.
Five categories cover most needs. Notice the naming: semantic (`--color-text-muted`) is more durable than literal (`--color-gray-600`).
:root {/* color, semantic naming */--color-bg: #ffffff;--color-bg-muted: #f8fafc;--color-text: #0f172a;--color-text-muted: #64748b;--color-brand: #FF4848;--color-brand-hover: #e03e3e;--color-border: #e2e8f0;/* spacing scale, 4px base */--space-1: 0.25rem; /* 4px */--space-2: 0.5rem; /* 8px */--space-4: 1rem; /* 16px */--space-8: 2rem; /* 32px *//* typography */--font-sm: 0.875rem;--font-base: 1rem;--font-lg: 1.125rem;--font-xl: 1.5rem;/* radii */--radius-md: 0.375rem;--radius-lg: 0.5rem;--radius-xl: 0.75rem;/* shadows */--shadow-sm: 0 1px 2px rgba(0,0,0,0.04);--shadow-md: 0 4px 12px rgba(0,0,0,0.08);}
Because variables are runtime values, you can re-declare them inside any selector and the new values cascade everywhere they're used. The standard pattern is to use a `data-theme` attribute on the `<html>` or `<body>` element. Switching the attribute via JS swaps every token at once — every component, every page, instantly themed. No CSS file rewrite, no component changes, no rebuild.
Declare the dark overrides under the attribute selector. The default declarations on `:root` are the light theme. Toggling `<html data-theme="dark">` swaps every token.
:root {--color-bg: #ffffff;--color-text: #0f172a;--color-brand: #FF4848;}[data-theme="dark"] {--color-bg: #0a1628;--color-text: #f1f5f9;--color-brand: #FF6464;}body {background: var(--color-bg);color: var(--color-text);/* No theme-aware logic here — the variable handles it. */}
Tailwind reads its color palette from `tailwind.config.js`. To make your CSS-variable tokens available as Tailwind utilities (so `bg-brand` works), extend the config to point at the variables instead of hardcoding values. This gives you the best of both: Tailwind's class ergonomics in markup, and a single CSS-variable source of truth that can be swapped at runtime. `theme.extend.colors.brand = 'var(--color-brand)'` is the one-line wire-up.
The starter has the same color literal `#FF4848` repeated in three places and `#0f172a` in two. Define them once on `:root` as `--color-brand` and `--color-text`, then update every selector to use `var(--…)`. Required patterns below check for the variable definitions on `:root` and at least two `var(--color-brand)` usages.
Tokens are for cross-cutting design decisions. Pick the one that's too specific.
Sign in and purchase access to unlock this lesson.