CSS custom properties enable dynamic styling with reusable values. Here's how to use them effectively.
Basic Syntax#
1/* Define custom properties */
2:root {
3 --primary-color: #007bff;
4 --secondary-color: #6c757d;
5 --font-size-base: 16px;
6 --spacing-unit: 8px;
7}
8
9/* Use with var() */
10.button {
11 background-color: var(--primary-color);
12 font-size: var(--font-size-base);
13 padding: calc(var(--spacing-unit) * 2);
14}
15
16/* With fallback value */
17.card {
18 background: var(--card-bg, white);
19 border-radius: var(--border-radius, 4px);
20}Scope and Inheritance#
1/* Global scope */
2:root {
3 --text-color: #333;
4}
5
6/* Component scope */
7.dark-section {
8 --text-color: #fff;
9 --bg-color: #1a1a1a;
10}
11
12/* Inherited by children */
13.dark-section p {
14 color: var(--text-color); /* #fff */
15}
16
17/* Override in child */
18.dark-section .highlight {
19 --text-color: #ffd700;
20 color: var(--text-color); /* #ffd700 */
21}Dynamic Theming#
1/* Light theme (default) */
2:root {
3 --bg-color: #ffffff;
4 --text-color: #333333;
5 --border-color: #e0e0e0;
6 --primary: #007bff;
7 --shadow: rgba(0, 0, 0, 0.1);
8}
9
10/* Dark theme */
11[data-theme="dark"] {
12 --bg-color: #1a1a1a;
13 --text-color: #f0f0f0;
14 --border-color: #333333;
15 --primary: #4da3ff;
16 --shadow: rgba(0, 0, 0, 0.3);
17}
18
19/* System preference */
20@media (prefers-color-scheme: dark) {
21 :root:not([data-theme="light"]) {
22 --bg-color: #1a1a1a;
23 --text-color: #f0f0f0;
24 --border-color: #333333;
25 }
26}
27
28/* Usage */
29body {
30 background-color: var(--bg-color);
31 color: var(--text-color);
32}
33
34.card {
35 border: 1px solid var(--border-color);
36 box-shadow: 0 2px 4px var(--shadow);
37}JavaScript Integration#
1// Get computed value
2const root = document.documentElement;
3const primaryColor = getComputedStyle(root)
4 .getPropertyValue('--primary-color')
5 .trim();
6
7// Set value
8root.style.setProperty('--primary-color', '#ff0000');
9
10// Set on element
11const element = document.querySelector('.card');
12element.style.setProperty('--card-bg', '#f0f0f0');
13
14// Remove (use initial value)
15root.style.removeProperty('--primary-color');
16
17// Theme toggle
18function toggleTheme() {
19 const current = document.documentElement.dataset.theme;
20 document.documentElement.dataset.theme =
21 current === 'dark' ? 'light' : 'dark';
22}Responsive Values#
1:root {
2 --container-width: 100%;
3 --font-size-heading: 1.5rem;
4 --grid-columns: 1;
5}
6
7@media (min-width: 640px) {
8 :root {
9 --container-width: 640px;
10 --font-size-heading: 2rem;
11 --grid-columns: 2;
12 }
13}
14
15@media (min-width: 1024px) {
16 :root {
17 --container-width: 1024px;
18 --font-size-heading: 2.5rem;
19 --grid-columns: 3;
20 }
21}
22
23.container {
24 max-width: var(--container-width);
25 margin: 0 auto;
26}
27
28.grid {
29 display: grid;
30 grid-template-columns: repeat(var(--grid-columns), 1fr);
31}Calculations#
1:root {
2 --spacing: 8px;
3 --columns: 12;
4 --gutter: 16px;
5}
6
7.container {
8 padding: calc(var(--spacing) * 2);
9}
10
11.col-6 {
12 width: calc((6 / var(--columns)) * 100%);
13}
14
15.row {
16 margin: calc(var(--gutter) / -2);
17}
18
19.col {
20 padding: calc(var(--gutter) / 2);
21}
22
23/* Complex calculations */
24:root {
25 --header-height: 60px;
26 --footer-height: 40px;
27}
28
29.main {
30 min-height: calc(100vh - var(--header-height) - var(--footer-height));
31}Color Systems#
1/* HSL-based color system */
2:root {
3 --hue-primary: 220;
4 --hue-success: 120;
5 --hue-warning: 45;
6 --hue-danger: 0;
7
8 --primary-50: hsl(var(--hue-primary), 100%, 95%);
9 --primary-100: hsl(var(--hue-primary), 100%, 90%);
10 --primary-500: hsl(var(--hue-primary), 100%, 50%);
11 --primary-700: hsl(var(--hue-primary), 100%, 30%);
12 --primary-900: hsl(var(--hue-primary), 100%, 10%);
13}
14
15/* Adjust just the hue for brand */
16.brand-orange {
17 --hue-primary: 30;
18}
19
20/* Alpha variations */
21:root {
22 --primary-rgb: 0, 123, 255;
23}
24
25.overlay {
26 background: rgba(var(--primary-rgb), 0.5);
27}
28
29.subtle {
30 background: rgba(var(--primary-rgb), 0.1);
31}Component Patterns#
1/* Button component with variants */
2.btn {
3 --btn-bg: var(--primary);
4 --btn-color: white;
5 --btn-padding: 0.5rem 1rem;
6 --btn-radius: 4px;
7
8 background: var(--btn-bg);
9 color: var(--btn-color);
10 padding: var(--btn-padding);
11 border-radius: var(--btn-radius);
12}
13
14.btn-secondary {
15 --btn-bg: var(--secondary);
16}
17
18.btn-outline {
19 --btn-bg: transparent;
20 --btn-color: var(--primary);
21 border: 2px solid var(--primary);
22}
23
24.btn-lg {
25 --btn-padding: 0.75rem 1.5rem;
26}
27
28.btn-sm {
29 --btn-padding: 0.25rem 0.5rem;
30}Animation Properties#
1:root {
2 --duration-fast: 150ms;
3 --duration-normal: 300ms;
4 --duration-slow: 500ms;
5 --easing-default: ease-in-out;
6 --easing-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
7}
8
9.fade {
10 transition: opacity var(--duration-normal) var(--easing-default);
11}
12
13.slide {
14 --slide-distance: 20px;
15 transition: transform var(--duration-normal) var(--easing-default);
16}
17
18.slide:hover {
19 transform: translateY(calc(var(--slide-distance) * -1));
20}
21
22/* Animated with custom properties */
23@keyframes pulse {
24 0%, 100% {
25 transform: scale(1);
26 }
27 50% {
28 transform: scale(var(--pulse-scale, 1.05));
29 }
30}
31
32.pulse {
33 animation: pulse var(--duration-slow) infinite;
34}
35
36.pulse-strong {
37 --pulse-scale: 1.1;
38}Typography Scale#
1:root {
2 --font-size-base: 1rem;
3 --font-scale: 1.25;
4
5 --font-size-xs: calc(var(--font-size-base) / var(--font-scale));
6 --font-size-sm: calc(var(--font-size-base) / 1.125);
7 --font-size-md: var(--font-size-base);
8 --font-size-lg: calc(var(--font-size-base) * var(--font-scale));
9 --font-size-xl: calc(var(--font-size-lg) * var(--font-scale));
10 --font-size-2xl: calc(var(--font-size-xl) * var(--font-scale));
11 --font-size-3xl: calc(var(--font-size-2xl) * var(--font-scale));
12}
13
14h1 { font-size: var(--font-size-3xl); }
15h2 { font-size: var(--font-size-2xl); }
16h3 { font-size: var(--font-size-xl); }
17h4 { font-size: var(--font-size-lg); }
18body { font-size: var(--font-size-md); }
19small { font-size: var(--font-size-sm); }Spacing System#
1:root {
2 --space-unit: 0.25rem;
3
4 --space-1: calc(var(--space-unit) * 1); /* 4px */
5 --space-2: calc(var(--space-unit) * 2); /* 8px */
6 --space-3: calc(var(--space-unit) * 3); /* 12px */
7 --space-4: calc(var(--space-unit) * 4); /* 16px */
8 --space-6: calc(var(--space-unit) * 6); /* 24px */
9 --space-8: calc(var(--space-unit) * 8); /* 32px */
10 --space-12: calc(var(--space-unit) * 12); /* 48px */
11 --space-16: calc(var(--space-unit) * 16); /* 64px */
12}
13
14.card {
15 padding: var(--space-4);
16 margin-bottom: var(--space-6);
17}
18
19.stack > * + * {
20 margin-top: var(--space-4);
21}Fallback Strategies#
1/* Multiple fallbacks */
2.element {
3 color: var(--theme-color, var(--default-color, black));
4}
5
6/* Feature detection */
7@supports (--custom: property) {
8 .element {
9 color: var(--text-color);
10 }
11}
12
13/* Fallback for older browsers */
14.element {
15 color: #333; /* Fallback */
16 color: var(--text-color);
17}Best Practices#
Naming:
✓ Use semantic names (--color-primary)
✓ Be consistent with conventions
✓ Group related variables
✓ Document purpose
Organization:
✓ Define globals in :root
✓ Scope to components when needed
✓ Use for repeated values
✓ Create systems (colors, spacing)
Performance:
✓ Custom properties are fast
✓ Avoid deeply nested vars
✓ Use for dynamic values
✓ Prefer for theming
Avoid:
✗ Using for single-use values
✗ Over-nesting var() calls
✗ Forgetting fallbacks
✗ Inconsistent naming
Conclusion#
CSS custom properties bring programming-like flexibility to stylesheets. Use them for theming, design systems, responsive values, and component customization. Combined with JavaScript, they enable dynamic styling without style recalculation. Build consistent, maintainable styles by creating systems for colors, typography, and spacing.