CSS custom properties go beyond simple variable storage. Here's how to leverage their full power.
Scoping and Inheritance#
1/* Global variables */
2:root {
3 --primary-color: #3b82f6;
4 --spacing-unit: 8px;
5}
6
7/* Component-scoped variables */
8.card {
9 --card-padding: 1rem;
10 --card-radius: 0.5rem;
11
12 padding: var(--card-padding);
13 border-radius: var(--card-radius);
14}
15
16/* Variant overrides */
17.card--large {
18 --card-padding: 2rem;
19 --card-radius: 1rem;
20}
21
22/* Inheritance in nested elements */
23.theme-dark {
24 --text-color: white;
25 --bg-color: #1a1a1a;
26}
27
28.theme-dark .nested {
29 /* Inherits --text-color and --bg-color */
30 color: var(--text-color);
31 background: var(--bg-color);
32}Fallback Values#
1/* Simple fallback */
2.element {
3 color: var(--text-color, black);
4}
5
6/* Chained fallbacks */
7.element {
8 color: var(--theme-text, var(--default-text, black));
9}
10
11/* Empty fallback (use default) */
12.element {
13 background: var(--custom-bg,);
14}
15
16/* Invalid value handling */
17:root {
18 --invalid: 20px;
19}
20
21.element {
22 /* Falls back to inherited value, not fallback */
23 color: var(--invalid);
24 /* color becomes inherited, not red */
25 color: var(--invalid, red);
26}
27
28/* Registered property with syntax */
29@property --size {
30 syntax: '<length>';
31 initial-value: 16px;
32 inherits: false;
33}Calculations#
1:root {
2 --base-size: 16px;
3 --scale: 1.25;
4 --spacing: 8px;
5}
6
7/* Basic calculations */
8.element {
9 font-size: calc(var(--base-size) * var(--scale));
10 padding: calc(var(--spacing) * 2);
11 margin: calc(var(--spacing) * 0.5);
12}
13
14/* Type scale */
15:root {
16 --font-xs: calc(var(--base-size) / var(--scale));
17 --font-sm: var(--base-size);
18 --font-md: calc(var(--base-size) * var(--scale));
19 --font-lg: calc(var(--base-size) * var(--scale) * var(--scale));
20 --font-xl: calc(var(--base-size) * var(--scale) * var(--scale) * var(--scale));
21}
22
23/* Spacing scale */
24:root {
25 --space-1: var(--spacing);
26 --space-2: calc(var(--spacing) * 2);
27 --space-3: calc(var(--spacing) * 3);
28 --space-4: calc(var(--spacing) * 4);
29 --space-6: calc(var(--spacing) * 6);
30 --space-8: calc(var(--spacing) * 8);
31}
32
33/* Fluid typography */
34.fluid-text {
35 font-size: clamp(
36 var(--font-min, 1rem),
37 calc(var(--font-min) + var(--font-diff, 1rem) * var(--fluid-ratio, 0.5)),
38 var(--font-max, 2rem)
39 );
40}Dynamic Values with JavaScript#
1:root {
2 --mouse-x: 50%;
3 --mouse-y: 50%;
4 --scroll-progress: 0;
5}
6
7.spotlight {
8 background: radial-gradient(
9 circle at var(--mouse-x) var(--mouse-y),
10 rgba(255, 255, 255, 0.2),
11 transparent
12 );
13}
14
15.progress-bar {
16 width: calc(var(--scroll-progress) * 100%);
17}1// Update mouse position
2document.addEventListener('mousemove', (e) => {
3 const x = (e.clientX / window.innerWidth) * 100;
4 const y = (e.clientY / window.innerHeight) * 100;
5
6 document.documentElement.style.setProperty('--mouse-x', `${x}%`);
7 document.documentElement.style.setProperty('--mouse-y', `${y}%`);
8});
9
10// Update scroll progress
11document.addEventListener('scroll', () => {
12 const scrollTop = window.scrollY;
13 const docHeight = document.documentElement.scrollHeight - window.innerHeight;
14 const progress = scrollTop / docHeight;
15
16 document.documentElement.style.setProperty('--scroll-progress', progress);
17});
18
19// Get computed value
20const styles = getComputedStyle(document.documentElement);
21const primaryColor = styles.getPropertyValue('--primary-color').trim();Animations#
1/* Animate custom properties */
2@property --angle {
3 syntax: '<angle>';
4 initial-value: 0deg;
5 inherits: false;
6}
7
8.rotating-gradient {
9 background: conic-gradient(
10 from var(--angle),
11 red,
12 yellow,
13 lime,
14 aqua,
15 blue,
16 magenta,
17 red
18 );
19 animation: rotate 3s linear infinite;
20}
21
22@keyframes rotate {
23 to {
24 --angle: 360deg;
25 }
26}
27
28/* Animatable color */
29@property --gradient-color {
30 syntax: '<color>';
31 initial-value: blue;
32 inherits: false;
33}
34
35.color-shift {
36 background: linear-gradient(
37 var(--gradient-color),
38 var(--gradient-color-end, white)
39 );
40 animation: shift-color 2s ease infinite alternate;
41}
42
43@keyframes shift-color {
44 to {
45 --gradient-color: purple;
46 }
47}
48
49/* Number animation */
50@property --count {
51 syntax: '<integer>';
52 initial-value: 0;
53 inherits: false;
54}
55
56.counter {
57 --count: 0;
58 counter-reset: count var(--count);
59 animation: count-up 2s ease-out forwards;
60}
61
62.counter::before {
63 content: counter(count);
64}
65
66@keyframes count-up {
67 to {
68 --count: 100;
69 }
70}Theming System#
1/* Define theme tokens */
2:root {
3 /* Primitive tokens */
4 --blue-500: #3b82f6;
5 --blue-600: #2563eb;
6 --gray-100: #f3f4f6;
7 --gray-900: #111827;
8
9 /* Semantic tokens */
10 --color-primary: var(--blue-500);
11 --color-primary-hover: var(--blue-600);
12 --color-background: white;
13 --color-text: var(--gray-900);
14 --color-text-muted: var(--gray-600);
15
16 /* Component tokens */
17 --button-bg: var(--color-primary);
18 --button-text: white;
19 --card-bg: var(--color-background);
20 --card-border: var(--gray-200);
21}
22
23/* Dark theme */
24[data-theme="dark"] {
25 --color-background: var(--gray-900);
26 --color-text: var(--gray-100);
27 --card-bg: var(--gray-800);
28 --card-border: var(--gray-700);
29}
30
31/* System preference */
32@media (prefers-color-scheme: dark) {
33 :root:not([data-theme="light"]) {
34 --color-background: var(--gray-900);
35 --color-text: var(--gray-100);
36 }
37}
38
39/* Per-component customization */
40.button {
41 --button-padding: 0.5rem 1rem;
42 --button-radius: 0.25rem;
43
44 padding: var(--button-padding);
45 border-radius: var(--button-radius);
46 background: var(--button-bg);
47 color: var(--button-text);
48}
49
50.button--large {
51 --button-padding: 0.75rem 1.5rem;
52 --button-radius: 0.5rem;
53}Responsive Variables#
1:root {
2 --container-padding: 1rem;
3 --grid-columns: 1;
4 --heading-size: 1.5rem;
5}
6
7@media (min-width: 640px) {
8 :root {
9 --container-padding: 1.5rem;
10 --grid-columns: 2;
11 --heading-size: 2rem;
12 }
13}
14
15@media (min-width: 1024px) {
16 :root {
17 --container-padding: 2rem;
18 --grid-columns: 3;
19 --heading-size: 2.5rem;
20 }
21}
22
23/* Usage */
24.container {
25 padding: var(--container-padding);
26}
27
28.grid {
29 display: grid;
30 grid-template-columns: repeat(var(--grid-columns), 1fr);
31}
32
33h1 {
34 font-size: var(--heading-size);
35}Component Composition#
1/* Base component with customizable properties */
2.input {
3 --input-height: 2.5rem;
4 --input-padding: 0.5rem 0.75rem;
5 --input-border-color: var(--gray-300);
6 --input-border-width: 1px;
7 --input-radius: 0.25rem;
8 --input-bg: white;
9 --input-focus-ring: var(--blue-500);
10
11 height: var(--input-height);
12 padding: var(--input-padding);
13 border: var(--input-border-width) solid var(--input-border-color);
14 border-radius: var(--input-radius);
15 background: var(--input-bg);
16}
17
18.input:focus {
19 outline: none;
20 box-shadow: 0 0 0 2px var(--input-focus-ring);
21}
22
23/* Compose with form group */
24.form-group {
25 --input-border-color: var(--gray-400);
26}
27
28.form-group--error {
29 --input-border-color: var(--red-500);
30 --input-focus-ring: var(--red-500);
31}
32
33/* Compose with theme */
34[data-theme="dark"] {
35 --input-bg: var(--gray-800);
36 --input-border-color: var(--gray-600);
37}State Management#
1/* Track state with custom properties */
2.accordion {
3 --is-open: 0;
4}
5
6.accordion[open] {
7 --is-open: 1;
8}
9
10.accordion-content {
11 /* Height animation based on state */
12 max-height: calc(var(--is-open) * 1000px);
13 opacity: var(--is-open);
14 overflow: hidden;
15 transition: max-height 0.3s, opacity 0.3s;
16}
17
18/* Toggle icon rotation */
19.accordion-icon {
20 transform: rotate(calc(var(--is-open) * 180deg));
21 transition: transform 0.3s;
22}
23
24/* Checkbox-based state */
25.toggle {
26 --checked: 0;
27}
28
29.toggle:checked {
30 --checked: 1;
31}
32
33.toggle-indicator {
34 transform: translateX(calc(var(--checked) * 20px));
35 background: color-mix(
36 in srgb,
37 var(--gray-400) calc((1 - var(--checked)) * 100%),
38 var(--green-500) calc(var(--checked) * 100%)
39 );
40}Performance Considerations#
1/* Prefer inheritance over global variables */
2/* Bad - causes repaint of entire page */
3:root {
4 --dynamic-value: /* changed frequently */;
5}
6
7/* Good - scoped to component */
8.component {
9 --dynamic-value: /* changed frequently */;
10}
11
12/* Use will-change for animated properties */
13.animated {
14 will-change: transform;
15 transform: translateX(var(--x, 0));
16}
17
18/* Avoid complex calculations in hot paths */
19.expensive {
20 /* Precalculate if possible */
21 --precalculated: 100px;
22 width: var(--precalculated);
23}Best Practices#
Organization:
✓ Use naming conventions (--color-, --size-)
✓ Group related properties
✓ Document complex variables
✓ Use semantic names for tokens
Scoping:
✓ Define globally in :root
✓ Override at component level
✓ Use for component APIs
✓ Avoid deep nesting
Performance:
✓ Scope dynamic values narrowly
✓ Minimize recalculations
✓ Use @property for animations
✓ Batch JavaScript updates
Maintainability:
✓ Create a token system
✓ Use fallback values
✓ Test in all themes
✓ Document the API
Conclusion#
CSS custom properties enable powerful theming, animations, and JavaScript integration. Use them to create flexible component APIs, implement design systems, and build dynamic interfaces. Scope variables appropriately and consider performance for frequently updated values.