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
DOMTokenListis live: it reflects the element’s current classes, so readingclassList.lengthor iterating always gives the up-to-date set.
The core methods
| Method | What it does | Returns |
|---|---|---|
add(...names) | Adds one or more classes; ignores duplicates | undefined |
remove(...names) | Removes one or more classes; ignores absent ones | undefined |
toggle(name) | Removes if present, adds if absent | boolean (now present?) |
toggle(name, force) | Adds if force is true, removes if false | boolean |
contains(name) | Tests whether the class is present | boolean |
replace(old, new) | Swaps one class for another | boolean (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
classListoverclassNamefor 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 anif/elsethat callsaddorremoveseparately. - Batch related changes by passing multiple names to
add/removein 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 booleantogglereturns. - Prefer BEM-style or
is-/has-prefixes so state classes are obvious and never collide with layout classes.