Modifying Element Styles and Classes

Week 4: Web Fundamentals - Thursday Afternoon Session

Dynamic Styling of Elements

Welcome to our session on modifying element styles and classes! Now that we understand how to select, traverse, create, and remove DOM elements, we'll explore how to dynamically change their appearance and behavior by manipulating their styles and CSS classes.

This lecture belongs to Week 4: Web Fundamentals and is part of our Python Full Stack Developer course. You'll find this content in the folder structure: /04week/04week_4day_h.html

The ability to change an element's appearance dynamically is crucial for creating interactive web applications. Whether it's highlighting a selected item, showing validation states, or creating animations, style manipulation is what brings life and interactivity to your interfaces.

Why Modify Styles Dynamically?

Before diving into the techniques, let's understand why dynamic styling is so important:

Stage Production Metaphor: Think of your web page as a stage production. The HTML elements are actors, JavaScript is the director, and CSS styles are the costumes, lighting, and props. By changing styles dynamically, you're essentially directing the show in real-time—changing costumes (classes), adjusting lighting (colors, visibility), and moving props (positioning)—all in response to the audience's (user's) reactions and the flow of the story (application state).

Working with Inline Styles

The most direct way to modify an element's appearance is by changing its inline styles through the style property.

The style Object

Each element has a style object that gives you direct access to set inline CSS properties:

// Get a reference to an element
const heading = document.getElementById('main-title');

// Change a single style property
heading.style.color = 'blue';
heading.style.fontSize = '24px';  // Note: hyphenated CSS properties (font-size) become camelCase in JS (fontSize)
heading.style.fontWeight = 'bold';
heading.style.textTransform = 'uppercase';

// Change positioning
const box = document.querySelector('.box');
box.style.position = 'relative';
box.style.left = '50px';
box.style.top = '20px';

// Change dimensions
box.style.width = '200px';
box.style.height = '150px';

// Change background
box.style.backgroundColor = '#f0f0f0';
box.style.backgroundImage = 'url("pattern.png")';

// Change borders
box.style.border = '1px solid black';
box.style.borderRadius = '5px';

CSS Property Translation: When working with the style object, CSS property names that contain hyphens are converted to camelCase in JavaScript:

CSS Property JavaScript Style Property
background-color backgroundColor
font-size fontSize
border-radius borderRadius
margin-left marginLeft
z-index zIndex

Reading Current Styles

Reading inline styles works the same way, but it only gives you styles that were set directly on the element's style attribute:

const heading = document.getElementById('main-title');

// Read inline styles
console.log(heading.style.color); // Returns the color if set directly as an inline style
console.log(heading.style.fontSize); // Returns font size if set directly as an inline style

// IMPORTANT: This doesn't give styles from stylesheets!
// If the color is set in a CSS file rather than inline, heading.style.color will return ''

To read the actual computed style of an element (including styles from stylesheets), you need to use getComputedStyle():

const heading = document.getElementById('main-title');

// Get all computed styles
const computedStyle = window.getComputedStyle(heading);

// Read specific properties
console.log(computedStyle.color); // Returns the actual color from any source
console.log(computedStyle.fontSize); // Returns the actual font size from any source

// For pseudo-elements like ::before or ::after
const beforeStyle = window.getComputedStyle(heading, '::before');
console.log(beforeStyle.content);

Important note: Computed styles are read-only. You cannot set properties on the object returned by getComputedStyle().

Setting Multiple Styles at Once

To set multiple styles at once, you can use the cssText property:

const box = document.querySelector('.box');

// Set multiple styles in one go
box.style.cssText = 'color: white; background-color: blue; padding: 10px; border-radius: 5px;';

// Or append to existing inline styles
box.style.cssText += 'margin-top: 20px; font-weight: bold;';

Alternatively, you can use the Object.assign() method to apply multiple styles:

const box = document.querySelector('.box');

// Apply multiple styles using Object.assign
Object.assign(box.style, {
    color: 'white',
    backgroundColor: 'blue',
    padding: '10px',
    borderRadius: '5px',
    marginTop: '20px',
    fontWeight: 'bold'
});

Limitations of Inline Styles

While inline styles provide direct control, they have several limitations:

  • High Specificity: Inline styles have very high specificity, which can make them hard to override
  • No Reusability: Styles applied to one element cannot be easily reused on other elements
  • No Media Queries: Inline styles cannot use media queries for responsive design
  • No Pseudo-Classes/Elements: Cannot target states like :hover or elements like ::before
  • Mixing Concerns: Puts presentation details in your JavaScript, reducing separation of concerns

For these reasons, changing CSS classes is often a better approach than direct style manipulation.

Working with CSS Classes

Manipulating CSS classes is a more powerful and maintainable approach to changing element styling:

The className Property

The traditional way to work with classes is through the className property, which represents the value of the element's class attribute:

const box = document.querySelector('.box');

// Get the current classes
console.log(box.className); // Returns a space-separated string like "box highlighted large"

// Replace all classes
box.className = 'box active'; // Completely replaces existing classes

// Add a class by appending to className
box.className += ' highlighted'; // Careful: easy to add duplicate classes this way

// Check if an element has a specific class (clunky way)
if (box.className.includes('active')) { // Problematic if 'active' is part of another class name
    console.log('Box is active');
}

Working with className can be error-prone because it operates on a space-separated string rather than a proper collection of classes. This is why modern browsers provide the classList API.

The classList API

The classList property provides methods to manipulate an element's classes in a much more convenient way:

const box = document.querySelector('.box');

// Add classes (doesn't add if already present)
box.classList.add('active');
box.classList.add('highlighted', 'large'); // Add multiple classes at once

// Remove classes
box.classList.remove('inactive');
box.classList.remove('small', 'hidden'); // Remove multiple classes at once

// Toggle classes (add if not present, remove if present)
box.classList.toggle('selected'); // Returns true if the class was added, false if removed

// Conditional toggle (second parameter determines whether to add or remove)
box.classList.toggle('disabled', isDisabled); // Add if isDisabled is true, remove if false

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

// Check if an element has a specific class
if (box.classList.contains('active')) {
    console.log('Box is active');
}

// Get the number of classes
console.log(box.classList.length);

// Access classes by index (classList is array-like)
console.log(box.classList[0]); // First class

// Iterate over all classes
for (const className of box.classList) {
    console.log(className);
}

The classList API is much more robust than className and should be your go-to method for manipulating classes.

Common Class Manipulation Patterns

Here are some common patterns for class manipulation in real-world scenarios:

// Toggle active state on click
document.querySelectorAll('.tab').forEach(tab => {
    tab.addEventListener('click', function() {
        // Remove active class from all tabs
        document.querySelectorAll('.tab').forEach(t => {
            t.classList.remove('active');
        });
        
        // Add active class to clicked tab
        this.classList.add('active');
    });
});

// Toggle panel visibility
document.querySelectorAll('.toggle-button').forEach(button => {
    button.addEventListener('click', function() {
        const panel = this.nextElementSibling;
        panel.classList.toggle('visible');
        
        // Update button text based on panel visibility
        if (panel.classList.contains('visible')) {
            this.textContent = 'Hide panel';
        } else {
            this.textContent = 'Show panel';
        }
    });
});

// Form validation feedback
document.querySelectorAll('input').forEach(input => {
    input.addEventListener('blur', function() {
        // Remove all state classes
        this.classList.remove('valid', 'invalid');
        
        if (this.checkValidity()) {
            this.classList.add('valid');
        } else {
            this.classList.add('invalid');
        }
    });
});

// Conditional rendering based on state
function updateUserStatus(userId, isOnline) {
    const userElement = document.querySelector(`.user[data-id="${userId}"]`);
    
    userElement.classList.toggle('online', isOnline);
    userElement.classList.toggle('offline', !isOnline);
}

Why Classes are Better than Inline Styles

There are several reasons to prefer class manipulation over direct style manipulation:

  • Separation of Concerns: Keep styling definitions in CSS, behavioral logic in JavaScript
  • Reusability: Apply the same styles to multiple elements by adding the same class
  • Maintainability: Update styles in one place (CSS) rather than throughout your JavaScript
  • Performance: Changing classes can be more efficient than setting multiple style properties
  • Full CSS Power: Use all CSS features including media queries, pseudo-classes, and CSS animations
  • Encapsulation: Group related styles together under a single class name
  • State Representation: Class names can semantically represent element states (e.g., 'active', 'disabled')

Chess Metaphor: If direct style manipulation is like moving individual chess pieces one property at a time (change color, change size, change position), class manipulation is like executing well-defined chess strategies with a single move (e.g., "castle kingside"). Both achieve the goal, but the latter is more efficient, maintainable, and communicates intent more clearly.

Manipulating CSS Variables

CSS custom properties (variables) provide another powerful way to modify styles dynamically:

What Are CSS Variables?

CSS variables are custom properties that you define and can reference throughout your stylesheet:

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

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

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

Unlike traditional CSS properties, CSS variables can be modified using JavaScript, making them ideal for dynamic theming and responsive adjustments.

Manipulating CSS Variables with JavaScript

You can set and get CSS variables using the setProperty() and getPropertyValue() methods:

// Set a CSS variable on the :root (affects the whole document)
document.documentElement.style.setProperty('--primary-color', '#ff5722');

// Set a CSS variable on a specific element
const header = document.querySelector('header');
header.style.setProperty('--header-height', '60px');

// Get the value of a CSS variable
const primaryColor = getComputedStyle(document.documentElement)
    .getPropertyValue('--primary-color').trim();
console.log('Primary color:', primaryColor);

// Using CSS variables for dynamic theming
function setTheme(theme) {
    if (theme === 'dark') {
        document.documentElement.style.setProperty('--background-color', '#222');
        document.documentElement.style.setProperty('--text-color', '#eee');
        document.documentElement.style.setProperty('--primary-color', '#5c6bc0');
    } else {
        document.documentElement.style.setProperty('--background-color', '#fff');
        document.documentElement.style.setProperty('--text-color', '#333');
        document.documentElement.style.setProperty('--primary-color', '#3f51b5');
    }
}

CSS variables are particularly useful for theme switching, responsive adjustments, and creating components with customizable styles.

Advantages of CSS Variables

CSS variables offer several benefits for dynamic styling:

  • Cascading Nature: Changes to variables cascade down to all elements using them
  • Reduced Redundancy: Define a value once and use it many places
  • Scope Control: Set variables at different levels (root, component, element)
  • Runtime Updates: Change appearance across the site by updating a few variables
  • Better Performance: More efficient than updating many individual style properties
  • Contextual Changes: Variables can be adjusted based on media queries or container context
// Example: Creating a customizable button component with CSS variables
.custom-button {
    --button-bg: #3498db;
    --button-color: white;
    --button-padding: 10px 20px;
    --button-radius: 4px;
    
    background-color: var(--button-bg);
    color: var(--button-color);
    padding: var(--button-padding);
    border-radius: var(--button-radius);
    border: none;
    cursor: pointer;
}

// JavaScript to customize button instances
document.querySelectorAll('.custom-button').forEach((button, index) => {
    // Create different colored buttons
    const hue = (index * 40) % 360;
    button.style.setProperty('--button-bg', `hsl(${hue}, 70%, 60%)`);
    
    // Make every third button rounded
    if (index % 3 === 0) {
        button.style.setProperty('--button-radius', '25px');
    }
});

Central Heating Metaphor: CSS variables are like a central heating system in a building. Instead of adjusting the temperature in each room individually (changing multiple style properties), you can simply adjust the thermostat (the CSS variable) and the change propagates throughout the entire building automatically.

Responding to Viewport and Media Changes

Modern web applications need to adapt to different screen sizes and media preferences:

Adding Responsive Classes

One approach is to add or remove classes based on viewport size:

// Update classes based on viewport width
function updateResponsiveClasses() {
    const viewport = window.innerWidth;
    const body = document.body;
    
    // Remove all responsive classes
    body.classList.remove('viewport-xs', 'viewport-sm', 'viewport-md', 'viewport-lg', 'viewport-xl');
    
    // Add appropriate class
    if (viewport < 576) {
        body.classList.add('viewport-xs');
    } else if (viewport < 768) {
        body.classList.add('viewport-sm');
    } else if (viewport < 992) {
        body.classList.add('viewport-md');
    } else if (viewport < 1200) {
        body.classList.add('viewport-lg');
    } else {
        body.classList.add('viewport-xl');
    }
}

// Initial call
updateResponsiveClasses();

// Update on window resize
window.addEventListener('resize', updateResponsiveClasses);

This approach allows you to apply different styles based on the current viewport size by targeting these classes in your CSS.

JavaScript Media Queries

The matchMedia() API allows you to check media queries directly in JavaScript:

// Check if viewport matches a media query
const isMobile = window.matchMedia('(max-width: 767px)').matches;

if (isMobile) {
    // Apply mobile-specific behaviors
    document.querySelector('.navigation').classList.add('collapsed');
}

// React to media query changes
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');

function handleDarkModeChange(e) {
    if (e.matches) {
        document.body.classList.add('dark-theme');
    } else {
        document.body.classList.remove('dark-theme');
    }
}

// Initial check
handleDarkModeChange(darkModeQuery);

// Listen for changes
darkModeQuery.addEventListener('change', handleDarkModeChange);

The matchMedia() API is especially useful for adapting JavaScript behavior based on the same media queries you use in CSS, ensuring consistent responsive behavior.

Dynamic Layout Manipulation

Sometimes you need to reorganize the DOM based on viewport size:

// Move elements in the DOM based on viewport size
function updateLayout() {
    const sidebar = document.getElementById('sidebar');
    const mainContent = document.getElementById('main-content');
    const sidebarContent = document.getElementById('sidebar-content');
    const mobileSidebarContainer = document.getElementById('mobile-sidebar-container');
    
    if (window.innerWidth < 768) {
        // Mobile layout: Move sidebar content to the mobile container
        if (sidebarContent.parentNode === sidebar) {
            mobileSidebarContainer.appendChild(sidebarContent);
        }
    } else {
        // Desktop layout: Move sidebar content back to sidebar
        if (sidebarContent.parentNode === mobileSidebarContainer) {
            sidebar.appendChild(sidebarContent);
        }
    }
}

// Initial call and resize listener
updateLayout();
window.addEventListener('resize', updateLayout);

This approach can be useful for more complex layout changes that cannot be achieved with CSS alone, though CSS should generally be preferred for responsive layouts when possible.

CSS Transitions and Animations with JavaScript

JavaScript can trigger CSS transitions and animations by changing classes or properties:

Working with CSS Transitions

CSS transitions provide smooth interpolation between property values:

/* CSS */
.box {
    width: 100px;
    height: 100px;
    background-color: #3498db;
    transition: all 0.3s ease-in-out;
}

.box.expanded {
    width: 200px;
    height: 200px;
    background-color: #e74c3c;
}

/* JavaScript */
const box = document.querySelector('.box');

box.addEventListener('click', function() {
    // Toggle the class to trigger the transition
    this.classList.toggle('expanded');
});

Using classes to trigger transitions is often the most maintainable approach. The transition logic stays in your CSS, while JavaScript simply toggles the state.

Transition Events

You can listen for the completion of a CSS transition:

const box = document.querySelector('.box');

box.addEventListener('click', function() {
    this.classList.add('expanded');
});

// Listen for the transition end event
box.addEventListener('transitionend', function(event) {
    console.log(`Transition for ${event.propertyName} has completed`);
    
    // You can perform actions after the transition completes
    if (event.propertyName === 'width' && this.classList.contains('expanded')) {
        this.textContent = 'Expanded!';
    }
});

The transitionend event fires for each CSS property that transitions, so you may need to check which property triggered the event.

Working with CSS Animations

CSS animations allow for more complex multi-step animations:

/* CSS */
@keyframes pulse {
    0% { transform: scale(1); }
    50% { transform: scale(1.2); }
    100% { transform: scale(1); }
}

.box {
    width: 100px;
    height: 100px;
    background-color: #3498db;
}

.box.animated {
    animation: pulse 1s ease-in-out infinite;
}

/* JavaScript */
const box = document.querySelector('.box');

box.addEventListener('mouseenter', function() {
    this.classList.add('animated');
});

box.addEventListener('mouseleave', function() {
    this.classList.remove('animated');
});

Like with transitions, the best practice is to define animations in CSS and use JavaScript to add/remove classes that apply those animations.

Animation Events

You can listen for animation events to respond to different stages:

const box = document.querySelector('.box');

// Animation starts
box.addEventListener('animationstart', function() {
    console.log('Animation has started');
});

// Animation iteration completes (useful for looped animations)
box.addEventListener('animationiteration', function() {
    console.log('Animation iteration completed');
});

// Animation ends (doesn't fire for infinite animations)
box.addEventListener('animationend', function() {
    console.log('Animation has ended');
    
    // Perhaps remove the class or change state after animation
    this.classList.remove('animated');
    this.classList.add('animation-completed');
});

Dynamic Control of Animations

You can control animation properties dynamically:

const box = document.querySelector('.box');

// Start animation on click
box.addEventListener('click', function() {
    // Set animation properties directly
    this.style.animation = 'pulse 1s ease-in-out infinite';
    
    // Or using more specific properties
    this.style.animationName = 'pulse';
    this.style.animationDuration = '1s';
    this.style.animationTimingFunction = 'ease-in-out';
    this.style.animationIterationCount = 'infinite';
});

// Speed up animation on hover
box.addEventListener('mouseenter', function() {
    this.style.animationDuration = '0.5s';
});

// Slow down animation on mouse leave
box.addEventListener('mouseleave', function() {
    this.style.animationDuration = '2s';
});

// Stop animation
document.getElementById('stop-button').addEventListener('click', function() {
    box.style.animation = 'none';
});

Using CSS variables with animations provides even more flexibility:

/* CSS */
.box {
    --animation-duration: 1s;
    --animation-scale: 1.2;
}

@keyframes pulse {
    0% { transform: scale(1); }
    50% { transform: scale(var(--animation-scale)); }
    100% { transform: scale(1); }
}

.box.animated {
    animation: pulse var(--animation-duration) ease-in-out infinite;
}

/* JavaScript */
const box = document.querySelector('.box');

document.getElementById('speed-slider').addEventListener('input', function() {
    // Update animation duration based on slider value (0.1s to 2s)
    const duration = (this.max - this.value) / 50 + 0.1;
    box.style.setProperty('--animation-duration', `${duration}s`);
});

document.getElementById('scale-slider').addEventListener('input', function() {
    // Update animation scale based on slider value (1.1 to 2.0)
    const scale = (this.value / 100) + 1.1;
    box.style.setProperty('--animation-scale', scale);
});

Working with Computed Styles

Sometimes you need to know the actual computed values of styles:

Getting Computed Styles

The getComputedStyle() method returns all computed styles for an element:

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

// Get specific computed properties
const width = computedStyle.width;
const color = computedStyle.color;
const fontSize = computedStyle.fontSize;

console.log(`Element dimensions: ${width} x ${computedStyle.height}`);
console.log(`Text styling: ${color} font at ${fontSize}`);

// Converting to actual pixels (computed values often include units)
// parseFloat removes the 'px' and converts to a number
const widthInPixels = parseFloat(computedStyle.width);
const heightInPixels = parseFloat(computedStyle.height);

// Now you can perform calculations
const area = widthInPixels * heightInPixels;
console.log(`Element area: ${area} square pixels`);

Computed styles are particularly useful when you need to:

  • Base animations on current dimensions or positions
  • Compare actual applied styles to expected values
  • Make calculations based on element dimensions
  • Check if styles are being properly applied

Reminder: Computed style objects are read-only. To change styles, use the style property or change classes.

Practical Uses for Computed Styles

// Example 1: Dynamic height animation based on content
function expandElement(element) {
    // First, get the natural height by temporarily removing constraints
    element.style.height = 'auto';
    const autoHeight = element.offsetHeight;
    
    // Reset to 0 height for animation starting point
    element.style.height = '0px';
    
    // Trigger reflow to ensure the browser recognizes the change
    element.offsetHeight; // This line forces a reflow
    
    // Now set transition and animate to full height
    element.style.transition = 'height 0.3s ease-out';
    element.style.height = autoHeight + 'px';
    
    // Remove the fixed height after animation completes
    element.addEventListener('transitionend', function handler() {
        element.style.height = 'auto';
        element.removeEventListener('transitionend', handler);
    });
}

// Example 2: Positioning an element relative to another
function positionTooltip(tooltip, targetElement) {
    const targetRect = targetElement.getBoundingClientRect();
    const computedStyle = window.getComputedStyle(targetElement);
    
    // Position tooltip centered above the target
    tooltip.style.left = `${targetRect.left + targetRect.width / 2}px`;
    tooltip.style.top = `${targetRect.top - parseFloat(computedStyle.marginTop) - 10}px`;
    tooltip.style.transform = 'translateX(-50%)';
}

Style Manipulation Performance

Style changes can trigger expensive browser operations. Here's how to optimize performance:

Understanding Reflow and Repaint

  • Reflow (Layout): Recalculating element positions and dimensions
  • Repaint: Redrawing pixels on the screen

Changing certain properties triggers both reflow and repaint (expensive):

  • width, height, margin, padding
  • position, top, left, right, bottom
  • font-size, font-family
  • display, float, text-align

Other properties only trigger repaint (less expensive):

  • color, background-color
  • visibility, text-decoration
  • box-shadow, border-radius

Performance Best Practices

  • Batch DOM Operations: Group style changes together to minimize reflows
  • Use CSS Classes: Change multiple styles with a single class change
  • Transform and Opacity: Use transform and opacity for animations when possible as they're GPU-accelerated
  • Avoid Inline Styles for Frequent Changes: Use predefined classes instead
  • Be Careful with getComputedStyle: It forces a reflow to get current values
  • Use requestAnimationFrame for Animations: Syncs with the browser's rendering cycle
// Bad: Multiple separate style changes
function updateElementBad(element) {
    element.style.width = '200px';      // Causes reflow
    element.style.height = '100px';     // Causes another reflow
    element.style.marginTop = '20px';   // Yet another reflow
    element.style.marginLeft = '15px';  // And another reflow
}

// Better: Batch changes with cssText
function updateElementBetter(element) {
    element.style.cssText = 'width: 200px; height: 100px; margin-top: 20px; margin-left: 15px;';
    // Only one reflow
}

// Best: Use class changes
function updateElementBest(element) {
    element.classList.add('updated-element');
    // Only one reflow, and style definitions remain in CSS
}

For animations, use requestAnimationFrame to sync with the browser's render cycle:

// Smooth animation with requestAnimationFrame
function animateElement(element, duration) {
    const startTime = performance.now();
    
    function update(currentTime) {
        const elapsed = currentTime - startTime;
        const progress = Math.min(elapsed / duration, 1);
        
        // Calculate current position
        const translateX = progress * 300; // Move 300px to the right
        
        // Update using transform (efficient)
        element.style.transform = `translateX(${translateX}px)`;
        
        // Continue animation if not complete
        if (progress < 1) {
            requestAnimationFrame(update);
        }
    }
    
    requestAnimationFrame(update);
}

Practical Applications

Let's explore some real-world examples of style and class manipulation:

Example 1: Theme Switcher

// HTML:
// <button id="theme-toggle">Toggle Dark Mode</button>
// <div id="app" class="light-theme">...content...</div>

// CSS:
// :root {
//   --light-bg: #ffffff;
//   --light-text: #333333;
//   --dark-bg: #222222;
//   --dark-text: #f0f0f0;
// }
//
// .light-theme {
//   --bg-color: var(--light-bg);
//   --text-color: var(--light-text);
// }
//
// .dark-theme {
//   --bg-color: var(--dark-bg);
//   --text-color: var(--dark-text);
// }
//
// #app {
//   background-color: var(--bg-color);
//   color: var(--text-color);
//   transition: background-color 0.3s, color 0.3s;
// }

// JavaScript
document.addEventListener('DOMContentLoaded', function() {
    const themeToggle = document.getElementById('theme-toggle');
    const app = document.getElementById('app');
    
    // Check for saved theme preference
    const savedTheme = localStorage.getItem('theme');
    if (savedTheme) {
        app.className = savedTheme;
        updateButtonText();
    }
    
    // Handle toggle click
    themeToggle.addEventListener('click', function() {
        // Toggle theme class
        if (app.classList.contains('light-theme')) {
            app.classList.replace('light-theme', 'dark-theme');
            localStorage.setItem('theme', 'dark-theme');
        } else {
            app.classList.replace('dark-theme', 'light-theme');
            localStorage.setItem('theme', 'light-theme');
        }
        
        updateButtonText();
    });
    
    // Update button text based on current theme
    function updateButtonText() {
        if (app.classList.contains('dark-theme')) {
            themeToggle.textContent = 'Switch to Light Mode';
        } else {
            themeToggle.textContent = 'Switch to Dark Mode';
        }
    }
    
    // Bonus: Also respect system preference
    const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)');
    
    systemPrefersDark.addEventListener('change', function(e) {
        // Only auto-switch if user hasn't set a preference
        if (!localStorage.getItem('theme')) {
            if (e.matches) {
                app.classList.replace('light-theme', 'dark-theme');
            } else {
                app.classList.replace('dark-theme', 'light-theme');
            }
            updateButtonText();
        }
    });
});

This example demonstrates how to implement a theme switcher using CSS variables and class toggling, with preferences saved to localStorage.

Example 2: Accordion Component

// HTML:
// <div class="accordion">
//   <div class="accordion-item">
//     <button class="accordion-header">Section 1</button>
//     <div class="accordion-content">Content for section 1...</div>
//   </div>
//   <div class="accordion-item">
//     <button class="accordion-header">Section 2</button>
//     <div class="accordion-content">Content for section 2...</div>
//   </div>
// </div>

// CSS:
// .accordion-content {
//   max-height: 0;
//   overflow: hidden;
//   transition: max-height 0.3s ease-out;
// }
//
// .accordion-header {
//   background-color: #f0f0f0;
//   transition: background-color 0.2s;
// }
//
// .accordion-header.active {
//   background-color: #e0e0e0;
// }
//
// .accordion-header::after {
//   content: '+';
//   float: right;
//   transition: transform 0.2s;
// }
//
// .accordion-header.active::after {
//   transform: rotate(45deg);
// }

// JavaScript
document.addEventListener('DOMContentLoaded', function() {
    const accordionHeaders = document.querySelectorAll('.accordion-header');
    
    accordionHeaders.forEach(header => {
        header.addEventListener('click', function() {
            // Toggle active class on the header
            this.classList.toggle('active');
            
            // Get the content panel
            const content = this.nextElementSibling;
            
            // Toggle the panel
            if (this.classList.contains('active')) {
                content.style.maxHeight = content.scrollHeight + 'px';
            } else {
                content.style.maxHeight = 0;
            }
            
            // Optional: Close other panels
            if (this.classList.contains('active')) {
                accordionHeaders.forEach(otherHeader => {
                    if (otherHeader !== this && otherHeader.classList.contains('active')) {
                        otherHeader.classList.remove('active');
                        otherHeader.nextElementSibling.style.maxHeight = 0;
                    }
                });
            }
        });
    });
});

This accordion example shows how to combine class toggling with direct style manipulation for animation, while still keeping most styling in CSS.

Example 3: Form Field Validation

// HTML:
// <form id="registration-form">
//   <div class="form-group">
//     <label for="username">Username:</label>
//     <input type="text" id="username" required minlength="3" maxlength="20">
//     <div class="error-message"></div>
//   </div>
//   <div class="form-group">
//     <label for="email">Email:</label>
//     <input type="email" id="email" required>
//     <div class="error-message"></div>
//   </div>
//   <div class="form-group">
//     <label for="password">Password:</label>
//     <input type="password" id="password" required minlength="8">
//     <div class="error-message"></div>
//   </div>
//   <button type="submit">Register</button>
// </form>

// CSS:
// .form-group {
//   margin-bottom: 15px;
//   position: relative;
// }
//
// input {
//   border: 2px solid #ddd;
//   transition: border-color 0.3s;
// }
//
// input.valid {
//   border-color: #2ecc71;
// }
//
// input.invalid {
//   border-color: #e74c3c;
// }
//
// .error-message {
//   color: #e74c3c;
//   font-size: 0.85em;
//   margin-top: 5px;
//   height: 0;
//   overflow: hidden;
//   transition: height 0.3s;
// }
//
// .error-message.visible {
//   height: auto;
//   min-height: 20px;
// }

// JavaScript
document.addEventListener('DOMContentLoaded', function() {
    const form = document.getElementById('registration-form');
    const inputs = form.querySelectorAll('input');
    
    // Validation rules object
    const validationRules = {
        username: {
            pattern: /^[a-zA-Z0-9_]{3,20}$/,
            message: 'Username must be 3-20 characters and contain only letters, numbers, and underscores.'
        },
        email: {
            pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
            message: 'Please enter a valid email address.'
        },
        password: {
            pattern: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/,
            message: 'Password must be at least 8 characters long and include at least one letter and one number.'
        }
    };
    
    // Add validation to each input
    inputs.forEach(input => {
        // Validate on blur
        input.addEventListener('blur', function() {
            validateInput(this);
        });
        
        // Real-time validation for better UX (with debounce)
        let debounceTimeout;
        input.addEventListener('input', function() {
            clearTimeout(debounceTimeout);
            debounceTimeout = setTimeout(() => {
                validateInput(this);
            }, 500);
        });
    });
    
    // Form submission
    form.addEventListener('submit', function(event) {
        let isValid = true;
        
        // Validate all inputs
        inputs.forEach(input => {
            if (!validateInput(input)) {
                isValid = false;
            }
        });
        
        // Prevent submission if any input is invalid
        if (!isValid) {
            event.preventDefault();
        }
    });
    
    // Validation function
    function validateInput(input) {
        // Remove existing states
        input.classList.remove('valid', 'invalid');
        
        const errorElement = input.nextElementSibling;
        errorElement.textContent = '';
        errorElement.classList.remove('visible');
        
        // Required field validation
        if (input.required && !input.value.trim()) {
            input.classList.add('invalid');
            errorElement.textContent = 'This field is required.';
            errorElement.classList.add('visible');
            return false;
        }
        
        // Skip other validation if field is empty and not required
        if (!input.value.trim()) {
            return true;
        }
        
        // Length validation
        if (input.minLength && input.value.length < input.minLength) {
            input.classList.add('invalid');
            errorElement.textContent = `Must be at least ${input.minLength} characters.`;
            errorElement.classList.add('visible');
            return false;
        }
        
        // Pattern validation
        const rule = validationRules[input.id];
        if (rule && !rule.pattern.test(input.value)) {
            input.classList.add('invalid');
            errorElement.textContent = rule.message;
            errorElement.classList.add('visible');
            return false;
        }
        
        // Type-specific validation
        if (input.type === 'email' && !validationRules.email.pattern.test(input.value)) {
            input.classList.add('invalid');
            errorElement.textContent = 'Please enter a valid email address.';
            errorElement.classList.add('visible');
            return false;
        }
        
        // If we've passed all validations
        input.classList.add('valid');
        return true;
    }
});

This form validation example shows how to manage validation states through classes, with real-time feedback and smooth transitions for error messages.

Practice Exercises

Exercise 1: Interactive Image Gallery

Create a gallery with these features:

  • Thumbnail images that enlarge on click
  • Navigation buttons to move between images
  • Smooth transitions between images
  • Lightbox mode that darkens the rest of the page
  • Close button to exit lightbox mode

This exercise will practice class manipulation, transitions, and element positioning.

Exercise 2: Customizable UI Component

Build a card component with these features:

  • UI controls to change appearance (color, size, border radius)
  • Settings panel that can be toggled open/closed
  • Option to save configuration to localStorage
  • Reset button to restore defaults

This exercise will practice CSS variables, inline styles, and local storage.

Exercise 3: Animated Navigation Menu

Create a responsive navigation menu with:

  • Dropdown submenus that animate open/closed
  • Highlight effects for the current page
  • Mobile hamburger menu that transforms into a full menu
  • Smooth transitions between states

This exercise will practice class toggling, media queries, and animations.

Lecture Summary

In this exploration of modifying element styles and classes, we've covered:

With these techniques, you can create dynamic, responsive interfaces that adapt to user interactions and preferences. Combined with element selection, traversal, creation, and event handling, you now have a comprehensive toolkit for DOM manipulation.

In our next sessions, we'll explore more advanced techniques for building interactive web applications, including handling browser storage and working with APIs.