Suggest an editImprove this articleRefine the answer for “What are data attributes in HTML”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Data attributes** are HTML attributes prefixed with `data-` that store custom metadata on elements, readable by JavaScript via `element.dataset`. ```html <button data-product-id="42" data-category="electronics">Buy</button> ``` ```javascript button.dataset.productId // "42" (kebab-case converts to camelCase) button.dataset.category // "electronics" ``` **Key point:** Data attributes are always strings. Parse numbers and JSON before using them.Shown above the full answer for quick recall.Answer (EN)Image**Data attributes** are custom HTML attributes prefixed with `data-` that store application-specific metadata directly on elements, readable by JavaScript via `element.dataset` without creating non-standard attributes. ## Theory ### TL;DR - Think of data attributes like sticky notes on a physical object: you attach extra info to an element without changing what the element is - The `data-` prefix tells the browser these are intentional app data, not spec violations - `element.dataset.productId` reads `data-product-id` automatically (kebab-case converts to camelCase) - Data attributes are always strings: parse numbers and JSON before using them - Decision rule: use `data-*` for metadata JavaScript needs to read; ARIA for accessibility; classes for styling ### Quick example ```html <button data-product-id="42" data-category="electronics"> Buy Now </button> <script> const button = document.querySelector("button"); console.log(button.dataset.productId); // "42" console.log(button.dataset.category); // "electronics" // getAttribute also works console.log(button.getAttribute("data-product-id")); // "42" </script> ``` `dataset` is a live `DOMStringMap`. Change a property and the HTML attribute updates too, and the reverse applies. ### Key difference from custom attributes Before `data-*`, developers wrote things like `<div user-id="123">` or shoved IDs into class names. Both approaches were either invalid HTML or a maintenance problem. I've seen codebases where every button had `class="btn id-42"` just to carry an ID into a click handler. One `data-product-id` attribute fixes that. The `data-` prefix is the official W3C way to add custom metadata, and the browser parses it into a clean `dataset` object that DevTools shows clearly. ### When to use - Storing IDs for event handlers: `<button data-user-id="123">` then read in the click handler - Event delegation: store identifiers on child elements, read from `e.target.closest('[data-todo-id]')` - Initializing JavaScript components: `<div data-chart-type="line" data-refresh-interval="5000">` - Server-rendered templates: Rails, Django, and PHP can write `data-*` values from the database directly - Testing selectors: `data-testid` is the standard pattern for Cypress and Playwright Avoid data attributes for: sensitive data (they are visible in HTML source and DevTools), large objects (use a `<script>` tag with JSON instead), and values updated in tight loops (each DOM write is a separate operation). ### How the browser handles this When the browser parses HTML, it creates a `DOMStringMap` on each element's DOM node. This map is a live proxy to all `data-*` attributes. Accessing `element.dataset.productId` converts the camelCase name back to `product-id`, looks up `data-product-id`, and returns its string value. Writes go the other direction: `element.dataset.newProp = 'value'` creates `data-new-prop` in the DOM. ### Common mistakes **Forgetting that data attributes are always strings** ```javascript // WRONG: relies on implicit JS type coercion const button = document.querySelector('[data-count]'); button.dataset.count = '5'; if (button.dataset.count > 3) { // works by accident console.log('Count is high'); } // RIGHT: convert explicitly const count = parseInt(button.dataset.count, 10); if (count > 3) { console.log('Count is high'); } ``` **Storing JSON without serializing** ```javascript // WRONG: stores "[object Object]" element.dataset.config = { theme: 'dark', lang: 'en' }; // RIGHT: stringify before writing, parse when reading element.dataset.config = JSON.stringify({ theme: 'dark', lang: 'en' }); const config = JSON.parse(element.dataset.config); console.log(config.theme); // "dark" ``` **Writing data attributes in a tight loop** ```javascript // WRONG: 1000 DOM writes for (let i = 0; i < 1000; i++) { element.dataset.score = i; } // RIGHT: compute in JS, one DOM write at the end let score = 0; for (let i = 0; i < 1000; i++) { score = i; } element.dataset.score = score; ``` **Storing sensitive data** ```html <!-- WRONG: visible to anyone who opens DevTools --> <div data-api-key="sk-1234567890abcdef"></div> <!-- RIGHT: fetch secrets from a secure endpoint --> <script> const apiKey = await fetch('/api/get-key').then(r => r.json()); </script> ``` ### Real-world usage - React: `data-testid` for Cypress and Playwright test selectors - jQuery plugins: `$('[data-toggle="modal"]')` for component initialization - Analytics: libraries read `data-event-*` attributes to track user clicks - Web components: pass initial config before JavaScript loads - CSS: `[data-priority="high"] { color: red; }` for attribute-based styling ### Follow-up questions **Q:** How do data attributes differ from storing data as JavaScript properties on the element? **A:** Data attributes persist in HTML and survive `cloneNode()` and server-side rendering. JS properties don't. Data attributes are also visible in DevTools and can be set from server templates. Use data attributes for metadata that belongs to the element's definition; use JS properties for runtime state. **Q:** Can you use data attributes in CSS? **A:** Yes. Attribute selectors work: `[data-priority="high"] { color: red; }`. You can read values in pseudo-elements with `content: attr(data-label)`. But CSS can only match presence or exact values, not compute or compare. For dynamic styling based on data that changes, add and remove classes from JavaScript instead. **Q:** What happens with uppercase letters in data attribute names? **A:** HTML attribute names are case-insensitive, so `data-ProductID` becomes `data-productid` in the DOM. Stick to lowercase letters and hyphens. The `dataset` API converts hyphens to camelCase: `data-product-id` becomes `dataset.productId`. **Q:** (Senior) What is the performance cost of reading from `dataset` vs. a cached JavaScript variable? **A:** Each `dataset` access does an attribute lookup, which is slightly slower than reading a local variable. For frequently accessed values, cache once: `const id = element.dataset.id;` then reuse `id`. For large objects, store a reference key in the attribute and keep the actual object in a JavaScript `Map`. ## Examples ### Event delegation with data attributes One listener on the parent handles clicks from all children. Data attributes give each child a unique identity without extra JavaScript objects: ```html <ul id="todo-list"> <li data-todo-id="1" data-priority="high"> <span>Fix auth bug</span> <button class="delete-btn">Delete</button> </li> <li data-todo-id="2" data-priority="low"> <span>Update docs</span> <button class="delete-btn">Delete</button> </li> </ul> <script> document.getElementById('todo-list').addEventListener('click', (e) => { // Walk up the DOM to the closest li with a todo ID const todoItem = e.target.closest('[data-todo-id]'); if (!todoItem) return; const todoId = todoItem.dataset.todoId; const priority = todoItem.dataset.priority; if (e.target.classList.contains('delete-btn')) { console.log(`Deleting todo ${todoId} (priority: ${priority})`); // Output: "Deleting todo 1 (priority: high)" todoItem.remove(); } }); </script> ``` `e.target.closest('[data-todo-id]')` walks up the DOM tree from the clicked element until it finds a matching ancestor. This correctly handles clicks on nested `<span>` or `<button>` elements inside the `<li>`. ### Passing server data to JavaScript Server-rendered templates can embed dynamic values into data attributes. JavaScript picks them up on page load without an extra API call: ```html <!-- Server renders this with real values from the database --> <div id="user-profile" data-user-id="99" data-role="admin" data-locale="en-US" > Welcome, Alex </div> <script> const profile = document.getElementById('user-profile'); const userId = profile.dataset.userId; // "99" const role = profile.dataset.role; // "admin" const locale = profile.dataset.locale; // "en-US" if (role === 'admin') { console.log(`Admin user ${userId}, locale: ${locale}`); // Output: "Admin user 99, locale: en-US" } </script> ``` For small amounts of config, this is simpler than a separate JSON endpoint. The server writes the values, JavaScript reads them immediately on load.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.