跳到主要内容

语义化与可访问性

问题

什么是 HTML 语义化?为什么需要关注可访问性(A11Y)?如何让网页对所有用户(包括残障用户)友好?

答案

一、HTML 语义化

语义化是指使用含义正确的 HTML 标签来表达内容结构,而非全部用 <div> + CSS 堆叠。

常用语义标签

标签语义替代了
<header>页头/区块头部<div class="header">
<nav>导航区域<div class="nav">
<main>页面主内容(唯一)<div class="main">
<article>独立完整内容(文章/帖子)<div class="post">
<section>主题性区块<div class="section">
<aside>侧边栏/附属内容<div class="sidebar">
<footer>页脚/区块底部<div class="footer">
<figure> / <figcaption>图片/图表及说明<div class="img-wrap">
<time>时间/日期<span>
<mark>高亮文本<span class="highlight">
<details> / <summary>可折叠内容JS 手写折叠

正确的页面结构

语义化页面结构
<body>
<header>
<nav aria-label="主导航">
<ul>
<li><a href="/">首页</a></li>
<li><a href="/about">关于</a></li>
</ul>
</nav>
</header>

<main>
<article>
<h1>文章标题</h1>
<time datetime="2026-02-28">2026年2月28日</time>
<section>
<h2>章节一</h2>
<p>内容...</p>
</section>
<figure>
<img src="chart.png" alt="2026年Q1销售数据柱状图" />
<figcaption>图1:2026年Q1销售数据</figcaption>
</figure>
</article>
<aside>侧边栏内容</aside>
</main>

<footer>
<p>&copy; 2026</p>
</footer>
</body>

语义化的好处

  1. 可访问性 — 屏幕阅读器能准确识别页面结构,盲人用户可直接跳转到 <nav><main>
  2. SEO — 搜索引擎更准确地理解内容层级和权重
  3. 可维护性 — 代码自描述,开发者一眼看懂结构
  4. 响应式基础 — 语义结构天然适合不同设备的布局调整

二、可访问性(A11Y)

A11Y(Accessibility 的缩写,a 和 y 之间 11 个字母)是指让所有人都能使用网页,包括视觉、听觉、运动、认知障碍用户。

WCAG 标准

WCAG(Web Content Accessibility Guidelines)定义了四大原则:

原则说明示例
可感知内容可被用户感知图片有 alt、视频有字幕
可操作界面可被用户操作键盘可导航、有足够点击区域
可理解内容和操作可被理解清晰的错误提示、一致的导航
健壮性兼容各种辅助技术正确使用 ARIA、语义化标签

合规等级:A(最低)→ AA(常见要求)→ AAA(最高)。大多数项目要求达到 AA 级别。

三、ARIA 属性

当原生 HTML 语义不够时,用 ARIA(Accessible Rich Internet Applications)补充语义信息。

核心原则

能用原生 HTML 就不用 ARIA<button> 自带按钮语义和键盘支持,不需要 <div role="button">。ARIA 是给自定义组件用的"语义补丁"。

常用 ARIA 属性

ARIA 使用示例
<!-- 角色 -->
<div role="dialog" aria-modal="true">弹窗</div>
<div role="tablist">
<button role="tab" aria-selected="true">Tab 1</button>
<button role="tab" aria-selected="false">Tab 2</button>
</div>

<!-- 状态 -->
<button aria-expanded="false" aria-controls="menu-1">菜单</button>
<ul id="menu-1" aria-hidden="true">...</ul>

<!-- 标签 -->
<input aria-label="搜索" type="search" />
<div aria-labelledby="dialog-title" role="dialog">
<h2 id="dialog-title">确认删除</h2>
</div>

<!-- 实时区域(屏幕阅读器自动播报变化) -->
<div aria-live="polite">搜索到 42 条结果</div>
<div aria-live="assertive">表单提交失败!</div>
属性用途场景
role声明元素角色自定义组件
aria-label提供文本标签图标按钮
aria-labelledby关联可见标签Dialog 标题
aria-describedby关联描述文本表单错误提示
aria-expanded展开/折叠状态下拉菜单
aria-hidden对辅助技术隐藏装饰性图标
aria-live动态内容播报搜索结果、通知
aria-required标记必填表单字段
aria-disabled标记禁用按钮

四、键盘导航

所有交互操作都必须可通过键盘完成:

按键行为
Tab在可交互元素间顺序移动焦点
Shift + Tab反向移动焦点
Enter / Space激活按钮、链接
Escape关闭弹窗、下拉菜单
Arrow Keys在菜单、Tab、列表内移动
键盘导航的下拉菜单
function Dropdown({ items }: { items: string[] }) {
const [open, setOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(0);

const handleKeyDown = (e: React.KeyboardEvent) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setActiveIndex(i => Math.min(i + 1, items.length - 1));
break;
case 'ArrowUp':
e.preventDefault();
setActiveIndex(i => Math.max(i - 1, 0));
break;
case 'Enter':
selectItem(items[activeIndex]);
break;
case 'Escape':
setOpen(false);
break;
}
};

return (
<div onKeyDown={handleKeyDown}>
<button
aria-expanded={open}
aria-haspopup="listbox"
onClick={() => setOpen(!open)}
>
选择...
</button>
{open && (
<ul role="listbox">
{items.map((item, i) => (
<li
key={item}
role="option"
aria-selected={i === activeIndex}
tabIndex={i === activeIndex ? 0 : -1}
>
{item}
</li>
))}
</ul>
)}
</div>
);
}

五、焦点管理

Dialog 焦点陷阱
function useFocusTrap(ref: React.RefObject<HTMLElement>, active: boolean) {
useEffect(() => {
if (!active || !ref.current) return;

const element = ref.current;
const focusableSelector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
const focusableElements = element.querySelectorAll<HTMLElement>(focusableSelector);
const firstEl = focusableElements[0];
const lastEl = focusableElements[focusableElements.length - 1];

// 打开时聚焦到第一个可交互元素
firstEl?.focus();

const handleKeyDown = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return;

// Tab 到最后一个元素时,跳回第一个(循环)
if (!e.shiftKey && document.activeElement === lastEl) {
e.preventDefault();
firstEl?.focus();
}
// Shift+Tab 到第一个元素时,跳到最后一个
if (e.shiftKey && document.activeElement === firstEl) {
e.preventDefault();
lastEl?.focus();
}
};

element.addEventListener('keydown', handleKeyDown);
return () => element.removeEventListener('keydown', handleKeyDown);
}, [active, ref]);
}

六、常见可访问性问题

常见错误
  1. 图片没有 alt — 屏幕阅读器无法描述图片内容
  2. <div> 做按钮 — 没有键盘支持、没有按钮语义,应使用 <button>
  3. 颜色对比度不足 — 文字与背景对比度低于 4.5:1,视弱用户看不清
  4. 只用颜色传达信息 — 红色表示错误但色盲用户无法区分,需要图标或文字辅助
  5. 缺少跳转链接 — 键盘用户每次都要 Tab 过整个导航栏才能到主内容
  6. 自动播放媒体 — 屏幕阅读器用户被干扰,应提供暂停控制
  7. 表单缺少 <label> — 屏幕阅读器不知道输入框的用途
Skip Link(跳转到主内容)
<!-- 视觉隐藏,Tab 时显示,让键盘用户跳过导航直达内容 -->
<a href="#main-content" class="skip-link">跳转到主内容</a>
<nav>...</nav>
<main id="main-content">...</main>

<style>
.skip-link {
position: absolute;
left: -9999px;
}
.skip-link:focus {
position: fixed;
top: 0;
left: 0;
z-index: 9999;
padding: 8px 16px;
background: #000;
color: #fff;
}
</style>

七、可访问性测试

工具类型说明
axe DevTools浏览器插件自动检测 WCAG 违规
Lighthouse浏览器内置Accessibility 评分
VoiceOver (macOS) / NVDA (Windows)屏幕阅读器真实体验测试
jest-axe单元测试CI 中自动检测
键盘测试手动拔掉鼠标,只用键盘操作

常见面试问题

Q1: 什么是 HTML 语义化?有什么好处?

答案

语义化是使用含义正确的 HTML 标签来表达内容结构,比如用 <nav> 表示导航而非 <div class="nav">。好处:

  1. 可访问性 — 屏幕阅读器可识别页面结构,用户可直接跳转到导航、主内容等
  2. SEO — 搜索引擎更准确理解内容层级,有助于排名
  3. 可维护性 — 代码自描述,团队协作效率更高
  4. 跨设备 — 语义结构天然适合不同设备的渲染和布局

Q2: <section><div> 有什么区别?<article><section> 呢?

答案

  • <div>无语义,纯粹的容器,用于布局和 CSS 分组
  • <section>有主题的区块,通常有标题,表示页面中的一个逻辑部分
  • <article>独立完整的内容,可以脱离上下文独立存在(如一篇博客、一条评论)

判断标准:内容能独立分享/订阅吗?能 → <article>;是页面的一个主题区块?→ <section>;纯布局需要?→ <div>

Q3: 什么是 ARIA?什么时候需要使用?

答案

ARIA(Accessible Rich Internet Applications)是一组 HTML 属性,用于给自定义组件补充语义信息。

使用原则:优先用原生 HTML(<button><input><select>),只在原生元素无法满足时才用 ARIA。比如自定义 Tab 组件需要 role="tablist"role="tab"aria-selected 等属性。

常见属性:role(角色)、aria-label(标签)、aria-expanded(展开状态)、aria-hidden(隐藏)、aria-live(动态内容播报)。

Q4: 如何让一个自定义组件(如 Dropdown)支持键盘操作?

答案

  1. 触发按钮用 <button>(原生键盘支持),设置 aria-expandedaria-haspopup
  2. 选项列表用 role="listbox",每个选项 role="option" + aria-selected
  3. 监听 onKeyDownArrowDown/ArrowUp 移动焦点,Enter 选中,Escape 关闭
  4. tabIndex 管理哪个选项可聚焦(roving tabindex 模式)
  5. 打开时焦点进入列表,关闭时焦点回到触发按钮

Q5: 什么是焦点陷阱(Focus Trap)?什么场景需要?

答案

焦点陷阱是指限制 Tab 焦点只在特定区域内循环,不会跳到背后的页面元素。

需要的场景:Modal/Dialog、全屏抽屉、确认弹窗。当这些覆盖层打开时,用户不应该 Tab 到被遮挡的内容上。

实现:拦截 Tab 事件,到最后一个元素时跳回第一个,Shift+Tab 到第一个时跳到最后一个。关闭时恢复之前的焦点。

Q6: alt 属性应该怎么写?什么时候可以为空?

答案

  • 信息性图片alt 描述图片内容:alt="2026年Q1销售数据柱状图"
  • 功能性图片(如链接内的图片) — alt 描述功能:alt="返回首页"
  • 装饰性图片alt="" 留空(不是省略 alt 属性),屏幕阅读器会跳过它
  • 复杂图表 — 简短 alt + aria-describedby 指向详细描述
<!-- 信息性 -->
<img src="chart.png" alt="2026年销售额同比增长23%" />

<!-- 功能性 -->
<a href="/"><img src="logo.png" alt="返回首页" /></a>

<!-- 装饰性 -->
<img src="divider.svg" alt="" />

Q7: aria-live 的作用是什么?politeassertive 有什么区别?

答案

aria-live 让屏幕阅读器自动播报区域内容的变化,无需用户手动导航到该区域。

  • polite — 等用户当前操作完成后再播报(如搜索结果数量更新)
  • assertive立即打断当前播报(如表单提交失败的错误消息)
  • off — 不播报(默认)

Q8: WCAG 的 AA 级别要求颜色对比度是多少?如何检测?

答案

  • 普通文字:对比度 ≥ 4.5:1
  • 大文字(≥ 18px 粗体或 ≥ 24px):对比度 ≥ 3:1
  • UI 组件和图形:对比度 ≥ 3:1

检测工具:Chrome DevTools(检查元素时显示对比度)、axe DevTools 插件、WebAIM Contrast Checker


相关链接