Det finns ett mönster jag sett i kodbaser överallt: utvecklare behöver bifoga anpassad data till ett DOM-element, så de kodar det i klassnamn (class="item item-42 status-active"), lagrar det i dolda inmatningsfält eller lägger till ett JavaScript-objekt indexerat av något ID. Allt detta är workarounds. data-*-attribut är det rätta verktyget för jobbet, och de är inbyggda direkt i HTML Living Standard.

Vad data-*-attribut är

Specifikationen för data-*-attribut låter dig bifoga valfri anpassad nyckel-värde-data till vilket HTML-element som helst. Namnet måste börja med data-, följt av minst ett tecken. Värdet är alltid en sträng.

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>

De påverkar inte renderingen alls. De lägger inte till några klasser, stilar eller layoutbeteenden. De är rent metadata, läsbara av JavaScript och CSS, ignorerade av webbläsarens renderingsmotor.

Namnkonventioner

Dataattributnamn är skiftlägesokänsliga i HTML (specifikationen gör dem gemena), och de mappas till camelCase i JavaScripts dataset-API. Konverteringen sker automatiskt:

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)

Regeln: bindestreck i HTML blir camelCase i JavaScript. data-item-countdataset.itemCount. Denna konvertering är dubbelriktad — att skriva till dataset.itemCount uppdaterar data-item-count i DOM.

Läsa och skriva med dataset

Egenskapen dataset är en DOMStringMap — den beter sig som ett vanligt JavaScript-objekt för läsning och skrivning, och DOM uppdateras live:

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');
}
Kom ihåg: Alla data-*-värden är strängar i HTML. Parsa alltid tal med parseInt() eller parseFloat(), och booleaner genom att jämföra med strängen 'true'. Att behandla dem som sin ursprungliga typ utan parsning är en vanlig källa till buggar.

Händelsedelegering med data-*-attribut

Ett av de mest praktiska användningsområdena för dataattribut: händelsedelegering. Istället för att bifoga enskilda händelseavlyssnare till dussintals knappar, bifoga en avlyssnare till ett förälderelement och läs åtgärden från elementets dataset:

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

Det här mönstret skalerar till valfritt antal element. event.target.closest() går upp i DOM från det klickade elementet för att hitta närmaste matchande förfader — så att klicka på knapptexten (en underordnad textnod) hittar fortfarande knappen. Det är renare än att jämföra event.target.tagName och mer tillförlitligt än att kontrollera exakta elementmatchningar.

Använda data-* med CSS attr()

CSS kan läsa dataattributvärden med hjälp av funktionen attr() i content-deklarationer. Detta är användbart för tooltips, märken och etiketter som kommer från data snarare än uppmärkning:

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; }

Observera att attr() för närvarande bara fungerar i content-egenskaper — du kan inte använda den för width, color eller andra CSS-egenskaper ännu (även om CSS Values Level 5-specifikationen arbetar med att utöka detta). För tillfället är attributselektorer ([data-status="complete"]) det praktiska sättet att variera stilar baserat på dataattributvärden.

Ett komplett interaktivt exempel — dragspel med data-*

Här är en verklig dragspelkomponent som drivs helt av dataattribut för tillstånd. Ingen klasstoggling för beteende, bara dataattribut — klasser används endast för styling:

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;
}

Tillståndet bor i data-open på containern. CSS läser det attributet för att rotera pilindikatorin. JavaScript togglar attributet och hanterar aria-expanded och hidden. Varje lager gör exakt ett jobb, och de samverkar rent.

Prestanda- och tillgänglighetsnoteringar

  • Prestanda. data-*-attribut är en del av DOM och att läsa dem är mycket snabbt — i princip samma sak som att läsa vilket annat attribut som helst. För extremt het kod (tusentals läsningar per bildruta) är en JavaScript Map indexerad efter elementreferens snabbare. Men för typisk UI-kod är dataset utmärkt.
  • Data-* är för skript, inte semantik. Dessa attribut är osynliga för skärmläsare om du inte uttryckligen binder dem till ARIA-tillstånd. Använd inte data-role eller data-label för att försöka förmedla semantik — använd korrekta HTML-element och WAI-ARIA-attribut för det.
  • Säkerhet. Lägg aldrig känslig data (tokens, lösenord, personuppgifter) i dataattribut. De är synliga i webbläsarens elementinspektör och läsbara av vilket JavaScript som helst på sidan, inklusive tredjepartsskript.
  • Sökning. Du kan välja element efter dataattribut med CSS-attributselektorer: document.querySelectorAll('[data-action="delete"]') — detta fungerar precis som du förväntar dig och är mycket användbart för massoperationer.

Verktyg för HTML-utveckling

När du arbetar med HTML håller HTML Formatter dina attributtunga markeringar läsbara, och HTML Validator fångar fel som duplicerade ID som kan bryta aria-controls-associationer. För live-redigering och förhandsgranskning av komponentmönster som dragspelet ovan låter HTML Editor dig se ändringar i realtid utan ett byggsteg.

Sammanfattning

Dataattribut är det rena, officiella sättet att bifoga anpassad metadata till DOM-element. De slår klassnamns-hacks, dolda inmatningsfält och externa JavaScript-kartor för de flesta användningsfall. dataset-API:et gör det enkelt att läsa och skriva dem, camelCase-konverteringen är automatisk, och de samverkar naturligt med CSS-attributselektorer och händelsedelegeringsmönster. Kom ihåg: dataattribut är för skriptläsbar metadata, inte för tillgänglighetssemantik — det är vad ARIA är till för.