data Attributes
data-* attributes for embedding element metadata — dataset API, CSS hooks, validation limits, and when not to store app state in the DOM.
- 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.
datasetconvertsdata-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
- Assuming
dataset.isActiveis booleantrue. - CamelCase mismatches (
data-userIdis invalid style; usedata-user-id). - Overusing data attributes instead of classes for purely presentational toggles.
- Sensitive data in HTML visible to any script.
- 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.
Related
Further reading
Related guides
- Accessibility Tree OverviewHow browsers build the accessibility tree from DOM and CSS — roles, names, states, what’s pruned, and how to inspect it in DevTools.
- Audio and Video ElementsNative audio/video — controls, sources, captions, autoplay policies, and accessibility requirements for media on the web.
- Autocomplete and Name Attributesname and autocomplete on form fields — password managers, autofill tokens, and why missing names break real users more than demos.
- Base Element and Relative URLsHow <base href> rewrites relative URLs for links, scripts, and forms — powerful for static hosts, dangerous when set accidentally.
- contenteditable Basicscontenteditable surfaces — what the browser gives you, sanitization, keyboard and a11y gaps, and when to pick a real editor library.