Il y a un schéma que j'ai vu dans des bases de code partout : les développeurs ont besoin d'associer des données personnalisées à un élément DOM, alors ils les encodent dans des noms de classes (class="item item-42 status-active"), les stockent dans des champs cachés, ou ajoutent un objet JavaScript indexé par un identifiant. Tout cela ce sont des contournements. Les attributs data-* sont le bon outil pour le travail, et ils sont intégrés directement dans le standard HTML Living Standard.

Ce que sont les attributs data-*

La spécification des attributs data-* vous permet d'associer des données clé-valeur personnalisées à n'importe quel élément HTML. Le nom doit commencer par data-, suivi d'au moins un caractère. La valeur est toujours une chaîne de caractères.

html
<!-- 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>

Ils n'affectent pas du tout le rendu. Ils n'ajoutent aucune classe, style ou comportement de mise en page. Ce sont purement des métadonnées, lisibles par JavaScript et CSS, ignorées par le moteur de rendu du navigateur.

Conventions de nommage

Les noms des attributs de données sont insensibles à la casse en HTML (la spécification les met en minuscules), et ils correspondent au camelCase dans l'API dataset de JavaScript. La conversion est automatique :

html
<!-- 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>
js
// 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 règle : les tirets en HTML deviennent du camelCase en JavaScript. data-item-countdataset.itemCount. Cette conversion est bidirectionnelle — écrire dans dataset.itemCount met à jour data-item-count dans le DOM.

Lecture et écriture avec dataset

La propriété dataset est une DOMStringMap — elle se comporte comme un objet JavaScript ordinaire pour la lecture et l'écriture, et le DOM se met à jour en temps réel :

js
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');
}
Rappel : Toutes les valeurs data-* sont des chaînes de caractères en HTML. Analysez toujours les nombres avec parseInt() ou parseFloat(), et les booléens en les comparant à la chaîne 'true'. Les traiter comme leur type d'origine sans analyse est une source courante de bugs.

Délégation d'événements avec les attributs data-*

L'une des utilisations les plus pratiques des attributs de données : la délégation d'événements. Au lieu d'attacher des écouteurs d'événements individuels à des dizaines de boutons, attachez un seul écouteur à un parent et lisez l'action depuis le dataset de l'élément :

html
<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>
js
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`);
}

Ce schéma s'adapte à n'importe quel nombre d'éléments. event.target.closest() remonte le DOM depuis l'élément cliqué pour trouver l'ancêtre correspondant le plus proche — ainsi cliquer sur le texte du bouton (un nœud de texte enfant) trouve quand même le bouton. C'est plus propre que de comparer event.target.tagName et plus fiable que de vérifier les correspondances exactes d'éléments.

Utilisation de data-* avec CSS attr()

CSS peut lire les valeurs des attributs de données en utilisant la fonction attr() dans les déclarations content. C'est utile pour les infobulles, les badges et les étiquettes qui proviennent des données plutôt que du balisage :

html
<!-- 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>
css
/* 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; }

Notez que attr() fonctionne actuellement uniquement dans les propriétés content — vous ne pouvez pas l'utiliser pour width, color ou d'autres propriétés CSS pour l'instant (bien que la spécification CSS Values Level 5 travaille à étendre cela). Pour l'instant, les sélecteurs d'attributs ([data-status="complete"]) sont le moyen pratique de varier les styles en fonction des valeurs des attributs de données.

Un exemple interactif complet — Accordéon avec data-*

Voici un vrai composant accordéon entièrement piloté par des attributs de données pour l'état. Pas de basculement de classes pour le comportement, juste des attributs de données — les classes restent uniquement pour le style :

html
<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>
js
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;
});
css
.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;
}

L'état vit dans data-open sur le conteneur. CSS lit cet attribut pour faire pivoter l'indicateur de flèche. JavaScript bascule l'attribut et gère aria-expanded et hidden. Chaque couche fait exactement un travail, et elles se composent proprement.

Notes sur les performances et l'accessibilité

  • Performances. Les attributs data-* font partie du DOM et les lire est très rapide — essentiellement identique à la lecture de n'importe quel autre attribut. Pour les chemins de code extrêmement sollicités (des milliers de lectures par image), un Map JavaScript indexé par référence d'élément est plus rapide. Mais pour le code d'interface utilisateur typique, dataset convient parfaitement.
  • Data-* est pour les scripts, pas pour la sémantique. Ces attributs sont invisibles pour les lecteurs d'écran à moins que vous ne les liiez explicitement aux états ARIA. N'utilisez pas data-role ou data-label pour tenter de transmettre une sémantique — utilisez des éléments HTML appropriés et les attributs WAI-ARIA pour cela.
  • Sécurité. Ne mettez jamais de données sensibles (jetons, mots de passe, informations personnelles) dans les attributs de données. Elles sont visibles dans l'inspecteur d'éléments du navigateur et lisibles par tout JavaScript sur la page, y compris les scripts tiers.
  • Requêtes. Vous pouvez sélectionner des éléments par attribut de données avec des sélecteurs d'attributs CSS : document.querySelectorAll('[data-action="delete"]') — cela fonctionne exactement comme prévu et est très utile pour les opérations en masse.

Outils pour le développement HTML

Lors du travail avec HTML, le Formateur HTML maintient votre balisage riche en attributs lisible, et le Validateur HTML détecte les erreurs comme les identifiants dupliqués qui peuvent rompre les associations aria-controls. Pour l'édition en direct et la prévisualisation de schémas de composants comme l'accordéon ci-dessus, l'Éditeur HTML vous permet de voir les changements en temps réel sans étape de compilation.

Conclusion

Les attributs de données sont la manière propre et officielle d'associer des métadonnées personnalisées aux éléments DOM. Ils surpassent les astuces de noms de classes, les champs cachés et les maps JavaScript externes pour la plupart des cas d'utilisation. L'API dataset rend leur lecture et écriture simples, la conversion camelCase est automatique, et ils se composent naturellement avec les sélecteurs d'attributs CSS et les schémas de délégation d'événements. Souvenez-vous simplement : les attributs de données sont pour les métadonnées lisibles par script, pas pour la sémantique d'accessibilité — c'est à cela que sert ARIA.