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.
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:
- Implement a light/dark theme toggle with smooth transitions
- Add a color picker to customize accent colors for the UI
- Create a font size adjuster that affects all text elements proportionally
- Add a toggle for a high-contrast mode for accessibility
- Implement a "reduced motion" setting that disables animations
- Create a layout switcher (grid/list view) for a content section
- Store user preferences in localStorage and apply them on page load
- 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
- JavaScript provides multiple ways to dynamically style elements:
- Class Manipulation: The most maintainable approach using
classListmethods - Direct Style Manipulation: For precise control with the
styleproperty - CSS Variables: For theme-wide adjustments with
setProperty()
- Class Manipulation: The most maintainable approach using
- Dynamic styling enables interactive UI features like:
- Theme switching and customization
- State-based visual feedback
- Interactive animations and transitions
- Advanced responsive layouts
- Performance optimization is critical for smooth user experiences:
- Batch DOM updates to minimize reflows
- Use CSS-based animations when possible
- Use requestAnimationFrame for JavaScript animations
- Modern applications increasingly support user customization:
- Persisting style preferences
- Respecting accessibility needs
- Providing interface customization options
Master these dynamic styling techniques to create highly interactive, responsive, and user-friendly web applications.