6.2 无障碍访问 无障碍(a11y)不是加分项而是合规底线:视障用户靠读屏软件与键盘操作你的页面,爬虫与语音助手读的是同一套语义。Bootstrap 在组件层已内置大量 aria 支撑,但语义选择、焦点管理、键盘路径、色彩对比四件事必须开发者自己判。本节立可考核的判例,每条都有验证方法。 位置:进阶四案第二案,也是 6.1 留下的伏笔(弹窗焦点、图标语义)的清算处。它回收前几章散落的 aria 属性,把「写了但不知道为什么」变成「知道在为谁写」。 判例一:语义优先,aria 补位 裁决口径:原生元素(button、a、nav、main、table)免费自带语义与键盘行为;div 加 aria 是付费模仿,且永远模仿不全。
无障碍(a11y)不是加分项而是合规底线:视障用户靠读屏软件与键盘操作你的页面,爬虫与语音助手读的是同一套语义。Bootstrap 在组件层已内置大量 aria 支撑,但语义选择、焦点管理、键盘路径、色彩对比四件事必须开发者自己判。本节立可考核的判例,每条都有验证方法。
位置:进阶四案第二案,也是 6.1 留下的伏笔(弹窗焦点、图标语义)的清算处。它回收前几章散落的 aria 属性,把「写了但不知道为什么」变成「知道在为谁写」。
<!-- 反例:什么都能 div,读屏只知道「有一堆块」 --> <div class="btn btn-primary" onclick="submit()">提交</div> <!-- 正例:语义标签优先,读屏直接念「按钮,提交」 --> <button class="btn btn-primary" type="submit">提交</button> <!-- aria 只在语义不够时补位:这个「按钮」其实是链接 --> <a href="/orders" class="btn btn-outline-primary">查看订单</a> <!-- 链接跳转语义正确,无需补位;若做成伪按钮(div onclick), 则必须补 role="button" 与 tabindex="0",且键盘事件全部自己写——成本立刻翻倍 -->
裁决口径:原生元素(button、a、nav、main、table)免费自带语义与键盘行为;div 加 aria 是付费模仿,且永远模仿不全。页面骨架先判地标:一个页面一个 main,导航包 nav 加 aria-label 区分(主导航与页脚导航各一个),页头页脚用 header、footer。
<!-- 装饰图标:对读屏隐藏,语义由文字承担 --> <button class="btn btn-outline-secondary" aria-label="搜索"> <i class="bi bi-search" aria-hidden="true"></i> </button> <!-- 状态传达:不能只靠颜色,加文字或图标双通道 --> <td> <span class="badge text-bg-danger">已逾期</span> <!-- 反例:只放一个红点 span,色弱用户与读屏都拿不到信息 --> </td> <!-- 动态更新的区域:aria-live 让读屏主动播报 --> <div aria-live="polite" class="mt-2 text-body-secondary"> <!-- 表单提交结果写进这里,读屏会自动念出,无需焦点跳转 --> </div>
aria-hidden 与 aria-live 是两个高频补位:前者把纯视觉元素移出读屏视野,后者把「视觉上顺其自然出现」的文本变成「听觉上主动播报」的文本。
<div class="modal fade" id="box" tabindex="-1" aria-labelledby="boxTitle" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="boxTitle">确认操作</h5> <!-- 框架契约一:打开时焦点自动移入弹窗;关闭后还给触发按钮 --> <!-- 框架契约二:Tab 循环被限制在弹窗内,不会跑到底层页面 --> <button class="btn-close" data-bs-dismiss="modal" aria-label="关闭"></button> </div> <div class="modal-body">…</div> </div> </div> </div>
modal 组件内置的焦点契约是 Bootstrap 无障碍投入最重的一处:打开时焦点入框(tabindex="-1" 让弹窗容器可编程聚焦)、Tab 与 Esc 被组件接管、关闭后焦点归还。开发者的义务是别破坏契约——不要给弹窗内容容器加 display:none 技巧、不要移除 tabindex="-1"、标题必须挂 aria-labelledby 指向的 id。
<!-- 跳转链接:键盘用户的头班车,放在 body 第一个元素 --> <a class="visually-hidden-focusable" href="#main">跳到主要内容</a> <main id="main"> <!-- 获得焦点前不可见,Tab 第一击显形——这是官方提供的现成类 --> </main> <!-- 自定义交互组件的键盘三问:Tab 能进吗?Enter/空格能触发吗?Esc 能退吗? 下拉菜单组件已内置箭头键与 Esc;自己拼的组件要自己补 --> <div class="dropdown"> <button class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">操作</button> <!-- 框架契约:点击后 aria-expanded 自动翻转为 true,读屏因此知道菜单开了 --> <ul class="dropdown-menu"> <li><a class="dropdown-item" href="#">复制</a></li> <li><a class="dropdown-item" href="#">移动</a></li> </ul> </div>
键盘自查有一套固定动作:拔掉鼠标(或用读屏模拟器),Tab 走完全页——每个可交互元素都要能到达、能触发、能退出;出现「焦点丢失」(Tab 突然跳回地址栏)通常意味着焦点跑进了被 display:none 隐藏的元素。
/* 正文对比至少 4.5 比 1,大字号(18.66px 加粗或 24px 以上)放宽到 3 比 1 */ /* Bootstrap 默认组合已达标,事故出在自定义覆盖: */ /* 反例:text-white 搭 bg-warning(黄底白字,对比约 1.9 比 1,不达标) */ /* 正例:黄底应配深字 */ .badge-on-warning { background: var(--bs-warning); color: #4d3800; } /* 约 7 比 1 */ /* 工具族里的安全牌:* -emphasis 系强调色都按浅底深字校过对比度 */
4.2 节的「同族三档」(subtle 底配 emphasis 字)就是为这条底线设计的;自定义配色时用开发工具里的对比度检查器复核,正文 4.5、大字与图形边 3.0,两个数字背下来。
⚠️ 常见坑:
aria-hidden="true"的元素里放了可聚焦元素(链接、按钮)——读屏不念它,键盘却还能 Tab 进去,出现「神秘焦点」。修法:隐藏整块交互区时配合inert属性或把焦点元素一并disabled。
背景:评论区上线后收到读屏用户反馈:「发布成功没提示」「点赞按钮念不出来」。键盘巡检又发现:评论很长时 Tab 要按二十多次才能越过楼中楼。
操作:结果提示区挂 aria-live="polite";点赞按钮原来是图标裸 span,改 button 加 aria-label(「点赞,当前 12」把计数也念出来);长评论区在楼中楼外包 details 折叠(原生语义、免脚本),Tab 一击直达「展开回复」;发布按钮异步期间 aria-busy="true"。
结果:读屏用户能听到发布结果与点赞状态;键盘用户过一层楼中楼从二十多击降到一击。
解读:四项整改没有一行「无障碍专用库」,全是原生语义加少量 aria——无障碍工程的常态正是如此:九成靠选对标签,一成靠补 aria。
变式:若评论区做成单页实时流(新评论不断插入),补两判:插入容器挂 aria-live="off" 避免读屏被刷屏,仅在用户主动点「查看新评论」时播报;插入内容保持 DOM 顺序与视觉顺序一致,颠倒会出现「念到的顺序和看到的顺序不同」。
下一节谈性能:预算怎么立、体积怎么裁。