classList and dataset
Toggle CSS classes with classList and read data-* attributes via dataset — tokens, naming, and DOM performance notes.
- javascript
- classlist
- dataset
- dom
String-smashing el.className is how you accidentally wipe framework classes. classList is a token API. dataset is the typed-ish view of data-* attributes. Together they cover most “light DOM state” you want without a full component system.
classList
const btn = document.querySelector('#save');
btn.classList.add('is-loading', 'disabled');
btn.classList.remove('disabled');
btn.classList.toggle('is-open'); // flip
btn.classList.toggle('is-open', true); // force on
btn.classList.toggle('is-open', false); // force off
btn.classList.contains('is-loading'); // boolean
btn.classList.replace('is-loading', 'is-done');
Tokens are space-separated class names. add/remove are idempotent for a single token — no duplicates from double add.
// bad: clobber
btn.className = 'is-loading'; // wiped other classes
// good
btn.classList.add('is-loading');
classList is iterable:
[...btn.classList]; // ['btn', 'is-loading']
dataset and data-*
HTML:
<div id="card" data-user-id="42" data-is-admin="true" data-role="owner"></div>
const card = document.querySelector('#card');
card.dataset.userId; // "42" — camelCase maps to data-user-id
card.dataset.isAdmin; // "true" — always strings
card.dataset.role; // "owner"
card.dataset.userId = '99';
// sets data-user-id="99"
delete card.dataset.role;
// removes the attribute
Naming rules: data-user-id ↔ dataset.userId. Multi-word attributes use kebab-case in HTML and camelCase in JS. Values are strings — coerce yourself:
const id = Number(card.dataset.userId);
const isAdmin = card.dataset.isAdmin === 'true';
Prefer getAttribute/setAttribute when you need exact attribute names that don’t map cleanly, or when working with SVG quirks.
Practical UI pattern
function setExpanded(panel, expanded) {
panel.classList.toggle('is-expanded', expanded);
panel.dataset.expanded = expanded ? 'true' : 'false';
panel.setAttribute('aria-expanded', String(expanded));
}
Keep ARIA on real attributes (aria-*), not only data-*. Use classList for visual state; use data-* for machine-readable state CSS can also target:
[data-state='error'] {
border-color: red;
}
input.dataset.state = valid ? 'ok' : 'error';
Footguns
| Trap | Detail |
|---|---|
| Boolean in dataset | "false" is truthy as a string |
className vs classList |
className replaces entire string |
| SVGElement | dataset works on modern browsers; older SVG issues existed |
| Huge class churn | Prefer toggling one state class over rewriting long lists |
| XSS | Never assign unsanitized HTML via related APIs; dataset text is attributes, still validate |
// force remove several
el.classList.remove('a', 'b', 'c');
// one-liner “only these modifiers”
el.className = 'card'; // base only — last resort
el.classList.add(...modifiers);
Interview answer
“classList add/remove/toggle/contains manipulates individual class tokens without clobbering others. dataset exposes data-* as camelCase string properties. I coerce types myself, keep ARIA on aria-* attributes, and toggle state classes instead of rewriting className.”
Related
SVG and animation notes
const icon = document.querySelector('svg');
icon.classList.add('is-spinning'); // works on SVGElement in modern browsers
// bulk sync from state object
function applyModifiers(el, mods) {
for (const [name, on] of Object.entries(mods)) {
el.classList.toggle(name, Boolean(on));
}
}
applyModifiers(button, {
'is-loading': pending,
'is-danger': variant === 'danger',
});
For CSS that keys off data attributes ([data-state="open"]), updating dataset is often cleaner than inventing parallel class names. Keep a single source of UI state — either classes or data attributes for the same flag, not both fighting each other.
// prefer one
panel.dataset.state = open ? 'open' : 'closed';
// CSS: [data-state="open"] { ... }
Token lists vs style attribute
// prefer class toggles over inline style soup for state
el.classList.toggle('is-hidden', !visible);
// not: el.style.display = visible ? '' : 'none' — unless you must
Inline styles beat classes only for truly dynamic values (pixel positions). State flags belong in classList or dataset so CSS owns presentation and you avoid fighting specificity.
Further reading
Related guides
- client, offset, and scroll DimensionsclientWidth vs offsetWidth vs scrollWidth — borders, scrollbars, and which box measurement to use for layout math.
- contentEditable PitfallsWhy contentEditable is hard: browser HTML mess, carets, sanitization, undo, and when to pick a real editor framework.
- Creating and Updating DOM NodescreateElement, textContent vs innerHTML, insert APIs, and batching updates — safe DOM writes without accidental XSS.
- Custom EventsCustomEvent, detail payloads, bubbles and composed — decoupling components without a global event bus mess.
- DocumentFragmentBuild subtrees off-DOM with DocumentFragment — one insert, fewer reflows, and how it differs from a wrapper div.