Skip to main content

🎨 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 classList API as your default styling tool
  • Use the style property (and cssText) for calculated, one-off values
  • Read applied styles with getComputedStyle and 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.

graph TD A[Dynamic styling] --> B[Class-based] A --> C[Direct style] A --> D[CSS variables] B --> B1[classList: states & themes] C --> C1[element.style: calculated values] D --> D1[setProperty: one change, many elements]
Choosing a styling approach Three panels: classList for named states, the style property for calculated one-off values, and CSS variables for coordinated theme-wide changes. classList named states .active .error your default element.style calculated values tooltip.left = x one-off / dynamic CSS variables theme-wide --accent one change, many
Figure 1 β€” Match the tool to the job: classes for states, the style property for values you compute, CSS variables for coordinated changes.
πŸ’‘ 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.

SituationBest tool
Named state (active, error, open)classList
Theme or multi-element changeCSS variable via setProperty
Position/size computed at runtimeelement.style
Reading what's currently appliedgetComputedStyle

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

  1. Clicking the button toggles a dark-theme class on <body> (which swaps the CSS variables).
  2. Update the button label and its aria-pressed to match the current theme.
  3. Save the choice in localStorage and re-apply it on the next page load.
  4. 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 classList your default; keep the look in CSS.
  • Use element.style only 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-motion and 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.style sets inline styles (high specificity); reserve it for runtime values.
  • getComputedStyle reads 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

πŸš€ 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.