CSS custom properties let you define a value once in :root and reference it across every rule in your stylesheet. Changes propagate everywhere the token is used, which makes theme updates and refactors much faster than find-and-replace. This lesson walks through defining tokens, using them with var(), overriding them by scope to create alternate themes, and providing fallback values when a token may be absent.
What this covers
- Defining shared design tokens in
:root
- Referencing tokens with
var(--token-name) in any declaration
- Scoping overrides on a wrapper class to create alternate themes without rewriting component CSS
var(--token, fallback) syntax for safe defaults when a token is missing
<main class="stack">
<h1>CSS Variables (Custom Properties)</h1>
<p class="hint">Use this lesson when setting up shared design tokens for your project styles.</p>
<p class="callout">Your goal: define reusable variables once, apply them across components, and override them intentionally by scope when needed.</p>
<section class="card stack">
<h2>1) Define Shared Tokens in <code>:root</code></h2>
<ul class="explain-list">
<li>Put project-wide tokens in <code>:root</code> so any selector can access them.</li>
<li>Name tokens by purpose, not by one specific component.</li>
<li>Use these tokens for colors, spacing, and any values repeated across files.</li>
</ul>
<pre class="code-sample"><code>:root {
--bg: #f8fafc;
--ink: #111827;
--line: #d1d5db;
--accent: #0f766e;
}</code></pre>
<div class="token-grid">
<article class="token-card">
<strong><code>--bg</code></strong>
<span class="meta">Background token</span>
<div class="swatch swatch-bg"></div>
</article>
<article class="token-card">
<strong><code>--ink</code></strong>
<span class="meta">Primary text token</span>
<div class="swatch swatch-ink"></div>
</article>
<article class="token-card">
<strong><code>--line</code></strong>
<span class="meta">Border/divider token</span>
<div class="swatch swatch-line"></div>
</article>
<article class="token-card">
<strong><code>--accent</code></strong>
<span class="meta">Action/focus token</span>
<div class="swatch swatch-accent"></div>
</article>
</div>
</section>
<section class="card stack">
<h2>2) Use Tokens with <code>var()</code></h2>
<ul class="explain-list">
<li>Reference a token with <code>var(--token-name)</code> inside any declaration.</li>
<li>This keeps component styles consistent and easier to update later.</li>
</ul>
<pre class="code-sample"><code>body {
background: var(--bg);
color: var(--ink);
}
.panel {
border: 1px solid var(--line);
}
.button-primary {
background: var(--accent);
}</code></pre>
<div class="demo-ui">
<article class="demo-panel">
Reusing tokens keeps visual styles consistent across all components.
<span class="meta">Uses <code>--panel-bg</code>/<code>--panel-line</code> defaults to root tokens.</span>
</article>
<a href="#" class="demo-action">Primary Action</a>
</div>
</section>
<section class="card stack">
<h2>3) Override Tokens by Scope</h2>
<ul class="explain-list">
<li>Variables cascade, so you can redefine tokens on a section, card, or page wrapper.</li>
<li>This lets you create alternate themes without rewriting component CSS.</li>
</ul>
<pre class="code-sample"><code>.theme-green {
--panel-bg: #f0fdf4;
--panel-line: #86efac;
--action-bg: #16a34a;
}</code></pre>
<div class="scope-wrap">
<section class="theme-blue demo-ui">
<article class="demo-panel">
Same markup, blue-scoped variables.
<span class="meta">Local override with <code>.theme-blue</code>.</span>
</article>
<a href="#" class="demo-action">Action</a>
</section>
<section class="theme-green demo-ui">
<article class="demo-panel">
Same markup, green-scoped variables.
<span class="meta">Local override with <code>.theme-green</code>.</span>
</article>
<a href="#" class="demo-action">Action</a>
</section>
</div>
</section>
<section class="card stack">
<h2>4) Use Fallback Values for Safety</h2>
<ul class="explain-list">
<li>If a token may be missing, add a fallback value in <code>var()</code>.</li>
<li>Fallback syntax: <code>var(--token-name, fallback-value)</code>.</li>
</ul>
<pre class="code-sample"><code>.message {
color: var(--missing-ink, #334155);
border-color: var(--missing-line, #94a3b8);
}</code></pre>
<p class="fallback-box">
This box uses fallback values because <code>--missing-ink</code> and <code>--missing-line</code> are not defined.
</p>
</section>
<section class="card stack">
<h2>Quick Self-Check</h2>
<ul class="explain-list">
<li>Can you define global tokens in <code>:root</code> before writing component rules?</li>
<li>Can you explain when to use global tokens vs scoped overrides?</li>
<li>Can you add a fallback value where token definitions may be missing?</li>
</ul>
</section>
</main> /* lesson.css base */
/* Shared design tokens used by all CSS demos.
Project setup pattern: define tokens once in :root, then reuse with var(--token). */
:root {
/* Surface/background colors */
--bg: #f8fafc;
--card: #ffffff;
/* Text colors */
--ink: #111827;
--subtle: #4b5563;
/* Border and accent colors */
--line: #d1d5db;
--accent: #0f766e;
}
/* Make element sizing predictable: width includes border/padding */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* Base page styles reused by each lesson page */
body {
margin: 0;
font-family: "Inter", "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
color: var(--ink);
background: var(--bg);
line-height: 1.5;
}
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
outline: 3px solid var(--accent);
outline-offset: 2px;
}
/* Content container */
main {
max-width: 960px;
margin: 0 auto;
padding: 2rem 1rem 3rem;
}
/* Generic content card */
.card {
background: var(--card);
border: 1px solid var(--line);
border-radius: 10px;
padding: 1rem;
}
/* Vertical rhythm helper: adds top margin to siblings only */
.stack > * + * {
margin-top: 0.75rem;
}
/* Secondary explanatory text */
.hint {
color: var(--subtle);
font-size: 0.95rem;
}
/* Short explanatory callout block for each lesson section */
.callout {
border-left: 3px solid var(--accent);
background: #f0fdfa;
padding: 0.6rem 0.75rem;
border-radius: 6px;
color: #134e4a;
font-size: 0.92rem;
}
/* Bullet list used to explain what each demo element is showing */
.explain-list {
margin: 0;
padding-left: 1.2rem;
}
.explain-list li + li {
margin-top: 0.35rem;
}
/* Inline CSS rule label shown inside demo elements */
.meta {
display: block;
margin-top: 0.3rem;
font-size: 0.72rem;
color: #475569;
}
/* Optional button helper class (not required in every demo) */
.button {
display: inline-block;
border: 0;
border-radius: 8px;
background: var(--accent);
color: #fff;
padding: 0.5rem 0.75rem;
text-decoration: none;
}
/* lesson-specific */
.code-sample {
margin: 0;
background: #0f172a;
color: #e2e8f0;
border-radius: 8px;
padding: 0.8rem;
overflow-x: auto;
font-size: 0.85rem;
line-height: 1.45;
}
.token-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 0.75rem;
}
.token-card {
border: 1px solid var(--line);
border-radius: 8px;
background: #fff;
padding: 0.75rem;
}
.swatch {
margin-top: 0.5rem;
height: 28px;
border-radius: 6px;
border: 1px solid var(--line);
}
.swatch-bg {
background: var(--bg);
}
.swatch-ink {
background: var(--ink);
border-color: #0f172a;
}
.swatch-line {
background: #ffffff;
border-color: var(--line);
border-width: 3px;
}
.swatch-accent {
background: var(--accent);
border-color: #0f766e;
}
.demo-ui {
display: grid;
gap: 0.75rem;
}
.demo-panel {
border: 1px solid var(--panel-line, var(--line));
border-radius: 8px;
background: var(--panel-bg, var(--card));
color: var(--panel-ink, var(--ink));
padding: 0.85rem;
}
.demo-action {
display: inline-block;
border: 0;
border-radius: 8px;
background: var(--action-bg, var(--accent));
color: var(--action-ink, #ffffff);
padding: 0.45rem 0.75rem;
text-decoration: none;
font-weight: 600;
}
.scope-wrap {
display: grid;
gap: 0.75rem;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.theme-blue {
--panel-bg: #eff6ff;
--panel-line: #93c5fd;
--panel-ink: #1e3a8a;
--action-bg: #2563eb;
}
.theme-green {
--panel-bg: #f0fdf4;
--panel-line: #86efac;
--panel-ink: #14532d;
--action-bg: #16a34a;
}
.fallback-box {
border: 1px dashed var(--missing-line, #94a3b8);
color: var(--missing-ink, #334155);
background: #f8fafc;
border-radius: 8px;
padding: 0.75rem;
}