Data Attributes
Data attributes are custom attributes on any HTML element that store extra information without non-standard attributes, hidden classes, or JavaScript variables. They follow the data-* naming convention and provide a clean, standard way to embed custom data in HTML.
Introduced in HTML5, data attributes solved a common problem: developers were using class or rel attributes to store application data, which polluted semantics and broke standards compliance. Data attributes provide a ◆dedicated mechanism for custom data that is valid HTML, accessible via JavaScript and CSS, and completely ignored by search engines and assistive technologies by default.
A data attribute name consists of the prefix data- followed by a lowercase string with words separated by hyphens. The attribute value is always a string — numbers, booleans, and objects must be serialized as strings and parsed when accessed.
| Rule | Valid | Invalid |
|---|---|---|
| Prefix | data-* (always lowercase) | Data-*, DATA-* |
| Naming | data-user-id, data-config | data-userId (no camelCase in HTML) |
| Characters | data-my-data_2 | data-my data, data-<XML> |
| Value type | data-count="42" | data-count=42 (unquoted numeric) |
| Empty values | data-active="" | data-active (bare boolean attribute) |
| 1 | <!-- Valid data attribute names --> |
| 2 | <div data-user-id="42"></div> |
| 3 | <div data-user-role="admin"></div> |
| 4 | <div data-config-theme="dark"></div> |
| 5 | <div data-timestamp="1720329600000"></div> |
| 6 | <div data-items="[1,2,3]"></div> |
| 7 | <div data-active="true"></div> |
| 8 | <div data-empty=""></div> |
| 9 | |
| 10 | <!-- Serialize complex data as JSON strings --> |
| 11 | <div |
| 12 | data-product='{"id":42,"name":"Widget","price":9.99}' |
| 13 | data-attributes='["red","blue","green"]' |
| 14 | ></div> |
| 15 | |
| 16 | <!-- Common pattern: data attribute with boolean presence --> |
| 17 | <button data-submitted disabled> |
| 18 | Already Submitted |
| 19 | </button> |
| 20 | |
| 21 | <!-- Data attributes work on any element --> |
| 22 | <nav data-active-section="pricing"> |
| 23 | <a href="/" data-page="home">Home</a> |
| 24 | <a href="/pricing" data-page="pricing">Pricing</a> |
| 25 | <a href="/contact" data-page="contact">Contact</a> |
| 26 | </nav> |
| 27 | |
| 28 | <input |
| 29 | type="text" |
| 30 | data-validation="email" |
| 31 | data-min-length="3" |
| 32 | data-max-length="254" |
| 33 | data-required="true" |
| 34 | placeholder="Email address" |
| 35 | /> |
| 36 | |
| 37 | <article |
| 38 | data-id="post-42" |
| 39 | data-author="jane_doe" |
| 40 | data-published="2026-07-07" |
| 41 | data-category="html" |
| 42 | data-tags='["semantic","attributes","html5"]' |
| 43 | > |
| 44 | <h2>Article Title</h2> |
| 45 | <p>Content...</p> |
| 46 | </article> |
warning
JavaScript provides two ways to access data attributes: the dataset property (modern, convenient) and the getAttribute / setAttribute methods (compatible, explicit). The dataset property returns a DOMStringMap with all data attributes as camelCase keys.
The camelCase conversion follows a simple rule: each hyphen removes the hyphen and capitalizes the next letter. So data-user-id becomes dataset.userId, and data-config-theme-mode becomes dataset.configThemeMode.
| HTML Attribute | dataset Property | getAttribute |
|---|---|---|
| data-user-id | element.dataset.userId | element.getAttribute('data-user-id') |
| data-config | element.dataset.config | element.getAttribute('data-config') |
| data-item-count | element.dataset.itemCount | element.getAttribute('data-item-count') |
| data-json-data | element.dataset.jsonData | element.getAttribute('data-json-data') |
| 1 | <!-- HTML with data attributes --> |
| 2 | <button |
| 3 | id="save-btn" |
| 4 | data-action="save" |
| 5 | data-document-id="doc_42" |
| 6 | data-version="3" |
| 7 | data-published="false" |
| 8 | > |
| 9 | Save Document |
| 10 | </button> |
| 11 | |
| 12 | <ul id="user-list"> |
| 13 | <li data-user-id="1" data-role="admin">Alice</li> |
| 14 | <li data-user-id="2" data-role="editor">Bob</li> |
| 15 | <li data-user-id="3" data-role="viewer">Charlie</li> |
| 16 | </ul> |
| 17 | |
| 18 | <script> |
| 19 | // Using the dataset property (camelCase conversion) |
| 20 | const btn = document.getElementById('save-btn'); |
| 21 | |
| 22 | console.log(btn.dataset.action); // "save" |
| 23 | console.log(btn.dataset.documentId); // "doc_42" |
| 24 | console.log(btn.dataset.version); // "3" (string, not number!) |
| 25 | console.log(btn.dataset.published); // "false" (string, not boolean!) |
| 26 | |
| 27 | // dataset is read/write — updates reflect in the DOM |
| 28 | btn.dataset.version = "4"; |
| 29 | btn.dataset.lastSaved = Date.now().toString(); |
| 30 | |
| 31 | // Using getAttribute / setAttribute (explicit, always works) |
| 32 | console.log(btn.getAttribute('data-action')); |
| 33 | btn.setAttribute('data-last-modified', new Date().toISOString()); |
| 34 | |
| 35 | // Parsing data attribute values |
| 36 | const version = parseInt(btn.dataset.version, 10); |
| 37 | const published = btn.dataset.published === 'true'; |
| 38 | |
| 39 | // Working with JSON data attributes |
| 40 | const listItems = document.querySelectorAll('[data-user-id]'); |
| 41 | listItems.forEach(item => { |
| 42 | const userId = parseInt(item.dataset.userId, 10); |
| 43 | const role = item.dataset.role; |
| 44 | console.log('User ' + userId + ':', role); |
| 45 | }); |
| 46 | |
| 47 | // Accessing all data attributes at once |
| 48 | const allData = btn.dataset; |
| 49 | for (const key in allData) { |
| 50 | console.log('data-' + key + ':', allData[key]); |
| 51 | } |
| 52 | |
| 53 | // Removing a data attribute |
| 54 | delete btn.dataset.tempData; |
| 55 | // or: btn.removeAttribute('data-temp-data'); |
| 56 | </script> |
info
The dataset camelCase conversion is one of the most common points of confusion. Remember: the hyphenated HTML attribute name maps to a camelCase JavaScript property:
| 1 | // data-my-name → dataset.myName |
| 2 | // data-config-theme-mode → dataset.configThemeMode |
| 3 | // data-api-endpoint-url → dataset.apiEndpointUrl |
| 4 | // data- → dataset. (invalid — must have at least one character after data-) |
| 5 | // data-s → dataset.s (single letter is fine) |
| 6 | // data-42 → invalid HTML (numbers not allowed after data-) |
| 7 | |
| 8 | // Special case: data- is the prefix only |
| 9 | // data-xml-data → dataset.xmlData |
| 10 | |
| 11 | // The reverse map: camelCase to HTML attribute |
| 12 | function toDataAttribute(camelCase) { |
| 13 | return camelCase.replace(/[A-Z]/g, c => '-' + c.toLowerCase()); |
| 14 | } |
| 15 | |
| 16 | console.log(toDataAttribute('userId')); // "user-id" |
| 17 | console.log(toDataAttribute('apiBaseUrl')); // "api-base-url" |
best practice
CSS provides two mechanisms for working with data attributes: attribute selectors to match elements, and the attr() function to display attribute values in pseudo-elements. The attr() function currently only works within content property of pseudo-elements, though CSS Values Level 5 proposes extending it to all CSS properties.
| 1 | /* Attribute selectors — target elements by data attribute */ |
| 2 | |
| 3 | /* All elements with data-user-id */ |
| 4 | [data-user-id] { |
| 5 | border-left: 3px solid #00FF41; |
| 6 | } |
| 7 | |
| 8 | /* Exact value match */ |
| 9 | [data-role="admin"] { |
| 10 | background: rgba(0, 255, 65, 0.1); |
| 11 | } |
| 12 | |
| 13 | [data-status="active"] { |
| 14 | opacity: 1; |
| 15 | } |
| 16 | |
| 17 | [data-status="inactive"] { |
| 18 | opacity: 0.4; |
| 19 | } |
| 20 | |
| 21 | /* Value contains substring */ |
| 22 | [data-category*="html"] { |
| 23 | color: #00FF41; |
| 24 | } |
| 25 | |
| 26 | /* Value starts with prefix */ |
| 27 | [data-attr^="data-"] { |
| 28 | font-family: monospace; |
| 29 | } |
| 30 | |
| 31 | /* Value ends with suffix */ |
| 32 | [data-file-type$=".pdf"]::after { |
| 33 | content: " PDF"; |
| 34 | } |
| 35 | |
| 36 | /* Combined selectors */ |
| 37 | [data-role="admin"][data-active="true"] { |
| 38 | font-weight: bold; |
| 39 | } |
| 40 | |
| 41 | /* Data attributes for state-driven styling */ |
| 42 | [data-theme="dark"] { |
| 43 | background: #0A0A0A; |
| 44 | color: #E0E0E0; |
| 45 | } |
| 46 | |
| 47 | [data-theme="light"] { |
| 48 | background: #FFFFFF; |
| 49 | color: #1A1A1A; |
| 50 | } |
| 51 | |
| 52 | /* Use attr() in pseudo-element content */ |
| 53 | [data-tooltip]:hover::after { |
| 54 | content: attr(data-tooltip); |
| 55 | position: absolute; |
| 56 | background: #111; |
| 57 | color: #00FF41; |
| 58 | padding: 4px 8px; |
| 59 | border-radius: 4px; |
| 60 | font-size: 11px; |
| 61 | white-space: nowrap; |
| 62 | z-index: 10; |
| 63 | } |
| 64 | |
| 65 | [data-count]::before { |
| 66 | content: "(" attr(data-count) ")"; |
| 67 | font-size: 10px; |
| 68 | color: #525252; |
| 69 | margin-right: 4px; |
| 70 | } |
| 71 | |
| 72 | /* Style empty data attributes */ |
| 73 | [data-empty=""] { |
| 74 | opacity: 0.5; |
| 75 | } |
| 76 | |
| 77 | /* Progress bar using data attributes */ |
| 78 | [data-progress] { |
| 79 | position: relative; |
| 80 | height: 8px; |
| 81 | background: #222; |
| 82 | border-radius: 4px; |
| 83 | } |
| 84 | |
| 85 | [data-progress]::before { |
| 86 | content: ""; |
| 87 | position: absolute; |
| 88 | height: 100%; |
| 89 | background: #00FF41; |
| 90 | border-radius: 4px; |
| 91 | } |
| 1 | <!-- Using data attributes for CSS-driven UI state --> |
| 2 | <div class="card" data-theme="dark" data-status="active"> |
| 3 | <h2>Card Title</h2> |
| 4 | <p>Content with data-driven styling.</p> |
| 5 | </div> |
| 6 | |
| 7 | <!-- Tooltip via data attribute --> |
| 8 | <button data-tooltip="Click to save your changes"> |
| 9 | Save |
| 10 | </button> |
| 11 | |
| 12 | <!-- Dynamic badges --> |
| 13 | <span data-count="12">Notifications</span> |
| 14 | |
| 15 | <!-- Tab system driven by data attributes --> |
| 16 | <div class="tabs"> |
| 17 | <button data-tab="overview" data-active="true">Overview</button> |
| 18 | <button data-tab="details" data-active="false">Details</button> |
| 19 | <button data-tab="settings" data-active="false">Settings</button> |
| 20 | </div> |
Data attributes are versatile and appear in a wide range of patterns. Here are the most common use cases with practical examples.
| Use Case | Pattern | Example |
|---|---|---|
| State tracking | Boolean or enumeration | data-active="true", data-state="loading" |
| Identification | IDs or keys | data-user-id="42", data-post-slug="hello-world" |
| Configuration | Settings for JS components | data-throttle="300", data-api-url="/api/v2" |
| Validation | Form field constraints | data-min-length="3", data-pattern="^[a-z]+$" |
| Analytics | Tracking metadata | data-track="click", data-category="pricing" |
| Testing | Test selectors | data-testid="submit-button", data-qa="header" |
| Internationalization | Translation keys | data-i18n="welcome.message" |
| Sorting / Filtering | Sort values | data-sort-value="100", data-filter="electronics" |
| 1 | <!-- State tracking: active states --> |
| 2 | <nav> |
| 3 | <a href="/dashboard" |
| 4 | data-page="dashboard" |
| 5 | data-active="true"> |
| 6 | Dashboard |
| 7 | </a> |
| 8 | <a href="/settings" |
| 9 | data-page="settings" |
| 10 | data-active="false"> |
| 11 | Settings |
| 12 | </a> |
| 13 | </nav> |
| 14 | |
| 15 | <!-- Configuration: JS behavior config --> |
| 16 | <div |
| 17 | class="carousel" |
| 18 | data-autoplay="true" |
| 19 | data-interval="5000" |
| 20 | data-loop="true" |
| 21 | data-transition="slide" |
| 22 | > |
| 23 | <div data-slide="1">Slide 1</div> |
| 24 | <div data-slide="2">Slide 2</div> |
| 25 | <div data-slide="3">Slide 3</div> |
| 26 | </div> |
| 27 | |
| 28 | <!-- Analytics tracking --> |
| 29 | <button |
| 30 | data-track="click" |
| 31 | data-category="signup" |
| 32 | data-label="pricing-page" |
| 33 | data-value="free-tier" |
| 34 | > |
| 35 | Sign Up Free |
| 36 | </button> |
| 37 | |
| 38 | <!-- Testing selectors --> |
| 39 | <form data-testid="login-form"> |
| 40 | <input |
| 41 | type="email" |
| 42 | data-testid="email-input" |
| 43 | placeholder="Email" |
| 44 | /> |
| 45 | <button |
| 46 | type="submit" |
| 47 | data-testid="submit-btn" |
| 48 | > |
| 49 | Log In |
| 50 | </button> |
| 51 | </form> |
| 52 | |
| 53 | <!-- Internationalization --> |
| 54 | <h1 data-i18n="home.title">Welcome</h1> |
| 55 | <p data-i18n="home.description"> |
| 56 | This text can be replaced by a translation system. |
| 57 | </p> |
| 58 | |
| 59 | <!-- Sorting data for JS-based sort --> |
| 60 | <table> |
| 61 | <thead> |
| 62 | <tr> |
| 63 | <th data-sort="name">Name</th> |
| 64 | <th data-sort="price">Price</th> |
| 65 | </tr> |
| 66 | </thead> |
| 67 | <tbody> |
| 68 | <tr data-sort-name="Widget" data-sort-price="9.99"> |
| 69 | <td>Widget</td> |
| 70 | <td>$9.99</td> |
| 71 | </tr> |
| 72 | <tr data-sort-name="Gadget" data-sort-price="24.99"> |
| 73 | <td>Gadget</td> |
| 74 | <td>$24.99</td> |
| 75 | </tr> |
| 76 | </tbody> |
| 77 | </table> |
Live preview of a data-driven UI with state tracking and configuration:
Data attributes can be updated dynamically via JavaScript to reflect application state. Changes to dataset properties are reflected in the DOM immediately, which means CSS selectors targeting those attributes react instantly — no additional JavaScript is needed to update styles.
This pattern is particularly powerful for theming, state management, and responsive behavior. You can change a single data attribute on a parent element and all descendant CSS selectors update automatically.
| 1 | <!-- Theme toggling via data attribute --> |
| 2 | <div id="app" data-theme="dark"> |
| 3 | <header> |
| 4 | <button id="theme-toggle" data-action="toggle-theme"> |
| 5 | Toggle Theme |
| 6 | </button> |
| 7 | </header> |
| 8 | <main> |
| 9 | <p>Current theme: <span id="theme-label">dark</span></p> |
| 10 | </main> |
| 11 | </div> |
| 12 | |
| 13 | <style> |
| 14 | #app[data-theme="dark"] { |
| 15 | background: #0A0A0A; |
| 16 | color: #E0E0E0; |
| 17 | } |
| 18 | #app[data-theme="light"] { |
| 19 | background: #FFFFFF; |
| 20 | color: #1A1A1A; |
| 21 | } |
| 22 | </style> |
| 23 | |
| 24 | <script> |
| 25 | const app = document.getElementById('app'); |
| 26 | const label = document.getElementById('theme-label'); |
| 27 | |
| 28 | document.getElementById('theme-toggle').addEventListener('click', () => { |
| 29 | const current = app.dataset.theme; |
| 30 | const next = current === 'dark' ? 'light' : 'dark'; |
| 31 | app.dataset.theme = next; |
| 32 | label.textContent = next; |
| 33 | }); |
| 34 | </script> |
| 35 | |
| 36 | <!-- Dynamic filtering with data attributes --> |
| 37 | <style> |
| 38 | .item { display: block; } |
| 39 | .item[data-hidden="true"] { display: none; } |
| 40 | </style> |
| 41 | |
| 42 | <div id="filter-controls"> |
| 43 | <button data-filter="all">All</button> |
| 44 | <button data-filter="html">HTML</button> |
| 45 | <button data-filter="css">CSS</button> |
| 46 | <button data-filter="js">JavaScript</button> |
| 47 | </div> |
| 48 | |
| 49 | <ul id="item-list"> |
| 50 | <li class="item" data-category="html">HTML Elements</li> |
| 51 | <li class="item" data-category="css">CSS Grid</li> |
| 52 | <li class="item" data-category="js">JavaScript Events</li> |
| 53 | <li class="item" data-category="html">Semantic HTML</li> |
| 54 | <li class="item" data-category="css">Flexbox Guide</li> |
| 55 | <li class="item" data-category="js">DOM Manipulation</li> |
| 56 | </ul> |
| 57 | |
| 58 | <script> |
| 59 | const filterButtons = document.querySelectorAll('[data-filter]'); |
| 60 | const items = document.querySelectorAll('.item'); |
| 61 | |
| 62 | filterButtons.forEach(btn => { |
| 63 | btn.addEventListener('click', () => { |
| 64 | const filter = btn.dataset.filter; |
| 65 | |
| 66 | items.forEach(item => { |
| 67 | if (filter === 'all' || item.dataset.category === filter) { |
| 68 | item.dataset.hidden = 'false'; |
| 69 | } else { |
| 70 | item.dataset.hidden = 'true'; |
| 71 | } |
| 72 | }); |
| 73 | }); |
| 74 | }); |
| 75 | </script> |
Data attributes are stored in the DOM and are part of the element's attribute list. Reading and writing them is fast — on par with other DOM attribute operations. However, there are a few performance considerations to keep in mind.
| Operation | Performance | Recommendation |
|---|---|---|
| Reading dataset | Fast (cached DOM property) | Use for infrequent reads |
| Writing dataset | Triggers attribute mutation | Batch updates, avoid layout thrash |
| CSS selector [data-*] | Fast (same as class selectors) | Use for styling, not JS data storage in hot loops |
| Large JSON in dataset | Slower (serialization cost) | Store a reference key, keep data in JS memory |
| Many elements with data-* | Memory proportional to attribute count | Avoid thousands of data attributes; use JS Map instead |
| 1 | // Anti-pattern: storing large data in data attributes |
| 2 | const items = document.querySelectorAll('[data-product]'); |
| 3 | items.forEach(item => { |
| 4 | // Each item has a large JSON string as data-product |
| 5 | const product = JSON.parse(item.dataset.product); |
| 6 | // This parses the JSON on every access |
| 7 | }); |
| 8 | |
| 9 | // Better: store a reference key, keep data in JS |
| 10 | const productData = new Map(); |
| 11 | |
| 12 | items.forEach(item => { |
| 13 | const id = item.dataset.productId; |
| 14 | const data = productData.get(id); |
| 15 | // Access productData map directly, no DOM parsing needed |
| 16 | }); |
| 17 | |
| 18 | // Anti-pattern: reading data attributes in a hot loop |
| 19 | for (let i = 0; i < 1000; i++) { |
| 20 | const val = element.dataset.value; |
| 21 | // dataset access touches the DOM each iteration |
| 22 | } |
| 23 | |
| 24 | // Better: cache the value outside the loop |
| 25 | const cached = element.dataset.value; |
| 26 | for (let i = 0; i < 1000; i++) { |
| 27 | // Use cached value, no DOM access in the loop |
| 28 | process(cached); |
| 29 | } |
| 30 | |
| 31 | // Anti-pattern: using data-* for computed/derived state |
| 32 | item.dataset.total = String(a + b); // String concatenation in DOM |
| 33 | |
| 34 | // Better: keep computed state in JS, use data-* for identification only |
| 35 | const state = { total: a + b }; |
| 36 | item.dataset.itemId = String(id); // Just for identification |
pro tip
Data attributes are visible in the DOM and can be read by any JavaScript running on the page, including third-party scripts. Never store sensitive information like API keys, authentication tokens, session IDs, or personal data in data attributes.
| Data Type | Safe in data-*? | Why |
|---|---|---|
| User IDs | Yes (public) | Safe as long as they are not secrets |
| API tokens | No | Visible in DOM inspector, accessible to any script |
| Session tokens | No | Should only be in HttpOnly cookies, never in DOM |
| Personal data | No | GDPR/PII compliance — data attributes are not encrypted |
| Public identifiers | Yes | Post slugs, item SKUs, category names are fine |
| Configuration flags | Yes | Feature toggles, UI state flags are acceptable |
| 1 | <!-- DANGEROUS: never store secrets in data attributes --> |
| 2 | <button |
| 3 | data-api-key="sk-abc123..." |
| 4 | data-session-token="eyJhbGciOi..." |
| 5 | data-user-ssn="123-45-6789" |
| 6 | > |
| 7 | ❌ Never do this |
| 8 | </button> |
| 9 | |
| 10 | <!-- ACCEPTABLE: public identifiers and configuration --> |
| 11 | <button |
| 12 | data-product-id="prod_42" |
| 13 | data-category="electronics" |
| 14 | data-currency="USD" |
| 15 | data-price="29.99" |
| 16 | > |
| 17 | Add to Cart — $29.99 |
| 18 | </button> |
| 19 | |
| 20 | <!-- SAFE: boolean state flags for UI behavior --> |
| 21 | <div |
| 22 | data-user-role="viewer" |
| 23 | data-feature-flags='["dark-mode","beta-search"]' |
| 24 | > |
| 25 | <p>Profile content for a viewer role user.</p> |
| 26 | </div> |
| 27 | |
| 28 | <!-- ALWAYS: sanitize data attribute values --> |
| 29 | <!-- If values come from user input, escape them properly --> |
| 30 | <div |
| 31 | data-description="Safe text — no HTML injection here" |
| 32 | ></div> |
| 33 | |
| 34 | <!-- NEVER: use data attributes to bypass CSP --> |
| 35 | <script> |
| 36 | // Dangerous: reading token from data attribute |
| 37 | const token = document.querySelector('[data-token]').dataset.token; |
| 38 | // This token is visible to every browser extension and third-party script |
| 39 | </script> |
danger
Data Attributes Checklist
best practice