CSS custom properties (variables) in real layouts: theming, component tokens, and scoping values where they actually belong. The intro series introduced --name: value; here we use them as part of a system, not scattered one-offs.
Global tokens on :root
Design tokens are named values your whole site shares: colours, spacing steps, font stacks, border radii. Define them on :root so every element can inherit them:
:root {
--color-text: #1a1a1a;
--color-surface: #ffffff;
--color-accent: #0366d6;
--color-muted: #6b7280;
--space-xs: 0.25rem;
--space-sm: 0.5rem;
--space-md: 1rem;
--space-lg: 1.5rem;
--space-xl: 2.5rem;
--font-sans: system-ui, sans-serif;
--font-mono: ui-monospace, monospace;
--radius-sm: 0.25rem;
--radius-md: 0.5rem;
}
Use tokens in component rules instead of raw values. When the brand colour changes, you update one line.
.button {
background: var(--color-accent);
color: var(--color-surface);
padding: var(--space-sm) var(--space-md);
border-radius: var(--radius-sm);
font-family: var(--font-sans);
}
Fallbacks and invalid values
var() accepts a fallback as the second argument. Use it when a token might be missing or when a component defines its own override:
.alert {
background: var(--alert-bg, #fef3cd);
border: 1px solid var(--alert-border, #ffc107);
padding: var(--space-md);
}
Custom properties are live. If you change --alert-bg on a parent element, descendants update instantly. That is different from preprocessor variables, which vanish at compile time.
Scoping tokens on components
Not every variable belongs on :root. Component-scoped tokens keep related values together and avoid polluting the global namespace:
.card {
--card-padding: var(--space-md);
--card-bg: var(--color-surface);
--card-border: 1px solid #e5e7eb;
--card-radius: var(--radius-md);
padding: var(--card-padding);
background: var(--card-bg);
border: var(--card-border);
border-radius: var(--card-radius);
}
.card--compact {
--card-padding: var(--space-sm);
}
.card--featured {
--card-bg: #f0f7ff;
--card-border: 1px solid var(--color-accent);
}
Modifiers change tokens, not every property. The base .card rule stays stable; variants adjust the variables it reads.
Theming with data attributes or classes
Dark mode and brand variants often swap a set of tokens at once. A class on html or body is the usual hook:
:root {
--color-text: #1a1a1a;
--color-surface: #ffffff;
--color-accent: #0366d6;
}
[data-theme="dark"] {
--color-text: #f3f4f6;
--color-surface: #111827;
--color-accent: #60a5fa;
}
Components keep using var(--color-text). The theme block redefines the tokens. No duplicate component CSS for light and dark.
Tokens for layout, not just colour
Spacing scales and type sizes benefit from tokens too. Pair them with clamp() for fluid values (covered in the fluid typography tutorial):
:root {
--text-base: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
--text-lg: clamp(1.25rem, 1.1rem + 0.75vw, 1.5rem);
--section-gap: clamp(2rem, 1.5rem + 2vw, 4rem);
}
.prose {
font-size: var(--text-base);
}
.prose h2 {
font-size: var(--text-lg);
margin-block: var(--section-gap) var(--space-md);
}
Custom properties in calc() and gradients
Because custom properties are substituted at computed-value time, they work inside calc(), transforms, and gradients:
.hero {
--hero-height: 60vh;
--hero-overlay: rgb(0 0 0 / 0.45);
min-height: var(--hero-height);
background:
linear-gradient(var(--hero-overlay), var(--hero-overlay)),
url("hero.jpg") center / cover;
}
You can even animate custom properties in some cases, though prefer animating properties that already accept lengths and colours directly when performance matters.
What not to tokenise
Do not create a variable for every literal in the stylesheet. One-off values – a shadow on a single promo banner, a z-index on one modal – can stay inline. Tokenise values that repeat, that theming must swap, or that designers reference by name in a spec.
Keep token names semantic (--color-text) rather than presentational (--gray-800) when the same token might map to different colours in another theme. Grey scales named by step are fine for a fixed palette; semantic names win for theme switching.
Layers and tokens together
Put token definitions in your base layer and component usage in the components layer. That way resets never accidentally redefine --color-accent, and utilities can reference tokens without redeclaring them:
@layer base {
:root {
--color-accent: #0366d6;
}
}
@layer utilities {
.text-accent {
color: var(--color-accent);
}
}
The next tutorial covers modern selectors – :is(), :where(), and especially :has() – for styling based on structure and state without extra classes everywhere.

