CSS custom properties (CSS variables) are far more powerful than just storing colors. They cascade, inherit, can be updated with JavaScript, and enable dynamic theming without class swapping. Firefox has full support including @property for typed custom properties.
The Basics
:root {
--color-primary: #3B82F6;
--color-primary-dark: #2563EB;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--border-radius: 6px;
--font-body: 'Inter', system-ui, sans-serif;
}
.button {
background: var(--color-primary);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--border-radius);
font-family: var(--font-body);
}
/* Fallback value (used if var is not defined) */
.button-alt {
background: var(--color-secondary, #6B7280); /* fallback: gray */
}
Cascading and Scoping
Custom properties cascade and can be overridden per scope:
/* Global default */
:root { --button-bg: #3B82F6; }
/* Override in dark mode */
@media (prefers-color-scheme: dark) {
:root { --button-bg: #1D4ED8; }
}
/* Override for a specific component */
.card {
--button-bg: #10B981; /* green buttons inside cards */
}
/* The button reads from wherever it is in the DOM */
.button {
background: var(--button-bg);
}
A .button inside .card gets green; outside, it gets blue. This is the cascade in action.
Theming with Custom Properties
The clean way to implement dark mode:
:root {
--bg: #ffffff;
--bg-secondary: #f9fafb;
--text: #111827;
--text-muted: #6B7280;
--border: #E5E7EB;
--card-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
[data-theme="dark"] {
--bg: #111827;
--bg-secondary: #1F2937;
--text: #F9FAFB;
--text-muted: #9CA3AF;
--border: #374151;
--card-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
/* All components use variables — zero style changes needed for dark mode */
body { background: var(--bg); color: var(--text); }
.card { background: var(--bg-secondary); border: 1px solid var(--border); }
// Toggle theme
document.documentElement.dataset.theme = 'dark';
// Clear to reset to light
delete document.documentElement.dataset.theme;
Updating Custom Properties with JavaScript
// Set a variable on the root
document.documentElement.style.setProperty('--spacing-unit', '4px');
// Set on a specific element (overrides for that element's subtree)
element.style.setProperty('--button-bg', '#EF4444');
// Read a variable's computed value
const value = getComputedStyle(document.documentElement)
.getPropertyValue('--color-primary').trim();
This enables dynamic theming without class manipulation:
// Color picker that updates CSS custom property
colorPicker.addEventListener('input', () => {
document.documentElement.style.setProperty('--color-primary', colorPicker.value);
});
Using Custom Properties in Calculations
:root {
--grid-columns: 3;
--grid-gap: 16px;
--base-font-size: 16;
}
.grid {
display: grid;
grid-template-columns: repeat(var(--grid-columns), 1fr);
gap: var(--grid-gap);
}
/* calc() with custom properties */
.sidebar {
width: calc(100% / var(--grid-columns) - var(--grid-gap));
}
/* Convert unitless to rem */
.heading {
font-size: calc(var(--base-font-size) * 1.5 * 1px); /* 24px */
}
Animation with Custom Properties
Animating custom properties is powerful but has a catch: by default, CSS doesn't know the type of a custom property, so it can't interpolate between values.
Without @property (no animation):
:root { --progress: 0; }
/* Can't animate this directly — CSS doesn't know it's a number */
With @property (animatable):
@property --progress {
syntax: '<number>';
inherits: false;
initial-value: 0;
}
.progress-bar {
--progress: 0;
transform: scaleX(calc(var(--progress) / 100));
transition: --progress 0.3s ease;
}
.progress-bar.loaded {
--progress: 75;
}
Now --progress smoothly animates from 0 to 75 because CSS knows it's a number.
@property Types
Firefox 128+ supports @property:
@property --hue {
syntax: '<angle>';
inherits: true;
initial-value: 0deg;
}
@property --opacity {
syntax: '<number>';
inherits: false;
initial-value: 1;
}
@property --color-primary {
syntax: '<color>';
inherits: true;
initial-value: #3B82F6;
}
@property --size {
syntax: '<length>';
inherits: false;
initial-value: 0px;
}
Supported syntax types: <color>, <length>, <angle>, <number>, <integer>, <percentage>, <length-percentage>, <url>, <image>, <transform-list>, <custom-ident>, * (anything).
Component Customization API Pattern
Design systems use custom properties to expose a component's styling "API":
/* Button component — public API via custom properties */
.button {
/* Defaults */
--_btn-bg: var(--button-bg, #3B82F6);
--_btn-text: var(--button-text, #ffffff);
--_btn-radius: var(--button-radius, 6px);
--_btn-padding: var(--button-padding, 8px 16px);
background: var(--_btn-bg);
color: var(--_btn-text);
border-radius: var(--_btn-radius);
padding: var(--_btn-padding);
}
Users of the component can override via:
/* Override globally */
:root { --button-bg: #8B5CF6; } /* purple buttons */
/* Override locally */
.danger-zone { --button-bg: #EF4444; } /* red buttons in danger zone */
No need to add variant classes — the component responds to its environment.
Debugging in Firefox DevTools
Firefox's Inspector shows all applied custom properties:
- Open Inspector → click an element
- In the Computed styles panel: search for
--to filter custom properties - Properties show their resolved value
- Click the value to copy or edit it live
The Styles panel shows var() references with a small tooltip showing the resolved value when you hover.
Top comments (0)