ESC

Type to search the knowledge base.

data Attributes

data-* attributes for embedding element metadata — dataset API, CSS hooks, validation limits, and when not to store app state in the DOM.

beginner3 min read
  • html
  • data-attributes

data-* attributes attach custom metadata to elements without inventing nonstandard attributes. CSS and JavaScript can read them; they are not a substitute for accessible names or a full client state store.

Docs: MDN data-*, HTMLElement.dataset.

Markup and the dataset API

<button
  type="button"
  class="chip"
  data-user-id="42"
  data-role="admin"
  data-is-active="true"
>
  Alex
</button>
const btn = document.querySelector(".chip");
btn.dataset.userId; // "42" — kebab to camelCase
btn.dataset.role; // "admin"
btn.dataset.isActive; // "true" (always string!)

btn.dataset.plan = "pro"; // sets data-plan="pro"
delete btn.dataset.role; // removes attribute

Rules:

  • Name after data- must be XML-compatible; avoid uppercase in the attribute form.
  • Values are strings. Parse booleans/JSON yourself.
  • dataset converts data-user-id ↔ userId.

CSS hooks

.chip[data-role="admin"] {
  border-color: var(--brand);
}

.chip[data-is-active="true"] {
  background: var(--brand-soft);
}
/* Useful with attr() in limited cases */
.badge::after {
  content: attr(data-count);
}

Prefer classes or custom properties for pure styling when you don’t need the value in JS.

Valid uses

Use Example
Element identity for progressive JS data-product-id
Analytics hooks data-analytics="signup_cta"
CSS state without class soup data-state="open"
Test selectors (sparingly) data-testid
<details data-state="closed">
  <summary>Shipping</summary>
  …
</details>
details.addEventListener("toggle", () => {
  details.dataset.state = details.open ? "open" : "closed";
});

What not to store

  • Large JSON blobs (payload size + parsing cost on every query)
  • Secrets or auth tokens
  • Human-readable UI strings that should be in text nodes for i18n/AT
  • React state that already lives in memory — avoid dual sources of truth
<!-- Smell -->
<div data-store='{"cart":[…thousands…]}'></div>

Security

data-* values written from user input into HTML must be escaped like any attribute. Reading dataset and injecting into innerHTML is an XSS footgun.

Interview out-loud

“data-* attributes store custom string metadata on elements. JS reads them via dataset with camelCase keys; CSS can select on them. I use them for IDs, lightweight state hooks, and tests—not for secrets or large app state. Values are always strings and need parsing.”

Footguns

  1. Assuming dataset.isActive is boolean true.
  2. CamelCase mismatches (data-userId is invalid style; use data-user-id).
  3. Overusing data attributes instead of classes for purely presentational toggles.
  4. Sensitive data in HTML visible to any script.
  5. Breaking caching/SSR markup with highly dynamic data attributes on root layouts.

Analytics and testing

<button type="button" data-analytics="cta_pricing_primary" data-testid="pricing-cta">
  Start trial
</button>
document.body.addEventListener("click", (e) => {
  const t = e.target.closest("[data-analytics]");
  if (t) sendEvent(t.dataset.analytics);
});

Event delegation on data-analytics keeps product markup declarative. Prefer data-testid only in non-production builds if you want cleaner DOM, or accept them as stable selectors better than brittle CSS classes.

JSON values

el.dataset.config = JSON.stringify({ page: 2, sort: "date" });
const config = JSON.parse(el.dataset.config);

Escape carefully when embedding into HTML strings. Prefer one data attribute per concern over mega-JSON when CSS needs to select on a single flag.

CSS state machines

[data-state="idle"] { opacity: 1; }
[data-state="loading"] { opacity: 0.7; pointer-events: none; }
[data-state="error"] { outline: 1px solid var(--danger); }

Keep the state vocabulary short and documented. Prefer booleans as "true"/"false" strings for attribute selectors rather than present/absent if you need ternary styles. Sync ARIA state separately when it affects AT (aria-busy, aria-invalid) — do not assume data-* is exposed to screen readers.

Further reading

Related guides