ESC

Type to search the knowledge base.

classList and dataset

Toggle CSS classes with classList and read data-* attributes via dataset — tokens, naming, and DOM performance notes.

beginner3 min read
  • 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.”

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