CSS Custom Properties (Variables) Mastery: Dynamic Theming & Beyond
Master CSS Custom Properties from basics to advanced patterns. Covers variable syntax, scoping, fallbacks, design tokens, dark mode theming, JavaScript manipulation, the @property rule, HSL color systems, and component-level variable APIs — all with interactive live previews.
- 1. CSS Flexbox Mastery: The Complete Visual Guide to Flexible Layouts
- 2. CSS Grid Mastery: From Basics to Advanced Layouts
- 3. CSS Custom Properties (Variables) Mastery: Dynamic Theming & Beyond
- 4. CSS Animations & Transitions Mastery: From Hover Effects to Keyframe Sequences
- 5. CSS Selectors Deep Dive: From Basic to :has() & :is()
- 6. CSS Box Model Mastery: Content, Padding, Border & Margin Explained
- 7. CSS Responsive Design Mastery: Mobile-First, clamp(), & Container Queries
- 8. CSS Specificity & Cascade Mastery: How Browsers Resolve Conflicts
- 9. CSS Modern Layout Techniques: Scroll Snap, Subgrid, Logical Properties & More
- 10. CSS Pseudo-Elements Mastery: ::before, ::after, Counters & Decorative Techniques
CSS Custom Properties — commonly called CSS Variables — are one of the most transformative features added to CSS. They let you define reusable values that can be updated dynamically, inherited through the DOM, and even changed with JavaScript — all without a preprocessor like Sass or Less.
Think of CSS Custom Properties as named containers that hold CSS values. You define them once, use them everywhere, and when you change the container's value, every place that references it updates automatically.
In this guide, we'll go from basic variable syntax to advanced patterns like dynamic theming, the @property rule, and runtime animation.
1. Defining and Using Custom Properties
Declaring a Variable
Custom properties always start with two dashes (--):
:root {
--primary-color: #3b82f6;
--spacing: 1rem;
--font-main: 'Inter', sans-serif;
}
Using a Variable
Use the var() function to reference a custom property:
.button {
background: var(--primary-color);
padding: var(--spacing);
font-family: var(--font-main);
}
2. The :root Selector and Scope
Global Variables with :root
The :root selector targets the <html> element and is the conventional place to define global variables:
:root {
--brand-primary: #6366f1;
--brand-secondary: #ec4899;
--text-color: #1e293b;
--bg-color: #ffffff;
}
Scoped Variables
Custom properties follow CSS inheritance. You can redefine a variable on any element, and all its children will use the new value:
:root {
--accent: #3b82f6; /* Global default = blue */
}
.danger-zone {
--accent: #ef4444; /* Override = red for this section */
}
.button {
background: var(--accent); /* Uses whatever --accent is in scope */
}
Key Insight: This scoping behavior is what makes custom properties far more powerful than Sass variables. Sass variables are compile-time only; CSS custom properties are live in the browser and can be dynamically overridden by any ancestor.
3. Fallback Values
The var() function accepts a fallback as the second argument:
.card {
background: var(--card-bg, #ffffff);
/* If --card-bg is not defined, use #ffffff */
}
Nested Fallbacks
You can chain fallbacks:
.card {
color: var(--card-text, var(--text-color, #000000));
/* Try --card-text → then --text-color → then #000000 */
}
Pro Tip: Always provide meaningful fallbacks for custom properties that might not be defined in every context. This makes your components more portable and resilient.
4. CSS Variables vs Preprocessor Variables
| Feature | CSS Custom Properties | Sass/Less Variables |
|---|---|---|
| Evaluated at | Runtime (in the browser) | Compile time (build step) |
| Can be changed dynamically | ✅ Yes (JS, media queries, hover) | ❌ No |
| Cascade & inherit | ✅ Yes (follow CSS rules) | ❌ No (flat scope) |
| Available in DevTools | ✅ Yes (inspect & modify) | ❌ No |
| Require build tool | ❌ No | ✅ Yes |
| Conditional per media query | ✅ Yes | ❌ No |
5. Building a Design Token System
Design tokens are the "atoms" of your design system — colors, spacing, typography, and shadows defined as CSS custom properties:
:root {
/* Colors */
--color-primary: #6366f1;
--color-primary-light: #818cf8;
--color-primary-dark: #4f46e5;
--color-surface: #ffffff;
--color-text: #1e293b;
--color-text-muted: #64748b;
/* Spacing Scale */
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */
/* Typography */
--font-sans: 'Inter', system-ui, sans-serif;
--font-mono: 'Fira Code', monospace;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
/* Border Radius */
--radius-sm: 0.25rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-full: 9999px;
}
6. Dark Mode with Custom Properties
This is one of the most popular use cases. Define two sets of values — one for light mode, one for dark mode — and toggle them with a class or media query:
Method 1: Class Toggle
:root {
--bg: #ffffff;
--text: #1e293b;
--surface: #f8fafc;
--border: #e2e8f0;
}
.dark {
--bg: #0f172a;
--text: #e2e8f0;
--surface: #1e293b;
--border: #334155;
}
body {
background: var(--bg);
color: var(--text);
}
Method 2: Media Query (System Preference)
:root {
--bg: #ffffff;
--text: #1e293b;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f172a;
--text: #e2e8f0;
}
}
7. Using Variables in calc()
Custom properties work seamlessly with calc():
:root {
--sidebar-width: 250px;
--gap: 1rem;
}
.main-content {
width: calc(100% - var(--sidebar-width) - var(--gap));
}
Dynamic Spacing Multiplier
:root {
--space-unit: 0.25rem;
}
.padding-4 { padding: calc(var(--space-unit) * 4); } /* 1rem */
.padding-8 { padding: calc(var(--space-unit) * 8); } /* 2rem */
.padding-16 { padding: calc(var(--space-unit) * 16); } /* 4rem */
8. Responsive Variables with Media Queries
You can change custom property values at different breakpoints:
:root {
--container-padding: 1rem;
--heading-size: 1.5rem;
--grid-columns: 1;
}
@media (min-width: 768px) {
:root {
--container-padding: 2rem;
--heading-size: 2rem;
--grid-columns: 2;
}
}
@media (min-width: 1024px) {
:root {
--container-padding: 3rem;
--heading-size: 2.5rem;
--grid-columns: 3;
}
}
.container {
padding: var(--container-padding);
}
h1 {
font-size: var(--heading-size);
}
.grid {
grid-template-columns: repeat(var(--grid-columns), 1fr);
}
Pro Tip: This pattern is incredibly powerful — you change values in one place (the media query), and every element that uses those variables adapts automatically.
9. Manipulating Variables with JavaScript
Custom properties can be read and written from JavaScript — making them the bridge between CSS and JS:
Reading a Variable
const root = document.documentElement;
const primaryColor = getComputedStyle(root).getPropertyValue('--primary-color');
console.log(primaryColor); // "#3b82f6"
Writing a Variable
document.documentElement.style.setProperty('--primary-color', '#ef4444');
// Every element using --primary-color instantly updates!
Scoped Changes
const card = document.querySelector('.card');
card.style.setProperty('--card-bg', '#fef3c7');
// Only this card and its children are affected
Use Case: Tracking Mouse Position
.card {
--mouse-x: 50%;
--mouse-y: 50%;
background: radial-gradient(
circle at var(--mouse-x) var(--mouse-y),
rgba(255, 255, 255, 0.15),
transparent 50%
);
}
card.addEventListener('mousemove', (e) => {
const rect = card.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
card.style.setProperty('--mouse-x', x + '%');
card.style.setProperty('--mouse-y', y + '%');
});
10. The @property Rule: Typed Variables
The @property rule (part of CSS Houdini) lets you define the type of a custom property. This enables CSS to understand what the value represents — and crucially, enables transitions and animations on custom properties.
@property --gradient-angle {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
.gradient-box {
background: linear-gradient(
var(--gradient-angle),
#6366f1,
#ec4899
);
transition: --gradient-angle 0.5s ease;
}
.gradient-box:hover {
--gradient-angle: 180deg;
}
Why @property Matters
Without @property, CSS doesn't know that --gradient-angle is an angle, so it can't animate it. With @property, the browser understands the type and can interpolate between values smoothly.
Supported Syntax Types
| Syntax | Description | Example |
|---|---|---|
<number> | Any number | 42, 3.14 |
<integer> | Whole numbers only | 1, 100 |
<length> | Length with unit | 10px, 2rem |
<percentage> | Percentage value | 50% |
<color> | Any color value | #ff0000, rgb() |
<angle> | An angle value | 45deg, 0.5turn |
<length-percentage> | Length or percentage | 10px, 50% |
11. Component-Level Variables
Custom properties are perfect for building reusable, configurable components:
.btn {
--btn-bg: #3b82f6;
--btn-text: #ffffff;
--btn-padding: 0.5rem 1.25rem;
--btn-radius: 0.5rem;
background: var(--btn-bg);
color: var(--btn-text);
padding: var(--btn-padding);
border-radius: var(--btn-radius);
border: none;
font-weight: 600;
cursor: pointer;
}
/* Variant: just override the variables */
.btn-danger {
--btn-bg: #ef4444;
}
.btn-success {
--btn-bg: #10b981;
}
.btn-outline {
--btn-bg: transparent;
--btn-text: #3b82f6;
border: 2px solid #3b82f6;
}
.btn-lg {
--btn-padding: 0.75rem 2rem;
--btn-radius: 0.75rem;
font-size: 1.1rem;
}
12. Advanced Patterns
Pattern 1: HSL Color System
Using HSL with custom properties gives you incredible flexibility:
:root {
--hue: 230;
--sat: 80%;
--color-primary: hsl(var(--hue), var(--sat), 55%);
--color-primary-light: hsl(var(--hue), var(--sat), 70%);
--color-primary-dark: hsl(var(--hue), var(--sat), 40%);
--color-primary-bg: hsl(var(--hue), var(--sat), 95%);
}
/* Change the ENTIRE color scheme by changing ONE value */
.theme-warm {
--hue: 15; /* Switches everything to warm orange */
}
Pattern 2: Contextual Spacing
:root {
--density: 1; /* 1 = normal, 0.75 = compact, 1.25 = comfortable */
}
.card {
padding: calc(1rem * var(--density));
gap: calc(0.75rem * var(--density));
font-size: calc(1rem * var(--density));
}
.compact {
--density: 0.75;
}
.comfortable {
--density: 1.25;
}
Pattern 3: Stacking with Counters
.card-stack {
--stack-offset: 4px;
}
.card-stack > :nth-child(1) { transform: translateY(0); }
.card-stack > :nth-child(2) { transform: translateY(var(--stack-offset)); }
.card-stack > :nth-child(3) { transform: translateY(calc(var(--stack-offset) * 2)); }
13. Common Mistakes & Gotchas
Mistake 1: Missing Fallback Values
/* ❌ No fallback — if --color is undefined, invalid value */
.box { background: var(--color); }
/* ✅ Safe with fallback */
.box { background: var(--color, #3b82f6); }
Mistake 2: Variables Don't Work in Media Queries
:root {
--breakpoint: 768px;
}
/* ❌ This does NOT work — media queries don't support var() */
@media (min-width: var(--breakpoint)) {
/* ... */
}
Custom properties can be changed inside media queries, but they cannot be used in media query conditions.
Mistake 3: Trying to Animate Default Custom Properties
/* ❌ Won't animate — CSS doesn't know the type */
:root { --my-color: #3b82f6; }
.box {
background: var(--my-color);
transition: --my-color 0.3s; /* Ignored! */
}
/* ✅ Use @property to define the type */
@property --my-color {
syntax: '<color>';
inherits: false;
initial-value: #3b82f6;
}
Mistake 4: Using Variables for Property Names
/* ❌ Variables can store values, not property names */
:root { --prop: background; }
.box { var(--prop): red; } /* Syntax error */
Conclusion
CSS Custom Properties are the foundation of modern CSS architecture:
--name: value— Define reusable, dynamic valuesvar(--name, fallback)— Use variables with safe fallbacks- Scoped overriding — Redefine variables for specific sections
- Dark mode — Swap entire themes by toggling variable sets
calc()integration — Dynamic calculations with variables- JavaScript bridge — Read and write variables from JS
@property— Type-safe variables that can be animated- Component APIs — Build configurable, variant-driven components
Custom properties replace the need for CSS preprocessor variables in most use cases, and they offer capabilities that preprocessors simply cannot match — like runtime changes, inheritance, and animation.
🧠 Test Your Knowledge
Now that you've learned the concepts, let's see if you can apply them! Take this quick quiz to test your understanding.