CSS custom properties (variables) let you define values once and reuse or override them. They work in all modern browsers and are part of the CSS standard. No preprocessor required.
Basic syntax
Defining variables
Variables start with two dashes:
:root {
--primary-color: #3b82f6;
--spacing-unit: 8px;
--font-sans: 'Inter', system-ui, sans-serif;
}
Using variables
Use var() to reference a variable:
.button {
background: var(--primary-color);
padding: var(--spacing-unit);
font-family: var(--font-sans);
}
Fallback values
Provide a fallback if the variable is not defined:
.card {
color: var(--text-color, #333);
border-radius: var(--radius, 4px);
}
Scoping and inheritance
CSS custom properties cascade like other CSS properties:
:root {
--accent: blue;
}
.card {
--accent: green;
}
.button {
background: var(--accent);
}
- A
.buttonin the document uses blue (from:root) - A
.buttoninside.carduses green (inherited from.card)
This makes component-specific theming natural.
Theme patterns
Light and dark mode
Define variables for both themes:
:root {
--bg: #ffffff;
--text: #1a1a1a;
--border: #e5e7eb;
--accent: #3b82f6;
}
.dark {
--bg: #1a1a1a;
--text: #f5f5f5;
--border: #374151;
--accent: #60a5fa;
}
Use the variables everywhere:
body {
background: var(--bg);
color: var(--text);
}
.card {
border: 1px solid var(--border);
}
.link {
color: var(--accent);
}
Toggle themes by adding/removing the .dark class on <html> or <body>.
System preference
Use prefers-color-scheme:
:root {
--bg: #ffffff;
--text: #1a1a1a;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1a1a1a;
--text: #f5f5f5;
}
}
Combine with a manual toggle:
:root {
--bg: #ffffff;
--text: #1a1a1a;
}
@media (prefers-color-scheme: dark) {
:root:not(.light) {
--bg: #1a1a1a;
--text: #f5f5f5;
}
}
.dark {
--bg: #1a1a1a;
--text: #f5f5f5;
}
.light {
--bg: #ffffff;
--text: #1a1a1a;
}
Spacing and typography systems
Spacing scale
Define a consistent spacing scale:
:root {
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-6: 24px;
--space-8: 32px;
--space-12: 48px;
--space-16: 64px;
}
.card {
padding: var(--space-4);
margin-bottom: var(--space-6);
}
.section {
padding: var(--space-12) var(--space-4);
}
Typography scale
:root {
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--text-3xl: 1.875rem;
--text-4xl: 2.25rem;
}
h1 {
font-size: var(--text-4xl);
}
.caption {
font-size: var(--text-sm);
}
Responsive adjustments
Change variables at breakpoints:
:root {
--container-padding: var(--space-4);
}
@media (min-width: 768px) {
:root {
--container-padding: var(--space-6);
}
}
@media (min-width: 1024px) {
:root {
--container-padding: var(--space-8);
}
}
.container {
padding-inline: var(--container-padding);
}
One variable change affects all elements using it.
Using calc() with variables
Variables work with calc():
:root {
--spacing: 8px;
}
.card {
padding: calc(var(--spacing) * 2);
margin: calc(var(--spacing) * 3);
}
.header {
height: calc(var(--spacing) * 8);
}
Complex calculations
:root {
--sidebar-width: 250px;
--header-height: 64px;
--gap: 16px;
}
.main {
width: calc(100% - var(--sidebar-width) - var(--gap));
min-height: calc(100vh - var(--header-height));
}
Component-scoped variables
Override variables per component:
.button {
--button-bg: var(--primary-color);
--button-text: white;
--button-padding: var(--space-2) var(--space-4);
background: var(--button-bg);
color: var(--button-text);
padding: var(--button-padding);
}
.button.secondary {
--button-bg: transparent;
--button-text: var(--primary-color);
}
.button.large {
--button-padding: var(--space-3) var(--space-6);
}
This pattern makes variants clean and maintainable.
JavaScript interaction
Read and write CSS variables with JavaScript:
// Read a variable
const root = document.documentElement;
const primaryColor = getComputedStyle(root).getPropertyValue('--primary-color');
// Set a variable
root.style.setProperty('--primary-color', '#10b981');
// Set on a specific element
const card = document.querySelector('.card');
card.style.setProperty('--accent', '#f59e0b');
This enables dynamic theming, user preferences, and animations.
Common patterns
Color with opacity
Define colors without opacity, then apply it:
:root {
--primary-rgb: 59, 130, 246;
}
.overlay {
background: rgb(var(--primary-rgb) / 0.5);
}
.hover-bg:hover {
background: rgb(var(--primary-rgb) / 0.1);
}
Animation values
:root {
--transition-fast: 150ms ease;
--transition-normal: 300ms ease;
--transition-slow: 500ms ease;
}
.button {
transition: all var(--transition-fast);
}
.modal {
transition: opacity var(--transition-normal);
}
Z-index scale
:root {
--z-dropdown: 100;
--z-modal: 200;
--z-tooltip: 300;
--z-toast: 400;
}
.dropdown {
z-index: var(--z-dropdown);
}
.modal {
z-index: var(--z-modal);
}
Best practices
| Practice | Why |
|---|---|
Define on :root for globals |
Accessible everywhere |
| Use semantic names | --primary-color not --blue |
| Group related variables | Colors, spacing, typography |
| Provide fallbacks | Handle undefined variables |
| Keep values simple | Colors, lengths, numbers work best |
| Document your system | Help team members use correctly |
CSS variables vs preprocessor variables
| Aspect | CSS variables | Sass/Less variables |
|---|---|---|
| Runtime | Yes | No (compiled away) |
| JavaScript access | Yes | No |
| Dynamic theming | Yes | No |
| Cascade/inherit | Yes | No |
| Browser support | Modern browsers | Any (compiles to plain CSS) |
Use CSS variables for runtime theming and values that change. Use preprocessor variables for build-time constants if you prefer.
Summary
CSS custom properties enable:
- Reusable values without a preprocessor
- Theme switching with variable overrides
- Scoped customization per component
- Dynamic changes via JavaScript
- Responsive adjustments at breakpoints
Define your design tokens as CSS variables, and your styles become maintainable and themeable.
For tools that work with CSS colors, see the CSS Color Names reference and Gradient Generator on this site.
