Er is een patroon dat ik overal in codebases zie: ontwikkelaars moeten aangepaste data koppelen aan een DOM-element, dus coderen ze het in klassenamen (class="item item-42 status-active"), slaan het op in verborgen inputs, of voegen een JavaScript-object toe met een ID als sleutel. Dit zijn allemaal omwegen. data-*-attributen zijn het juiste gereedschap voor de klus, en ze zijn rechtstreeks ingebouwd in de HTML Living Standard.
Wat zijn data-*-attributen
De specificatie van data-*-attributen laat je aangepaste sleutel-waarde-data koppelen aan elk HTML-element. De naam moet beginnen met data-, gevolgd door minimaal één teken. De waarde is altijd een string.
<!-- data-* attributes: custom data right on the element -->
<button
data-action="delete"
data-item-id="284"
data-item-name="Summer Newsletter"
data-confirm="true"
>
Delete
</button>
<li
data-product-id="SKU-9921"
data-price="29.99"
data-in-stock="true"
data-category="electronics"
>
Wireless Keyboard
</li>Ze hebben geen invloed op de weergave. Ze voegen geen klassen, stijlen of layoutgedrag toe. Het zijn puur metadata, leesbaar door JavaScript en CSS, genegeerd door de rendering-engine van de browser.
Naamgevingsconventies
Namen van data-attributen zijn hoofdletterongevoelig in HTML (de specificatie zet ze om naar kleine letters), en ze worden in camelCase omgezet in de dataset-API van JavaScript. De conversie is automatisch:
<!-- HTML attribute names use kebab-case -->
<div
data-user-id="42"
data-first-name="Alice"
data-account-type="premium"
data-last-login-at="2026-04-16T09:00:00Z"
>
</div>// JavaScript dataset uses camelCase (automatic conversion)
const el = document.querySelector('div');
el.dataset.userId; // "42" (from data-user-id)
el.dataset.firstName; // "Alice" (from data-first-name)
el.dataset.accountType; // "premium" (from data-account-type)
el.dataset.lastLoginAt; // "2026-04-16T09:00:00Z" (from data-last-login-at)De regel: koppeltekens in HTML worden camelCase in JavaScript. data-item-count → dataset.itemCount. Deze conversie is bidirectioneel — schrijven naar dataset.itemCount werkt data-item-count in de DOM bij.
Lezen en schrijven met dataset
De eigenschap dataset is een DOMStringMap — het gedraagt zich als een gewoon JavaScript-object voor lezen en schrijven, en de DOM wordt live bijgewerkt:
const card = document.querySelector('.product-card');
// Reading
const productId = card.dataset.productId; // "SKU-9921"
const price = parseFloat(card.dataset.price); // 29.99 (remember: always a string, parse as needed)
const inStock = card.dataset.inStock === 'true'; // convert string to boolean
// Writing (updates the DOM attribute in real time)
card.dataset.price = '24.99'; // sets data-price="24.99"
card.dataset.inStock = 'false'; // sets data-in-stock="false"
card.dataset.badgeText = 'Sale'; // adds data-badge-text="Sale" (new attribute)
// Deleting
delete card.dataset.badgeText; // removes data-badge-text from the element
// Checking existence
if ('productId' in card.dataset) {
console.log('This is a product card');
}data-*-waarden zijn strings in HTML. Parseer getallen altijd met parseInt() of parseFloat(), en booleans door ze te vergelijken met de string 'true'. Ze behandelen als hun oorspronkelijke type zonder parsing is een veelvoorkomende bron van bugs.Gebeurtenisdelegatie met data-*-attributen
Een van de meest praktische toepassingen van data-attributen: gebeurtenisdelegatie. In plaats van individuele gebeurtenislisteners te koppelen aan tientallen knoppen, koppel je één listener aan een bovenliggend element en lees je de actie uit het dataset van het element:
<ul id="task-list">
<li data-task-id="101" data-status="pending">
Write article draft
<button data-action="complete" data-task-id="101">Complete</button>
<button data-action="delete" data-task-id="101">Delete</button>
</li>
<li data-task-id="102" data-status="pending">
Review pull request
<button data-action="complete" data-task-id="102">Complete</button>
<button data-action="delete" data-task-id="102">Delete</button>
</li>
</ul>const taskList = document.getElementById('task-list');
// One listener handles all buttons — even ones added dynamically later
taskList.addEventListener('click', (event) => {
const button = event.target.closest('button[data-action]');
if (!button) return; // click was somewhere else in the list
const action = button.dataset.action;
const taskId = button.dataset.taskId;
switch (action) {
case 'complete':
markTaskComplete(taskId);
break;
case 'delete':
deleteTask(taskId);
break;
}
});
function markTaskComplete(id) {
const item = taskList.querySelector(`li[data-task-id="${id}"]`);
item.dataset.status = 'complete'; // CSS can react to this change
console.log(`Task ${id} marked complete`);
}
function deleteTask(id) {
const item = taskList.querySelector(`li[data-task-id="${id}"]`);
item.remove();
console.log(`Task ${id} deleted`);
}Dit patroon schaalt naar elk aantal items. event.target.closest() loopt omhoog door de DOM vanaf het aangeklikte element om de dichtstbijzijnde overeenkomende voorouder te vinden — dus klikken op de knoptekst (een onderliggende tekstknoop) vindt de knop toch. Het is schoner dan het vergelijken van event.target.tagName en betrouwbaarder dan het controleren van exacte elementovereenkomsten.
data-* gebruiken met CSS attr()
CSS kan data-attribuutwaarden lezen met de functie attr() in content-declaraties. Dit is handig voor tooltips, badges en labels die uit data komen in plaats van uit markup:
<!-- Tooltip text from data attribute -->
<span class="tooltip" data-tip="Saves automatically every 30 seconds">
Auto-save enabled
</span>
<!-- Status badge text from data attribute -->
<li class="task" data-status="in-progress">Review PR #412</li>
<li class="task" data-status="complete">Write unit tests</li>/* Tooltip using attr() in CSS */
.tooltip {
position: relative;
cursor: help;
text-decoration: underline dotted;
}
.tooltip::after {
content: attr(data-tip);
position: absolute;
bottom: 100%;
left: 0;
background: #1a1a2e;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.85rem;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
.tooltip:hover::after,
.tooltip:focus::after {
opacity: 1;
}
/* Status badge via attr() */
.task::before {
content: '[' attr(data-status) '] ';
font-size: 0.75rem;
font-weight: bold;
text-transform: uppercase;
}
.task[data-status="complete"] { text-decoration: line-through; color: #6b7280; }
.task[data-status="in-progress"] { color: #d97706; }Let op dat attr() momenteel alleen werkt in content-eigenschappen — je kunt het niet gebruiken voor width, color of andere CSS-eigenschappen (hoewel de CSS Values Level 5-specificatie werkt aan uitbreiding hiervan). Voorlopig zijn attribuutselectors ([data-status="complete"]) de praktische manier om stijlen te variëren op basis van data-attribuutwaarden.
Een volledig interactief voorbeeld — Accordion met data-*
Hier is een accordeoncomponent uit de praktijk die volledig wordt aangestuurd door data-attributen voor de status. Geen klassewisseling voor gedrag, alleen data-attributen — klassen blijven alleen voor styling:
<div id="faq-accordion" role="list">
<div class="accordion-item" role="listitem" data-open="false">
<button
class="accordion-trigger"
data-target="faq-1"
aria-expanded="false"
aria-controls="faq-1"
>
What is the difference between article and section?
</button>
<div id="faq-1" class="accordion-panel" data-panel hidden>
<p>An <code>article</code> is self-contained content that could stand alone.
A <code>section</code> is a thematic grouping within a larger context.</p>
</div>
</div>
<div class="accordion-item" role="listitem" data-open="false">
<button
class="accordion-trigger"
data-target="faq-2"
aria-expanded="false"
aria-controls="faq-2"
>
When should I use ARIA roles?
</button>
<div id="faq-2" class="accordion-panel" data-panel hidden>
<p>Only when no native HTML element conveys the same semantics.
Native elements should always be preferred.</p>
</div>
</div>
</div>const accordion = document.getElementById('faq-accordion');
accordion.addEventListener('click', (event) => {
const trigger = event.target.closest('.accordion-trigger');
if (!trigger) return;
const targetId = trigger.dataset.target;
const panel = document.getElementById(targetId);
const item = trigger.closest('.accordion-item');
const isOpen = item.dataset.open === 'true';
// Toggle this item
item.dataset.open = isOpen ? 'false' : 'true';
trigger.ariaExpanded = isOpen ? 'false' : 'true';
panel.hidden = isOpen;
});.accordion-trigger {
width: 100%;
text-align: left;
background: none;
border: none;
padding: 1rem;
font-size: 1rem;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
/* Arrow rotates based on data-open state */
.accordion-item[data-open="false"] .accordion-trigger::after {
content: '▼';
transition: transform 0.2s;
}
.accordion-item[data-open="true"] .accordion-trigger::after {
content: '▲';
}
.accordion-panel {
padding: 0 1rem 1rem;
}
.accordion-panel[hidden] {
display: none;
}De status leeft in data-open op de container. CSS leest dat attribuut om de pijlindicator te roteren. JavaScript schakelt het attribuut en beheert aria-expanded en hidden. Elke laag doet precies één taak, en ze vormen samen een geheel.
Opmerkingen over prestaties en toegankelijkheid
- Prestaties.
data-*-attributen maken deel uit van de DOM en ze lezen is zeer snel — in wezen hetzelfde als het lezen van een ander attribuut. Voor extreem frequente codepaden (duizenden leesbewerkingen per frame) is een JavaScript Map met elementreferentie als sleutel sneller. Maar voor typische UI-code isdatasetprima. - Data-* is voor scripts, niet voor semantiek. Deze attributen zijn onzichtbaar voor schermlezers, tenzij je ze expliciet koppelt aan ARIA-statussen. Gebruik
data-roleofdata-labelniet om semantiek over te brengen — gebruik daarvoor geschikte HTML-elementen en WAI-ARIA-attributen. - Beveiliging. Zet nooit gevoelige data (tokens, wachtwoorden, PII) in data-attributen. Ze zijn zichtbaar in de elementinspector van de browser en leesbaar door elke JavaScript op de pagina, inclusief scripts van derden.
- Opvragen. Je kunt elementen selecteren op data-attribuut met CSS-attribuutselectors:
document.querySelectorAll('[data-action="delete"]')— dit werkt precies zoals je zou verwachten en is zeer nuttig voor bulkbewerkingen.
Hulpmiddelen voor HTML-ontwikkeling
Bij het werken met HTML houdt HTML Formatter je attribuutrijke markup leesbaar, en HTML Validator detecteert fouten zoals dubbele ID's die aria-controls-associaties kunnen verbreken. Voor live bewerking en voorbeeldweergave van componentpatronen zoals de bovenstaande accordion, laat de HTML Editor je wijzigingen in realtime zien zonder een bouwstap.
Samenvatting
Data-attributen zijn de schone, officiële manier om aangepaste metadata te koppelen aan DOM-elementen. Ze verslaan klassenaam-hacks, verborgen inputs en externe JavaScript-maps voor de meeste use cases. De dataset-API maakt lezen en schrijven eenvoudig, de camelCase-conversie is automatisch, en ze vormen van nature een geheel met CSS-attribuutselectors en gebeurtenisdelegatiepatronen. Onthoud alleen: data-attributen zijn voor door scripts leesbare metadata, niet voor toegankelijkheidssemantieken — dat is waarvoor ARIA dient.