11. 自訂資料屬性(data-*)(Data Attributes)

HTML 自訂資料屬性用來在元素上儲存應用程式專用資訊,名稱以 data- 開始。

<article
  class="course-card"
  data-course-id="html-basics"
  data-level="beginner"
>
  <h2>HTML 入門</h2>
</article>

在這個例子中:

  • data-course-id 提供程式使用的課程識別值。
  • data-level 儲存課程程度。
  • 這兩個屬性都不會自動顯示文字。

自訂資料屬性應用於應用程式資料,不應取代語意化 HTML、可見內容或無障礙屬性。

11.1 基本語法

自訂資料屬性具有以 data- 開始的名稱及對應值:

<p data-category="announcement">
  課程指南現已提供。
</p>

同一元素可以有多個資料屬性:

<article
  data-course-id="css-basics"
  data-level="beginner"
  data-duration-hours="4"
>
  <h2>CSS 入門</h2>
</article>

實用命名慣例包括:

  • 使用小寫字母。
  • 使用連字號分隔詞語。
  • 選擇具描述性的名稱。
  • 避免在名稱內加入空格或不常見的標點符號。
  • 在 data- 之後加入有意義的名稱。

例如:

屬性可能用途
data-course-id應用程式識別值
data-category篩選分類
data-duration-hours課程時數
data-action供 JavaScript 使用的操作識別值

小寫、以連字號分隔的名稱,方便配合 JavaScript 的 dataset 介面使用。

11.2 屬性值是字串

即使資料屬性值看起來像數字或布林值,它們仍然是文字:

<article
  id="html-course"
  data-duration-hours="3"
  data-featured="false"
>
  <h2>HTML 入門</h2>
</article>

JavaScript 會將這些值讀取為字串:

const course = document.getElementById("html-course");

console.log(course.dataset.durationHours); // "3"
console.log(course.dataset.featured);      // "false"

有需要時,應明確轉換:

const duration = Number(course.dataset.durationHours);
const featured = course.dataset.featured === "true";

不要用以下方式判斷字串 "false":

const featured = Boolean(course.dataset.featured);

非空字串會被視為真值,因此結果是 true。

如果這些值會影響應用程式行為,應驗證它們,而不是假設格式正確。例如,Number() 會將空字串轉換成 0,無效的數字字串則可能得到 NaN。

11.3 使用 dataset 讀取資料

dataset 屬性讓 JavaScript 存取自訂資料屬性。

以連字號分隔的名稱,通常會轉換成駝峰式命名:

HTML 屬性JavaScript 屬性
data-leveldataset.level
data-course-iddataset.courseId
data-duration-hoursdataset.durationHours

例如:

<article id="course-card" data-course-id="html-basics">
  <h2>HTML 入門</h2>
</article>
const card = document.getElementById("course-card");

if (card) {
  console.log(card.dataset.courseId); // "html-basics"
}

如果資料屬性不存在,對應的 dataset 屬性通常會回傳 undefined。

你也可以使用完整的屬性名稱:

const courseId = card.getAttribute("data-course-id");

當屬性不存在時,getAttribute() 會回傳 null。

以上讀取方式都要求 card 是已找到的元素;如果元素可能不存在,應先檢查。

11.4 更新與移除資料

為 dataset 屬性賦值,會更新對應的 HTML 屬性:

const card = document.getElementById("course-card");

if (card) {
  card.dataset.level = "intermediate";
  card.dataset.durationHours = "5";
}

更新後的屬性包括:

data-level="intermediate"
data-duration-hours="5"

賦值時,資料會轉換成字串:

card.dataset.durationHours = 5; // 儲存為 "5"。
card.dataset.note = null;      // 儲存為 "null",不是移除屬性。

使用 delete 移除資料屬性:

delete card.dataset.note;

也可以使用一般屬性方法:

card.setAttribute("data-level", "beginner");
card.removeAttribute("data-level");

修改資料屬性不會自動更新可見文字,也不會自動向輔助技術宣告變更。應用程式需要按需要實作這些行為。

11.5 空值與不存在的屬性

資料屬性可以存在,但值為空:

<div id="example" data-ready=""></div>

讀取時會得到空字串:

const example = document.getElementById("example");

console.log(example.dataset.ready); // ""
console.log(example.hasAttribute("data-ready")); // true

如果你的慣例是以屬性是否存在作為旗標,應明確檢查存在與否:

const ready = example.hasAttribute("data-ready");

不要依靠值的真假判斷:

if (example.dataset.ready) {
  // 當值為空字串時,不會執行。
}

自訂資料屬性不是原生 HTML 布林屬性。例如,data-ready="false" 仍然存在,而且包含字串 "false"。

11.6 按資料屬性選取元素

CSS 和 JavaScript 都可以使用屬性選擇器。

選取具有某個屬性的元素

[data-featured] {
  border: 2px solid #005ea8;
}

這會符合所有具有 data-featured 的元素,包括值為 "false" 的元素。

選取具有特定值的元素

[data-featured="true"] {
  border: 2px solid #005ea8;
}

JavaScript 使用相同的選擇器語法:

const beginnerCourses = document.querySelectorAll(
  '[data-level="beginner"]'
);

一般自訂資料屬性的值比對會區分大小寫,除非你明確要求不區分:

<article data-level="Beginner">...</article>

這不會符合 [data-level="beginner"] 選擇器。

一般用途的樣式通常使用 class 更清晰。當樣式取決於應用程式定義的資料值時,資料屬性選擇器便很有用。

11.7 資料屬性與其他屬性的分別

按用途選擇屬性:

需要適合的屬性
可重用的樣式群組class
唯一元素識別碼或片段目標id
內容語言lang
連結目的地href
原生停用控制項disabled
無障礙展開狀態適當使用 aria-expanded
自訂應用程式中繼資料data-*

例如:

<button type="button" data-action="show-details">
  顯示詳情
</button>

data-action 可以幫助 JavaScript 識別預期操作,但不會實作該操作。

同樣地:

<button type="button" data-disabled="true">
  報名
</button>

這個按鈕沒有被原生停用。如果需要停用行為,應使用真正的 disabled 屬性。

不要使用私有資料慣例取代無障礙狀態:

<button
  type="button"
  aria-expanded="false"
  aria-controls="course-details"
>
  顯示課程詳情
</button>

這裏的 aria-expanded 向輔助技術傳達相關狀態。單靠 data-expanded 不會做到這一點。

這些 ARIA 屬性本身也不會實作展開操作;程式仍需控制詳情區塊,並同步更新狀態。

11.8 讓重要資訊保持可見

資料屬性不會顯示為一般內容:

<article data-fee-hkd="300">
  <h2>HTML 入門</h2>
</article>

單靠這段標記,讀者看不到費用。

如果資訊對讀者重要,應將它寫在可見內容中:

<article data-fee-hkd="300">
  <h2>HTML 入門</h2>
  <p>費用:HK$300</p>
</article>

如果同時維護可見內容及中繼資料,應確保它們一致。

CSS 可以透過產生的內容顯示屬性值:

.course-card::after {
  content: attr(data-level);
}

不過,CSS 產生的內容不能可靠地取代重要的文件文字。重要標籤、指引及描述應直接放在 HTML 內容中。

11.9 安全與私隱

資料屬性可以在瀏覽器中被檢查及修改。

不要將它們視為以下資料的可信儲存位置:

  • 密碼。
  • API 金鑰或存取權杖。
  • 機密個人資料。
  • 授權決策。
  • 可信的價格或帳戶權限。

例如:

<button type="button" data-price-hkd="300">
  選擇課程
</button>

訪客可以修改這個屬性。處理交易之前,伺服器必須獨立核實課程及實際價格。

也不要將資料屬性值當作可信標記插入 innerHTML。如果預期輸出是純文字,應使用 textContent。

11.10 儲存較複雜的資料

資料屬性可以包含 JSON,但需要小心處理引號及跳脫:

<div
  id="course-options"
  data-options='{"level":"beginner","durationHours":3}'
></div>

JavaScript 可以剖析它:

const element = document.getElementById("course-options");

if (element) {
  try {
    const options = JSON.parse(element.dataset.options);
    console.log(options.level);
  } catch {
    console.error("課程設定不是有效的 JSON。");
  }
}

這個例子使用單引號包住 HTML 屬性值,讓 JSON 的雙引號可以出現在內部。

動態產生屬性值時,應使用正確的 HTML 屬性值跳脫處理。位置不當的引號可能會意外結束屬性。

成功剖析 JSON 不代表資料結構符合要求;應按用途驗證欄位及類型。

對大型或經常更新的資料結構,JavaScript 物件或適當的 JSON 資源,通常比長篇屬性值更容易維護。

11.11 語言與資料值

資料屬性不會設定內容語言:

<article data-level="beginner" lang="zh-Hant-HK">
  <h2>HTML 入門</h2>
  <p>適合初學者。</p>
</article>

在這個例子中:

  • data-level 是應用程式資料值。
  • lang 標示可見內容的語言。

通常可以保持 beginner 等內部值穩定,同時翻譯提供給讀者的標籤。

如果屬性值或可見文字包含中文或其他 Unicode 字元,應使用 UTF-8。

11.12 完整範例:篩選課程

以下中文版網頁使用 data-level 篩選課程卡片。課程程度也會顯示在可見內容中;停用 JavaScript 時,所有課程仍然可以閱讀。

id: hong-kong-html-data-attributes
name: 自訂資料屬性 — 課程篩選
type: html
content: |-
  <!doctype html>
  <html lang="zh-Hant-HK">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>自訂資料屬性 | 課程篩選</title>
    <style>
      * {
        box-sizing: border-box;
      }

      body {
        margin: 0;
        color: #222;
        background: #f5f7fa;
        font-family: system-ui, sans-serif;
        line-height: 1.8;
      }

      main {
        max-width: 46rem;
        margin-inline: auto;
        padding: 2rem 1rem;
      }

      h1,
      h2 {
        color: #005ea8;
        line-height: 1.4;
      }

      .filter-panel {
        margin-block: 1.5rem;
        padding: 1rem;
        border: 1px solid #767676;
        border-radius: 0.5rem;
        background: white;
      }

      label {
        display: block;
        margin-block-end: 0.5rem;
        font-weight: 700;
      }

      select {
        max-width: 100%;
        padding: 0.5rem;
        color: #222;
        background: white;
        border: 1px solid #767676;
        border-radius: 0.25rem;
        font: inherit;
      }

      .course-card {
        margin-block: 1.5rem;
        padding: 1.25rem;
        border: 1px solid #767676;
        border-radius: 0.5rem;
        background: white;
      }

      .course-card h2 {
        margin-block-start: 0;
      }

      .course-card > :last-child {
        margin-block-end: 0;
      }

      .note {
        padding: 1rem;
        border-inline-start: 4px solid #005ea8;
        background: #eaf2f8;
      }

      :focus-visible {
        outline: 3px solid #005ea8;
        outline-offset: 4px;
      }

      /* 即使日後加入其他 display 規則,也保持隱藏內容不顯示。 */
      [hidden] {
        display: none !important;
      }
    </style>
  </head>
  <body>
    <main>
      <h1>探索課程示例</h1>
      <p>
        按程度篩選虛構課程。
        每張卡片都以資料屬性儲存課程程度。
      </p>

      <p class="note">
        data 屬性供程式讀取。
        讀者需要的課程程度也會顯示在頁面內容中。
      </p>

      <div id="filter-panel" class="filter-panel" hidden>
        <label for="level-filter">按程度顯示課程</label>
        <select id="level-filter">
          <option value="all">所有程度</option>
          <option value="beginner">入門</option>
          <option value="intermediate">中級</option>
        </select>
      </div>

      <p id="filter-status" role="status" aria-atomic="true">
        顯示全部 3 個課程。
      </p>

      <div id="course-list">
        <article
          class="course-card"
          data-course-id="html-basics"
          data-level="beginner"
        >
          <h2>HTML 入門</h2>
          <p>程度:入門</p>
          <p>學習標題、段落、連結、清單及表格。</p>
        </article>

        <article
          class="course-card"
          data-course-id="css-basics"
          data-level="beginner"
        >
          <h2>CSS 入門</h2>
          <p>程度:入門</p>
          <p>認識選擇器、文字樣式、間距及顏色。</p>
        </article>

        <article
          class="course-card"
          data-course-id="responsive-layout"
          data-level="intermediate"
        >
          <h2>響應式版面</h2>
          <p>程度:中級</p>
          <p>使用 CSS Grid 及 Flexbox 建立可調整的版面。</p>
        </article>
      </div>

      <noscript>
        <p>
          篩選功能需要 JavaScript。
          上方已顯示所有課程。
        </p>
      </noscript>
    </main>

    <script>
      const panel = document.getElementById("filter-panel");
      const filter = document.getElementById("level-filter");
      const status = document.getElementById("filter-status");
      const cards = document.querySelectorAll(
        "#course-list .course-card[data-level]"
      );

      function updateCourses() {
        const selectedLevel = filter.value;
        let visibleCount = 0;

        cards.forEach((card) => {
          const matches =
            selectedLevel === "all" ||
            card.dataset.level === selectedLevel;

          card.hidden = !matches;

          if (matches) {
            visibleCount += 1;
          }
        });

        status.textContent = `顯示 ${visibleCount} 個課程。`;
      }

      filter.addEventListener("change", updateCourses);
      updateCourses();

      // 完成篩選功能初始化後,才顯示控制項。
      panel.hidden = false;
    </script>
  </body>
  </html>

在這個範例中:

  • data-level 儲存篩選所用的值。
  • dataset.level 讀取該值。
  • 原生 hidden 屬性控制每張卡片是否顯示。
  • 可見文字標示每個課程的程度。
  • 狀態區域向輔助技術傳達更新後的結果數量。
  • 沒有 JavaScript 時,所有課程仍可閱讀,未啟用的篩選控制項則保持隱藏。

11.13 常見錯誤

錯誤較好的做法
使用沒有 data- 前綴的自訂屬性使用有效的自訂資料屬性
假設資料值是數字或布林值明確轉換及驗證字串
將 "false" 當作布林假值與約定值比較,例如 "true"
混淆空值與不存在的屬性檢查存在與否時使用 hasAttribute()
將 data-course-id 讀取為 dataset.course-id使用 dataset.courseId
以賦值 null 移除屬性使用 delete 或 removeAttribute()
只想符合 "true",卻使用 [data-featured]使用 [data-featured="true"]
只將重要資訊放在中繼資料中同時提供可見內容
用資料屬性取代原生或 ARIA 屬性使用能提供預期行為或語意的屬性
信任可由用戶端修改的價格或權限在伺服器驗證權威資料
在資料屬性中儲存機密不要將機密放入傳送至用戶端的 HTML

本章總結: 使用 data-* 儲存小量應用程式專用資訊,並透過 dataset 讀取及更新。記住屬性值是字串,並將可見內容、無障礙功能及安全責任分開處理。

Scroll to Top