To wzorzec, który widziałem w wielu bazach kodu: programiści potrzebują dołączyć własne dane do elementu DOM, więc kodują je w nazwach klas (class="item item-42 status-active"), przechowują w ukrytych polach formularza albo dodają obiekt JavaScript indeksowany jakimś ID. Wszystkie te podejścia to obejścia. Atrybuty data-* to właściwe narzędzie do tego zadania — są wbudowane bezpośrednio w standard HTML Living Standard.
Czym są atrybuty data-*
Specyfikacja atrybutów data-* pozwala dołączać dowolne własne dane klucz-wartość do każdego elementu HTML. Nazwa musi zaczynać się od data-, po czym następuje co najmniej jeden znak. Wartość jest zawsze ciągiem znaków.
<!-- 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>Atrybuty te w żaden sposób nie wpływają na renderowanie. Nie dodają klas, stylów ani zachowań układu. Są to czyste metadane, odczytywalne przez JavaScript i CSS, ignorowane przez silnik renderowania przeglądarki.
Konwencje nazewnictwa
Nazwy atrybutów data są niewrażliwe na wielkość liter w HTML (specyfikacja zamienia je na małe litery) i są mapowane na camelCase w API dataset w JavaScript. Konwersja odbywa się automatycznie:
<!-- 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)Zasada: myślniki w HTML stają się camelCase w JavaScript. data-item-count → dataset.itemCount. Ta konwersja jest dwukierunkowa — zapisanie do dataset.itemCount aktualizuje data-item-count w DOM.
Odczyt i zapis za pomocą dataset
Właściwość dataset jest obiektem DOMStringMap — zachowuje się jak zwykły obiekt JavaScript przy odczycie i zapisie, a DOM aktualizuje się na żywo:
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-* są ciągami znaków w HTML. Zawsze parsuj liczby za pomocą parseInt() lub parseFloat(), a wartości logiczne przez porównanie z ciągiem 'true'. Traktowanie ich jako wartości oryginalnego typu bez parsowania jest częstym źródłem błędów.Delegowanie zdarzeń z atrybutami data-*
Jedno z najbardziej praktycznych zastosowań atrybutów data: delegowanie zdarzeń. Zamiast dołączać indywidualne nasłuchiwacze zdarzeń do dziesiątek przycisków, dołącz jeden do elementu nadrzędnego i odczytaj akcję z dataset elementu:
<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`);
}Ten wzorzec skaluje się do dowolnej liczby elementów. event.target.closest() przechodzi w górę drzewa DOM od klikniętego elementu, szukając najbliższego pasującego przodka — więc kliknięcie w tekst przycisku (potomny węzeł tekstowy) nadal znajdzie przycisk. Jest to czystsze niż porównywanie event.target.tagName i bardziej niezawodne niż sprawdzanie dokładnych dopasowań elementów.
Używanie data-* z CSS attr()
CSS może odczytywać wartości atrybutów data za pomocą funkcji attr() w deklaracjach content. Jest to przydatne przy podpowiedziach, odznakach i etykietach, których treść pochodzi z danych, a nie z znaczników:
<!-- 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; }Należy pamiętać, że attr() działa obecnie tylko we właściwościach content — nie można jej używać dla width, color ani innych właściwości CSS (choć specyfikacja CSS Values Level 5 pracuje nad rozszerzeniem tej funkcjonalności). Na razie selektory atrybutów ([data-status="complete"]) to praktyczny sposób na zróżnicowanie stylów w oparciu o wartości atrybutów data.
Kompletny interaktywny przykład — akordeon z data-*
Oto rzeczywisty komponent akordeonu napędzany wyłącznie atrybutami data do zarządzania stanem. Żadnego przełączania klas dla zachowania — tylko atrybuty data. Klasy służą wyłącznie do stylowania:
<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;
}Stan przechowywany jest w data-open na kontenerze. CSS odczytuje ten atrybut, by obrócić wskaźnik strzałki. JavaScript przełącza atrybut i zarządza aria-expanded oraz hidden. Każda warstwa wykonuje dokładnie jedno zadanie i elegancko ze sobą współpracuje.
Uwagi o wydajności i dostępności
- Wydajność. Atrybuty
data-*są częścią DOM i ich odczyt jest bardzo szybki — zasadniczo tak samo jak odczyt dowolnego innego atrybutu. W przypadku bardzo intensywnego kodu (tysiące odczytów na klatkę) szybsza jest mapa JavaScript indeksowana przez referencje do elementów. Ale w typowym kodzie UIdatasetjest wystarczający. - Data-* służy skryptom, nie semantyce. Te atrybuty są niewidoczne dla czytników ekranu, chyba że jawnie powiążesz je ze stanami ARIA. Nie używaj
data-roleanidata-label, aby przekazywać semantykę — do tego służą właściwe elementy HTML i atrybuty WAI-ARIA. - Bezpieczeństwo. Nigdy nie umieszczaj wrażliwych danych (tokenów, haseł, danych osobowych) w atrybutach data. Są one widoczne w inspektorze elementów przeglądarki i mogą być odczytane przez dowolny JavaScript na stronie, w tym skrypty zewnętrzne.
- Zapytania. Możesz wybierać elementy po atrybucie data za pomocą selektorów atrybutów CSS:
document.querySelectorAll('[data-action="delete"]')— działa dokładnie tak jak można się spodziewać i jest bardzo przydatne przy operacjach masowych.
Narzędzia do pracy z HTML
Podczas pracy z HTML, HTML Formatter dba o czytelność znaczników z wieloma atrybutami, a HTML Validator wykrywa błędy takie jak zduplikowane ID, które mogą psuć powiązania aria-controls. Do edytowania na żywo i podglądu wzorców komponentów takich jak powyższy akordeon, HTML Editor pozwala zobaczyć zmiany w czasie rzeczywistym bez kroku budowania.
Podsumowanie
Atrybuty data to czysty, oficjalny sposób dołączania własnych metadanych do elementów DOM. Przewyższają haki z nazwami klas, ukryte pola formularzy i zewnętrzne mapy JavaScript w większości przypadków użycia. API dataset sprawia, że odczyt i zapis jest prosty, konwersja camelCase jest automatyczna, a atrybuty naturalnie komponują się z selektorami atrybutów CSS i wzorcami delegowania zdarzeń. Pamiętaj tylko: atrybuty data służą do metadanych odczytywalnych przez skrypty, nie do semantyki dostępności — do tego służy ARIA.