C'è uno schema che ho visto in molte codebase: gli sviluppatori hanno bisogno di allegare dati personalizzati a un elemento del DOM, quindi li codificano nei nomi delle classi (class="item item-42 status-active"), li memorizzano in input nascosti, o aggiungono un oggetto JavaScript indicizzato per ID. Queste sono tutte soluzioni alternative. Gli attributi data-* sono lo strumento giusto per questo lavoro, e sono integrati direttamente nello standard HTML Living Standard.
Cosa sono gli attributi data-*
La specifica degli attributi data-* consente di allegare qualsiasi dato chiave-valore personalizzato a qualsiasi elemento HTML. Il nome deve iniziare con data-, seguito da almeno un carattere. Il valore è sempre una stringa.
<!-- 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>Non influenzano affatto il rendering. Non aggiungono classi, stili o comportamenti di layout. Sono puramente metadati, leggibili da JavaScript e CSS, ignorati dal motore di rendering del browser.
Convenzioni di denominazione
I nomi degli attributi data sono case-insensitive in HTML (la specifica li converte in minuscolo), e vengono mappati in camelCase nell'API dataset di JavaScript. La conversione è automatica:
<!-- 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)La regola: i trattini in HTML diventano camelCase in JavaScript. data-item-count → dataset.itemCount. Questa conversione è bidirezionale — scrivere su dataset.itemCount aggiorna data-item-count nel DOM.
Lettura e scrittura con dataset
La proprietà dataset è una DOMStringMap — si comporta come un normale oggetto JavaScript per lettura e scrittura, e il DOM si aggiorna in tempo reale:
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-* sono stringhe in HTML. Analizza sempre i numeri con parseInt() o parseFloat(), e i booleani confrontandoli con la stringa 'true'. Trattarli come il loro tipo originale senza analisi è una fonte comune di bug.Delega di eventi con attributi data-*
Uno degli usi più pratici degli attributi data: la delega di eventi. Invece di allegare singoli listener di eventi a decine di pulsanti, collega un listener a un genitore e leggi l'azione dal dataset dell'elemento:
<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`);
}Questo schema si adatta a qualsiasi numero di elementi. event.target.closest() risale il DOM dall'elemento cliccato per trovare l'antenato corrispondente più vicino — quindi fare clic sul testo del pulsante (un nodo testo figlio) trova comunque il pulsante. È più pulito rispetto al confronto di event.target.tagName e più affidabile del controllo di corrispondenze esatte degli elementi.
Utilizzo di data-* con CSS attr()
CSS può leggere i valori degli attributi data usando la funzione attr() nelle dichiarazioni content. Questo è utile per tooltip, badge ed etichette che provengono dai dati piuttosto che dal 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; }Nota che attr() attualmente funziona solo nelle proprietà content — non puoi usarla per width, color o altre proprietà CSS (anche se la specifica CSS Values Level 5 sta lavorando per espanderlo). Per ora, i selettori di attributo ([data-status="complete"]) sono il modo pratico per variare gli stili in base ai valori degli attributi data.
Un esempio interattivo completo — Accordion con data-*
Ecco un componente accordion reale guidato interamente dagli attributi data per lo stato. Nessun toggle di classe per il comportamento, solo attributi data — le classi restano solo per lo stile:
<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;
}Lo stato risiede in data-open sul contenitore. CSS legge quell'attributo per ruotare l'indicatore a freccia. JavaScript alterna l'attributo e gestisce aria-expanded e hidden. Ogni livello fa esattamente un lavoro, e si compongono in modo pulito.
Note su prestazioni e accessibilità
- Prestazioni. Gli attributi
data-*fanno parte del DOM e leggerli è molto veloce — essenzialmente lo stesso che leggere qualsiasi altro attributo. Per percorsi di codice estremamente frequenti (migliaia di letture per frame), una JavaScript Map indicizzata per riferimento all'elemento è più veloce. Ma per il tipico codice UI,datasetva bene. - Data-* è per gli script, non per la semantica. Questi attributi sono invisibili agli screen reader a meno che non li leghi esplicitamente agli stati ARIA. Non usare
data-roleodata-labelper trasmettere semantica — usa elementi HTML appropriati e attributi WAI-ARIA per questo. - Sicurezza. Non inserire mai dati sensibili (token, password, PII) negli attributi data. Sono visibili nell'ispettore degli elementi del browser e leggibili da qualsiasi JavaScript sulla pagina, inclusi gli script di terze parti.
- Interrogazione. Puoi selezionare elementi per attributo data con i selettori di attributo CSS:
document.querySelectorAll('[data-action="delete"]')— funziona esattamente come ti aspetti ed è molto utile per le operazioni in blocco.
Strumenti per lo sviluppo HTML
Quando lavori con HTML, HTML Formatter mantiene leggibile il tuo markup ricco di attributi, e HTML Validator rileva errori come ID duplicati che possono interrompere le associazioni aria-controls. Per la modifica e l'anteprima in tempo reale di pattern di componenti come l'accordion di cui sopra, l'Editor HTML ti consente di vedere le modifiche in tempo reale senza un passaggio di build.
In conclusione
Gli attributi data sono il modo pulito e ufficiale per allegare metadati personalizzati agli elementi DOM. Battono gli hack dei nomi di classe, gli input nascosti e le mappe JavaScript esterne per la maggior parte dei casi d'uso. L'API dataset rende la lettura e la scrittura semplice, la conversione camelCase è automatica, e si compongono naturalmente con i selettori di attributo CSS e i pattern di delega degli eventi. Ricorda solo: gli attributi data sono per i metadati leggibili dagli script, non per la semantica di accessibilità — quello è il compito di ARIA.