Skip to content

data-* 自定义数据属性

data-* 自定义数据属性允许在 HTML 元素上存储额外的自定义数据,这些数据不会影响元素的渲染,但可以通过 JavaScript 的 dataset API 进行读写。data-* 属性是前端开发中实现数据驱动视图、组件状态管理以及 CSS 状态控制的常用手段。

前置知识

阅读本节前,建议先了解:spellcheck 拼写检查

基础概念

什么是 data-* 属性

data-* 是 HTML5 引入的全局属性,允许开发者在任意 HTML 元素上存储自定义的、仅供内部使用的数据。这些数据不会对页面渲染产生任何影响,浏览器会自动忽略它们。

html
<!-- data-* 属性的基本格式 -->
<div data-user-id="12345" data-role="admin" data-last-login="2024-01-15">
  张三的管理面板
</div>

data-* 属性的命名规则:

规则说明正确示例错误示例
前缀必须为 data-区分标准属性data-namename, my-name
至少包含一个字符data- 后不能为空data-xdata-
只能用小写字母大小写不敏感但约定小写data-userIddata-UserID(浏览器会转为小写)
不能包含 XML 冒号避免与 XML 命名空间冲突data-my-propdata-my:prop
连字符分隔多词使用连字符data-user-namedata_userName(CSS 属性选择器)

dataset API

浏览器会将 data-* 属性映射到元素的 dataset DOMStringMap 对象上。转换规则:

  • data- 前缀被移除
  • 连字符 - 转为驼峰命名(camelCase)
  • 属性值始终是字符串类型
html
<!-- HTML 中的 data-* 属性 -->
<div id="user"
     data-user-id="1001"
     data-first-name="张三"
     data-is-active="true"
     data-scores="95,87,92">
</div>

<script>
  const userEl = document.getElementById('user');

  // 读取 data-* 属性
  console.log(userEl.dataset.userId);     // "1001"
  console.log(userEl.dataset.firstName);   // "张三"
  console.log(userEl.dataset.isActive);    // "true"(注意:字符串,不是布尔值)
  console.log(userEl.dataset.scores);      // "95,87,92"

  // 设置 data-* 属性
  userEl.dataset.lastName = '李四';       // 添加 data-last-name
  userEl.dataset.isActive = 'false';        // 修改 data-is-active

  // 删除 data-* 属性
  delete userEl.dataset.scores;            // 删除 data-scores
</script>

转换对照表

HTML 属性dataset 键名
data-iddataset.id
data-user-namedataset.userName
data-is-activedataset.isActive
data-max-scoredataset.maxScore
data-x-http-headerdataset.xHttpHeader

语法

HTML 中的用法

html
<!-- 在各种元素上使用 data-* -->
<li data-index="0" data-category="tech">文章标题</li>

<button data-action="delete" data-confirm="确定删除吗?">
  删除
</button>

<tr data-record-id="A001" data-status="active">
  <td>客户信息</td>
</tr>

<img src="photo.jpg" data-src="hd-photo.jpg" data-caption="风景照" alt="风景">

CSS 中的用法

css
/* 使用属性选择器匹配 data-* */
[data-role="admin"] {
  border: 2px solid gold;
}

/* data-* 属性值作为 CSS 变量 */
.card[data-theme="dark"] {
  --bg: #1a1a2e;
  --fg: #eaeaea;
  background-color: var(--bg);
  color: var(--fg);
}

/* data-state 控制样式 */
[data-state="open"] .panel {
  max-height: 500px;
}

[data-state="closed"] .panel {
  max-height: 0;
}

JavaScript 中的用法

javascript
// 创建 data-* 属性
element.dataset.newAttr = 'value';

// 读取
const val = element.dataset.newAttr;

// 修改
element.dataset.newAttr = 'newValue';

// 删除
delete element.dataset.newAttr;

// 检查是否存在
if ('newAttr' in element.dataset) {
  // 属性存在
}

// 获取所有 data-* 属性名
const keys = Object.keys(element.dataset);

详细说明

数据类型处理

data-* 属性值始终是字符串,需要进行类型转换:

javascript
const card = document.querySelector('.card');

// 字符串 - 直接使用
const name = card.dataset.name;

// 数字 - 使用 Number() 或 parseInt()
const id = Number(card.dataset.id);
const max = parseInt(card.dataset.maxItems, 10);

// 布尔值 - 使用严格比较
const isActive = card.dataset.active === 'true';

// JSON 数据 - 使用 JSON.parse()
try {
  const config = JSON.parse(card.dataset.config);
  console.log(config.theme, config.layout);
} catch (e) {
  console.error('解析 JSON 失败:', e);
}

// 数组 - 使用 split() 或 JSON.parse()
const tags = card.dataset.tags.split(',');
const items = JSON.parse(card.dataset.items);

惰性加载(Lazy Loading)模式

data-* 常用于实现图片和内容的惰性加载:

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>惰性加载示例</title>
  <style>
    img[data-src] {
      opacity: 0;
      transition: opacity 0.3s ease;
    }
    img.loaded {
      opacity: 1;
    }
  </style>
</head>
<body>
  <!-- 使用 data-src 保存真实图片地址 -->
  <img class="lazy-img"
       src="placeholder.svg"
       data-src="https://picsum.photos/800/600"
       data-srcset="https://picsum.photos/400/300 400w,
                   https://picsum.photos/800/600 800w"
       alt="风景照片">

  <script>
    // IntersectionObserver 实现惰性加载
    const lazyImages = document.querySelectorAll('img[data-src]');

    const imageObserver = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const img = entry.target;
          img.src = img.dataset.src;
          if (img.dataset.srcset) {
            img.srcset = img.dataset.srcset;
          }
          img.classList.add('loaded');
          imageObserver.unobserve(img);
        }
      });
    }, { rootMargin: '100px' });

    lazyImages.forEach(img => imageObserver.observe(img));
  </script>
</body>
</html>

配置驱动组件

使用 data-* 属性驱动组件行为,避免为每种配置编写不同的 JavaScript:

html
<!-- 通过 data-* 配置行为 -->
<button class="confirm-btn"
        data-message="确定要删除此记录吗?"
        data-confirm-text="确定"
        data-cancel-text="取消"
        data-danger="true">
  删除记录
</button>

<button class="confirm-btn"
        data-message="确定要退出编辑吗?未保存的更改将丢失。"
        data-confirm-text="保存并退出"
        data-cancel-text="继续编辑"
        data-danger="false">
  退出编辑
</button>

<script>
  document.querySelectorAll('.confirm-btn').forEach(btn => {
    btn.addEventListener('click', () => {
      const message = btn.dataset.message || '确定执行此操作吗?';
      const confirmText = btn.dataset.confirmText || '确定';
      const cancelText = btn.dataset.cancelText || '取消';
      const isDanger = btn.dataset.danger === 'true';

      // 创建确认弹窗
      const confirmed = createConfirmDialog({
        message,
        confirmText,
        cancelText,
        danger: isDanger
      });

      if (confirmed) {
        executeAction(btn);
      }
    });
  });
</script>

CSS 状态驱动

使用 data-* 属性控制 CSS 状态,实现纯数据驱动的样式切换:

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>CSS 状态驱动示例</title>
  <style>
    /* 状态切换动画 */
    .toggle-card {
      --duration: 0.3s;
      padding: 20px;
      border-radius: 8px;
      background: #fff;
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
      transition: all var(--duration) ease;
    }

    /* 使用 data-state 控制样式 */
    .toggle-card[data-state="expanded"] {
      grid-column: span 2;
      background: #f0f7ff;
    }

    .toggle-card[data-state="expanded"] .detail-content {
      display: block;
    }

    .toggle-card[data-state="collapsed"] .detail-content {
      display: none;
    }

    /* 使用 data-color 控制主题 */
    .theme-card[data-theme="blue"] {
      --primary: #2563eb;
      --primary-light: #dbeafe;
    }
    .theme-card[data-theme="green"] {
      --primary: #16a34a;
      --primary-light: #dcfce7;
    }
    .theme-card[data-theme="red"] {
      --primary: #dc2626;
      --primary-light: #fee2e2;
    }
    .theme-card {
      border-left: 4px solid var(--primary);
      background: var(--primary-light);
    }

    /* 进度指示器 */
    .progress[data-value] {
      --value: attr(data-value);  /* 实际需 JS 配合 */
    }
  </style>
</head>
<body>
  <div class="toggle-card" data-state="expanded">
    <h3>可展开卡片</h3>
    <div class="detail-content">
      <p>这是展开后显示的详细内容...</p>
    </div>
  </div>

  <div class="theme-card" data-theme="blue">
    <h4>蓝色主题</h4>
  </div>

  <script>
    // 切换 data-state
    document.querySelectorAll('.toggle-card').forEach(card => {
      card.addEventListener('click', () => {
        const state = card.dataset.state === 'expanded' ? 'collapsed' : 'expanded';
        card.dataset.state = state;
      });
    });
  </script>
</body>
</html>

排序与筛选

data-* 属性非常适合在表格和列表中实现客户端排序与筛选:

html
<table id="product-table">
  <thead>
    <tr>
      <th data-sort="name">产品名称</th>
      <th data-sort="price" data-type="number">价格</th>
      <th data-sort="stock" data-type="number">库存</th>
      <th>操作</th>
    </tr>
  </thead>
  <tbody>
    <tr data-name="笔记本电脑" data-price="5999" data-stock="23">
      <td>笔记本电脑</td>
      <td>¥5,999</td>
      <td>23</td>
    </tr>
    <tr data-name="无线鼠标" data-price="129" data-stock="156">
      <td>无线鼠标</td>
      <td>¥129</td>
      <td>156</td>
    </tr>
    <tr data-name="机械键盘" data-price="459" data-stock="67">
      <td>机械键盘</td>
      <td>¥459</td>
      <td>67</td>
    </tr>
  </tbody>
</table>

<script>
  const table = document.getElementById('product-table');
  const rows = Array.from(table.querySelectorAll('tbody tr'));

  // 排序功能
  table.querySelector('thead').addEventListener('click', (e) => {
    const th = e.target.closest('[data-sort]');
    if (!th) return;

    const key = th.dataset.sort;
    const isNumeric = th.dataset.type === 'number';
    const direction = th.dataset.order === 'asc' ? -1 : 1;

    rows.sort((a, b) => {
      let valA = a.dataset[key];
      let valB = b.dataset[key];

      if (isNumeric) {
        valA = Number(valA);
        valB = Number(valB);
      }

      return valA > valB ? direction : -direction;
    });

    // 重新插入排序后的行
    rows.forEach(row => table.querySelector('tbody').appendChild(row));
    th.dataset.order = th.dataset.order === 'asc' ? 'desc' : 'asc';
  });
</script>

实战示例

完整的标签页组件

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Data-* 标签页组件</title>
  <style>
    /* 标签页容器 */
    .tabs {
      border: 1px solid #e2e8f0;
      border-radius: 8px;
      overflow: hidden;
    }

    /* 标签按钮 */
    .tab-btn {
      padding: 10px 20px;
      border: none;
      background: #f8fafc;
      cursor: pointer;
      transition: all 0.2s;
    }

    .tab-btn[data-active="true"] {
      background: #3b82f6;
      color: white;
      font-weight: 600;
    }

    /* 面板 - 通过 data-tab 匹配 */
    .tab-panel {
      display: none;
      padding: 20px;
    }

    .tab-panel[data-visible="true"] {
      display: block;
    }
  </style>
</head>
<body>
  <div class="tabs" data-default-tab="overview">
    <div class="tab-header">
      <button class="tab-btn" data-tab="overview" data-active="true">概述</button>
      <button class="tab-btn" data-tab="features" data-active="false">特性</button>
      <button class="tab-btn" data-tab="docs" data-active="false">文档</button>
    </div>

    <div class="tab-panel" data-tab="overview" data-visible="true">
      <h2>组件概述</h2>
      <p>这是一个使用 data-* 属性驱动的标签页组件。</p>
    </div>

    <div class="tab-panel" data-tab="features" data-visible="false">
      <h2>特性列表</h2>
      <ul>
        <li>支持多个标签页</li>
        <li>支持默认激活标签</li>
        <li>支持键盘导航</li>
      </ul>
    </div>

    <div class="tab-panel" data-tab="docs" data-visible="false">
      <h2>使用文档</h2>
      <p>在 HTML 中使用 data-tab 属性标识标签页。</p>
    </div>
  </div>

  <script>
    class DataTabs {
      constructor(container) {
        this.container = container;
        this.buttons = container.querySelectorAll('.tab-btn[data-tab]');
        this.panels = container.querySelectorAll('.tab-panel[data-tab]');
        this.defaultTab = container.dataset.defaultTab;
        this.init();
      }

      init() {
        // 绑定点击事件
        this.buttons.forEach(btn => {
          btn.addEventListener('click', () => this.switchTab(btn.dataset.tab));
        });

        // 键盘导航
        this.container.addEventListener('keydown', (e) => {
          const buttons = Array.from(this.buttons);
          const currentIndex = buttons.indexOf(document.activeElement);

          if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
            e.preventDefault();
            const next = e.key === 'ArrowRight'
              ? (currentIndex + 1) % buttons.length
              : (currentIndex - 1 + buttons.length) % buttons.length;
            buttons[next].focus();
            buttons[next].click();
          }
        });
      }

      switchTab(tabId) {
        // 更新按钮状态
        this.buttons.forEach(btn => {
          btn.dataset.active = btn.dataset.tab === tabId ? 'true' : 'false';
          btn.setAttribute('aria-selected', btn.dataset.tab === tabId);
        });

        // 更新面板可见性
        this.panels.forEach(panel => {
          panel.dataset.visible = panel.dataset.tab === tabId ? 'true' : 'false';
        });
      }
    }

    // 初始化
    document.querySelectorAll('.tabs').forEach(tabs => new DataTabs(tabs));
  </script>
</body>
</html>

注意事项

避免存储敏感数据

data-* 属性对任何能查看 HTML 源代码的人都是可见的:

html
<!-- 错误:不要在 data-* 中存储敏感信息 -->
<div data-user-password="secret123" data-api-key="sk-abc123">
  用户面板
</div>

<!-- 正确:敏感数据通过安全渠道传输 -->
<div data-user-id="1001">
  用户面板
</div>

不要过度使用

data-* 属性应该用于存储少量、简单的自定义数据。如果数据量大或结构复杂,应考虑其他方案:

场景推荐方案
简单配置值data-* 属性
结构化配置对象<script type="application/json">
大量数据JavaScript 对象 / API 请求
状态管理状态管理库(如 Pinia)
组件 propsWeb Components / 框架 props

性能考虑

javascript
// 较慢:频繁读写 dataset
for (let i = 0; i < 1000; i++) {
  element.dataset.counter = String(i);
}

// 较快:使用变量缓存,最后一次性写入
let counter = 0;
for (let i = 0; i < 1000; i++) {
  counter = i;
}
element.dataset.counter = String(counter);

最佳实践

命名规范

html
<!-- 推荐:使用有意义的、符合 BEM 风格的命名 -->
<div
  data-component="modal"
  data-modal-id="delete-confirm"
  data-modal-size="large"
  data-modal-closable="true"
>
  <div data-modal-role="header">确认删除</div>
  <div data-modal-role="body">此操作不可撤销</div>
  <div data-modal-role="footer">
    <button data-modal-action="confirm">确定</button>
    <button data-modal-action="cancel">取消</button>
  </div>
</div>

<!-- 避免:过于简短或无意义的命名 -->
<div data-x="1" data-y="abc" data-z="true">
  ...
</div>

与 ARIA 属性配合

html
<!-- data-* 驱动行为 + ARIA 表达语义 -->
<div
  role="tablist"
  data-tabs="main-nav"
  aria-label="主导航标签页"
>
  <button
    role="tab"
    data-tab="home"
    data-active="true"
    aria-selected="true"
    aria-controls="panel-home"
    id="tab-home"
  >
    首页
  </button>
  <button
    role="tab"
    data-tab="about"
    data-active="false"
    aria-selected="false"
    aria-controls="panel-about"
    id="tab-about"
  >
    关于
  </button>
</div>

<div
  role="tabpanel"
  data-tab="home"
  data-visible="true"
  aria-labelledby="tab-home"
  id="panel-home"
>
  首页内容
</div>

下一节

继续学习:role 与 aria-* 无障碍

参考链接