ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ARIA属性与WCAG合规:无障碍适配的语义契约与工程实践

ARIA属性与WCAG合规:无障碍适配的语义契约与工程实践 1. 为什么“无障碍适配”不是加几个属性就完事了我第一次在项目里被提“做无障碍适配”是在一个上线前两天的晨会。产品经理甩过来一份PDF标题是《WCAG 2.1 AA级合规自查表》里面密密麻麻列着56条检查项。我当时心里想不就是加个aria-label、设个tabindex0吗两小时搞定。结果三天没睡好。不是因为代码写不出来而是因为——无障碍不是前端工程师的“锦上添花”而是数字产品的“呼吸系统”。你给一个按钮加了aria-hiddentrue它对屏幕阅读器彻底消失你忘了给折叠菜单设置aria-expanded视障用户点开时根本不知道内容已展开你用div模拟下拉框却没处理键盘焦点流键盘党卡在第3个选项就再也按不到回车……这些都不是“样式错位”或“接口超时”那种能靠日志快速定位的问题它们像毛细血管里的微血栓——单个不致命但累积起来直接让整条交互路径窒息。更关键的是无障碍失效的代价是不对称的健全用户可能只觉得“这个下拉有点卡”而视障用户面对的是一堵完全无法穿越的墙。这不是体验降级是功能剥夺。所以这篇笔记不叫《无障碍入门指南》也不叫《5分钟学会ARIA》它叫《关于无障碍适配的笔记》——因为它是我在真实项目里用37次PR被拒、11次用户访谈录音、4轮辅助技术实测换来的碎片化思考。没有标准答案只有踩坑后留下的刻度。核心关键词其实已经浮出水面aria-hidden、aria-labelledby、aria-expanded、tabindex、WCAG。但请注意它们不是孤立的API而是一套语义契约体系——开发者用HTML和ARIA向辅助技术“声明”“这个区域是导航栏”“这个按钮控制面板展开”“这段文字是标题而非装饰”。一旦声明与实际行为不符比如aria-expandedtrue但DOM里内容仍是display: none契约即告破裂辅助技术就会给出错误反馈。提示别把ARIA当成CSS类名来“打补丁”。rolebutton不能替代button的原生语义和键盘行为aria-label覆盖不了缺失的视觉标签带来的上下文断裂。真正的无障碍始于语义化HTML成于ARIA精准补位验于真实辅助技术流。我后来整理出一个判断铁律凡是需要JS动态控制状态的交互组件必须同步管理三件事——DOM结构、CSS可见性、ARIA状态属性。缺一不可。比如一个手风琴面板DOM里要有h3 idpanel1-header和div idpanel1-content的明确关联CSS要通过max-height或height过渡实现平滑展开而非粗暴display: none/blockARIA要实时同步aria-expanded控制态和aria-controls关联态。这三者不同步就是给屏幕阅读器埋雷。而这种不同步在React/Vue等框架里尤其隐蔽——因为状态更新可能异步DOM渲染可能延迟ARIA属性更新若没绑定到正确的生命周期钩子就会出现“阅读器说已展开但内容还没渲染出来”的诡异现象。所以这篇笔记的起点不是教你怎么写aria-labelledby而是带你回到那个最朴素的问题当一个用户不碰鼠标、不看屏幕、只靠键盘和语音指令操作你的页面时他感知到的世界和你设计稿里的世界是否是同一个2.aria-hidden的“隐身术”何时该藏何时不该藏aria-hiddentrue看起来最简单设了它屏幕阅读器就忽略这个元素。但正是这种“简单”让它成了无障碍适配里最危险的属性之一。我见过太多团队把它当万能橡皮擦——弹窗蒙层加aria-hiddentrue图标加aria-hiddentrue甚至整个侧边栏都加……结果测试时发现键盘焦点卡死在蒙层上用户按Tab键永远出不去。问题出在隐藏逻辑的边界模糊。aria-hidden不是CSS的visibility: hidden它切断的是辅助技术的语义通道但不改变键盘焦点流。这意味着一个aria-hiddentrue的div如果它内部有tabindex0的按钮键盘用户依然能Tab进去但屏幕阅读器却告诉用户“这里什么都没有”。我们来拆解它的正确使用场景2.1 真正该隐藏的纯装饰性内容比如一个用CSS绘制的分隔线div classdivider/div或者背景图里的装饰性图标img srcdeco-star.svg alt /。这类内容对理解页面信息毫无价值且无交互行为。此时aria-hiddentrue是精准的——它告诉辅助技术“请跳过此节点”。但注意必须确保它真的“纯装饰”。曾有个项目把带title属性的SVG图标设为aria-hiddentrue结果测试发现鼠标悬停时title仍会显示但屏幕阅读器完全读不出含义。最后改成svg aria-label搜索use href#search-icon //svg既保留视觉提示又提供语义。2.2 绝对禁止隐藏的焦点可及的交互元素这是最高频的误用。某次重构中团队为“提升性能”给所有未展开的导航菜单项加了aria-hiddentrue。逻辑是“反正用户没点开先隐藏省资源”。结果测试员用NVDAChrome测试时键盘Tab到第一个菜单项就停住了——因为aria-hiddentrue让整个ul从辅助树中消失后续所有li自然不可达。正确做法是用display: none或visibility: hidden控制可见性用tabindex-1移除键盘焦点用aria-expanded标记状态。例如!-- 错误隐藏整个列表焦点流断裂 -- nav aria-label主菜单 button aria-expandedfalse产品/button ul aria-hiddentrue !-- ❌ 危险整个列表不可访问 -- lia href/prod1产品A/a/li lia href/prod2产品B/a/li /ul /nav!-- 正确保持DOM存在仅控制焦点和状态 -- nav aria-label主菜单 button aria-expandedfalse aria-controlsproduct-submenu 产品 /button ul idproduct-submenu classsubmenu-collapsed !-- CSS控制display:none -- tabindex-1 !-- 移除键盘焦点 -- lia href/prod1产品A/a/li lia href/prod2产品B/a/li /ul /nav关键区别在于aria-hiddentrue删除了辅助树节点而tabindex-1只是让元素不可通过Tab进入但它仍在辅助树中屏幕阅读器仍能通过方向键读取其内容当用户主动探索时。2.3 隐藏的连锁反应父容器的aria-hidden会劫持子元素这是最隐蔽的坑。aria-hidden具有继承性如果父元素设为aria-hiddentrue其所有子元素无论自身属性如何都会被辅助技术忽略。曾有个模态框组件开发者为“确保背景不可读”给body加了aria-hiddentrue。结果整个页面包括模态框本身都消失了——因为模态框是body的子元素。解决方案只有两个严格限定作用域aria-hiddentrue只加在真正需要隐藏的局部容器上如蒙层背后的main并确保模态框是同级兄弟节点动态切换打开模态框时给main加aria-hiddentrue同时给模态框加aria-modaltrue现代标准或roledialog兼容旧版。!-- 正确的模态框结构 -- body main aria-hiddentrue.../main !-- 背景内容隐藏 -- div roledialog aria-labelledbymodal-title aria-describedbymodal-desc tabindex-1 h2 idmodal-title确认删除/h2 p idmodal-desc此操作不可撤销/p button取消/button button确定/button /div /body注意aria-modaltrue在Safari中支持不佳生产环境建议用roledialog手动管理焦点。具体做法是模态框打开时将焦点捕获到第一个可聚焦元素如标题或确认按钮关闭时将焦点返回触发按钮。这需要监听keydown事件拦截Tab/ShiftTab并用focus()方法强制重定向。我总结出一条红线只要元素可能成为用户操作目标点击、键盘聚焦、屏幕阅读器探索就绝不能用aria-hiddentrue将其从辅助树中抹除。它的正确位置永远在“装饰性占位符”和“临时不可见但需保留语义的容器”之间。3.aria-labelledby与aria-describedby谁在给谁“指路”如果说aria-hidden是“隐身术”那aria-labelledby和aria-describedby就是“指路牌”——它们不改变元素本身而是为辅助技术提供语义锚点告诉阅读器“这个按钮的标签其实是远处那个h3的文字”“这段输入框的说明藏在下面那个p里”。但很多人混淆了它们的职责边界。我见过最典型的错误是把aria-labelledby当aria-label的“高级版”来用——以为多引几个ID就能让标签更丰富。结果测试时发现屏幕阅读器把所有引用文本连读成一长串中间毫无停顿用户根本分不清哪句是标签、哪句是说明。我们先看本质区别属性作用读取时机语义权重aria-labelledby定义元素的可访问名称Accessible Name等同于label for或alt的作用元素获得焦点时优先读取作为主要身份标识★★★★★决定“这是什么”aria-describedby提供元素的可访问描述Accessible Description补充上下文、操作提示或约束条件元素获得焦点后延后读取作为次要补充信息★★★☆☆解释“怎么用”举个真实案例一个带搜索图标的输入框。!-- 常见错误用aria-labelledby强行拼接 -- div classsearch-box input typetext aria-labelledbysearch-icon search-label !-- ❌ 把图标和标签ID都塞进来 -- / svg idsearch-icon aria-hiddentrue.../svg span idsearch-label搜索商品/span /divNVDA读出来是“搜索图标搜索商品 编辑”。用户一头雾水搜索图标是什么是按钮还是装饰正确解法是职责分离!-- 正确aria-labelledby只指代核心标签aria-describedby补充操作方式 -- div classsearch-box input typetext aria-labelledbysearch-label !-- ✅ 核心名称搜索商品 -- aria-describedbysearch-hint !-- ✅ 补充描述按回车开始搜索 -- / span idsearch-label搜索商品/span span idsearch-hint按回车键开始搜索/span /div这样NVDA读出来是“搜索商品 编辑按回车键开始搜索”。清晰、有层次。3.1aria-labelledby的进阶陷阱ID引用的“可见性悖论”aria-labelledby要求引用的ID对应元素必须存在于DOM中且非aria-hiddentrue。但很多团队为了“视觉简洁”把标签文字用CSS隐藏position: absolute; left: -9999px却忘了这些文字对辅助技术仍是可见的——结果aria-labelledby成功引用但屏幕阅读器读出来的却是“搜索商品 搜索商品 搜索商品”因为多个隐藏元素都含相同文本。更糟的是当引用的元素本身是aria-hiddentrue时整个aria-labelledby链就断了。曾有个项目把面包屑导航放在nav aria-hiddentrue里然后用aria-labelledby指向其中某个li结果所有依赖它的按钮都变成“无名称”。解决方案很朴素所有被aria-labelledby引用的元素必须保证视觉可见性与辅助技术可见性一致。如果真需要视觉隐藏用clip-path: inset(50%)或clip: rect(0 0 0 0)这类对辅助技术友好的隐藏方式它们不影响语义暴露。3.2 多ID引用的语法细节空格分隔顺序即读取顺序aria-labelledbyid1 id2 id3中的ID用空格分隔且阅读器严格按此顺序朗读。这给了我们精细控制语义流的能力。比如一个带状态指示的开关按钮button aria-labelledbyswitch-label switch-status aria-checkedfalse span idswitch-label夜间模式/span span idswitch-status已关闭/span /button读出来是“夜间模式 已关闭”用户立刻知道当前状态。如果把顺序颠倒button aria-labelledbyswitch-status switch-label.../button读出来是“已关闭 夜间模式”语义混乱。再比如表单验证错误提示input typeemail aria-labelledbyemail-label email-error aria-invalidtrue / label idemail-label邮箱地址/label p idemail-error classerror请输入有效的邮箱格式/p读出来是“邮箱地址 请输入有效的邮箱格式”用户一次获取完整上下文无需再手动探索错误区域。提示aria-labelledby和aria-describedby都支持跨DOM层级引用ID可以指向任何位置的元素包括template里的内容。但务必避免循环引用A引用BB又引用A这会导致辅助技术无限递归崩溃。4.aria-expanded与tabindex动态组件的“心跳监测仪”如果把aria-hidden比作隐身术aria-labelledby比作指路牌那aria-expanded和tabindex就是动态交互组件的生命体征监测仪——它们实时反映组件的“开合状态”和“可交互资格”让辅助技术能同步用户的操作意图。我曾经负责重构一个电商网站的商品筛选面板。原版用jQuery实现点击“价格区间”标题下方输入框展开。开发同学很自信“加了aria-expanded肯定没问题。”结果测试时屏幕阅读器用户说“我点了标题听到‘已展开’但按Tab键找不到输入框。”问题出在tabindex的缺失。aria-expandedtrue只告诉阅读器“内容已展开”但不自动让内容获得键盘焦点。用户仍需手动按ShiftTab或方向键去探索体验断层。4.1aria-expanded状态声明必须与DOM真实状态100%同步aria-expanded的值只能是true或false字符串它描述的是关联内容区域的可见性状态。关键点在于“关联”——必须通过aria-controls明确指定受控元素ID。错误示范!-- ❌ 无aria-controls阅读器不知“展开”指什么 -- h3 aria-expandedtrue价格区间/h3 div classprice-range.../div正确写法!-- ✅ 明确关联状态与DOM同步 -- h3 aria-expandedtrue aria-controlsprice-range-panel rolebutton !-- 告诉阅读器这是可点击区域 -- 价格区间 /h3 div idprice-range-panel classexpanded input typenumber placeholder最低价 / input typenumber placeholder最高价 / /div但同步不是写死的。当用户点击标题时JS必须原子化更新三处切换aria-expanded值切换aria-controls指向如果有多组内容切换DOM的class或style如display: block→display: none。漏掉任何一步契约即失效。我们曾因CSS动画导致display: none延迟应用造成aria-expandedtrue但内容尚未渲染阅读器读到“已展开”却探索不到内容用户以为功能故障。4.2tabindex键盘焦点的“交通管制员”tabindex有三个取值各自承担不同角色tabindex0元素可被Tab键聚焦且按DOM顺序加入焦点流。适用于div模拟的按钮、卡片等原生不可聚焦元素。tabindex-1元素不可被Tab键聚焦但可通过JS的focus()方法获得焦点。适用于模态框、下拉菜单等需要程序化控制焦点的场景。tabindexNN0强制调整焦点顺序。这是反模式会破坏用户预期导致键盘用户迷失。WCAG明确反对。最常见的误用是给div加tabindex0却不处理键盘事件。比如一个用div classcard实现的商品卡片加了tabindex0但没监听Enter/Space键用户按了回车毫无反应——这比不加tabindex更糟糕因为它给了用户“可交互”的错误承诺。正确姿势是tabindex0必须配套完整的键盘交互逻辑// 商品卡片的键盘支持 const card document.querySelector(.card); card.tabIndex 0; card.addEventListener(keydown, (e) { if (e.key Enter || e.key ) { e.preventDefault(); // 阻止空格滚动页面 window.location.href card.dataset.url; // 跳转详情页 } });更关键的是焦点管理策略。对于手风琴、下拉菜单这类嵌套结构必须遵循WCAG的“焦点环”原则用户用Tab进入标题时焦点落在标题上按Down Arrow键焦点应直接跳入内容区第一个可聚焦元素如输入框按Up Arrow键焦点应跳回标题内容区按Tab键焦点在内部元素间流转按Esc键焦点应回到标题并收起内容。这需要监听keydown事件并手动focus()而不是依赖浏览器默认行为。4.3aria-expanded与tabindex的协同作战以折叠面板为例我们用一个真实折叠面板代码片段展示二者如何配合!-- HTML结构 -- section classaccordion h3 idfaq1-header rolebutton aria-expandedfalse aria-controlsfaq1-content tabindex0 如何修改收货地址 /h3 div idfaq1-content classaccordion-content collapsed aria-hiddentrue p您可以在“我的账户” “地址管理”中编辑.../p button立即前往/button /div /section// JS逻辑精简 const header document.getElementById(faq1-header); const content document.getElementById(faq1-content); header.addEventListener(click, togglePanel); header.addEventListener(keydown, (e) { if (e.key Enter || e.key ) { e.preventDefault(); togglePanel(); } }); function togglePanel() { const isExpanded header.getAttribute(aria-expanded) true; // 1. 同步ARIA状态 header.setAttribute(aria-expanded, !isExpanded); content.setAttribute(aria-hidden, isExpanded); // 2. 同步DOM可见性CSS控制 content.classList.toggle(collapsed, isExpanded); // 3. 焦点管理展开时将焦点移入内容区第一个可聚焦元素 if (!isExpanded) { const firstFocusable content.querySelector(button, [href], input, select, textarea, [tabindex]); if (firstFocusable) { firstFocusable.focus(); } } }这个例子体现了无障碍的底层逻辑状态声明ARIA、视觉反馈CSS、焦点控制JS必须三位一体。少任何一个环节对辅助技术用户来说就是一次认知断层。注意aria-expanded只适用于有明确“展开/收起”状态的组件如手风琴、下拉菜单、折叠面板。不要用在普通链接或按钮上。曾有个项目给所有a标签加aria-expandedfalse结果阅读器每读一个链接都说“已关闭”用户彻底困惑。5. WCAG合规不是终点而是用户旅程的起点很多人把WCAGWeb Content Accessibility Guidelines当成一份待勾选的清单“满足AA级就算过关”。我在三次无障碍审计中发现这种心态恰恰是最大的障碍。WCAG 2.1 AA级共13条成功标准但真正决定用户体验的是这13条背后的人类行为逻辑。比如WCAG 2.4.3焦点可见性要求“任何用户界面组件获得焦点时焦点指示必须至少满足最小对比度”。技术上加个outline: 2px solid #0066cc就能过审。但真实场景中我看到用户用JAWS读到一个蓝色焦点框却因为页面背景也是深蓝根本看不到焦点在哪——技术达标了体验崩塌了。所以合规检查必须回归到用户任务流。我给自己定了一套“三问法”每次改完无障碍代码必问5.1 问任务用户想完成什么路径是否完整以“注册新账号”流程为例视觉用户看表单标题 → 填写邮箱 → 输入密码 → 点击注册按钮屏幕阅读器用户听“注册新账号” → 听“邮箱地址 输入框” → 听“密码 输入框” → 听“注册按钮” → 按回车。如果中间某个字段缺少aria-labelledby或按钮没有aria-label用户就卡在“这是什么”的疑问里。我们曾发现一个密码强度提示用div实现既无role也无ARIA阅读器直接跳过用户输完密码却不知是否符合要求。解决方案不是堆属性而是按用户心智模型重构DOM顺序。把强度提示放在密码输入框之后、确认密码之前并用aria-describedby关联input typepassword idpwd aria-describedbypwd-strength / div idpwd-strength classstrength-meter span强度中等/span /div input typepassword idpwd-confirm /这样阅读器会自然读出“密码 输入框强度中等”。5.2 问工具用户用什么辅助技术行为是否一致NVDAWindows、VoiceOvermacOS/iOS、TalkBackAndroid对ARIA的支持程度不同。比如aria-modaltrue在旧版Safari中被忽略导致模态框背景仍可聚焦aria-currentpage在某些版本JAWS中不朗读。我们的应对策略是分层降级第一层用原生语义button、nav、main保证基础可用第二层用ARIA 1.1标准属性aria-expanded、aria-labelledby覆盖主流场景第三层对关键交互如模态框用JS检测辅助技术类型动态注入兼容方案如为Safari手动管理焦点流。工具检测代码示例// 检测是否在VoiceOver中 function isVoiceOver() { return /Mac OS X.*Version\/\d\.\d.*Safari/.test(navigator.userAgent) speechSynthesis in window; } // 检测是否在NVDA中 function isNVDA() { return typeof window ! undefined window.navigator.userAgent.indexOf(NVDA) ! -1; }5.3 问环境用户在什么场景下使用干扰是否最小化无障碍不是真空环境测试。真实用户可能在地铁上用TalkBack周围嘈杂需要更长的语音停顿用眼动仪操作需要更大的点击热区有认知障碍需要更简明的错误提示。这就要求我们超越WCAG的“技术合规”进入“情境设计”。比如表单错误提示WCAG要求错误信息要与输入框关联aria-describedby情境设计要求错误信息要用主动语态具体操作而非被动描述。❌ “邮箱格式不正确”✅ “请输入包含符号的有效邮箱例如nameexample.com”后者直接告诉用户“哪里错了”和“怎么改”减少认知负荷。再比如加载状态。WCAG 4.1.3状态更改要求“当状态更改对用户重要时应以可访问的方式通知用户”。技术上用aria-livepolite即可。但情境中如果用户正在用键盘填写表单突然弹出“加载中…”的语音提示会打断输入节奏。更好的做法是只在状态真正影响用户操作时才播报比如提交按钮变灰后才用aria-liveassertive播报“正在提交请稍候”。最后分享一个血泪教训我们曾为一个数据看板添加aria-live区域监控图表刷新。结果当用户用NVDA浏览左侧菜单时右侧图表每秒刷新一次NVDA就每秒播报一次“图表已更新”用户根本没法操作。解决方案是aria-live区域必须与用户当前焦点区域隔离且只在用户明确触发刷新如点击“刷新”按钮后才激活。所以WCAG不是终点线而是标尺。它丈量的是技术底线而真正的无障碍是让用户在任何设备、任何环境、任何能力状态下都能以自己习惯的方式完成想做的事——不被技术阻挡不被设计忽视不被假设排除。6. 我的无障碍工作流从需求评审到上线验证写了这么多原理和陷阱你可能想知道在真实项目里到底该怎么落地没有银弹但我打磨出一套可复用的工作流贯穿需求、开发、测试全周期。它不追求“一次性完美”而是把无障碍变成可测量、可迭代、可沉淀的日常实践。6.1 需求阶段把“无障碍”写进用户故事很多团队的无障碍问题根源在需求文档里就埋下了。产品经理写“用户点击筛选按钮展开价格区间面板”但没写“视障用户应能通过键盘操作此面板并获知当前展开状态”。我的做法是在每个交互型用户故事后追加一条‘无障碍验收标准’。例如用户故事作为买家我希望能按价格区间筛选商品以便快速找到预算内商品。无障碍验收标准筛选标题必须是button或带rolebutton的元素支持键盘聚焦点击/回车后标题的aria-expanded属性实时切换展开的内容区必须有aria-hidden同步控制内容区第一个输入框在展开后自动获得焦点整个面板需通过Tab键在标题与内容间流畅切换。这些标准会进入研发任务卡成为CRCode Review的必检项。技术负责人不再问“要不要做无障碍”而是问“这条标准是否满足”。6.2 开发阶段建立“无障碍检查清单”A11y Checklist我维护一份团队共享的Markdown清单按组件类型分类每项标注“必做”“建议”“禁用”。例如“下拉选择器”部分检查项类型说明示例必须用select原生标签必做除非有强定制需求否则禁用div模拟selectoption北京/option/select自定义下拉需设rolelistbox必做替代select时的语义补位div rolelistbox选项必须设roleoption必做并用aria-selected标记选中态div roleoption aria-selectedtrue北京/div禁用aria-hiddentrue在选项上禁用会导致选项完全不可访问❌ div aria-hiddentrue北京/div这份清单不是文档而是开发时打开的IDE侧边栏。每次写新组件先扫一遍对应条目。它把抽象原则转化成具体动作降低认知成本。6.3 测试阶段三步验证法自动化人工真实用户自动化工具如axe-core、Lighthouse能抓80%的明显问题缺失alt、tabindex错误但抓不住逻辑错误。所以我坚持三步走第一步自动化扫描CI集成在GitLab CI中加入axe-core扫描PR提交时自动运行。失败则阻断合并。配置重点检查aria-*属性拼写与值合法性tabindex大于0的元素aria-hiddentrue的父容器下是否有可聚焦子元素。第二步人工辅助技术遍历每周固定2小时我和两位同事轮流用以下组合实测Windows NVDA Chrome主力组合覆盖最多用户macOS VoiceOver Safari检验苹果生态兼容性Android TalkBack Chrome移动场景。重点测试核心用户旅程首页→搜索→商品列表→详情页→加入购物车→结算。记录所有“卡点”比如“在商品列表页按Tab键第7次后焦点丢失”。第三步真实用户测试每季度1次与本地盲人协会合作邀请2-3位视障用户远程参与。给他们一个真实任务如“用手机下单一件T恤”我们录屏观察操作过程不干预只记录。曾有用户反复点击一个图标却无反应我们才发现那个图标是img标签但alt属性为空且没加rolebutton——自动化工具不会报错人工测试容易忽略只有真实用户会暴露。6.4 上线后建立“无障碍健康分”看板技术债会累积无障碍问题同样。我推动团队在Datadog中建了一个“无障碍健康分”看板指标包括axe-scan-fail-rate自动化扫描失败率目标5%a11y-bug-count近30天无障碍相关Bug数目标0user-complaint-rate客服工单中提及“读不出来”“点不了”等关键词的比例目标0.1%。这个看板每天更新出现在晨会大屏上。它让无障碍从“看不见的付出”变成“可量化、可追踪、可改进”的工程指标。最后说一句掏心窝的话无障碍适配不是给代码“贴金”而是给用户“铺路”。当你写的每一行HTML、每一个ARIA属性都在为某个看不见屏幕、听不见提示、握不住鼠标的用户多争取一厘米的行动自由时那种职业成就感远胜于任何技术炫技。这条路没有终点但每修复一个aria-expanded不同步的bug每优化一次键盘焦点流你都在让数字世界离“人人可用”更近一点。
返回列表