HTML Custom Data Attributes
1. Introduction
Custom data attributes โ written as data-* โ allow you to store arbitrary private data directly in HTML elements without polluting the DOM with non-standard attributes. Introduced in HTML5, they bridge the gap between HTML markup and JavaScript logic: a clean, standardised way to annotate elements with information that scripts can read, modify, and act upon. Any attribute prefixed with data- followed by at least one character is valid in any HTML element.
2. Concept
data-* vs Other Approaches
| Method | Valid HTML5? | Accessible to JS? | Problem |
|---|---|---|---|
data-* attributes |
Yes | Yes (via dataset) | None โ use this |
Custom attributes (e.g. mydata) |
No | Yes (via getAttribute) | Fails HTML validation |
| Stuffing data in class names | Yes but misuse | Complex parsing required | Confuses styling logic |
| Hidden input fields | Yes | Via form serialisation | Adds to form submission |
dataset API converts hyphenated names to camelCase automatically: data-product-id becomes element.dataset.productId in JavaScript.data-* attributes hold state and configuration. Never mix the two responsibilities.3. Basic Example
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Data Attributes Demo</title></head>
<body>
<!-- Single data attribute -->
<button
type="button"
data-action="add-to-cart"
data-product-id="SKU-4821"
data-quantity="1"
>
Add to Cart
</button>
<!-- List items with metadata -->
<ul id="task-list">
<li data-task-id="1" data-status="complete" data-priority="high">
Design homepage wireframe
</li>
<li data-task-id="2" data-status="pending" data-priority="medium">
Write API documentation
</li>
<li data-task-id="3" data-status="pending" data-priority="low">
Set up CI/CD pipeline
</li>
</ul>
<p id="output"></p>
<script>
// Reading via dataset API
var btn = document.querySelector('[data-action="add-to-cart"]');
btn.addEventListener('click', function() {
var id = btn.dataset.productId; // "SKU-4821"
var qty = btn.dataset.quantity; // "1"
document.getElementById('output').textContent =
'Added product ' + id + ' (qty: ' + qty + ') to cart.';
});
// Filtering tasks by status
var pending = document.querySelectorAll('[data-status="pending"]');
console.log('Pending tasks:', pending.length); // 2
// Updating a data attribute
pending[0].dataset.status = 'in-progress';
console.log(pending[0].dataset.status); // "in-progress"
</script>
</body>
</html>
4. How It Works
Step 1 โ Defining data-* Attributes
Any attribute on any HTML element prefixed with data- is valid. The name after the prefix must contain at least one character and consist of lowercase letters, digits, hyphens, underscores, or dots. Example: data-product-id, data-user.role.
Step 2 โ Reading with dataset
element.dataset is a DOMStringMap. Each data-* attribute appears as a property with the prefix stripped and hyphens converted to camelCase. data-product-id โ dataset.productId. Reading returns a string โ use Number() or JSON.parse() for typed values.
Step 3 โ Writing and Updating
Assign to dataset.propertyName to set or update the attribute. This immediately reflects in the DOM as data-property-name="value". You can also remove an attribute with delete element.dataset.propertyName.
Step 4 โ CSS Attribute Selectors
CSS can target data attributes with attribute selectors: [data-status="complete"] { opacity: 0.5; }. This allows style variations driven by HTML state without adding/removing CSS classes โ a clean separation of concerns.
5. Real-World Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Product Catalogue</title>
<style>
.product-card[data-in-stock="false"] { opacity: 0.5; }
.product-card[data-in-stock="false"] .add-btn { cursor: not-allowed; }
[data-category="sale"] .price::before { content: "SALE "; color: #dc2626; font-weight: bold; }
:focus-visible { outline: 3px solid #0070f3; outline-offset: 2px; }
</style>
</head>
<body>
<h1>Our Products</h1>
<div id="catalogue">
<article class="product-card"
data-product-id="PB15"
data-category="laptop"
data-in-stock="true"
data-price="1299.00"
>
<h2>ProBook 15</h2>
<p class="price">ยฃ1,299.00</p>
<button class="add-btn" type="button">Add to Cart</button>
</article>
<article class="product-card"
data-product-id="M4K"
data-category="sale"
data-in-stock="true"
data-price="249.00"
>
<h2>4K Monitor</h2>
<p class="price">ยฃ249.00</p>
<button class="add-btn" type="button">Add to Cart</button>
</article>
<article class="product-card"
data-product-id="KBD"
data-category="accessory"
data-in-stock="false"
data-price="89.00"
>
<h2>Mechanical Keyboard</h2>
<p class="price">ยฃ89.00</p>
<button class="add-btn" type="button" disabled>Out of Stock</button>
</article>
</div>
<div id="cart" aria-live="polite"></div>
<script>
document.querySelectorAll('.add-btn:not([disabled])').forEach(function(btn) {
btn.addEventListener('click', function() {
var card = btn.closest('[data-product-id]');
var id = card.dataset.productId;
var name = card.querySelector('h2').textContent;
var price = card.dataset.price;
document.getElementById('cart').textContent =
'Added: ' + name + ' (' + id + ') โ ยฃ' + price;
});
});
</script>
</body>
</html>
6. Common Mistakes
❌ Using non-standard custom attributes (fails validation)
<button productid="SKU-123">Buy</button>
✓ Always use the data-* prefix for custom attributes
<button data-product-id="SKU-123">Buy</button>
❌ Storing sensitive data in data attributes (visible in page source)
<div data-api-token="secret123"></div>
✓ Use server-side sessions for sensitive information
<div data-user-id="42" data-session-ref="public-ref-only"></div>
7. Try It Yourself
8. Quick Reference
| Operation | Syntax | Example |
|---|---|---|
| Define in HTML | data-name=”value” | <div data-user-id=”42″> |
| Read in JS | element.dataset.name | el.dataset.userId โ “42” |
| Write in JS | element.dataset.name = val | el.dataset.status = “active” |
| Delete in JS | delete element.dataset.name | delete el.dataset.status |
| Read in CSS | attr(data-name) | content: attr(data-label) |
| CSS selector | [data-name=”value”] | [data-status=”complete”] |
| Hyphen โ camelCase | data-product-id โ dataset.productId | Automatic in dataset API |