CSS Custom Properties: JavaScript Integration

Manipulating Custom Properties with JavaScript

One of the most powerful features of CSS Custom Properties is the ability to modify them with JavaScript at runtime, enabling dynamic styling without DOM manipulation.

Reading Custom Property Values


// Get computed styles for an element
const element = document.querySelector('.my-element');
const styles = getComputedStyle(element);

// Read a custom property value
const primaryColor = styles.getPropertyValue('--primary-color');
console.log(primaryColor); // " #3498db" (note the leading space)

// Clean up the value if needed
const cleanValue = primaryColor.trim();
console.log(cleanValue); // "#3498db"

// Read from :root
const rootStyles = getComputedStyle(document.documentElement);
const fontSize = rootStyles.getPropertyValue('--font-size-base').trim();
                        

Setting Custom Property Values


// Set a custom property on a specific element
element.style.setProperty('--background-color', '#f8f9fa');

// Set a custom property on :root (global)
document.documentElement.style.setProperty('--primary-color', '#ff0000');

// Remove a custom property
element.style.removeProperty('--background-color');
                        
Note: When setting custom properties with JavaScript, you don't need to include quotes around color values, pixel values, etc. The browser will handle the formatting correctly.

Real-time UI Updates

Color Picker Example

Create a color picker that updates UI elements in real-time:


<!-- HTML -->
<div class="theme-customizer">
  <label for="primary-color">Primary Color:</label>
  <input type="color" id="primary-color" value="#3498db">
  
  <label for="secondary-color">Secondary Color:</label>
  <input type="color" id="secondary-color" value="#2ecc71">
  
  <label for="text-color">Text Color:</label>
  <input type="color" id="text-color" value="#333333">
</div>

<div class="preview">
  <button class="btn-primary">Primary Button</button>
  <button class="btn-secondary">Secondary Button</button>
  <p class="sample-text">Sample Text</p>
</div>

/* CSS */
:root {
  --primary-color: #3498db;
  --secondary-color: #2ecc71;
  --text-color: #333333;
}

.btn-primary {
  background-color: var(--primary-color);
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
}

.btn-secondary {
  background-color: var(--secondary-color);
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
}

.sample-text {
  color: var(--text-color);
}

// JavaScript
document.getElementById('primary-color').addEventListener('input', function(e) {
  document.documentElement.style.setProperty('--primary-color', e.target.value);
});

document.getElementById('secondary-color').addEventListener('input', function(e) {
  document.documentElement.style.setProperty('--secondary-color', e.target.value);
});

document.getElementById('text-color').addEventListener('input', function(e) {
  document.documentElement.style.setProperty('--text-color', e.target.value);
});
                        

Slider Controls for Spacing


<!-- HTML -->
<div class="spacing-controls">
  <label for="spacing-slider">Base Spacing: <span id="spacing-value">8px</span></label>
  <input type="range" id="spacing-slider" min="4" max="16" value="8">
</div>

<div class="preview">
  <div class="card">
    <div class="card-header">Card Title</div>
    <div class="card-body">Card content goes here...</div>
    <div class="card-footer">Card Footer</div>
  </div>
</div>

/* CSS */
:root {
  --spacing-unit: 8px;
  --spacing-small: calc(var(--spacing-unit) * 0.5);
  --spacing-medium: var(--spacing-unit);
  --spacing-large: calc(var(--spacing-unit) * 2);
}

.card {
  border: 1px solid #ddd;
  border-radius: 4px;
  margin-bottom: var(--spacing-large);
}

.card-header, .card-footer {
  padding: var(--spacing-medium);
  background-color: #f8f9fa;
}

.card-body {
  padding: var(--spacing-large);
}

// JavaScript
const spacingSlider = document.getElementById('spacing-slider');
const spacingValue = document.getElementById('spacing-value');

spacingSlider.addEventListener('input', function(e) {
  const value = e.target.value;
  spacingValue.textContent = value + 'px';
  document.documentElement.style.setProperty('--spacing-unit', value + 'px');
});
                        

Event-Driven Property Changes

Scroll-Based Properties

Update custom properties based on scroll position:


/* CSS */
:root {
  --scroll-progress: 0;
  --header-opacity: 1;
  --header-height: 80px;
}

.progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  height: 4px;
  background-color: #3498db;
  width: calc(var(--scroll-progress) * 100%);
  z-index: 1000;
}

.header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: var(--header-height);
  background-color: rgba(255, 255, 255, var(--header-opacity));
  box-shadow: 0 2px 10px rgba(0, 0, 0, calc(0.1 * var(--header-opacity)));
  transition: background-color 0.3s, box-shadow 0.3s;
}

// JavaScript
window.addEventListener('scroll', function() {
  // Calculate scroll progress (0 to 1)
  const scrollTop = window.scrollY;
  const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
  const scrollProgress = scrollTop / scrollHeight;
  
  // Update scroll progress property
  document.documentElement.style.setProperty('--scroll-progress', scrollProgress);
  
  // Update header opacity (fade out as user scrolls down)
  const headerOpacity = Math.max(0.8, 1 - (scrollTop / 200));
  document.documentElement.style.setProperty('--header-opacity', headerOpacity);
  
  // Shrink header height as user scrolls
  const headerHeight = Math.max(60, 80 - (scrollTop / 20));
  document.documentElement.style.setProperty('--header-height', headerHeight + 'px');
});
                        

Mouse Position Effects

Create hover effects that follow the mouse position:


/* CSS */
:root {
  --mouse-x: 0;
  --mouse-y: 0;
}

.card {
  position: relative;
  width: 300px;
  height: 200px;
  background-color: #fff;
  border-radius: 8px;
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
  overflow: hidden;
}

.card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: radial-gradient(
    circle at calc(var(--mouse-x) * 100%) calc(var(--mouse-y) * 100%),
    rgba(255, 255, 255, 0.8) 0%,
    rgba(255, 255, 255, 0) 50%
  );
  pointer-events: none;
}

// JavaScript
document.querySelectorAll('.card').forEach(card => {
  card.addEventListener('mousemove', function(e) {
    // Calculate mouse position relative to the card (0 to 1)
    const rect = card.getBoundingClientRect();
    const x = (e.clientX - rect.left) / rect.width;
    const y = (e.clientY - rect.top) / rect.height;
    
    // Update custom properties
    card.style.setProperty('--mouse-x', x);
    card.style.setProperty('--mouse-y', y);
  });
  
  card.addEventListener('mouseleave', function() {
    // Reset values when mouse leaves
    card.style.setProperty('--mouse-x', 0.5);
    card.style.setProperty('--mouse-y', 0.5);
  });
});
                        

Form Input Binding

Range Input to CSS Property


<!-- HTML -->
<div class="controls">
  <div class="control-group">
    <label for="border-radius">Border Radius: <span id="radius-value">4px</span></label>
    <input type="range" id="border-radius" min="0" max="20" value="4">
  </div>
  
  <div class="control-group">
    <label for="shadow-blur">Shadow Blur: <span id="blur-value">10px</span></label>
    <input type="range" id="shadow-blur" min="0" max="30" value="10">
  </div>
  
  <div class="control-group">
    <label for="shadow-opacity">Shadow Opacity: <span id="opacity-value">0.1</span></label>
    <input type="range" id="shadow-opacity" min="0" max="100" value="10">
  </div>
</div>

<div class="preview">
  <div class="example-element">
    <p>Custom Styled Element</p>
  </div>
</div>

/* CSS */
:root {
  --border-radius: 4px;
  --shadow-blur: 10px;
  --shadow-opacity: 0.1;
}

.example-element {
  width: 200px;
  height: 200px;
  background-color: white;
  border-radius: var(--border-radius);
  box-shadow: 0 4px var(--shadow-blur) rgba(0, 0, 0, var(--shadow-opacity));
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  transition: border-radius 0.3s, box-shadow 0.3s;
}

// JavaScript
// Border radius control
const radiusInput = document.getElementById('border-radius');
const radiusValue = document.getElementById('radius-value');

radiusInput.addEventListener('input', function(e) {
  const value = e.target.value;
  radiusValue.textContent = value + 'px';
  document.documentElement.style.setProperty('--border-radius', value + 'px');
});

// Shadow blur control
const blurInput = document.getElementById('shadow-blur');
const blurValue = document.getElementById('blur-value');

blurInput.addEventListener('input', function(e) {
  const value = e.target.value;
  blurValue.textContent = value + 'px';
  document.documentElement.style.setProperty('--shadow-blur', value + 'px');
});

// Shadow opacity control
const opacityInput = document.getElementById('shadow-opacity');
const opacityValue = document.getElementById('opacity-value');

opacityInput.addEventListener('input', function(e) {
  const value = e.target.value / 100;
  opacityValue.textContent = value.toFixed(2);
  document.documentElement.style.setProperty('--shadow-opacity', value);
});
                        

Storing User Preferences


// Save preferences to localStorage
function savePreferences() {
  const preferences = {
    primaryColor: getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim(),
    fontSize: getComputedStyle(document.documentElement).getPropertyValue('--font-size-base').trim(),
    spacing: getComputedStyle(document.documentElement).getPropertyValue('--spacing-unit').trim(),
    borderRadius: getComputedStyle(document.documentElement).getPropertyValue('--border-radius').trim()
  };
  
  localStorage.setItem('userPreferences', JSON.stringify(preferences));
}

// Load preferences from localStorage
function loadPreferences() {
  const savedPreferences = localStorage.getItem('userPreferences');
  
  if (savedPreferences) {
    const preferences = JSON.parse(savedPreferences);
    
    // Apply saved preferences
    document.documentElement.style.setProperty('--primary-color', preferences.primaryColor);
    document.documentElement.style.setProperty('--font-size-base', preferences.fontSize);
    document.documentElement.style.setProperty('--spacing-unit', preferences.spacing);
    document.documentElement.style.setProperty('--border-radius', preferences.borderRadius);
    
    // Update input controls to match saved values
    document.getElementById('primary-color').value = preferences.primaryColor;
    document.getElementById('font-size').value = parseInt(preferences.fontSize);
    document.getElementById('spacing').value = parseInt(preferences.spacing);
    document.getElementById('border-radius').value = parseInt(preferences.borderRadius);
  }
}

// Call on page load
document.addEventListener('DOMContentLoaded', loadPreferences);

// Save button event listener
document.getElementById('save-preferences').addEventListener('click', savePreferences);
                        

Best Practices for JavaScript Integration

Performance Considerations

  • Batch Updates: Group multiple property changes together to minimize repaints
  • Use requestAnimationFrame: For smooth animations and changes tied to rendering
  • Throttle Event Handlers: Especially for scroll, resize, and mousemove events
  • Limit Scope: Apply changes to specific elements rather than :root when possible

Batching Example


// Bad: Multiple separate updates
function updateTheme(hue) {
  document.documentElement.style.setProperty('--primary-h', hue);
  document.documentElement.style.setProperty('--primary-s', '70%');
  document.documentElement.style.setProperty('--primary-l', '50%');
  document.documentElement.style.setProperty('--secondary-h', (hue + 180) % 360);
  document.documentElement.style.setProperty('--secondary-s', '70%');
  document.documentElement.style.setProperty('--secondary-l', '50%');
}

// Better: Use requestAnimationFrame and batch updates
function updateTheme(hue) {
  requestAnimationFrame(() => {
    const root = document.documentElement.style;
    root.setProperty('--primary-h', hue);
    root.setProperty('--primary-s', '70%');
    root.setProperty('--primary-l', '50%');
    root.setProperty('--secondary-h', (hue + 180) % 360);
    root.setProperty('--secondary-s', '70%');
    root.setProperty('--secondary-l', '50%');
  });
}
                        

Throttling Example


// Throttle function
function throttle(callback, delay) {
  let isThrottled = false;
  
  return function(...args) {
    if (isThrottled) return;
    
    isThrottled = true;
    callback.apply(this, args);
    
    setTimeout(() => {
      isThrottled = false;
    }, delay);
  };
}

// Throttled scroll handler
const handleScroll = throttle(() => {
  const scrollTop = window.scrollY;
  const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
  const scrollProgress = scrollTop / scrollHeight;
  
  document.documentElement.style.setProperty('--scroll-progress', scrollProgress);
}, 50); // Update at most every 50ms

window.addEventListener('scroll', handleScroll);
                        
Pro Tip: For complex UI components that need frequent updates, consider using CSS custom properties for the dynamic parts and static CSS for everything else. This approach gives you the flexibility of dynamic styling with the performance of static CSS.

Next Steps

Now that you understand how to integrate CSS Custom Properties with JavaScript, you can explore more advanced topics in our other guides:

Remember: CSS Custom Properties combined with JavaScript provide a powerful way to create dynamic, interactive interfaces without heavy DOM manipulation. This approach is more performant than changing classes or inline styles for many use cases.