Skip to content
JavaScript js dom 4 min read

Working with Classes

CSS classes are how you connect JavaScript behavior to visual state. When a menu opens, a tab becomes active, or a form field turns invalid, the cleanest way to express that is by flipping a class on or off — letting your stylesheet own the actual styling. The classList property gives every element a tidy, array-like interface for doing exactly that, replacing the error-prone string surgery you’d otherwise do on className.

Why classList beats className

Every element exposes element.className, a plain string of all its classes separated by spaces. Mutating it directly means manual parsing: to remove one class you’d have to split the string, filter, and re-join, all while preserving the others. One typo and you wipe out unrelated classes.

element.classList returns a live DOMTokenList — an ordered set of the element’s classes with purpose-built methods. It never duplicates a class, never disturbs neighbors, and reads far more clearly.

// Fragile string juggling
el.className += " active";              // leading space if className was empty
el.className = el.className.replace("active", ""); // also matches "inactive"!

// Clear and safe
el.classList.add("active");
el.classList.remove("active");

The DOMTokenList is live: it reflects the element’s current classes, so reading classList.length or iterating always gives the up-to-date set.

The core methods

MethodWhat it doesReturns
add(...names)Adds one or more classes; ignores duplicatesundefined
remove(...names)Removes one or more classes; ignores absent onesundefined
toggle(name)Removes if present, adds if absentboolean (now present?)
toggle(name, force)Adds if force is true, removes if falseboolean
contains(name)Tests whether the class is presentboolean
replace(old, new)Swaps one class for anotherboolean (did it replace?)

Both add and remove accept multiple arguments, so you can batch changes in a single call:

const card = document.querySelector(".card");

card.classList.add("card--highlighted", "card--shadow");
card.classList.remove("card--muted");

console.log(card.classList.contains("card--shadow")); // true
console.log([...card.classList]);                      // spread into a real array

Output:

true
[ 'card', 'card--highlighted', 'card--shadow' ]

Toggling UI state

toggle is the workhorse for stateful UI. Called with just a name, it flips the class and returns whether it ended up present — perfect for syncing ARIA attributes or driving a label.

const btn = document.querySelector("#menu-btn");
const nav = document.querySelector("#nav");

btn.addEventListener("click", () => {
  const open = nav.classList.toggle("is-open");
  btn.setAttribute("aria-expanded", String(open));
  btn.textContent = open ? "Close" : "Menu";
});

The optional second argument, force, turns toggle into a conditional add/remove. Pass a boolean and the class is added when true, removed when false — ideal when state comes from data rather than a flip:

input.addEventListener("input", () => {
  input.classList.toggle("is-invalid", input.value.length < 3);
});

Here’s a self-contained, runnable demo combining toggle, force, and contains:

<style>
  .box {
    width: 160px; padding: 24px; border-radius: 12px;
    font-family: system-ui, sans-serif; text-align: center;
    background: #e2e8f0; color: #1e293b; transition: 0.2s;
  }
  .box.active { background: #6366f1; color: #fff; }
  .box.shake { animation: shake 0.3s; }
  @keyframes shake {
    25% { transform: translateX(-6px); }
    75% { transform: translateX(6px); }
  }
</style>

<div id="box" class="box">Click me</div>
<button id="reset">Force off</button>

<script>
  const box = document.getElementById("box");
  const reset = document.getElementById("reset");

  box.addEventListener("click", () => {
    const on = box.classList.toggle("active");
    box.textContent = on ? "Active!" : "Click me";
    box.classList.add("shake");
    setTimeout(() => box.classList.remove("shake"), 300);
  });

  // force the class off regardless of current state
  reset.addEventListener("click", () => {
    box.classList.toggle("active", false);
    box.textContent = "Click me";
  });
</script>

Replacing and swapping classes

When you move between mutually exclusive states — light/dark, small/large — replace swaps one class for another atomically and tells you whether the old class was actually there:

const theme = document.body;

// returns false because "theme-light" isn't present yet
theme.classList.replace("theme-light", "theme-dark");
theme.classList.add("theme-light");
theme.classList.replace("theme-light", "theme-dark"); // returns true

For a set of options where only one should be active at a time, remove the whole group, then add the winner:

<style>
  .tabs button { padding: 8px 16px; border: 0; cursor: pointer;
    background: #e2e8f0; font: inherit; }
  .tabs button.selected { background: #6366f1; color: #fff; }
</style>

<div class="tabs">
  <button>Overview</button>
  <button>Pricing</button>
  <button>Docs</button>
</div>

<script>
  const buttons = document.querySelectorAll(".tabs button");
  buttons.forEach((btn) => {
    btn.addEventListener("click", () => {
      buttons.forEach((b) => b.classList.remove("selected"));
      btn.classList.add("selected");
    });
  });
</script>

Iterating and inspecting

Because DOMTokenList is iterable, you can loop it directly, spread it, or read .value for the raw string equivalent of className:

const el = document.querySelector(".badge.badge--new");

el.classList.forEach((cls) => console.log(cls));
console.log(el.classList.value); // "badge badge--new"
console.log(el.classList.item(0)); // "badge" — index access

Output:

badge
badge--new
badge badge--new
badge

Best Practices

  • Reach for classList over className for anything beyond reading the full string — it’s safer and clearer.
  • Let CSS own appearance; use JavaScript only to add or remove state classes (is-open, is-active, has-error).
  • Use toggle(name, condition) instead of an if/else that calls add or remove separately.
  • Batch related changes by passing multiple names to add/remove in one call.
  • Keep mutually exclusive states in a known group so you can clear them all before setting the active one.
  • Mirror visual state into ARIA attributes (aria-expanded, aria-selected) using the boolean toggle returns.
  • Prefer BEM-style or is-/has- prefixes so state classes are obvious and never collide with layout classes.
Last updated June 1, 2026
Was this helpful?