Dynamic Styling and Classes

Controlling Visual Presentation with JavaScript

Introduction to Dynamic Styling

In our previous lectures, we explored how to create and modify DOM elements. Now we'll focus specifically on the visual aspects of DOM manipulation by diving deeper into dynamic styling techniques.

Dynamic styling is one of the most powerful aspects of JavaScript DOM manipulation, allowing us to create responsive, interactive interfaces that react to user actions, data changes, and application state.

graph TD A[Dynamic Styling Approaches] --> B[Direct Style Manipulation] A --> C[Class-Based Styling] A --> D[CSS Variable Manipulation] B --> B1[element.style] B --> B2[element.style.cssText] C --> C1[className property] C --> C2[classList API] D --> D1[Document-level variables] D --> D2[Element-level variables] classDef default fill:#f9f9f9,stroke:#333,stroke-width:1px;

Real-World Analogy: Theatrical Production

Dynamic styling in web development is like controlling a theatrical production:

  • Direct style manipulation is like adjusting a spotlight during the show - immediate, precise, but potentially distracting
  • Class-based styling is like switching between pre-defined scene settings - cohesive, thematic changes that affect multiple elements at once
  • CSS variables are like the master control board - allowing coordinated adjustments across the entire production

Just as a good theatrical director chooses the right lighting technique for each moment in the play, a good web developer selects the appropriate styling approach for different UI needs.

Working with Classes

CSS classes are the most maintainable and flexible way to control styling in your application. JavaScript provides multiple ways to work with classes:

The className Property

The most basic way to work with classes is through the className property, which represents the value of the element's class attribute as a space-separated string:

const element = document.getElementById('my-element');

// Get all classes as a string
console.log(element.className); // e.g., "btn primary large"

// Replace all classes
element.className = 'btn secondary';

// Add a class (be careful not to overwrite existing classes)
element.className += ' disabled';

// Remove a specific class (requires string manipulation)
element.className = element.className
    .replace('primary', '')
    .trim();

// Check if an element has a class
if (element.className.includes('active')) {
    // Do something
}

While className works, it's often cumbersome for operations on individual classes because you need to manage the space-separated string manually.

The ClassList API

The classList property provides a much more convenient interface for working with classes. It's an array-like object with methods for adding, removing, toggling, and checking classes:

const element = document.getElementById('my-element');

// Add one or more classes
element.classList.add('active');
element.classList.add('highlight', 'visible'); // Add multiple

// Remove one or more classes
element.classList.remove('disabled');
element.classList.remove('loading', 'error'); // Remove multiple

// Toggle a class (add if not present, remove if present)
element.classList.toggle('expanded');

// Toggle with condition (second parameter forces state)
element.classList.toggle('selected', isSelected);
// Equivalent to:
// if (isSelected) element.classList.add('selected');
// else element.classList.remove('selected');

// Check if an element has a class
if (element.classList.contains('editable')) {
    // Allow editing
}

// Replace one class with another
element.classList.replace('inactive', 'active');

The classList API is supported in all modern browsers and provides a much cleaner way to manipulate classes than working with className directly.

Practical Example: State-Based UI

Classes are perfect for representing different UI states:

// Function to update button state
function updateButtonState(button, isLoading, isSuccess, isError) {
    // Reset all state classes
    button.classList.remove('loading', 'success', 'error');
    
    // Disable button during loading
    button.disabled = isLoading;
    
    // Apply appropriate class based on state
    if (isLoading) {
        button.classList.add('loading');
        button.innerHTML = '<span class="spinner"></span> Loading...';
    } else if (isSuccess) {
        button.classList.add('success');
        button.innerHTML = '<span class="icon-check"></span> Success!';
    } else if (isError) {
        button.classList.add('error');
        button.innerHTML = '<span class="icon-error"></span> Error';
    } else {
        // Default state
        button.innerHTML = 'Submit';
    }
}

// Example usage in a form submission
const submitButton = document.getElementById('submit-btn');

submitButton.addEventListener('click', async function(e) {
    e.preventDefault();
    
    // Show loading state
    updateButtonState(submitButton, true, false, false);
    
    try {
        // Simulate API request
        await fetch('/api/submit', {
            method: 'POST',
            body: new FormData(document.getElementById('my-form'))
        });
        
        // Show success state
        updateButtonState(submitButton, false, true, false);
        
        // Reset after 2 seconds
        setTimeout(() => {
            updateButtonState(submitButton, false, false, false);
        }, 2000);
    } catch (error) {
        // Show error state
        updateButtonState(submitButton, false, false, true);
        
        // Reset after 3 seconds
        setTimeout(() => {
            updateButtonState(submitButton, false, false, false);
        }, 3000);
    }
});

Direct Style Manipulation

While classes are generally preferred for most styling needs, there are situations where direct style manipulation is necessary or more appropriate:

The style Property

Every element has a style property that directly manipulates the element's inline styles:

const element = document.getElementById('my-element');

// Set individual style properties
element.style.color = '#3498db';
element.style.backgroundColor = '#ecf0f1';
element.style.padding = '10px 15px';
element.style.borderRadius = '4px';
element.style.boxShadow = '0 2px 5px rgba(0,0,0,0.1)';

// Note: CSS properties with hyphens are converted to camelCase in JavaScript
// 'background-color' → 'backgroundColor'
// 'border-radius' → 'borderRadius'

// Read computed style (after all CSS rules are applied)
const computedStyle = window.getComputedStyle(element);
console.log(computedStyle.width); // e.g., "300px"
console.log(computedStyle.display); // e.g., "block"

// Note: getComputedStyle returns a read-only CSSStyleDeclaration
// To modify styles, you must use element.style

Using style.cssText

For setting multiple styles at once, style.cssText can be more efficient:

// Set multiple styles at once with cssText (overwrites all inline styles)
element.style.cssText = 'color: #fff; background-color: #3498db; padding: 10px 15px; border-radius: 4px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);';

// Append styles (preserving existing inline styles)
element.style.cssText += '; margin-top: 20px; font-weight: bold;';

Using cssText causes only one reflow/repaint, whereas setting individual properties may cause multiple.

When to Use Direct Style Manipulation

Use Case Why Direct Styling Works Best
Dynamic positioning When element position must be calculated in JavaScript (e.g., tooltips, drag and drop)
Animations For precise control over timing and values (though CSS animations are often better)
User-controlled values When users can adjust properties (e.g., resizing, color pickers)
One-off style changes For styles that don't warrant creating a separate CSS class
Responsive adjustments When styles need to be calculated based on viewport or element dimensions

Practical Example: Tooltip Positioning

function positionTooltip(tooltipElement, targetElement) {
    // Get the position and dimensions of the target element
    const targetRect = targetElement.getBoundingClientRect();
    
    // Get the dimensions of the tooltip
    const tooltipRect = tooltipElement.getBoundingClientRect();
    
    // Calculate the position (centered above the target)
    const top = targetRect.top - tooltipRect.height - 10; // 10px gap
    const left = targetRect.left + (targetRect.width / 2) - (tooltipRect.width / 2);
    
    // Check if tooltip would be off-screen and adjust if necessary
    const rightEdge = left + tooltipRect.width;
    const windowWidth = window.innerWidth;
    
    if (rightEdge > windowWidth) {
        // Adjust so it's fully visible
        const offset = rightEdge - windowWidth + 10; // 10px buffer
        tooltipElement.style.left = (left - offset) + 'px';
    } else if (left < 0) {
        // Prevent it from going off the left edge
        tooltipElement.style.left = '10px';
    } else {
        tooltipElement.style.left = left + 'px';
    }
    
    // Set the top position
    if (top < 0) {
        // If there's not enough space above, position it below
        tooltipElement.style.top = (targetRect.bottom + 10) + 'px';
        tooltipElement.classList.add('tooltip-bottom');
        tooltipElement.classList.remove('tooltip-top');
    } else {
        tooltipElement.style.top = top + 'px';
        tooltipElement.classList.add('tooltip-top');
        tooltipElement.classList.remove('tooltip-bottom');
    }
    
    // Make sure the tooltip is visible
    tooltipElement.style.visibility = 'visible';
    tooltipElement.style.opacity = '1';
}

CSS Variables (Custom Properties)

CSS variables are a powerful way to create dynamic styles that can be updated with JavaScript and affect multiple elements at once.

Basics of CSS Variables

CSS variables are declared using the -- prefix and accessed using the var() function:

/* CSS */
:root {
    --primary-color: #3498db;
    --secondary-color: #2ecc71;
    --text-size: 16px;
    --spacing-unit: 8px;
}

.button {
    background-color: var(--primary-color);
    color: white;
    padding: calc(var(--spacing-unit) * 2) calc(var(--spacing-unit) * 3);
    font-size: var(--text-size);
}

/* Variables can be scoped to specific elements */
.dark-theme {
    --primary-color: #2980b9;
    --secondary-color: #27ae60;
}

Manipulating CSS Variables with JavaScript

You can get and set CSS variables using JavaScript:

// Get CSS variable values
function getCSSVariable(variableName) {
    // Get the styles from the root element
    const rootStyles = getComputedStyle(document.documentElement);
    // Return the variable value (trim removes whitespace)
    return rootStyles.getPropertyValue(`--${variableName}`).trim();
}

// Set CSS variable at the document level
function setCSSVariable(variableName, value) {
    document.documentElement.style.setProperty(`--${variableName}`, value);
}

// Example usage
console.log(getCSSVariable('primary-color')); // "#3498db"
setCSSVariable('primary-color', '#e74c3c'); // Changes to red

// Set CSS variable for a specific element and its children
function setElementCSSVariable(element, variableName, value) {
    element.style.setProperty(`--${variableName}`, value);
}

// Example: Change spacing for a specific container
const container = document.getElementById('special-container');
setElementCSSVariable(container, 'spacing-unit', '12px');

Practical Example: Theme Switcher

// CSS (in your stylesheet)
/*
:root {
    --bg-color: #ffffff;
    --text-color: #333333;
    --heading-color: #2c3e50;
    --link-color: #3498db;
    --border-color: #e0e0e0;
    --shadow-color: rgba(0, 0, 0, 0.1);
}

.dark-theme {
    --bg-color: #2c3e50;
    --text-color: #ecf0f1;
    --heading-color: #3498db;
    --link-color: #e74c3c;
    --border-color: #34495e;
    --shadow-color: rgba(0, 0, 0, 0.3);
}
*/

// Theme switching function
function setTheme(themeName) {
    // Remove all theme classes
    document.body.classList.remove('light-theme', 'dark-theme', 'sepia-theme');
    
    // Add the selected theme class
    if (themeName !== 'light-theme') {
        document.body.classList.add(themeName);
    }
    
    // Store the preference
    localStorage.setItem('theme', themeName);
    
    // Update the UI
    document.querySelectorAll('.theme-option').forEach(option => {
        option.classList.toggle('active', option.dataset.theme === themeName);
    });
}

// Initialize theme from saved preference
function initTheme() {
    const savedTheme = localStorage.getItem('theme') || 'light-theme';
    setTheme(savedTheme);
}

// Set up theme switcher buttons
document.querySelectorAll('.theme-option').forEach(option => {
    option.addEventListener('click', () => {
        setTheme(option.dataset.theme);
    });
});

// Initialize on page load
document.addEventListener('DOMContentLoaded', initTheme);

Advanced: User-Customizable UI with CSS Variables

// Example: Font size adjustment with slider
const fontSizeSlider = document.getElementById('font-size-slider');
const fontSizeDisplay = document.getElementById('font-size-display');

fontSizeSlider.addEventListener('input', function() {
    // Update the CSS variable based on slider value
    const newSize = this.value;
    document.documentElement.style.setProperty('--base-font-size', newSize + 'px');
    
    // Update the display
    fontSizeDisplay.textContent = newSize + 'px';
    
    // Store the preference
    localStorage.setItem('fontSize', newSize);
});

// Initialize font size from saved preference
const savedFontSize = localStorage.getItem('fontSize') || '16';
fontSizeSlider.value = savedFontSize;
fontSizeDisplay.textContent = savedFontSize + 'px';
document.documentElement.style.setProperty('--base-font-size', savedFontSize + 'px');

// Example: Color theme customization
const colorPickers = document.querySelectorAll('.color-picker');

colorPickers.forEach(picker => {
    const colorVariable = picker.dataset.colorVar;
    
    // Set initial color from current CSS var
    const initialColor = getComputedStyle(document.documentElement)
        .getPropertyValue('--' + colorVariable).trim();
    picker.value = initialColor;
    
    // Update when the color changes
    picker.addEventListener('input', function() {
        document.documentElement.style.setProperty(
            '--' + colorVariable, 
            this.value
        );
        
        // Store the preference
        localStorage.setItem('color-' + colorVariable, this.value);
    });
});

// Initialize color pickers from saved preferences
colorPickers.forEach(picker => {
    const colorVariable = picker.dataset.colorVar;
    const savedColor = localStorage.getItem('color-' + colorVariable);
    
    if (savedColor) {
        picker.value = savedColor;
        document.documentElement.style.setProperty(
            '--' + colorVariable, 
            savedColor
        );
    }
});

Animation with JavaScript

While CSS is often the preferred method for animations, there are cases where JavaScript-driven animations provide more flexibility:

Simple CSS Transitions with JavaScript Triggers

The simplest approach combines CSS transitions with JavaScript class toggling:

/* CSS */
/*
.fade-element {
    opacity: 0;
    transition: opacity 0.3s ease-in-out;
}

.fade-element.visible {
    opacity: 1;
}

.slide-element {
    transform: translateY(20px);
    opacity: 0;
    transition: transform 0.4s ease-out, opacity 0.4s ease-out;
}

.slide-element.visible {
    transform: translateY(0);
    opacity: 1;
}
*/

// JavaScript to trigger transitions
function showElement(element) {
    // Force a browser reflow before adding the visible class
    // This ensures the transition works reliably
    void element.offsetWidth;
    element.classList.add('visible');
}

function hideElement(element) {
    element.classList.remove('visible');
}

// Example usage
const notification = document.getElementById('notification');
showElement(notification);

// Hide after 3 seconds
setTimeout(() => {
    hideElement(notification);
}, 3000);

Animation with requestAnimationFrame

For more complex animations or when you need precise control, use requestAnimationFrame:

function animateElement(element, duration, easingFunction, animationProps) {
    const startTime = performance.now();
    const initialValues = {};
    
    // Store initial values
    for (const prop in animationProps) {
        // Get computed value (e.g., '200px' or 'rgb(255, 0, 0)')
        const computedValue = window.getComputedStyle(element)[prop];
        
        // Convert to a number if possible
        const numValue = parseFloat(computedValue);
        
        if (!isNaN(numValue)) {
            // Store numeric value and the unit
            initialValues[prop] = {
                value: numValue,
                unit: computedValue.replace(numValue.toString(), '') || ''
            };
        }
    }
    
    function animate(currentTime) {
        // Calculate progress (0 to 1)
        const elapsed = currentTime - startTime;
        let progress = Math.min(elapsed / duration, 1);
        
        // Apply easing function if provided
        if (easingFunction) {
            progress = easingFunction(progress);
        }
        
        // Update each property based on progress
        for (const prop in animationProps) {
            if (initialValues[prop]) {
                const startValue = initialValues[prop].value;
                const endValue = animationProps[prop];
                const unit = initialValues[prop].unit;
                
                // Calculate current value
                const currentValue = startValue + (endValue - startValue) * progress;
                
                // Apply to element
                element.style[prop] = currentValue + unit;
            }
        }
        
        // Continue animation if not finished
        if (progress < 1) {
            requestAnimationFrame(animate);
        }
    }
    
    // Start animation
    requestAnimationFrame(animate);
}

// Example easing functions
const easingFunctions = {
    linear: t => t,
    easeInQuad: t => t * t,
    easeOutQuad: t => t * (2 - t),
    easeInOutQuad: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
    easeInCubic: t => t * t * t,
    easeOutCubic: t => (--t) * t * t + 1,
    easeInOutCubic: t => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
};

// Example usage
const box = document.getElementById('animated-box');

animateElement(box, 1000, easingFunctions.easeOutQuad, {
    opacity: 1,
    width: 300,
    height: 200
});

// Chain animations
setTimeout(() => {
    animateElement(box, 800, easingFunctions.easeInOutCubic, {
        opacity: 0.7,
        width: 200,
        height: 150
    });
}, 1500);

CSS vs. JavaScript Animations

Use CSS Animations/Transitions when:

  • The animation is simple (e.g., hover effects, simple transitions)
  • Performance is critical (CSS animations can be hardware-accelerated)
  • You don't need precise programmatic control

Use JavaScript Animations when:

  • You need complex logic or dynamic calculations
  • You need to synchronize with other events or animations
  • You need fine-grained control over each frame
  • You need to support older browsers without CSS animation capabilities

Responding to User Interaction

Dynamic styling often needs to respond to user interactions like hover, click, focus, etc. While CSS can handle many of these with pseudo-classes, JavaScript provides more flexibility:

Toggle States on Interaction

// Toggle dropdown visibility
const dropdownToggle = document.querySelector('.dropdown-toggle');
const dropdownMenu = document.querySelector('.dropdown-menu');

dropdownToggle.addEventListener('click', function(e) {
    e.preventDefault();
    
    // Toggle active state
    this.classList.toggle('active');
    
    // Toggle menu visibility
    dropdownMenu.classList.toggle('visible');
});

// Close the dropdown when clicking outside
document.addEventListener('click', function(e) {
    if (!dropdownToggle.contains(e.target) && !dropdownMenu.contains(e.target)) {
        dropdownToggle.classList.remove('active');
        dropdownMenu.classList.remove('visible');
    }
});

Hover Effects Beyond CSS

// Magnify image on hover
const productImage = document.querySelector('.product-image');
const zoomContainer = document.querySelector('.zoom-container');

productImage.addEventListener('mousemove', function(e) {
    // Make zoom container visible
    zoomContainer.style.display = 'block';
    
    // Get image and container dimensions
    const imgRect = this.getBoundingClientRect();
    
    // Calculate mouse position as percentages
    const xPos = (e.clientX - imgRect.left) / imgRect.width * 100;
    const yPos = (e.clientY - imgRect.top) / imgRect.height * 100;
    
    // Move background image in zoom container
    zoomContainer.style.backgroundImage = `url('${this.src}')`;
    zoomContainer.style.backgroundPosition = `${xPos}% ${yPos}%`;
    zoomContainer.style.backgroundSize = '250%';
});

productImage.addEventListener('mouseleave', function() {
    // Hide zoom container when not hovering
    zoomContainer.style.display = 'none';
});

Form Validation Styling

// Validate form fields as user types
const passwordField = document.getElementById('password');
const passwordStrength = document.getElementById('password-strength');
const passwordStrengthBar = document.getElementById('strength-bar');

passwordField.addEventListener('input', function() {
    const password = this.value;
    
    // Reset classes
    this.classList.remove('weak', 'medium', 'strong');
    passwordStrength.classList.remove('weak', 'medium', 'strong');
    
    if (!password) {
        passwordStrength.textContent = '';
        passwordStrengthBar.style.width = '0';
        return;
    }
    
    // Calculate password strength
    let strength = 0;
    
    // Length check
    if (password.length >= 8) strength += 1;
    if (password.length >= 12) strength += 1;
    
    // Complexity checks
    if (/[A-Z]/.test(password)) strength += 1;
    if (/[a-z]/.test(password)) strength += 1;
    if (/[0-9]/.test(password)) strength += 1;
    if (/[^A-Za-z0-9]/.test(password)) strength += 1;
    
    // Determine strength level
    let strengthLevel, strengthText, barWidth;
    
    if (strength < 3) {
        strengthLevel = 'weak';
        strengthText = 'Weak';
        barWidth = '33%';
    } else if (strength < 5) {
        strengthLevel = 'medium';
        strengthText = 'Medium';
        barWidth = '67%';
    } else {
        strengthLevel = 'strong';
        strengthText = 'Strong';
        barWidth = '100%';
    }
    
    // Apply styling
    this.classList.add(strengthLevel);
    passwordStrength.textContent = strengthText;
    passwordStrength.classList.add(strengthLevel);
    passwordStrengthBar.style.width = barWidth;
    passwordStrengthBar.className = `strength-bar ${strengthLevel}`;
});

Responsive Design with JavaScript

While CSS media queries handle most responsive design needs, JavaScript can enhance responsiveness with dynamic calculations and adjustments:

Element-Based Responsiveness

// Responsive adjustments based on element size, not just viewport
function adjustColumnLayout() {
    const container = document.querySelector('.gallery-container');
    const items = container.querySelectorAll('.gallery-item');
    const containerWidth = container.clientWidth;
    
    // Determine how many columns based on container width
    let columns;
    if (containerWidth < 500) {
        columns = 1;
    } else if (containerWidth < 800) {
        columns = 2;
    } else if (containerWidth < 1200) {
        columns = 3;
    } else {
        columns = 4;
    }
    
    // Update container class
    container.className = `gallery-container columns-${columns}`;
    
    // Optionally, set explicit item widths
    const itemWidth = `calc(${100 / columns}% - ${(columns - 1) * 10 / columns}px)`;
    items.forEach(item => {
        item.style.width = itemWidth;
    });
}

// Run on load and resize
window.addEventListener('load', adjustColumnLayout);
window.addEventListener('resize', adjustColumnLayout);

Content-Aware Adjustments

// Adjust text size based on content length
function adjustTextSize() {
    const headings = document.querySelectorAll('.card-title');
    
    headings.forEach(heading => {
        // Reset to default
        heading.style.fontSize = '';
        
        // Check if content is overflowing
        if (heading.scrollWidth > heading.clientWidth) {
            // Content is too wide, reduce font size
            const currentSize = parseFloat(getComputedStyle(heading).fontSize);
            const ratio = heading.clientWidth / heading.scrollWidth;
            const newSize = Math.floor(currentSize * ratio * 0.9); // 0.9 for a little buffer
            
            heading.style.fontSize = newSize + 'px';
        }
    });
}

// Truncate text with "..." if needed
function truncateText() {
    const descriptions = document.querySelectorAll('.card-description');
    
    descriptions.forEach(desc => {
        // Store the original text if not already stored
        if (!desc.dataset.originalText) {
            desc.dataset.originalText = desc.textContent;
        }
        
        // Reset to original text
        desc.textContent = desc.dataset.originalText;
        
        // Check if height exceeds max (assuming max-height is set in CSS)
        const maxHeight = parseInt(getComputedStyle(desc).maxHeight);
        
        if (desc.scrollHeight > maxHeight) {
            // Text overflows, need to truncate
            let text = desc.textContent;
            let lastSpace;
            
            // Binary search to find optimal truncation point
            while (desc.scrollHeight > maxHeight && text.length > 0) {
                // Reduce by about 10% each time
                const newLength = Math.floor(text.length * 0.9);
                lastSpace = text.lastIndexOf(' ', newLength);
                
                if (lastSpace > 0) {
                    text = text.substring(0, lastSpace);
                } else {
                    text = text.substring(0, newLength);
                }
                
                desc.textContent = text + '...';
            }
        }
    });
}

// Run when content or window size changes
window.addEventListener('load', function() {
    adjustTextSize();
    truncateText();
});
window.addEventListener('resize', function() {
    adjustTextSize();
    truncateText();
});

Real-World Applications

Application 1: Interactive Image Gallery with Filters

Dynamic styling to create an interactive image gallery with filter effects:

// Image gallery with filter effects
document.addEventListener('DOMContentLoaded', function() {
    const gallery = document.querySelector('.gallery');
    const images = gallery.querySelectorAll('.gallery-item');
    const filterButtons = document.querySelectorAll('.filter-btn');
    const filterControls = document.querySelector('.filter-controls');
    
    // Set up filter buttons
    filterButtons.forEach(button => {
        button.addEventListener('click', function() {
            // Update active filter button
            filterButtons.forEach(btn => btn.classList.remove('active'));
            this.classList.add('active');
            
            // Get the selected filter
            const filter = this.dataset.filter;
            
            // Update the gallery class
            gallery.className = 'gallery ' + filter;
            
            // Optionally, add specific classes to each image
            images.forEach(img => {
                img.classList.remove('saturate', 'grayscale', 'sepia', 'invert', 'blur');
                
                if (filter !== 'normal') {
                    img.classList.add(filter);
                }
            });
        });
    });
    
    // Add intensity control for the current filter
    const intensitySlider = document.getElementById('filter-intensity');
    intensitySlider.addEventListener('input', function() {
        const activeFilter = document.querySelector('.filter-btn.active').dataset.filter;
        const intensity = this.value;
        
        // Skip for 'normal' filter
        if (activeFilter === 'normal') return;
        
        // Apply filter with intensity using CSS variables
        document.documentElement.style.setProperty('--filter-intensity', intensity);
        
        // Different filters might need different units
        let filterValue;
        switch (activeFilter) {
            case 'saturate':
            case 'sepia':
                filterValue = `${activeFilter}(${intensity}%)`;
                break;
            case 'grayscale':
                filterValue = `grayscale(${intensity}%)`;
                break;
            case 'invert':
                filterValue = `invert(${intensity}%)`;
                break;
            case 'blur':
                filterValue = `blur(${intensity * 0.1}px)`;
                break;
            default:
                filterValue = 'none';
        }
        
        // Apply to all images
        images.forEach(img => {
            img.style.filter = filterValue;
        });
    });
    
    // Add image hover effect
    images.forEach(img => {
        img.addEventListener('mouseenter', function() {
            this.classList.add('hover');
            
            // Create and show a zoom icon
            if (!this.querySelector('.zoom-icon')) {
                const zoomIcon = document.createElement('div');
                zoomIcon.className = 'zoom-icon';
                zoomIcon.innerHTML = '🔍';
                this.appendChild(zoomIcon);
            }
        });
        
        img.addEventListener('mouseleave', function() {
            this.classList.remove('hover');
        });
        
        // Full-screen view on click
        img.addEventListener('click', function() {
            const fullscreen = document.createElement('div');
            fullscreen.className = 'fullscreen-view';
            
            const fullImg = document.createElement('img');
            fullImg.src = this.querySelector('img').src.replace('thumbnail', 'full');
            fullImg.alt = this.querySelector('img').alt;
            
            // Preserve current filters
            fullImg.style.filter = this.querySelector('img').style.filter;
            
            fullscreen.appendChild(fullImg);
            
            // Add close button
            const closeBtn = document.createElement('button');
            closeBtn.className = 'close-fullscreen';
            closeBtn.innerHTML = '×';
            closeBtn.addEventListener('click', function(e) {
                e.stopPropagation();
                fullscreen.classList.add('closing');
                setTimeout(() => {
                    document.body.removeChild(fullscreen);
                }, 300);
            });
            
            fullscreen.appendChild(closeBtn);
            document.body.appendChild(fullscreen);
            
            // Allow clicking outside to close
            fullscreen.addEventListener('click', function(e) {
                if (e.target === fullscreen) {
                    fullscreen.classList.add('closing');
                    setTimeout(() => {
                        document.body.removeChild(fullscreen);
                    }, 300);
                }
            });
        });
    });
});

Application 2: Customizable Dashboard Interface

Creating a dashboard with customizable themes and layouts:

document.addEventListener('DOMContentLoaded', function() {
    // Load user preferences
    const userPrefs = loadUserPreferences();
    
    // Apply initial preferences
    applyUserPreferences(userPrefs);
    
    // Set up preference controls
    setupPreferenceControls(userPrefs);
});

function loadUserPreferences() {
    // Try to load from localStorage
    let prefs;
    try {
        prefs = JSON.parse(localStorage.getItem('dashboard-prefs'));
    } catch (e) {
        console.error('Error loading preferences:', e);
    }
    
    // Default preferences
    return prefs || {
        theme: 'light',
        fontSize: 14,
        density: 'normal',
        layout: 'grid',
        highlight: '#3498db',
        colorBlindMode: false
    };
}

function applyUserPreferences(prefs) {
    // Apply theme
    document.body.classList.remove('theme-light', 'theme-dark', 'theme-blue');
    document.body.classList.add(`theme-${prefs.theme}`);
    
    // Set CSS variables for customization
    document.documentElement.style.setProperty('--base-font-size', `${prefs.fontSize}px`);
    document.documentElement.style.setProperty('--highlight-color', prefs.highlight);
    
    // Apply layout
    const dashboard = document.querySelector('.dashboard');
    dashboard.classList.remove('layout-grid', 'layout-list', 'layout-compact');
    dashboard.classList.add(`layout-${prefs.layout}`);
    
    // Apply density
    dashboard.classList.remove('density-compact', 'density-normal', 'density-spacious');
    dashboard.classList.add(`density-${prefs.density}`);
    
    // Apply color blind mode if needed
    if (prefs.colorBlindMode) {
        document.documentElement.classList.add('color-blind-mode');
    } else {
        document.documentElement.classList.remove('color-blind-mode');
    }
    
    // Update widgets
    document.querySelectorAll('.widget').forEach(widget => {
        refreshWidget(widget);
    });
}

function setupPreferenceControls(prefs) {
    // Theme selector
    document.querySelectorAll('.theme-option').forEach(option => {
        option.classList.toggle('active', option.dataset.theme === prefs.theme);
        
        option.addEventListener('click', function() {
            prefs.theme = this.dataset.theme;
            saveAndApplyPreferences(prefs);
            
            // Update active state
            document.querySelectorAll('.theme-option').forEach(opt => {
                opt.classList.toggle('active', opt === this);
            });
        });
    });
    
    // Font size control
    const fontSizeSlider = document.getElementById('font-size-slider');
    const fontSizeValue = document.getElementById('font-size-value');
    
    fontSizeSlider.value = prefs.fontSize;
    fontSizeValue.textContent = prefs.fontSize + 'px';
    
    fontSizeSlider.addEventListener('input', function() {
        const newSize = parseInt(this.value);
        fontSizeValue.textContent = newSize + 'px';
        prefs.fontSize = newSize;
        document.documentElement.style.setProperty('--base-font-size', `${newSize}px`);
    });
    
    fontSizeSlider.addEventListener('change', function() {
        saveAndApplyPreferences(prefs);
    });
    
    // Layout toggle
    document.querySelectorAll('.layout-option').forEach(option => {
        option.classList.toggle('active', option.dataset.layout === prefs.layout);
        
        option.addEventListener('click', function() {
            prefs.layout = this.dataset.layout;
            saveAndApplyPreferences(prefs);
            
            // Update active state
            document.querySelectorAll('.layout-option').forEach(opt => {
                opt.classList.toggle('active', opt === this);
            });
        });
    });
    
    // Highlight color picker
    const colorPicker = document.getElementById('highlight-color');
    colorPicker.value = prefs.highlight;
    
    colorPicker.addEventListener('input', function() {
        prefs.highlight = this.value;
        document.documentElement.style.setProperty('--highlight-color', this.value);
    });
    
    colorPicker.addEventListener('change', function() {
        saveAndApplyPreferences(prefs);
    });
    
    // Color blind mode toggle
    const colorBlindToggle = document.getElementById('color-blind-toggle');
    colorBlindToggle.checked = prefs.colorBlindMode;
    
    colorBlindToggle.addEventListener('change', function() {
        prefs.colorBlindMode = this.checked;
        saveAndApplyPreferences(prefs);
    });
}

function saveAndApplyPreferences(prefs) {
    // Save to localStorage
    localStorage.setItem('dashboard-prefs', JSON.stringify(prefs));
    
    // Apply the new preferences
    applyUserPreferences(prefs);
}

function refreshWidget(widget) {
    // Add a loading animation
    widget.classList.add('loading');
    
    // Simulate an API call to refresh the widget
    setTimeout(() => {
        // Remove loading animation
        widget.classList.remove('loading');
        
        // If it's a chart widget, redraw the chart
        if (widget.classList.contains('chart-widget')) {
            // In a real app, you would redraw your chart here
            console.log('Redrawing chart for widget:', widget.id);
        }
    }, 800);
}

Application 3: Interactive Product Configurator

A visual product configurator with real-time style updates:

document.addEventListener('DOMContentLoaded', function() {
    // Get elements
    const productImage = document.getElementById('product-image');
    const colorOptions = document.querySelectorAll('.color-option');
    const sizeOptions = document.querySelectorAll('.size-option');
    const materialOptions = document.querySelectorAll('.material-option');
    const priceDisplay = document.getElementById('product-price');
    
    // Product configuration state
    const config = {
        color: 'red',
        size: 'medium',
        material: 'cotton',
        basePrice: 29.99
    };
    
    // Price modifiers
    const priceModifiers = {
        colors: { red: 0, blue: 0, black: 2, pattern: 5 },
        sizes: { small: -2, medium: 0, large: 2, xlarge: 4 },
        materials: { cotton: 0, polyester: -5, premium: 10 }
    };
    
    // Initialize the configurator
    updateProductDisplay();
    
    // Set up color options
    colorOptions.forEach(option => {
        option.addEventListener('click', function() {
            // Remove active class from all options
            colorOptions.forEach(opt => opt.classList.remove('active'));
            
            // Set active class on selected option
            this.classList.add('active');
            
            // Update configuration
            config.color = this.dataset.color;
            
            // Update product display
            updateProductDisplay();
        });
    });
    
    // Set up size options
    sizeOptions.forEach(option => {
        option.addEventListener('click', function() {
            sizeOptions.forEach(opt => opt.classList.remove('active'));
            this.classList.add('active');
            config.size = this.dataset.size;
            updateProductDisplay();
        });
    });
    
    // Set up material options
    materialOptions.forEach(option => {
        option.addEventListener('click', function() {
            materialOptions.forEach(opt => opt.classList.remove('active'));
            this.classList.add('active');
            config.material = this.dataset.material;
            updateProductDisplay();
        });
    });
    
    function updateProductDisplay() {
        // Update product image
        productImage.className = 'product-image';
        productImage.classList.add(
            `color-${config.color}`,
            `size-${config.size}`,
            `material-${config.material}`
        );
        
        // Update image source (in a real app this would use actual images)
        productImage.src = `/images/products/tshirt-${config.color}.jpg`;
        
        // Update product description elements
        document.getElementById('selected-color').textContent = config.color;
        document.getElementById('selected-size').textContent = config.size.toUpperCase();
        document.getElementById('selected-material').textContent = config.material;
        
        // Calculate and update price
        const colorModifier = priceModifiers.colors[config.color] || 0;
        const sizeModifier = priceModifiers.sizes[config.size] || 0;
        const materialModifier = priceModifiers.materials[config.material] || 0;
        
        const totalPrice = config.basePrice + colorModifier + sizeModifier + materialModifier;
        priceDisplay.textContent = `$${totalPrice.toFixed(2)}`;
        
        // Add a subtle highlight effect to show changes
        priceDisplay.classList.remove('price-updated');
        void priceDisplay.offsetWidth; // Force reflow
        priceDisplay.classList.add('price-updated');
        
        // Update the Add to Cart button state
        const addToCartBtn = document.getElementById('add-to-cart');
        addToCartBtn.disabled = false;
        addToCartBtn.textContent = 'Add to Cart';
        
        // Check inventory (simulated)
        const isInStock = checkInventory(config);
        if (!isInStock) {
            addToCartBtn.disabled = true;
            addToCartBtn.textContent = 'Out of Stock';
            
            // Add out of stock indicator
            productImage.classList.add('out-of-stock');
        }
    }
    
    // Simulate inventory check
    function checkInventory(config) {
        // Simulated out-of-stock combinations
        const outOfStock = [
            { color: 'black', size: 'small', material: 'premium' },
            { color: 'pattern', size: 'xlarge', material: 'cotton' }
        ];
        
        return !outOfStock.some(item => 
            item.color === config.color && 
            item.size === config.size && 
            item.material === config.material
        );
    }
});

Practical Exercise

Create a comprehensive theme system with the following features:

  1. Implement a light/dark theme toggle with smooth transitions
  2. Add a color picker to customize accent colors for the UI
  3. Create a font size adjuster that affects all text elements proportionally
  4. Add a toggle for a high-contrast mode for accessibility
  5. Implement a "reduced motion" setting that disables animations
  6. Create a layout switcher (grid/list view) for a content section
  7. Store user preferences in localStorage and apply them on page load
  8. Add a "reset to defaults" button

This exercise will give you practice with various dynamic styling techniques including CSS variables, class manipulation, direct style modification, and state persistence.

Best Practices

Performance

  • Batch DOM updates to minimize reflows and repaints
  • Use CSS classes instead of inline styles when possible
  • Leverage CSS animations/transitions over JavaScript for simple animations
  • Use requestAnimationFrame for JavaScript animations
  • Consider hardware acceleration for animations (transform, opacity)

Maintainability

  • Follow a consistent naming convention for classes
  • Separate concerns - keep presentation in CSS, behavior in JavaScript
  • Organize styles by component or feature
  • Document theme variables and their purpose
  • Create utility functions for common styling operations

Accessibility

  • Ensure sufficient color contrast in all themes
  • Respect user preferences like reduced motion
  • Use semantic elements with appropriate ARIA attributes
  • Test with screen readers to ensure dynamic changes are announced
  • Ensure keyboard focus is visible in all themes

Browser Compatibility

  • Test across browsers to ensure consistent appearance
  • Provide fallbacks for newer CSS features
  • Consider using feature detection before using advanced features
  • Use vendor prefixes or autoprefixer for CSS properties that need them

Summary

Master these dynamic styling techniques to create highly interactive, responsive, and user-friendly web applications.

Additional Resources