LEARNING OBJECTIVES โต
- Understand how the browser parses space-delimited class tokens into a
DOMTokenList. - Apply the industry-standard BEM (Block, Element, Modifier) architecture to eliminate CSS specificity wars.
- Manipulate element classes efficiently using
classList.add(),remove(),toggle(),contains(), andreplace(). - Compare
element.classNamestring manipulation with atomicelement.classListoperations. - Analyze the performance and maintainability tradeoffs between semantic classes and utility-first classes.
๐ The Mental Model & Story (Intuitive Foundation)
Imagine a wardrobe system for employees in a modern tech enterprise.
An employeeโs ID card (id) is completely unique. But an employee can wear multiple role badges, skill pins, and status tags on their lanyard simultaneously:
class="engineer backend team-lead on-call"
+-------------------------------------------------------------------------------+
| MULTI-CLASS TOKEN COMPOSITION |
+-------------------------------------------------------------------------------+
| |
| <button class="btn btn--primary btn--large is-loading">Submit</button> |
| |
| +---------------+ +---------------+ +---------------+ +---------------+ |
| | Base Role | | Visual Style | | Size Variant | | Runtime State | |
| | "btn" | | "btn--primary"| | "btn--large" | | "is-loading" | |
| +---------------+ +---------------+ +---------------+ +---------------+ |
| | | | | |
| v v v v |
| Padding, radius Blue background 20px font-size Spinner graphic |
| cursor pointer white text color 48px height disabled cursor |
| |
+-------------------------------------------------------------------------------+
Each class token is an independent modifier. Instead of reinventing a button from scratch for every visual variation, you compose small, reusable class tokens together. In JavaScript, these tokens are managed through an atomic set interface called DOMTokenList.
Technical Deep Dive & Specifications
WHATWG Class Specification & Token Parsing
According to the WHATWG HTML Living Standard:
- The
classattribute assigns one or more class names to an element. - The attribute value is parsed as a set of space-separated tokens (ASCII whitespace: space, tab, line-feed, form-feed, carriage-return).
- Consecutive whitespace characters are collapsed, and leading/trailing whitespace is ignored.
<!-- The browser parses this as three distinct tokens: ["card", "elevated", "active"] -->
<div class=" card elevated active "></div>
The BEM (Block, Element, Modifier) Architecture
To keep large codebases scalable and prevent CSS selector specificity escalation, frontend teams widely adopt BEM:
.block__element--modifier
| | |
| | +---> Modifier: Represents state or variation (e.g. --active, --disabled)
| +--------------> Element: Component child part dependent on block (e.g. __title, __icon)
+-----------------------> Block: Standalone reusable component entity (e.g. .card, .navbar)
+-------------------------------------------------------------+
| .card |
| +-------------------------------------------------------+ |
| | .card__header | |
| | +-------------------------------------------------+ | |
| | | .card__title | | |
| | +-------------------------------------------------+ | |
| +-------------------------------------------------------+ |
| +-------------------------------------------------------+ |
| | .card__body | |
| | <p>...</p> | |
| | <button class="card__button card__button--primary"> | |
| +-------------------------------------------------------+ |
+-------------------------------------------------------------+
BEM Rules & Best Practices:
- Single Flat Specificity: Every BEM selector is a single class
(0, 0, 1, 0), eliminating nested selector specificity wars (.nav ul li a:hover). - Never Chain Elements: Never write
.card__header__title. If an element is a child inside header, it is still part of the card block:.card__title. - Modifiers Define State/Theme: Use double hyphens (
--dark,--expanded) or state classes (is-active,is-open).
The DOMTokenList API (element.classList)
In modern JavaScript, the element.classList property returns a live DOMTokenList representing the element's space-separated tokens.
| Method / Property | Signature | Description |
|---|---|---|
add() |
classList.add(...tokens) |
Adds one or more class tokens. Ignores duplicates automatically. |
remove() |
classList.remove(...tokens) |
Removes one or more class tokens if present. |
toggle() |
classList.toggle(token, force?) |
Adds token if absent; removes it if present. If force boolean is provided, acts as conditional add/remove. |
contains() |
classList.contains(token) |
Returns true if token exists, false otherwise. |
replace() |
classList.replace(oldToken, newToken) |
Replaces oldToken with newToken. Returns true if succeeded. |
length |
classList.length |
Returns total number of unique tokens. |
entries(), keys(), values() |
Standard Iterators | Iterates through all tokens like an Array or Set. |
const banner = document.querySelector(".banner");
// Safe atomic manipulation:
banner.classList.add("banner--highlight", "is-visible");
banner.classList.remove("is-hidden");
// Conditional toggle (force param):
const isDarkMode = true;
banner.classList.toggle("theme-dark", isDarkMode); // Adds if true, removes if false
// Fast token replacement:
banner.classList.replace("status-pending", "status-approved");
// Inspection:
if (banner.classList.contains("is-visible")) {
console.log("Banner is actively displayed");
}
className vs. classList: The Overwrite Hazard
const el = document.querySelector("div");
// Initial markup: <div class="badge badge--success is-active">
// DANGEROUS: element.className is a raw string!
el.className = "is-disabled";
// Result: <div class="is-disabled">
// (All previous classes were wiped out!)
// SAFE: element.classList operates on individual tokens
el.classList.add("is-disabled");
// Result: <div class="badge badge--success is-active is-disabled">
๐ป Interactive Code Playground
Starter Code
Line-by-Line Code Breakdown
- Lines 13โ24 (
.toast): Defines the Block styles with consistent padding and theme base. - Lines 27โ42 (
.toast__message,.toast__action): Defines Elements scoped cleanly to the.toastblock. - Lines 45โ56 (
.toast--success,.toast--error,.toast--dismissed): Defines Modifiers for visual status and dynamic animation states. - Lines 76โ89 (
toast.classList.replace): Uses modernDOMTokenList.replace()to atomically swap between.toast--successand.toast--errorwithout string concatenation bugs.
Expected Browser Render Output
+-------------------------------------------------------------+
| | Deployment completed successfully in 14.2s. ร |
+-------------------------------------------------------------+
(Green border on the left. Clicking 'Toggle Error/Success State' swaps to Red border)๐๏ธ Hands-On Exercise
๐ฏ The Challenge: Refactor Unstructured Class Soup to Clean BEM
You have inherited a poorly structured product card markup. The classes are nested, unstructured, and modified via fragile string manipulation.
Your Task:
- Refactor the HTML markup to adhere strictly to BEM conventions using
product-cardas the Block. - Structure the title as
.product-card__title, the price as.product-card__price, and the button as.product-card__btn. - Add a modifier
.product-card--featuredwhen the product is flagged. - Implement a JavaScript function
toggleFavorite(cardElement)that useselement.classList.toggle()to toggle the state classis-favorited.
๐ Starter Code Sandbox
โ ๏ธ Common Pitfalls
- Overwriting Classes with
className: Assigning directly toelement.className = "active"replaces every other class token on the element. Always useelement.classList.add("active"). - Deep BEM Nesting (
block__elem1__elem2): BEM does not mimic the DOM tree depth. Never write.nav__list__item__link. Instead, write.nav__link. Keep elements flat relative to their root block. - Passing Comma-Separated Strings to
classList.add: WritingclassList.add("class1, class2")throws an invalid character error.DOMTokenListmethods take comma-separated arguments:classList.add("class1", "class2").
๐ก Pro Tips
- Use
classList.toggle(name, condition)as a Pure Conditional: Avoid writing verboseif/elseblocks to add or remove classes. Passing a boolean condition as the second argument (element.classList.toggle("is-active", count > 0)) handles both cases in one line. - Leverage
DOMTokenList.replace()for State Machines: When transitioning between finite states (e.g.is-loading$\to$is-success),classList.replace('is-loading', 'is-success')performs atomic replacement in a single DOM mutation cycle. - Keep Specificity Flat with Utility / BEM Pairing: By ensuring all component selectors share equal
(0,0,1,0)specificity, you eliminate the need for!importanttags when applying theme or responsive overrides.
๐ Key Takeaways
- The
classattribute holds a space-separated list of tokens representing an element's classifications. - The CSS specificity of every class selector is
(0, 0, 1, 0). - BEM (
block__element--modifier) enforces component modularity and prevents CSS cascade collisions. element.classListexposes theDOMTokenListAPI with atomic methods:add,remove,toggle,contains, andreplace.- Avoid mutating
element.classNamedirectly as it obliterates other classes present on the element. - --