CSS Custom Properties: Dynamic Styling with CSS Variables

March 23, 2025

Introduction

CSS Custom Properties, commonly known as CSS Variables, revolutionize how we write and maintain CSS. Unlike preprocessor variables (Sass/Less), CSS custom properties are live values that can be changed at runtime, enabling dynamic theming, responsive design patterns, and more maintainable code.

They bridge the gap between static CSS and JavaScript, allowing for truly dynamic styling without the need for complex JavaScript DOM manipulations.

Basic Syntax and Usage

Declaring Custom Properties

Custom properties are declared using the -- prefix and can be defined at any level:

:root {
  /* Global custom properties */
  --primary-color: #3498db;
  --secondary-color: #2ecc71;
  --font-size-base: 16px;
  --spacing-unit: 1rem;
  --border-radius: 4px;
}

.card {
  /* Local custom properties */
  --card-padding: 1.5rem;
  --card-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  
  padding: var(--card-padding);
  box-shadow: var(--card-shadow);
  border-radius: var(--border-radius);
}

Using Custom Properties

.button {
  background-color: var(--primary-color);
  color: white;
  padding: var(--spacing-unit);
  border-radius: var(--border-radius);
  border: none;
  font-size: var(--font-size-base);
}

/* With fallback values */
.button--secondary {
  background-color: var(--secondary-color, #gray); /* Falls back to gray */
  color: var(--text-color, black);
}

Primary Button

Secondary Button

Dynamic Theming

Light/Dark Mode Toggle

:root {
  /* Light theme (default) */
  --bg-color: #ffffff;
  --text-color: #333333;
  --card-bg: #f8f9fa;
  --border-color: #e1e5e9;
}

[data-theme="dark"] {
  /* Dark theme */
  --bg-color: #1a1a1a;
  --text-color: #ffffff;
  --card-bg: #2d2d2d;
  --border-color: #404040;
}

body {
  background-color: var(--bg-color);
  color: var(--text-color);
  transition: background-color 0.3s ease, color 0.3s ease;
}

.card {
  background-color: var(--card-bg);
  border: 1px solid var(--border-color);
  padding: 1.5rem;
  border-radius: 8px;
}
// Theme toggle functionality
function toggleTheme() {
  const currentTheme = document.documentElement.getAttribute('data-theme');
  const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
  
  document.documentElement.setAttribute('data-theme', newTheme);
  localStorage.setItem('theme', newTheme);
}

// Load saved theme
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
  document.documentElement.setAttribute('data-theme', savedTheme);
}

Card Component

This card would automatically adapt to light/dark themes using CSS custom properties.

JavaScript Integration

Updating Variables Dynamically

// Get custom property value
const primaryColor = getComputedStyle(document.documentElement)
  .getPropertyValue('--primary-color');

// Set custom property value
document.documentElement.style.setProperty('--primary-color', '#e74c3c');

// Update multiple properties
function updateThemeColors(colors) {
  Object.entries(colors).forEach(([property, value]) => {
    document.documentElement.style.setProperty(`--${property}`, value);
  });
}

updateThemeColors({
  'primary-color': '#9b59b6',
  'secondary-color': '#f39c12',
  'accent-color': '#1abc9c'
});

Responsive Custom Properties

:root {
  --container-padding: 1rem;
  --font-size-h1: 2rem;
  --grid-columns: 1;
}

@media (min-width: 768px) {
  :root {
    --container-padding: 2rem;
    --font-size-h1: 2.5rem;
    --grid-columns: 2;
  }
}

@media (min-width: 1200px) {
  :root {
    --container-padding: 3rem;
    --font-size-h1: 3rem;
    --grid-columns: 3;
  }
}

.container {
  padding: 0 var(--container-padding);
}

h1 {
  font-size: var(--font-size-h1);
}

.grid {
  display: grid;
  grid-template-columns: repeat(var(--grid-columns), 1fr);
  gap: 1rem;
}

Advanced Patterns

1. Component-Level Theming

/* Button component with customizable properties */
.button {
  /* Component defaults */
  --button-bg: #3498db;
  --button-color: white;
  --button-padding: 0.75rem 1.5rem;
  --button-border-radius: 4px;
  --button-font-size: 1rem;
  --button-hover-bg: #2980b9;
  
  background-color: var(--button-bg);
  color: var(--button-color);
  padding: var(--button-padding);
  border-radius: var(--button-border-radius);
  font-size: var(--button-font-size);
  border: none;
  cursor: pointer;
  transition: background-color 0.2s ease;
}

.button:hover {
  background-color: var(--button-hover-bg);
}

/* Variant overrides */
.button--large {
  --button-padding: 1rem 2rem;
  --button-font-size: 1.125rem;
}

.button--danger {
  --button-bg: #e74c3c;
  --button-hover-bg: #c0392b;
}

.button--ghost {
  --button-bg: transparent;
  --button-color: var(--primary-color);
  --button-hover-bg: rgba(52, 152, 219, 0.1);
  border: 1px solid var(--primary-color);
}

2. Animation with Custom Properties

.progress-bar {
  --progress-width: 0%;
  --progress-color: #3498db;
  --progress-bg: #ecf0f1;
  --progress-height: 8px;
  --progress-border-radius: 4px;
  --progress-duration: 0.3s;
  
  width: 100%;
  height: var(--progress-height);
  background-color: var(--progress-bg);
  border-radius: var(--progress-border-radius);
  overflow: hidden;
}

.progress-bar::before {
  content: '';
  display: block;
  height: 100%;
  width: var(--progress-width);
  background-color: var(--progress-color);
  transition: width var(--progress-duration) ease;
}

/* JavaScript to update progress */
.progress-bar[data-progress="25"] { --progress-width: 25%; }
.progress-bar[data-progress="50"] { --progress-width: 50%; }
.progress-bar[data-progress="75"] { --progress-width: 75%; }
.progress-bar[data-progress="100"] { --progress-width: 100%; }

Progress: 65%

3. CSS Grid with Custom Properties

.dynamic-grid {
  --grid-min-width: 250px;
  --grid-gap: 1rem;
  --grid-max-columns: 4;
  
  display: grid;
  grid-template-columns: repeat(
    auto-fit, 
    minmax(var(--grid-min-width), 1fr)
  );
  gap: var(--grid-gap);
}

/* Responsive adjustments */
@media (max-width: 768px) {
  .dynamic-grid {
    --grid-min-width: 200px;
    --grid-gap: 0.5rem;
  }
}

@media (max-width: 480px) {
  .dynamic-grid {
    --grid-min-width: 100%;
    --grid-gap: 1rem;
  }
}

Real-World Examples

1. Design System Implementation

:root {
  /* Color palette */
  --color-primary-50: #eff6ff;
  --color-primary-100: #dbeafe;
  --color-primary-500: #3b82f6;
  --color-primary-600: #2563eb;
  --color-primary-900: #1e3a8a;
  
  /* Typography scale */
  --font-size-xs: 0.75rem;
  --font-size-sm: 0.875rem;
  --font-size-base: 1rem;
  --font-size-lg: 1.125rem;
  --font-size-xl: 1.25rem;
  --font-size-2xl: 1.5rem;
  
  /* Spacing scale */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-4: 1rem;
  --space-6: 1.5rem;
  --space-8: 2rem;
  
  /* Shadows */
  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
  --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
  --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
}

/* Component using design tokens */
.alert {
  --alert-bg: var(--color-primary-50);
  --alert-border: var(--color-primary-200);
  --alert-text: var(--color-primary-900);
  
  background-color: var(--alert-bg);
  border: 1px solid var(--alert-border);
  color: var(--alert-text);
  padding: var(--space-4);
  border-radius: var(--border-radius);
  box-shadow: var(--shadow-sm);
}

2. Utility Classes with Variables

/* Spacing utilities */
.p-1 { padding: var(--space-1); }
.p-2 { padding: var(--space-2); }
.p-4 { padding: var(--space-4); }

.m-1 { margin: var(--space-1); }
.m-2 { margin: var(--space-2); }
.m-4 { margin: var(--space-4); }

/* Color utilities */
.text-primary { color: var(--color-primary-600); }
.text-secondary { color: var(--color-gray-600); }
.bg-primary { background-color: var(--color-primary-600); }

/* Size utilities */
.text-sm { font-size: var(--font-size-sm); }
.text-lg { font-size: var(--font-size-lg); }
.text-xl { font-size: var(--font-size-xl); }

Performance Considerations

1. Inheritance and Cascading

/* Custom properties inherit naturally */
.parent {
  --text-color: #333;
  --font-size: 16px;
}

.child {
  color: var(--text-color); /* Inherits from parent */
  font-size: var(--font-size); /* Inherits from parent */
}

/* Override at component level */
.special-section {
  --text-color: #666; /* Only affects this section and descendants */
}

2. Avoiding Repaints

/* Good - Use transform for animations */
.animated-element {
  --scale: 1;
  transform: scale(var(--scale));
  transition: transform 0.3s ease;
}

.animated-element:hover {
  --scale: 1.1;
}

/* Avoid - Causes layout recalculations */
.bad-animation {
  --width: 100px;
  width: var(--width);
  transition: width 0.3s ease;
}

Browser Support and Fallbacks

/* Progressive enhancement with fallbacks */
.button {
  /* Fallback for older browsers */
  background-color: #3498db;
  color: white;
  
  /* Enhanced with custom properties */
  background-color: var(--primary-color, #3498db);
  color: var(--button-text-color, white);
}

/* Feature detection */
@supports (--css: variables) {
  .modern-component {
    /* Use custom properties freely */
    background: var(--gradient-bg);
    border: var(--border-width) solid var(--border-color);
  }
}

Debugging Custom Properties

/* Debug custom properties in DevTools */
:root {
  --debug-mode: 1; /* Set to 0 to disable */
}

.debug-element {
  position: relative;
}

.debug-element::after {
  content: 'Primary: ' var(--primary-color) ' | Font: ' var(--font-size-base);
  position: absolute;
  top: 100%;
  left: 0;
  background: black;
  color: white;
  padding: 0.25rem;
  font-size: 0.75rem;
  white-space: nowrap;
  display: var(--debug-mode, none);
}

Best Practices

1. Naming Conventions

:root {
  /* Good naming - semantic and hierarchical */
  --color-primary: #3498db;
  --color-primary-dark: #2980b9;
  --color-primary-light: #5dade2;
  
  --font-family-primary: 'Inter', sans-serif;
  --font-family-monospace: 'Fira Code', monospace;
  
  --spacing-xs: 0.25rem;
  --spacing-sm: 0.5rem;
  --spacing-md: 1rem;
  --spacing-lg: 1.5rem;
  --spacing-xl: 2rem;
  
  /* Component-specific */
  --button-padding-x: 1rem;
  --button-padding-y: 0.5rem;
  --card-border-radius: 8px;
}

2. Organization Strategies

:root {
  /* 1. Color palette */
  --gray-50: #f9fafb;
  --gray-100: #f3f4f6;
  /* ... */
  
  /* 2. Typography */
  --font-family-sans: ui-sans-serif, system-ui;
  --font-weight-normal: 400;
  /* ... */
  
  /* 3. Layout */
  --container-max-width: 1200px;
  --header-height: 64px;
  /* ... */
  
  /* 4. Components */
  --button-height: 44px;
  --input-border-color: var(--gray-300);
  /* ... */
}

Conclusion

CSS Custom Properties are a game-changer for modern CSS development. They enable:

  • Dynamic theming without JavaScript DOM manipulation
  • Maintainable code through centralized value management
  • Component-based styling with local property overrides
  • Responsive design with breakpoint-specific values
  • JavaScript integration for runtime customization

Key benefits include:

  • Live values that update in real-time
  • Natural inheritance following CSS cascade rules
  • No build step required unlike preprocessor variables
  • Excellent browser support (IE11+ with PostCSS polyfill)
  • Debugging-friendly with DevTools integration

Start incorporating CSS Custom Properties into your projects today. Begin with simple color and spacing variables, then gradually expand to more complex patterns like component theming and responsive design systems. Your CSS will become more maintainable, flexible, and powerful.