CSS Variables & Custom Properties
Master CSS Variables (Custom Properties) to create dynamic, maintainable, and themeable stylesheets. Learn scoping, inheritance, JavaScript manipulation, and advanced patterns.
Introduction
CSS Custom Properties (commonly called CSS Variables) allow you to store values that can be reused throughout your CSS. They're dynamic, can be changed at runtime with JavaScript, and follow the cascade and inheritance rules of CSS.
/* Define a variable */
:root {
--primary-color: #0066ff;
--spacing: 1rem;
}
/* Use the variable */
.button {
background: var(--primary-color);
padding: var(--spacing);
}
Key Benefits:
- Reduce repetition and improve maintainability
- Create dynamic themes that update in real-time
- Manipulate values with JavaScript
- Scope variables to specific components
- Use with calc() for dynamic calculations
Declaring Variables
CSS Variables are declared using the --
prefix and can be defined on any selector.
Global Variables
/* Global variables using :root pseudo-class */
:root {
--primary-color: #0066ff;
--secondary-color: #6366f1;
--text-color: #1a1a1a;
--background: #ffffff;
/* Spacing system */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 2rem;
--spacing-xl: 4rem;
/* Typography */
--font-sans: system-ui, -apple-system, sans-serif;
--font-mono: 'Courier New', monospace;
--font-size-base: 16px;
--line-height: 1.5;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
/* Border radius */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 16px;
--radius-full: 9999px;
}
Component-Scoped Variables
/* Scoped to a specific component */
.card {
--card-padding: 1.5rem;
--card-bg: white;
--card-border: 1px solid #e5e5e5;
--card-radius: 8px;
padding: var(--card-padding);
background: var(--card-bg);
border: var(--card-border);
border-radius: var(--card-radius);
}
/* Override variables for variants */
.card.large {
--card-padding: 2.5rem;
--card-radius: 12px;
}
.card.dark {
--card-bg: #1a1a1a;
--card-border: 1px solid #333;
}
Naming Conventions
:root {
/* By category */
--color-primary: #0066ff;
--color-secondary: #6366f1;
--color-success: #10b981;
--color-warning: #f59e0b;
--color-danger: #ef4444;
/* By scale */
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-200: #e5e7eb;
--gray-500: #6b7280;
--gray-900: #111827;
/* By usage */
--text-primary: var(--gray-900);
--text-secondary: var(--gray-500);
--bg-page: var(--gray-50);
--border-color: var(--gray-200);
}
Using Variables
Access custom properties using the var()
function.
Basic Usage
:root {
--primary: #0066ff;
--padding: 1rem;
}
/* Use with var() */
.button {
background: var(--primary);
padding: var(--padding);
color: white;
}
/* Use in multiple properties */
.card {
border: 2px solid var(--primary);
box-shadow: 0 4px 6px var(--primary);
}
Variables in Shorthand Properties
:root {
--border-width: 2px;
--border-color: #0066ff;
--border-style: solid;
}
.element {
/* Individual properties */
border: var(--border-width) var(--border-style) var(--border-color);
/* Padding shorthand */
padding: var(--spacing-sm) var(--spacing-md);
/* Background shorthand */
background: var(--primary) url('pattern.png') repeat;
}
Variables in Values
:root {
--hue: 210;
--saturation: 100%;
--lightness: 50%;
}
.element {
/* HSL color */
color: hsl(var(--hue), var(--saturation), var(--lightness));
/* RGB with alpha */
--r: 0;
--g: 102;
--b: 255;
background: rgb(var(--r), var(--g), var(--b));
border-color: rgba(var(--r), var(--g), var(--b), 0.5);
}
/* Transform values */
.transform {
--rotate: 45deg;
--scale: 1.5;
transform: rotate(var(--rotate)) scale(var(--scale));
}
Scope & Inheritance
CSS Variables follow the cascade and are inherited by child elements.
How Inheritance Works
/* Global scope */
:root {
--color: blue;
}
/* Available to all elements */
.child {
color: var(--color); /* blue */
}
/* Override in a specific element */
.parent {
--color: red;
}
/* Children inherit the overridden value */
.parent .child {
color: var(--color); /* red */
}
/* Override again in grandchild */
.parent .child .grandchild {
--color: green;
color: var(--color); /* green */
}
Scoping to Components
/* Each component has its own variables */
.card {
--spacing: 1.5rem;
--bg: white;
padding: var(--spacing);
background: var(--bg);
}
.sidebar {
--spacing: 1rem;
--bg: #f5f5f5;
padding: var(--spacing);
background: var(--bg);
}
/* Variables don't leak between components */
.card .item {
margin: var(--spacing); /* Uses card's spacing */
}
.sidebar .item {
margin: var(--spacing); /* Uses sidebar's spacing */
}
Preventing Inheritance
/* Reset to initial value */
.no-inherit {
--color: initial;
color: var(--color); /* Uses browser default */
}
/* Use a specific fallback */
.override {
color: var(--color, black); /* Falls back to black */
}
Fallback Values
Provide fallback values in case a variable is not defined.
Basic Fallbacks
/* If --primary is not defined, use blue */
.element {
color: var(--primary, blue);
background: var(--bg-color, #ffffff);
padding: var(--spacing, 1rem);
}
/* Multiple fallbacks */
.nested {
/* Try --brand, then --primary, then blue */
color: var(--brand, var(--primary, blue));
}
/* Fallback with calc() */
.calculated {
width: var(--width, calc(100% - 2rem));
}
Complex Fallbacks
/* Fallback with multiple values */
.element {
padding: var(--padding, 1rem 2rem);
border: var(--border, 2px solid #ccc);
box-shadow: var(--shadow, 0 2px 4px rgba(0,0,0,0.1));
}
/* Fallback with color functions */
.color-fallback {
background: var(--bg, hsl(210, 100%, 50%));
color: var(--text, rgb(0, 0, 0));
}
/* Fallback with gradients */
.gradient {
background: var(
--gradient,
linear-gradient(135deg, #667eea 0%, #764ba2 100%)
);
}
Chain of Fallbacks
:root {
--color-brand: #0066ff;
}
.element {
/* Try in order: theme → brand → fallback */
color: var(
--color-theme,
var(--color-brand,
var(--color-primary, #333))
);
}
/* Fallback to another variable or value */
.button {
background: var(--btn-bg, var(--primary, #0066ff));
color: var(--btn-text, var(--text-light, white));
}
Calculations with Variables
Combine CSS Variables with calc()
and other CSS functions for dynamic values.
Math with calc()
:root {
--base-spacing: 1rem;
--multiplier: 2;
}
.element {
/* Multiplication */
margin: calc(var(--base-spacing) * 2);
padding: calc(var(--base-spacing) * var(--multiplier));
/* Addition and subtraction */
width: calc(100% - var(--sidebar-width));
height: calc(100vh - var(--header-height) - var(--footer-height));
/* Division */
font-size: calc(var(--base-font-size) / 1.5);
/* Complex expressions */
margin: calc(
(var(--container-width) - var(--content-width)) / 2
);
}
Responsive Calculations
:root {
--min-size: 1rem;
--max-size: 3rem;
--preferred-size: 2.5vw;
}
/* Fluid typography */
.heading {
font-size: clamp(
var(--min-size),
var(--preferred-size),
var(--max-size)
);
}
/* Responsive spacing */
:root {
--spacing-unit: 0.5rem;
--spacing-scale: 1;
}
@media (min-width: 768px) {
:root {
--spacing-scale: 1.5;
}
}
.element {
padding: calc(var(--spacing-unit) * var(--spacing-scale));
}
Color Manipulation
:root {
--hue: 210;
--saturation: 100%;
--lightness: 50%;
}
.element {
/* Base color */
background: hsl(var(--hue), var(--saturation), var(--lightness));
/* Lighter variant */
border-color: hsl(
var(--hue),
var(--saturation),
calc(var(--lightness) + 20%)
);
/* Darker variant */
color: hsl(
var(--hue),
var(--saturation),
calc(var(--lightness) - 30%)
);
}
/* RGB channel manipulation */
:root {
--r: 0;
--g: 102;
--b: 255;
}
.transparent {
background: rgba(var(--r), var(--g), var(--b), 0.5);
}
With Other Functions
/* With min() and max() */
.element {
width: min(var(--max-width), 100%);
height: max(var(--min-height), 50vh);
}
/* With clamp() */
.responsive {
font-size: clamp(
var(--min-font),
calc(var(--base-font) * var(--scale)),
var(--max-font)
);
}
/* With gradients */
:root {
--gradient-start: #667eea;
--gradient-end: #764ba2;
--gradient-angle: 135deg;
}
.gradient {
background: linear-gradient(
var(--gradient-angle),
var(--gradient-start),
var(--gradient-end)
);
}
Theming
CSS Variables make it easy to create and switch between themes.
Dark Mode
/* Default light theme */
:root {
--bg-primary: #ffffff;
--bg-secondary: #f5f5f5;
--text-primary: #1a1a1a;
--text-secondary: #666666;
--border-color: #e5e5e5;
}
/* Dark theme */
:root[data-theme="dark"],
.dark-mode {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--text-primary: #ffffff;
--text-secondary: #a0a0a0;
--border-color: #404040;
}
/* System preference */
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--text-primary: #ffffff;
--text-secondary: #a0a0a0;
--border-color: #404040;
}
}
/* Apply variables */
body {
background: var(--bg-primary);
color: var(--text-primary);
}
.card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
}
This is a theme demo. Click the button to toggle between light and dark modes.
Multiple Themes
/* Base variables */
:root {
--primary: #0066ff;
--secondary: #6366f1;
}
/* Ocean theme */
[data-theme="ocean"] {
--primary: #0ea5e9;
--secondary: #06b6d4;
--accent: #14b8a6;
}
/* Forest theme */
[data-theme="forest"] {
--primary: #10b981;
--secondary: #059669;
--accent: #84cc16;
}
/* Sunset theme */
[data-theme="sunset"] {
--primary: #f59e0b;
--secondary: #ef4444;
--accent: #ec4899;
}
/* Apply theme */
.button {
background: var(--primary);
border: 2px solid var(--secondary);
}
.highlight {
color: var(--accent);
}
Component Theming
/* Button component with theme variables */
.btn {
--btn-bg: var(--color-primary, #0066ff);
--btn-text: white;
--btn-hover: var(--color-primary-dark, #0052cc);
--btn-padding: 0.75rem 1.5rem;
--btn-radius: 6px;
background: var(--btn-bg);
color: var(--btn-text);
padding: var(--btn-padding);
border-radius: var(--btn-radius);
border: none;
cursor: pointer;
transition: background 0.2s;
}
.btn:hover {
background: var(--btn-hover);
}
/* Button variants */
.btn-success {
--btn-bg: var(--color-success, #10b981);
--btn-hover: var(--color-success-dark, #059669);
}
.btn-danger {
--btn-bg: var(--color-danger, #ef4444);
--btn-hover: var(--color-danger-dark, #dc2626);
}
JavaScript Manipulation
Read and modify CSS Variables dynamically with JavaScript.
Getting Variable Values
// Get computed value of a variable
const root = document.documentElement;
const primaryColor = getComputedStyle(root)
.getPropertyValue('--primary-color');
console.log(primaryColor); // "#0066ff"
// Get from specific element
const card = document.querySelector('.card');
const cardPadding = getComputedStyle(card)
.getPropertyValue('--card-padding');
// Get and trim whitespace
const value = getComputedStyle(root)
.getPropertyValue('--spacing')
.trim();
Setting Variable Values
// Set on root element
document.documentElement.style
.setProperty('--primary-color', '#ff0066');
// Set on specific element
const element = document.querySelector('.card');
element.style.setProperty('--card-bg', '#f5f5f5');
// Set multiple variables
const setTheme = (theme) => {
const root = document.documentElement;
root.style.setProperty('--primary', theme.primary);
root.style.setProperty('--secondary', theme.secondary);
root.style.setProperty('--bg', theme.background);
};
// Use an object for theme
const darkTheme = {
primary: '#4d94ff',
secondary: '#7c3aed',
background: '#1a1a1a'
};
setTheme(darkTheme);
Interactive Examples
// Color picker
const colorPicker = document.querySelector('#colorPicker');
colorPicker.addEventListener('input', (e) => {
document.documentElement.style
.setProperty('--primary-color', e.target.value);
});
// Range slider
const sizeSlider = document.querySelector('#sizeSlider');
sizeSlider.addEventListener('input', (e) => {
document.documentElement.style
.setProperty('--font-size', `${e.target.value}px`);
});
// Theme toggle
const toggleTheme = () => {
const root = document.documentElement;
const isDark = root.getAttribute('data-theme') === 'dark';
root.setAttribute('data-theme', isDark ? 'light' : 'dark');
};
// Scroll-based variable
window.addEventListener('scroll', () => {
const scrollPercent = window.scrollY /
(document.body.scrollHeight - window.innerHeight);
document.documentElement.style
.setProperty('--scroll-progress', scrollPercent);
});
Interactive Demo
Change the values below to see CSS Variables update in real-time:
Animation with Variables
// Animate a variable over time
let hue = 0;
function animateColor() {
hue = (hue + 1) % 360;
document.documentElement.style
.setProperty('--animated-hue', hue);
requestAnimationFrame(animateColor);
}
animateColor();
/* Use the animated variable in CSS */
.animated-element {
background: hsl(var(--animated-hue), 70%, 60%);
transition: background 0.1s linear;
}
Responsive Design
Use CSS Variables to create responsive layouts that adapt to different screen sizes.
Breakpoint-Based Values
/* Base (mobile) values */
:root {
--container-padding: 1rem;
--heading-size: 2rem;
--grid-columns: 1;
--gap: 1rem;
}
/* Tablet */
@media (min-width: 768px) {
:root {
--container-padding: 2rem;
--heading-size: 2.5rem;
--grid-columns: 2;
--gap: 1.5rem;
}
}
/* Desktop */
@media (min-width: 1024px) {
:root {
--container-padding: 3rem;
--heading-size: 3rem;
--grid-columns: 3;
--gap: 2rem;
}
}
/* Apply responsive variables */
.container {
padding: var(--container-padding);
}
h1 {
font-size: var(--heading-size);
}
.grid {
display: grid;
grid-template-columns: repeat(var(--grid-columns), 1fr);
gap: var(--gap);
}
Responsive Demo
This element scales based on viewport size using CSS Variables. Resize your browser to see it change.
Container Queries
/* Define container */
.card-container {
container-type: inline-size;
}
/* Default card variables */
.card {
--card-columns: 1;
--card-font-size: 1rem;
--card-padding: 1rem;
}
/* Adjust variables based on container size */
@container (min-width: 400px) {
.card {
--card-columns: 2;
--card-font-size: 1.125rem;
}
}
@container (min-width: 600px) {
.card {
--card-columns: 3;
--card-font-size: 1.25rem;
--card-padding: 1.5rem;
}
}
/* Apply container-responsive variables */
.card {
display: grid;
grid-template-columns: repeat(var(--card-columns), 1fr);
font-size: var(--card-font-size);
padding: var(--card-padding);
}
Fluid Scaling System
/* Fluid type scale */
:root {
--fluid-min-width: 320;
--fluid-max-width: 1200;
--fluid-min-size: 14;
--fluid-max-size: 18;
--fluid-min-ratio: 1.2;
--fluid-max-ratio: 1.333;
/* Calculated fluid value */
--fluid-bp: calc(
(var(--fluid-max-width) - var(--fluid-min-width)) * 1px
);
--fluid-size: calc(
(var(--fluid-min-size) * 1px) +
(var(--fluid-max-size) - var(--fluid-min-size)) *
((100vw - (var(--fluid-min-width) * 1px)) / var(--fluid-bp))
);
}
body {
font-size: clamp(
calc(var(--fluid-min-size) * 1px),
var(--fluid-size),
calc(var(--fluid-max-size) * 1px)
);
}
Viewport-Based Variables
:root {
--vh: 1vh;
--vw: 1vw;
}
/* JavaScript to handle mobile viewport issues */
/*
function setVhVariable() {
const vh = window.innerHeight * 0.01;
document.documentElement.style
.setProperty('--vh', `${vh}px`);
}
window.addEventListener('resize', setVhVariable);
setVhVariable();
*/
/* Use the corrected viewport height */
.full-height {
height: calc(var(--vh, 1vh) * 100);
}
/* Responsive spacing based on viewport */
:root {
--spacing-fluid: clamp(1rem, 5vw, 3rem);
--max-width: min(90vw, 1200px);
}
.section {
padding: var(--spacing-fluid);
max-width: var(--max-width);
margin: 0 auto;
}
Common Patterns
Practical patterns and techniques for using CSS Variables effectively.
Design Token System
/* Core tokens */
:root {
/* Colors */
--color-blue-50: #eff6ff;
--color-blue-500: #3b82f6;
--color-blue-900: #1e3a8a;
/* Spacing scale */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-6: 1.5rem;
--space-8: 2rem;
/* Typography scale */
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
}
/* Semantic tokens (reference core tokens) */
:root {
--color-primary: var(--color-blue-500);
--color-bg: var(--color-blue-50);
--spacing-component: var(--space-4);
--spacing-section: var(--space-8);
--font-size-body: var(--text-base);
--font-size-heading: var(--text-2xl);
}
State Management
/* Component with state variables */
.button {
--btn-bg: var(--color-primary);
--btn-border: var(--btn-bg);
--btn-text: white;
background: var(--btn-bg);
border: 2px solid var(--btn-border);
color: var(--btn-text);
transition: all 0.2s;
}
/* Hover state */
.button:hover {
--btn-bg: var(--color-primary-dark);
}
/* Active state */
.button:active {
--btn-bg: var(--color-primary-darker);
}
/* Disabled state */
.button:disabled {
--btn-bg: var(--color-gray-300);
--btn-text: var(--color-gray-500);
cursor: not-allowed;
}
/* Focus state */
.button:focus-visible {
--btn-border: var(--color-focus);
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
Utility Classes with Variables
/* Spacing utilities */
.p-1 { padding: var(--space-1); }
.p-2 { padding: var(--space-2); }
.p-4 { padding: var(--space-4); }
.p-8 { padding: var(--space-8); }
.m-1 { margin: var(--space-1); }
.m-2 { margin: var(--space-2); }
.m-4 { margin: var(--space-4); }
.m-8 { margin: var(--space-8); }
/* Color utilities */
.text-primary { color: var(--color-primary); }
.text-secondary { color: var(--color-secondary); }
.bg-primary { background: var(--color-primary); }
.bg-secondary { background: var(--color-secondary); }
/* Typography utilities */
.text-xs { font-size: var(--text-xs); }
.text-sm { font-size: var(--text-sm); }
.text-lg { font-size: var(--text-lg); }
.text-xl { font-size: var(--text-xl); }
Modifier Pattern
/* Base component */
.card {
--card-padding: var(--space-4);
--card-gap: var(--space-3);
--card-radius: var(--radius-md);
--card-bg: white;
--card-shadow: var(--shadow-sm);
padding: var(--card-padding);
gap: var(--card-gap);
border-radius: var(--card-radius);
background: var(--card-bg);
box-shadow: var(--card-shadow);
}
/* Size modifiers */
.card--sm { --card-padding: var(--space-2); }
.card--lg { --card-padding: var(--space-6); }
/* Style modifiers */
.card--elevated { --card-shadow: var(--shadow-lg); }
.card--outlined {
--card-shadow: none;
border: 1px solid var(--color-border);
}
/* Color modifiers */
.card--primary { --card-bg: var(--color-primary); }
.card--secondary { --card-bg: var(--color-secondary); }
Progressive Enhancement
/* Provide fallbacks for older browsers */
.element {
/* Fallback for browsers without custom properties */
background: #0066ff;
/* Modern browsers use the variable */
background: var(--primary-color, #0066ff);
}
/* Feature detection */
@supports (--css: variables) {
.element {
background: var(--primary-color);
}
}
/* Or check for specific features */
@supports (backdrop-filter: blur(10px)) {
.glass {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(var(--blur-amount, 10px));
}
}
Performance & Best Practices
Tips for using CSS Variables efficiently and avoiding common pitfalls.
Performance Considerations
✓ Good Practices:
- CSS Variables are very performant for theme switching
- Changing a variable is faster than changing multiple properties
- Variables don't trigger layout recalculations
- Scoped variables reduce cascade complexity
/* ✓ Good: Change one variable */
.theme-switcher {
--primary: #0066ff;
}
.theme-switcher.dark {
--primary: #4d94ff;
}
/* All elements using --primary update automatically */
.button { background: var(--primary); }
.link { color: var(--primary); }
.border { border-color: var(--primary); }
/* ✗ Avoid: Changing many properties individually */
.dark-theme .button { background: #4d94ff; }
.dark-theme .link { color: #4d94ff; }
.dark-theme .border { border-color: #4d94ff; }
Naming Conventions
/* ✓ Good: Clear, semantic names */
:root {
--color-primary: #0066ff;
--spacing-large: 2rem;
--border-radius-button: 6px;
}
/* ✓ Good: Grouped by category */
:root {
/* Colors */
--color-brand-primary: #0066ff;
--color-brand-secondary: #6366f1;
/* Spacing */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
/* Typography */
--font-size-body: 1rem;
--font-size-heading: 2rem;
}
/* ✗ Avoid: Vague or cryptic names */
:root {
--c1: #0066ff;
--s: 2rem;
--br: 6px;
}
Organization
/* Organize variables in a logical hierarchy */
/* 1. Base tokens (raw values) */
:root {
--blue-50: #eff6ff;
--blue-500: #3b82f6;
--blue-900: #1e3a8a;
--space-1: 0.25rem;
--space-4: 1rem;
}
/* 2. Semantic tokens (reference base) */
:root {
--color-primary: var(--blue-500);
--color-text: var(--blue-900);
--spacing-default: var(--space-4);
}
/* 3. Component tokens (reference semantic) */
.button {
--btn-bg: var(--color-primary);
--btn-padding: var(--spacing-default);
}
/* This creates a maintainable cascade:
component → semantic → base */
Common Pitfalls
/* ✗ Don't use quotes around values */
:root {
--spacing: "1rem"; /* Wrong */
--spacing: 1rem; /* Correct */
}
/* ✗ Don't concatenate strings */
:root {
--base-url: "/images/";
}
.element {
background: url(var(--base-url) + "photo.jpg"); /* Wrong */
}
/* ✓ Include full value in variable */
:root {
--bg-image: url("/images/photo.jpg");
}
.element {
background: var(--bg-image); /* Correct */
}
/* ✗ Don't forget fallbacks for critical properties */
.element {
color: var(--text-color); /* Risky */
}
/* ✓ Always provide fallbacks */
.element {
color: var(--text-color, #333); /* Safe */
}
Browser Support
Browser Support: CSS Variables are supported in all modern browsers including Chrome, Firefox, Safari, Edge, and Opera. Internet Explorer does not support CSS Variables.
- Chrome/Edge: ✓ Supported (v49+)
- Firefox: ✓ Supported (v31+)
- Safari: ✓ Supported (v9.1+)
- Opera: ✓ Supported (v36+)
- Internet Explorer: ✗ Not Supported
/* Graceful degradation for older browsers */
.element {
/* Static fallback */
color: #0066ff;
/* Variable for modern browsers */
color: var(--primary-color);
}
/* Or use @supports */
.element {
color: #0066ff;
}
@supports (--css: variables) {
.element {
color: var(--primary-color);
}
}