あらゆるコードベースで見かけるパターンがあります。開発者はDOM要素にカスタムデータを付与する必要があり、クラス名にエンコードしたり(class="item item-42 status-active")、hidden inputに格納したり、何らかのIDをキーとするJavaScriptオブジェクトを追加したりします。これらはすべて回避策です。data-*属性がこの仕事に適したツールであり、HTML Living Standardに組み込まれています。

data-*属性とは何か

data-*属性の仕様により、任意のHTMLに任意のカスタムキー・バリューデータを付与できます。名前はdata-で始まり、その後に少なくとも1文字が続く必要があります。値は常に文字列です。

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>

レンダリングにはまったく影響しません。クラス、スタイル、レイアウトの挙動を追加しません。純粋にメタデータであり、JavaScriptとCSSで読み取り可能で、ブラウザのレンダリングエンジンには無視されます。

命名規則

データ属性名はHTML内では大文字・小文字を区別しません(仕様では小文字に変換されます)。JavaScriptのdataset APIではキャメルケースにマッピングされます。変換は自動的に行われます:

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)

ルール:HTMLのハイフンはJavaScriptではキャメルケースになります。data-item-countdataset.itemCount。この変換は双方向で、dataset.itemCountに書き込むとDOMのdata-item-countが更新されます。

datasetを使った読み書き

datasetプロパティはDOMStringMapです。読み書きには通常のJavaScriptオブジェクトと同様に振る舞い、DOMはリアルタイムで更新されます:

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');
}
覚えておいてください:HTMLのdata-*の値はすべて文字列です。数値は必ずparseInt()またはparseFloat()でパースし、真偽値は文字列'true'との比較で変換してください。パースせずに元の型として扱うことはよくあるバグの原因です。

data-*属性を使ったイベント委譲

データ属性の最も実用的な使い方の一つがイベント委譲です。数十のボタンに個別のイベントリスナーを付与する代わりに、親要素に一つのリスナーを付与し、要素の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`);
}

このパターンは任意の数の要素に対してスケールします。event.target.closest()はクリックされた要素からDOMを上に辿り、最も近い一致する祖先を見つけます。そのためボタンのテキスト(子テキストノード)をクリックしても、ボタンが見つかります。event.target.tagNameの比較より簡潔で、厳密な要素マッチングより信頼性が高いです。

CSS attr()とdata-*の活用

CSSはcontent宣言内でattr()関数を使用してデータ属性値を読み取ることができます。これはマークアップではなくデータから来るツールチップ、バッジ、ラベルに便利です:

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

attr()は現在contentプロパティでのみ動作します。widthcolorなどの他のCSSプロパティにはまだ使用できません(CSS Values Level 5仕様でこの拡張が進行中です)。現時点では、属性セレクター[data-status="complete"])がデータ属性値に基づいてスタイルを変えるための実用的な方法です。

完全なインタラクティブ例 — data-*を使ったアコーディオン

状態のために完全にデータ属性で駆動する実際のアコーディオンコンポーネントを見てみましょう。動作のためのクラス切り替えはなく、データ属性のみを使い、クラスはスタイリングにのみ使用します:

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

状態はコンテナのdata-openに格納されます。CSSはその属性を読み取り矢印インジケーターを回転させます。JavaScriptは属性を切り替え、aria-expandedhiddenを管理します。各レイヤーはただ一つの仕事をし、きれいに組み合わさります。

パフォーマンスとアクセシビリティに関する注意

  • パフォーマンス。data-*属性はDOMの一部であり、読み取りは非常に高速です。他の属性を読み取るのとほぼ同じです。非常にホットなコードパス(フレームあたり数千回の読み取り)では、要素参照をキーとするJavaScript Mapの方が高速です。しかし一般的なUIコードではdatasetで十分です。
  • data-*はスクリプト用であり、セマンティクス用ではありません。これらの属性はARIA状態に明示的にバインドしない限り、スクリーンリーダーには見えません。セマンティクスを伝えようとしてdata-roledata-labelを使用しないでください。そのためには適切なHTML要素とWAI-ARIA属性を使用してください。
  • セキュリティ。データ属性に機密データ(トークン、パスワード、個人情報)を入れないでください。ブラウザの要素インスペクターで表示され、サードパーティスクリプトを含むページ上のあらゆるJavaScriptで読み取り可能です。
  • クエリ。CSSの属性セレクターを使ってデータ属性で要素を選択できます:document.querySelectorAll('[data-action="delete"]') — これは期待通りに動作し、一括操作に非常に便利です。

HTML開発のためのツール

HTMLを扱う際、HTMLフォーマッターは属性が多いマークアップを読みやすく保ち、HTML バリデーターaria-controlsの関連付けを壊す可能性のある重複IDなどのエラーを検出します。上記のアコーディオンのようなコンポーネントパターンのライブ編集とプレビューには、HTMLエディターでビルドステップなしにリアルタイムで変更を確認できます。

まとめ

データ属性はDOM要素にカスタムメタデータを付与するためのクリーンで公式な方法です。ほとんどのユースケースでクラス名のハック、hidden input、外部JavaScriptマップよりも優れています。dataset APIで読み書きが簡単になり、キャメルケース変換は自動的で、CSS属性セレクターやイベント委譲パターンと自然に組み合わせることができます。ただし忘れずに:データ属性はスクリプトで読めるメタデータのためのものであり、アクセシビリティのセマンティクスのためではありません。それはARIAの役割です。