Back to Blog
CSSCustom PropertiesVariablesAdvanced

CSS Custom Properties Advanced

Advanced CSS custom properties techniques. From calculations to animations to JavaScript integration.

B
Bootspring Team
Engineering
December 15, 2020
6 min read

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.

Share this article

Help spread the word about Bootspring