π¨ Dynamic Styling and Classes
A dropdown opens, a theme flips to dark, a form field turns red β these are all JavaScript changing how the page looks. This lesson covers the three levers you have for dynamic styling, and, just as importantly, when to reach for each one.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Drive appearance with the
classListAPI as your default styling tool - Use the
styleproperty (andcssText) for calculated, one-off values - Read applied styles with
getComputedStyleand know why it's read-only - Update CSS custom properties from JavaScript to theme many elements at once
- Trigger smooth transitions by toggling classes rather than animating in JS
Estimated Time: 30β40 minutes β’ Difficulty: Intermediate
Hands-on: Build a light/dark theme toggle backed by a CSS variable and saved to localStorage.
In This Lesson
Three Ways to Style
JavaScript can change appearance through three distinct mechanisms. They aren't ranked best-to-worst β each shines in a different situation, and good code uses all three deliberately.
π‘ Analogy β running the lights on a stage. A single element.style tweak is nudging one spotlight by hand. A class swap is calling a pre-set lighting scene. Changing a CSS variable is turning a dial on the master board that shifts every light at once.
Class-Based Styling
This is the approach to reach for first. You define how a state looks in CSS, then JavaScript simply flips the class on and off. Presentation stays in the stylesheet; behavior stays in the script.
const el = document.getElementById('panel');
el.classList.add('active');
el.classList.remove('disabled');
el.classList.toggle('expanded'); // add if absent, remove if present
el.classList.toggle('selected', isChosen); // force to a boolean state
el.classList.replace('loading', 'loaded');
el.classList.contains('open'); // β true / false
A common real pattern is representing a button's lifecycle with mutually exclusive state classes:
function setButtonState(button, state) {
// state is one of: 'idle' | 'loading' | 'success' | 'error'
button.classList.remove('loading', 'success', 'error');
button.disabled = state === 'loading';
if (state === 'loading') { button.classList.add('loading'); button.textContent = 'Savingβ¦'; }
else if (state === 'success') { button.classList.add('success'); button.textContent = 'Saved!'; }
else if (state === 'error') { button.classList.add('error'); button.textContent = 'Retry'; }
else { button.textContent = 'Save'; }
}
const saveBtn = document.getElementById('save');
saveBtn.addEventListener('click', async () => {
setButtonState(saveBtn, 'loading');
try {
await fetch('/api/save', { method: 'POST' });
setButtonState(saveBtn, 'success');
} catch {
setButtonState(saveBtn, 'error');
}
});
β Why classes win by default
All the color, spacing, and animation lives in CSS where designers can find it, the same look is reusable across elements, and toggling a single class is cheaper than setting a dozen style properties one by one.
Direct Style Manipulation
Sometimes a value only exists at runtime β a tooltip's left depends on where an element sits, a progress bar's width depends on a percentage. That's what the style property is for: it writes inline styles directly on the element.
const el = document.getElementById('box');
el.style.color = '#3498db';
el.style.backgroundColor = '#ecf0f1'; // camelCase: background-color β backgroundColor
el.style.padding = '10px 15px';
el.style.transform = `translateX(${offset}px)`; // a value computed in JS
To set several properties in one shot, cssText writes them together (one update instead of several). Note it replaces all existing inline styles unless you append:
el.style.cssText = 'color:#fff; background:#3498db; padding:10px 15px;';
el.style.cssText += ' margin-top:20px;'; // append, keeping the above
β οΈ Inline styles win specificity fights
Styles set via element.style land on the element as inline styles, which beat almost every stylesheet rule short of !important. That makes them hard to override later. Use them for genuinely dynamic, calculated values β not for looks a CSS class could express.
| Situation | Best tool |
|---|---|
| Named state (active, error, open) | classList |
| Theme or multi-element change | CSS variable via setProperty |
| Position/size computed at runtime | element.style |
| Reading what's currently applied | getComputedStyle |
Reading Computed Styles
The style property only sees inline styles β it's blind to anything coming from your stylesheets. To read what the browser has actually applied after all CSS rules cascade, use getComputedStyle:
const el = document.getElementById('box');
console.log(el.style.width); // '' β no inline width set
const computed = getComputedStyle(el);
console.log(computed.width); // e.g. '320px' (the real value)
console.log(computed.display); // e.g. 'block'
π‘ It's read-only
getComputedStyle returns a live, read-only snapshot β you cannot assign to it to change styles. To read, use getComputedStyle; to write, use element.style or toggle a class. Also note it returns resolved values (colors as rgb(...), sizes in px), which may differ from what you wrote in CSS.
CSS Custom Properties
CSS custom properties (a.k.a. CSS variables) are the bridge between your stylesheet and your JavaScript. You declare them once, use them across many rules, and updating one value from JS re-styles everything that references it β no per-element loop required.
:root {
--accent: #3498db;
--space: 8px;
}
.button {
background: var(--accent);
padding: var(--space) calc(var(--space) * 2);
}
const root = document.documentElement;
// Read a variable (note the leading --, and trim the result):
const accent = getComputedStyle(root).getPropertyValue('--accent').trim();
// Set it at the document level β every rule using var(--accent) updates:
root.style.setProperty('--accent', '#e74c3c');
// Or scope a variable to one subtree:
document.getElementById('sidebar').style.setProperty('--space', '12px');
This is exactly how a color-customizer or font-size slider works β bind an input to a single variable:
const slider = document.getElementById('font-size');
const display = document.getElementById('font-size-value');
slider.addEventListener('input', () => {
document.documentElement.style.setProperty('--base-font-size', slider.value + 'px');
display.textContent = slider.value + 'px';
localStorage.setItem('fontSize', slider.value); // remember the choice
});
π Why variables beat looping
Without variables, "make the accent red everywhere" means selecting every affected element and setting its style. With a variable, you change one property on :root and the browser recomputes all dependents for you β less code, and impossible for elements to drift out of sync.
Transitions via Classes
You rarely need to animate in JavaScript. Define the transition in CSS, then let JS toggle a class to trigger it β the browser handles the smooth interpolation, often with hardware acceleration.
.toast {
opacity: 0;
transform: translateY(20px);
transition: opacity .3s ease, transform .3s ease;
}
.toast.visible {
opacity: 1;
transform: translateY(0);
}
const toast = document.getElementById('toast');
function showToast() {
toast.classList.add('visible'); // CSS animates the change
setTimeout(() => toast.classList.remove('visible'), 3000);
}
π‘ CSS vs. JavaScript animation
Prefer CSS transitions/animations for hover effects, fades, and simple state changes β they're performant and declarative. Reach for JavaScript (with requestAnimationFrame) only when you need per-frame logic, values computed on the fly, or coordination with other events. And always honor @media (prefers-reduced-motion: reduce) for users who ask for less motion.
Hands-on Exercise
ποΈ Persistent Theme Toggle
Objective: Combine a class toggle, a CSS variable, and localStorage into a real theme switcher.
Starter CSS & markup
:root { --bg: #ffffff; --fg: #1a1a1a; }
.dark-theme{ --bg: #12141c; --fg: #e8e8e8; }
body { background: var(--bg); color: var(--fg); transition: background .3s, color .3s; }
<button id="theme-btn" aria-pressed="false">π Dark mode</button>
Your goal
- Clicking the button toggles a
dark-themeclass on<body>(which swaps the CSS variables). - Update the button label and its
aria-pressedto match the current theme. - Save the choice in
localStorageand re-apply it on the next page load. - Stretch: if there's no saved choice, default to the OS preference via
matchMedia('(prefers-color-scheme: dark)').
π‘ Hint
classList.toggle('dark-theme') returns true when the class ends up present β capture that boolean and drive the label, ARIA, and storage from it. Read localStorage.getItem('theme') on load before the user interacts.
β Sample solution
const btn = document.getElementById('theme-btn');
function applyTheme(isDark) {
document.body.classList.toggle('dark-theme', isDark);
btn.setAttribute('aria-pressed', String(isDark));
btn.textContent = isDark ? 'βοΈ Light mode' : 'π Dark mode';
}
// On load: saved choice, else OS preference.
const saved = localStorage.getItem('theme');
const prefersDark = matchMedia('(prefers-color-scheme: dark)').matches;
applyTheme(saved ? saved === 'dark' : prefersDark);
btn.addEventListener('click', () => {
const isDark = !document.body.classList.contains('dark-theme');
applyTheme(isDark);
localStorage.setItem('theme', isDark ? 'dark' : 'light');
});
Best Practices
β Do
- Make
classListyour default; keep the look in CSS. - Use
element.styleonly for values you compute at runtime. - Use CSS variables for theming and any change that touches many elements.
- Animate by toggling classes and letting CSS transitions run.
- Respect
prefers-reduced-motionand keep sufficient color contrast in every theme.
β οΈ Avoid
- Setting many individual
style.*properties for something a class could handle. - Trying to write through
getComputedStyleβ it's read-only. - Overusing inline styles, which are hard to override and mix presentation into logic.
- Hand-rolling JS animation loops when a CSS transition would be simpler and smoother.
Summary & Quiz
π Key Takeaways
- Three levers: classList (states), element.style (calculated values), CSS variables (theme-wide).
- Prefer classes β presentation stays in CSS and toggling is cheap and reusable.
element.stylesets inline styles (high specificity); reserve it for runtime values.getComputedStylereads the applied result and is read-only.- Change one CSS variable to restyle everything that references it; toggle classes to fire CSS transitions.
π― Quick Quiz
Question 1: You want to change your app's accent color everywhere at once with the least code. What's the best approach?
Question 2: An element's width is set by a stylesheet rule, with no inline style. What does element.style.width return?
Question 3: You want a panel to fade in smoothly. What's the recommended technique?
π Further Reading
- MDN β Element.classList
- MDN β Using CSS custom properties
- MDN β window.getComputedStyle()
- web.dev β Animations and performance
π What's Next?
You can now build, change, and style elements. The missing piece is reacting to the user β clicks, keystrokes, submissions. That's the DOM Event Model and Types, where interactivity really begins.