Chapter 20: CSS Variables, @property (Houdini), env() & Dark Mode Architecture
CSS Custom Properties (Variables) introduce dynamic value scoping, fallback cascades, and component tokenization to modern web stylesheets. Combined with CSS Houdini @property for typed variable gradient & angle animations, environmental variables (env()), attr() attribute reflections, and system theme architecture (prefers-color-scheme), developers can engineer scalable design systems.
20.1 CSS Custom Properties (--var) & Multi-Level Fallbacks
CSS variables are declared using double dashes (--variable-name) on a target element or globally on :root. They support nested fallback defaults via var(--primary, var(--fallback, #38bdf8)):
| Scope Level | Declaration Example | Cascade & Fallback Rule |
|---|---|---|
| Global Scope | :root { --primary-color: #6366f1; } |
Available across the entire document cascade. |
| Component Scope | .card { --primary-color: #10b981; } |
Overrides global value for .card and its DOM descendants. |
| Nested Fallbacks | color: var(--theme-color, var(--brand-accent, #38bdf8)); |
Evaluates first valid variable in chain before resorting to literal fallback value. |
<!DOCTYPE html>
<html>
<head>
<style>
*, *::before, *::after { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; background: #0f172a; color: #f8fafc; padding: 24px; margin: 0; }
h4 { margin-top: 0; color: #38bdf8; font-size: 14px; }
:root {
--brand-blue: #38bdf8;
--card-padding: 16px;
}
.var-card {
/* Scoped local variable override */
--card-bg: #1e293b;
--card-border: var(--brand-blue);
background: var(--card-bg);
border: 2px solid var(--card-border);
border-radius: 12px;
padding: var(--card-padding);
max-width: 380px;
}
</style>
</head>
<body>
<h4>Scoped Custom Properties & Fallbacks</h4>
<div class="var-card">
<strong style="color:var(--brand-blue);">Token-Driven Component</strong>
<p style="font-size:12px;color:#94a3b8;margin:6px 0 0;">Styled using scoped variables and global :root tokens.</p>
</div>
</body>
</html>
20.2 Typed CSS Variables (@property) & Gradient/Angle Animations
Standard CSS custom properties cannot be animated smoothly because browser engines treat them as untyped raw strings. CSS Houdini @property registers custom properties with strict data types (e.g. syntax: "<color>", "<angle>", "<percentage>"), enabling smooth gradient transitions and rotating borders!
| Syntax Type | Example Syntax String | Animatable Use Case |
|---|---|---|
"<color>" |
syntax: "<color>"; |
Smooth CSS gradient color transition on :hover. |
"<angle>" |
syntax: "<angle>"; |
Rotating linear/conic gradient angle animations ($0^\circ \rightarrow 360^\circ$). |
"<percentage>" |
syntax: "<percentage>"; |
Animatable gradient stop positions and progress indicators. |
"<length>" |
syntax: "<length>"; |
Animatable custom spacing, shadow blurs, or border widths. |
<!DOCTYPE html>
<html>
<head>
<style>
*, *::before, *::after { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; background: #0f172a; color: #f8fafc; padding: 24px; margin: 0; display: flex; flex-direction: column; gap: 20px; }
h4 { margin-top: 0; color: #10b981; font-size: 14px; }
/* 1. Typed Color Variables for Gradient Transition */
@property --c1 { syntax: "<color>"; inherits: false; initial-value: #4f46e5; }
@property --c2 { syntax: "<color>"; inherits: false; initial-value: #06b6d4; }
.grad-btn {
background: linear-gradient(135deg, var(--c1), var(--c2));
color: #fff; border: none; padding: 12px 24px; border-radius: 10px;
font-weight: 700; font-size: 13px; cursor: pointer; max-width: 260px;
transition: --c1 0.5s ease, --c2 0.5s ease;
}
.grad-btn:hover { --c1: #ec4899; --c2: #f59e0b; }
/* 2. Typed Angle Variable for Rotating Gradient Border */
@property --border-angle { syntax: "<angle>"; inherits: false; initial-value: 0deg; }
.g-card {
width: 260px; height: 90px; border-radius: 12px; padding: 2px;
background: conic-gradient(from var(--border-angle), #6366f1, #ec4899, #38bdf8, #6366f1);
animation: rotateBorder 4s linear infinite;
}
.g-card-inner {
background: #1e293b; width: 100%; height: 100%; border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 12px; font-weight: bold; color: #a5b4fc;
}
@keyframes rotateBorder {
to { --border-angle: 360deg; }
}
</style>
</head>
<body>
<div>
<h4>1. @property Gradient Color Transition (Hover Button)</h4>
<button class="grad-btn">Hover for Gradient Morph</button>
</div>
<div>
<h4>2. @property Conic Border Angle Rotation</h4>
<div class="g-card">
<div class="g-card-inner">360° Rotating Gradient Border</div>
</div>
</div>
</body>
</html>
20.3 Environmental Variables (env()) & Dynamic attr() Content
The env() function reads user-agent environment variables provided by the device operating system (e.g. mobile safe area insets). The attr() function extracts HTML attribute values to display dynamic content without hardcoded text:
| Function | Syntax Example | Purpose & Use Case |
|---|---|---|
env() |
padding-top: env(safe-area-inset-top, 20px); |
Reads device environment offsets (notches, home bars) with fallback support. |
attr() |
content: attr(data-tooltip); |
Retrieves element HTML attributes inside CSS ::before or ::after pseudo-elements. |
<!DOCTYPE html>
<html>
<head>
<style>
*, *::before, *::after { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; background: #0f172a; color: #f8fafc; padding: 24px; margin: 0; }
h4 { color: #f59e0b; margin-top: 0; }
/* Tooltip using attr(data-tooltip) */
.tooltip-btn {
position: relative; background: #3b82f6; color: #fff; border: none;
padding: 10px 18px; border-radius: 8px; font-weight: bold; cursor: pointer;
}
.tooltip-btn:hover::after {
content: attr(data-tooltip); /* Pulls text directly from HTML attribute */
position: absolute; bottom: 125%; left: 50%; transform: translateX(-50%);
background: #1e293b; color: #38bdf8; border: 1px solid #38bdf8;
padding: 6px 12px; border-radius: 6px; font-size: 11px; white-space: nowrap;
pointer-events: none; box-shadow: 0 4px 12px rgba(0,0,0,0.4);
}
</style>
</head>
<body>
<h4>Dynamic CSS attr() Tooltip Demo</h4>
<p style="font-size:12px;color:#64748b;">Hover button below to render tooltip generated via CSS attr(data-tooltip):</p>
<button class="tooltip-btn" data-tooltip="⚡ Tooltip powered by CSS attr()!">
Hover Me for Tooltip
</button>
</body>
</html>
20.4 Dynamic Dark / Light Mode Switcher Architecture
Architecting theme switching using CSS variables scoped under [data-theme="dark"] and [data-theme="light"] attributes, combined with system preference detection via prefers-color-scheme:
<!DOCTYPE html>
<html>
<head>
<style>
*, *::before, *::after { box-sizing: border-box; }
/* Default Light Theme Variables */
:root, [data-theme="light"] {
--bg-body: #f8fafc;
--text-main: #0f172a;
--card-bg: #ffffff;
--card-border: #e2e8f0;
--accent: #3b82f6;
}
/* Dark Theme Variable Overrides */
[data-theme="dark"] {
--bg-body: #0f172a;
--text-main: #f8fafc;
--card-bg: #1e293b;
--card-border: #334155;
--accent: #38bdf8;
}
body {
font-family: system-ui, sans-serif;
background: var(--bg-body);
color: var(--text-main);
padding: 24px; margin: 0;
transition: background 0.3s ease, color 0.3s ease;
}
.theme-card {
background: var(--card-bg);
border: 2px solid var(--card-border);
border-radius: 12px; padding: 24px; max-width: 380px; margin: 0 auto;
box-shadow: 0 8px 24px rgba(0,0,0,0.1); text-align: center;
transition: background 0.3s ease, border-color 0.3s ease;
}
.toggle-btn {
background: var(--accent); color: #fff; border: none;
padding: 10px 18px; border-radius: 8px; font-weight: bold; cursor: pointer;
margin-top: 14px; transition: background 0.3s ease;
}
</style>
</head>
<body id="theme-body" data-theme="dark">
<div class="theme-card">
<h4 style="margin-top:0;color:var(--accent);">Dynamic Theme Switcher</h4>
<p style="font-size:12px;">Click button to toggle <code>data-theme</code> attribute between dark and light themes!</p>
<button class="toggle-btn" onclick="const b = document.getElementById('theme-body'); b.dataset.theme = b.dataset.theme === 'dark' ? 'light' : 'dark';">
Toggle Dark / Light Mode 🌓
</button>
</div>
</body>
</html>
20.5 Interactive Sandbox — Dark Mode, @property & attr() Playground
Test theme variable switching, @property angle rotation, and dynamic attr() tooltips live: