ARTICLE DETAIL

资讯详情

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

React 组件无障碍(a11y)模式实战指南:ARIA、键盘导航与焦点管理全解析

React 组件无障碍(a11y)模式实战指南:ARIA、键盘导航与焦点管理全解析 React 组件无障碍a11y模式实战指南ARIA、键盘导航与焦点管理全解析【免费下载链接】agentsMulti-harness agentic plugin marketplace for Claude Code, Codex, Cursor, OpenCode, GitHub Copilot, and Google Antigravity项目地址: https://gitcode.com/GitHub_Trending/agents24/agents本指南以ui-design插件中 web-component-design 技能 的 无障碍模式参考文档 为核心系统讲解在 React 组件库中落地 WCAG 无障碍所需的六大核心能力ARIA 语义、焦点管理、键盘导航、表单可访问性、动态内容播报与颜色对比度校验。读完本文你将获得一套可以直接复制到项目中的可访问组件实现方案并能用仓库提供的 无障碍审计命令 验证成果。一、无障碍先行为什么组件库必须内置 a11y在 web-component-design 技能文档的最佳实践中Accessible by Default默认无障碍被列为组件设计的第一原则同时指出常见问题之一是 Accessibility Gaps无障碍缺口。也就是说可访问性不能靠事后修补而应从组件 API 设计阶段就内建 ARIA 属性与键盘支持。仓库中的 accessibility-expert Agent 将这项工作归纳为三个维度ARIA 实现为自定义组件补充角色role、状态state与属性property键盘导航与焦点管理Tab 顺序、焦点陷阱focus trap、跳转链接、roving tabindex颜色与视觉无障碍WCAG AA4.5:1与 AAA7:1对比度、非颜色信息传达、焦点可见性。下面的章节逐一给出这些能力的可运行代码模式。二、ARIA 模式四个高频组件的完整实现无障碍模式参考文档 提供了四个核心组件的完整实现模态对话框、下拉菜单、组合框自动完成与表单校验。它们共同覆盖了 ARIA 的三大核心角色族dialog、menu、combobox。2.1 模态对话框Modal Dialogroledialog 焦点陷阱模态对话框是无障碍组件中最容易出错的类型因为它同时涉及三个问题焦点必须被困在对话框内、关闭后焦点必须归还给触发元素、背景内容不能被屏幕阅读器访问。import { useEffect, useRef, type ReactNode } from react; import { createPortal } from react-dom; interface ModalProps { isOpen: boolean; onClose: () void; title: string; children: ReactNode; } export function Modal({ isOpen, onClose, title, children }: ModalProps) { const dialogRef useRefHTMLDivElement(null); const previousActiveElement useRefElement | null(null); useEffect(() { if (isOpen) { previousActiveElement.current document.activeElement; dialogRef.current?.focus(); document.body.style.overflow hidden; } else { document.body.style.overflow ; (previousActiveElement.current as HTMLElement)?.focus(); } return () { document.body.style.overflow ; }; }, [isOpen]); useEffect(() { const handleKeyDown (e: KeyboardEvent) { if (e.key Escape) onClose(); if (e.key Tab) trapFocus(e, dialogRef.current); }; if (isOpen) { document.addEventListener(keydown, handleKeyDown); } return () document.removeEventListener(keydown, handleKeyDown); }, [isOpen, onClose]); if (!isOpen) return null; return createPortal( div classNamefixed inset-0 z-50 flex items-center justify-center aria-hidden{!isOpen} {/* Backdrop */} div classNameabsolute inset-0 bg-black/50 onClick{onClose} aria-hiddentrue / {/* Dialog */} div ref{dialogRef} roledialog aria-modaltrue aria-labelledbymodal-title tabIndex{-1} classNamerelative z-10 w-full max-w-md rounded-lg bg-white p-6 shadow-xl h2 idmodal-title classNametext-lg font-semibold {title} /h2 button onClick{onClose} aria-labelClose dialog classNameabsolute right-4 top-4 p-1 XIcon aria-hiddentrue / /button div classNamemt-4{children}/div /div /div, document.body, ); } function trapFocus(e: KeyboardEvent, container: HTMLElement | null) { if (!container) return; const focusableElements container.querySelectorAllHTMLElement( button, [href], input, select, textarea, [tabindex]:not([tabindex-1]), ); const firstElement focusableElements[0]; const lastElement focusableElements[focusableElements.length - 1]; if (e.shiftKey document.activeElement firstElement) { e.preventDefault(); lastElement.focus(); } else if (!e.shiftKey document.activeElement lastElement) { e.preventDefault(); firstElement.focus(); } }该实现中的关键点roledialogaria-modaltrue向辅助技术声明这是一个模态对话框背景内容不可交互aria-labelledbymodal-title将h2标题关联为对话框的可访问名称tabIndex{-1}使对话框容器可以接收程序化焦点.focus()从而在打开瞬间将焦点移入打开前保存document.activeElement关闭后归还焦点这是 WCAG 2.1.2「无键盘陷阱」与焦点顺序2.4.3的落地trapFocus循环当焦点到达第一个/最后一个可聚焦元素时按 Tab / ShiftTab 反向循环防止焦点逃逸到背景页面createPortal(..., document.body)将对话框渲染到document.body避免被祖先容器的overflow或z-index上下文裁剪。这个焦点循环选择器button, [href], input, select, textarea, [tabindex]:not([tabindex-1])与 web-component-design 中useFocusTrap的实现完全一致可复用到任何需要陷阱的组件。仓库中 aria-patterns.md 还给出了rolealertdialog的变体用于需要强制用户确认的危险操作。2.2 下拉菜单Dropdown Menuaria-haspopup 完整方向键协议菜单组件遵循 WAI-ARIA Authoring Practices 的 menu 模式触发按钮声明aria-haspopupmenu与aria-expanded菜单容器使用rolemenu每一项使用rolemenuitem并通过方向键实现 roving focus焦点漫游。import { useState, useRef, useEffect, type ReactNode } from react; interface DropdownProps { trigger: ReactNode; children: ReactNode; label: string; } export function Dropdown({ trigger, children, label }: DropdownProps) { const [isOpen, setIsOpen] useState(false); const containerRef useRefHTMLDivElement(null); const menuRef useRefHTMLDivElement(null); const triggerRef useRefHTMLButtonElement(null); useEffect(() { const handleClickOutside (e: MouseEvent) { if ( containerRef.current !containerRef.current.contains(e.target as Node) ) { setIsOpen(false); } }; document.addEventListener(mousedown, handleClickOutside); return () document.removeEventListener(mousedown, handleClickOutside); }, []); const handleKeyDown (e: React.KeyboardEvent) { switch (e.key) { case Escape: setIsOpen(false); triggerRef.current?.focus(); break; case ArrowDown: e.preventDefault(); if (!isOpen) { setIsOpen(true); } else { focusNextItem(menuRef.current, 1); } break; case ArrowUp: e.preventDefault(); if (isOpen) { focusNextItem(menuRef.current, -1); } break; case Home: e.preventDefault(); focusFirstItem(menuRef.current); break; case End: e.preventDefault(); focusLastItem(menuRef.current); break; } }; return ( div ref{containerRef} classNamerelative onKeyDown{handleKeyDown} button ref{triggerRef} aria-haspopupmenu aria-expanded{isOpen} aria-label{label} onClick{() setIsOpen(!isOpen)} classNameflex items-center gap-2 px-3 py-2 {trigger} ChevronDownIcon aria-hiddentrue className{transition-transform ${isOpen ? rotate-180 : }} / /button {isOpen ( div ref{menuRef} rolemenu aria-orientationvertical classNameabsolute left-0 mt-1 min-w-48 rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5 {children} /div )} /div ); } interface MenuItemProps { children: ReactNode; onClick?: () void; disabled?: boolean; } export function MenuItem({ children, onClick, disabled }: MenuItemProps) { return ( button rolemenuitem disabled{disabled} onClick{onClick} classNamew-full px-4 py-2 text-left text-sm hover:bg-gray-100 disabled:opacity-50 tabIndex{-1} {children} /button ); } function focusNextItem(menu: HTMLElement | null, direction: 1 | -1) { if (!menu) return; const items menu.querySelectorAllHTMLElement( [rolemenuitem]:not([disabled]), ); const currentIndex Array.from(items).indexOf( document.activeElement as HTMLElement, ); const nextIndex (currentIndex direction items.length) % items.length; items[nextIndex]?.focus(); } function focusFirstItem(menu: HTMLElement | null) { menu ?.querySelectorHTMLElement([rolemenuitem]:not([disabled])) ?.focus(); } function focusLastItem(menu: HTMLElement | null) { const items menu?.querySelectorAllHTMLElement( [rolemenuitem]:not([disabled]), ); items?.[items.length - 1]?.focus(); }这里需要注意两个细节tabIndex{-1}放在每个menuitem上这正是 roving tabindex 模式——整个菜单在 Tab 序列中只占一个位置触发按钮菜单项只能通过方向键在内部移动焦点。方向键按(currentIndex direction items.length) % items.length取模实现首尾循环Home/End跳转到首/末项Escape关闭并把焦点还给触发按钮这是菜单键盘协议对应 ARIA APG的标准行为也符合 WCAG 2.1.1 键盘可操作与 2.4.7 焦点可见的要求。2.3 组合框Combobox / Autocompletearia-activedescendant联动组合框是最复杂的 ARIA 模式之一需要把输入框与列表联动起来。参考文档的实现使用了aria-activedescendant方案焦点始终停留在输入框上通过aria-activedescendant指向当前高亮的选项从而避免在列表项上做真实的 DOM 焦点切换。import { useState, useRef, useId, type ChangeEvent, type KeyboardEvent, } from react; interface Option { value: string; label: string; } interface ComboboxProps { options: Option[]; value: string; onChange: (value: string) void; label: string; placeholder?: string; } export function Combobox({ options, value, onChange, label, placeholder, }: ComboboxProps) { const [isOpen, setIsOpen] useState(false); const [inputValue, setInputValue] useState(); const [activeIndex, setActiveIndex] useState(-1); const inputRef useRefHTMLInputElement(null); const listboxRef useRefHTMLUListElement(null); const inputId useId(); const listboxId useId(); const filteredOptions options.filter((option) option.label.toLowerCase().includes(inputValue.toLowerCase()), ); const handleInputChange (e: ChangeEventHTMLInputElement) { setInputValue(e.target.value); setIsOpen(true); setActiveIndex(-1); }; const handleSelect (option: Option) { onChange(option.value); setInputValue(option.label); setIsOpen(false); inputRef.current?.focus(); }; const handleKeyDown (e: KeyboardEvent) { switch (e.key) { case ArrowDown: e.preventDefault(); if (!isOpen) { setIsOpen(true); } else { setActiveIndex((prev) prev filteredOptions.length - 1 ? prev 1 : prev, ); } break; case ArrowUp: e.preventDefault(); setActiveIndex((prev) (prev 0 ? prev - 1 : prev)); break; case Enter: e.preventDefault(); if (activeIndex 0 filteredOptions[activeIndex]) { handleSelect(filteredOptions[activeIndex]); } break; case Escape: setIsOpen(false); break; } }; return ( div classNamerelative label htmlFor{inputId} classNameblock text-sm font-medium mb-1 {label} /label input ref{inputRef} id{inputId} typetext rolecombobox aria-expanded{isOpen} aria-autocompletelist aria-controls{listboxId} aria-activedescendant{ activeIndex 0 ? option-${activeIndex} : undefined } value{inputValue} placeholder{placeholder} onChange{handleInputChange} onKeyDown{handleKeyDown} onFocus{() setIsOpen(true)} onBlur{() setTimeout(() setIsOpen(false), 200)} classNamew-full rounded-md border px-3 py-2 / {isOpen filteredOptions.length 0 ( ul ref{listboxRef} id{listboxId} rolelistbox aria-label{label} classNameabsolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5 {filteredOptions.map((option, index) ( li key{option.value} id{option-${index}} roleoption aria-selected{activeIndex index} onClick{() handleSelect(option)} className{cursor-pointer px-3 py-2 ${ activeIndex index ? bg-blue-100 : hover:bg-gray-100 } ${value option.value ? font-medium : }} {option.label} /li ))} /ul )} {isOpen filteredOptions.length 0 ( div classNameabsolute z-10 mt-1 w-full rounded-md bg-white px-3 py-2 shadow-lg No results found /div )} /div ); }本实现完整覆盖了 combobox 模式的命名关系属性值作用rolecombobox输入框声明组合框角色aria-expandedisOpen告知列表展开状态aria-autocompletelist固定声明自动完成类型为列表建议aria-controls{listboxId}列表 id建立输入框与列表的控制关系aria-activedescendantoption-{index}指向当前高亮选项 id替代真实焦点移动roleoptionaria-selected每个列表项声明选项角色与选中状态两个工程细节值得注意其一useId()生成inputId与listboxId避免硬编码 id 在 SSR 或多实例场景下冲突其二onBlur中用setTimeout(..., 200)延迟关闭给鼠标点击选项留下触发onClick的时间窗口这是点击外部关闭与点击列表项选择两者不冲突的经典处理。2.4 表单校验Form Validationaria-invalidaria-describedbyrolealert表单的可访问性核心是每个输入都必须有标签Label、错误必须与输入建立关联、错误出现时必须被屏幕阅读器播报。参考文档通过一个FormField渲染属性组件统一封装了这三件事。import { useId, type FormEvent } from react; interface FormFieldProps { label: string; error?: string; required?: boolean; children: (props: { id: string; aria-describedby: string | undefined; aria-invalid: boolean; }) ReactNode; } export function FormField({ label, error, required, children, }: FormFieldProps) { const id useId(); const errorId ${id}-error; return ( div classNamespace-y-1 label htmlFor{id} classNameblock text-sm font-medium {label} {required ( span aria-hiddentrue classNameml-1 text-red-500 * /span )} /label {children({ id, aria-describedby: error ? errorId : undefined, aria-invalid: !!error, })} {error ( p id{errorId} rolealert classNametext-sm text-red-600 {error} /p )} /div ); } // Usage function ContactForm() { const [errors, setErrors] useStateRecordstring, string({}); const handleSubmit (e: FormEvent) { e.preventDefault(); // Validation logic... }; return ( form onSubmit{handleSubmit} noValidate FormField labelEmail error{errors.email} required {(props) ( input {...props} typeemail required className{w-full rounded border px-3 py-2 ${ props[aria-invalid] ? border-red-500 : border-gray-300 }} / )} /FormField button typesubmit classNamemt-4 px-4 py-2 bg-blue-600 text-white rounded Submit /button /form ); }实现要点useId()生成 id 并把 label 关联到输入框htmlFor{id}满足 WCAG 1.3.1 信息与关系、3.3.2 标签或说明aria-invalid{!!error}告知辅助技术该输入当前无效对应 3.3.1 错误标识aria-describedby{errorId}把错误文案与输入框建立描述关系屏幕阅读器聚焦输入框时会读出错误错误文案使用rolealert其隐式语义为aria-liveassertive错误出现时会立即打断播报这正是 aria-patterns.md 中重要错误应使用 assertive 播报建议的实践必填星号用aria-hiddentrue包裹避免读屏器播报无意义的*同时保留视觉提示noValidate 自管校验禁用浏览器原生气泡保证错误提示样式与播报行为一致可控。该模式与仓库中 wcag-guidelines.md 的 3.3.1 示例FormField使用aria-invalidrolealert如出一辙是经过 WCAG 标准对齐后的推荐写法。三、跳转链接Skip Links绕过重复导航跳转链接对应 WCAG 2.4.1「绕过重复内容」Level A让键盘与屏幕阅读器用户能直接跳到主内容而不用反复 Tab 过整个导航。参考文档的实现用 Tailwind 的sr-onlyfocus-within:not-sr-only实现平时隐藏、聚焦时显示export function SkipLinks() { return ( div classNamesr-only focus-within:not-sr-only a href#main-content classNameabsolute left-4 top-4 z-50 rounded bg-blue-600 px-4 py-2 text-white focus:outline-none focus:ring-2 Skip to main content /a a href#main-navigation classNameabsolute left-4 top-16 z-50 rounded bg-blue-600 px-4 py-2 text-white focus:outline-none focus:ring-2 Skip to navigation /a /div ); }工程要点跳转目标#main-content应放置tabIndex{-1}否则部分浏览器如 Safari不会把焦点移入目标容器跳转链接必须位于页面 DOM 的最前面确保它是键盘 Tab 序列的第一个元素链接自身需要有高对比度背景与明显焦点样式focus:ring-2满足 2.4.7 焦点可见。在 wcag-guidelines.md 的 2.4.1 示例中可以看到完全相同的跳过主内容 跳过导航双链接结构属于 WCAG 官方推荐模式。四、动态区域Live Regions让屏幕阅读器听到变化SPA 中异步加载、搜索过滤等动态内容不会自动被屏幕阅读器感知必须通过aria-live区域主动播报。参考文档提供了一个通用LiveAnnouncer组件并封装了先清空、再延迟 100ms 设置的技巧import { useState, useEffect } from react; interface LiveAnnouncerProps { message: string; politeness?: polite | assertive; } export function LiveAnnouncer({ message, politeness polite, }: LiveAnnouncerProps) { const [announcement, setAnnouncement] useState(); useEffect(() { // Clear first, then set - ensures screen readers pick up the change setAnnouncement(); const timer setTimeout(() setAnnouncement(message), 100); return () clearTimeout(timer); }, [message]); return ( div rolestatus aria-live{politeness} aria-atomictrue classNamesr-only {announcement} /div ); } // Usage in a search component function SearchResults({ results, loading, }: { results: Item[]; loading: boolean; }) { const message loading ? Loading results... : ${results.length} results found; return ( LiveAnnouncer message{message} / ul{/* results */}/ul / ); }关键设计解读rolestatus隐式等价于aria-livepolite而politeness参数允许切换到assertive用于必须打断的紧急信息aria-atomictrue告知辅助技术整块替换播报内容而不是逐字比较差异先清空再延迟设置如果新消息与旧消息相同屏幕阅读器可能忽略 DOM 无变化的更新通过先置空再在 100ms 后写入强制触发一次完整的播报。这正是代码注释所强调的 Clear first, then set 技巧classNamesr-only区域在视觉上隐藏、但对读屏器可见——这就是视觉隐藏但仍在无障碍树中的正确用法。对比 aria-patterns.md 中列出的常见错误用display:none的内容不会被播报二者不可混用。在 aria-patterns.md 的 Live Regions 一节还给出了两种更细粒度的变体进度提示rolestatus Loading: X% complete与聊天日志rolelogaria-relevantadditions后者只播报新增消息适合高频追加内容的场景。五、焦点管理工具useFocusReturn 与 useFocusTrap将上文散落在各个组件里的焦点逻辑提取成可复用的 Hooks是组件库工程化的关键。参考文档给出了两个 Hook// useFocusReturn - restore focus after closing function useFocusReturn() { const previousElement useRefElement | null(null); const saveFocus () { previousElement.current document.activeElement; }; const restoreFocus () { (previousElement.current as HTMLElement)?.focus(); }; return { saveFocus, restoreFocus }; } // useFocusTrap - keep focus within container function useFocusTrap(containerRef: RefObjectHTMLElement, isActive: boolean) { useEffect(() { if (!isActive || !containerRef.current) return; const container containerRef.current; const focusableSelector button, [href], input, select, textarea, [tabindex]:not([tabindex-1]); const handleKeyDown (e: KeyboardEvent) { if (e.key ! Tab) return; const focusableElements container.querySelectorAllHTMLElement(focusableSelector); const first focusableElements[0]; const last focusableElements[focusableElements.length - 1]; if (e.shiftKey document.activeElement first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey document.activeElement last) { e.preventDefault(); first.focus(); } }; container.addEventListener(keydown, handleKeyDown); return () container.removeEventListener(keydown, handleKeyDown); }, [containerRef, isActive]); }useFocusReturn在组件打开时调用saveFocus()记录document.activeElement关闭时调用restoreFocus()归还焦点——这正是模态框、弹层、抽屉等瞬时层组件防止焦点丢失的标准做法对应 2.4.3 焦点顺序useFocusTrap把 2.1 节模态框中的trapFocus逻辑参数化为容器 激活标志。当isActive为 false 或容器未挂载时useEffect直接返回不注册监听避免内存泄漏与无效监听两个 Hook 与 web-component-design 强调的Forward Refs转发 ref 让父组件访问 DOM 节点协作良好容器 ref 通常来自forwardRef转发的节点。六、颜色对比度工具用 WCAG 公式在代码中做校验视觉无障碍的最后一道关卡是颜色对比度。参考文档给出了一段自包含的对比度计算与 WCAG 判定函数可在开发期如 CI 或 Storybook 检查直接复用// Check if colors meet WCAG requirements function getContrastRatio(fg: string, bg: string): number { const getLuminance (hex: string): number { const rgb parseInt(hex.slice(1), 16); const r (rgb 16) 0xff; const g (rgb 8) 0xff; const b rgb 0xff; const [rs, gs, bs] [r, g, b].map((c) { c c / 255; return c 0.03928 ? c / 12.92 : Math.pow((c 0.055) / 1.055, 2.4); }); return 0.2126 * rs 0.7152 * gs 0.0722 * bs; }; const l1 getLuminance(fg); const l2 getLuminance(bg); const lighter Math.max(l1, l2); const darker Math.min(l1, l2); return (lighter 0.05) / (darker 0.05); } function meetsWCAG( fg: string, bg: string, level: AA | AAA AA, ): boolean { const ratio getContrastRatio(fg, bg); return level AAA ? ratio 7 : ratio 4.5; }这段代码实现的是 WCAG 2.x 官方的相对亮度与对比度公式把#RRGGBB拆成 RGB 三个通道对每个通道做gamma 校正c/255 ≤ 0.03928时线性映射为c/12.92否则使用((c 0.055) / 1.055)^2.4进行 sRGB 非线性变换按0.2126 R 0.7152 G 0.0722 B加权得到相对亮度人眼对绿色最敏感因此绿色权重最高对比度 (较亮 0.05) / (较暗 0.05)meetsWCAG默认按AA 级 4.5:1普通文本判定AAA 级要求7:1。需要说明的适用范围对应 WCAG 1.4.3 对比度最小化普通文本AA 4.5:1、AAA 7:1大号文本18pt 以上或 14pt 加粗以上AA 3:1、AAA 4.5:1UI 组件与图形1.4.11 非文本对比度3:1。在 wcag-guidelines.md 的 1.4.3 一节中可以找到这些阈值的 CSS 注释原文本工具函数即与之对应。仓库的 无障碍审计命令 也内置了同样的对比度计算流程从设计令牌或 CSS 中提取文本/背景颜色组合逐对计算并标记不达标项。七、在项目中验证接入无障碍审计与测试模式写完之后需要可验证的闭环。ui-design 插件为此提供了两条路径7.1 使用无障碍审计命令在 Claude Code 中安装插件后可执行 accessibility-audit 命令/plugin install ui-design /ui-design:accessibility-audit --file src/components/Modal.tsx --level AA命令会做四类检查正好覆盖本文的模式静态代码分析按 WCAG 四大原则Perceivable / Operable / Understandable / Robust逐条勾选例如图片是否有alt、交互元素是否可键盘操作、是否存在键盘陷阱、跳转链接是否存在、表单是否有关联标签、错误提示是否可被读屏器感知反模式正则检测内置一组正则如img缺少alt、onClick没有onKeyDown、div/span挂 click 处理器、tabIndex{[1-9]}正值、autoFocus等颜色对比度分析提取颜色组合并按 4.5:1 / 7:1 / 3:1 阈值判定与第六节工具函数同一套公式ARIA 校验验证 role 有效性、必填属性是否齐全、是否存在冗余 ARIA如rolebutton加在button上。审计结果会生成在.ui-design/audits/{audit_id}.md按 Critical / Serious / Moderate / Minor 四级严重度分类每条问题附带 WCAG 准则编号、影响分析与逐步修复建议。7.2 接入自动化测试审计命令生成的报告会附带测试建议例如用 jest-axe 做回归拦截import { axe, toHaveNoViolations } from jest-axe; expect.extend(toHaveNoViolations); test(component has no accessibility violations, async () { const { container } render(Component /); const results await axe(container); expect(results).toHaveNoViolations(); });此外accessibility-compliance 技能 还给出了一套手工测试清单可作为发布前的最终关卡全程仅用键盘完成页面导航用 VoiceOver / NVDA 实际走查一遍屏幕阅读器体验200% 缩放下验证可用性高对比度模式下验证可读性确认焦点指示器始终可见用prefers-reduced-motion验证动画可关闭。八、常见陷阱自查清单综合参考文档与仓库中 aria-patterns.md 的 Common Mistakes 章节以下反模式最容易在组件开发中反复出现陷阱错误示例正确做法冗余 ARIAbutton rolebutton、aria-label与可见文本重复优先使用原生语义元素无效 ARIA在无角色元素上用aria-selected必须搭配roleoption等正确角色断开的控制关系aria-expanded没有配套aria-controls用aria-controls指向被控制元素 id隐藏内容仍被播报视觉隐藏但未从无障碍树移除用display:none/hidden彻底移除或用aria-hiddentrue显式隐藏装饰内容焦点丢失关闭弹层后焦点留在 body用useFocusReturn归还焦点给触发元素键盘陷阱弹层内 Tab 焦点逃逸到背景用useFocusTrap循环锁定焦点只靠颜色传达信息仅红色边框表示校验失败叠加aria-invalid、图标与文字错误提示缺失跳转链接长导航无法跳过页面顶部放置SkipLinks九、模式之间的协作关系最后把本文的组件放在一起看它们如何形成体系SkipLinks解决进入页面的导航效率Dropdown/Combobox解决复杂交互的键盘协议Modal与useFocusTrap/useFocusReturn解决焦点闭环FormField解决输入与错误的语义关联LiveAnnouncer解决动态更新的播报getContrastRatio/meetsWCAG解决视觉呈现的合规校验。这六类能力共同支撑起 web-component-design 中 Accessible by Default 的组件库目标也与 accessibility-compliance 技能 的 WCAG 2.2 合规路线Level A / AA / AAA完全对齐。每个模式都可作为独立的组件规格沉淀到组件库中并配合 accessibility-audit 命令 在 CI 中持续校验最终形成先设计、后实现、再验证的无障碍工程闭环。【免费下载链接】agentsMulti-harness agentic plugin marketplace for Claude Code, Codex, Cursor, OpenCode, GitHub Copilot, and Google Antigravity项目地址: https://gitcode.com/GitHub_Trending/agents24/agents创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表