Provides React and Next.js accessibility patterns: semantic HTML, ARIA attributes, form labels, keyboard navigation, focus management, and screen reader support. Use when building or reviewing UI components and forms.
How this skill is triggered — by the user, by Claude, or both
Slash command
/everything-claude-code:frontend-a11yThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
React 和 Next.js 的实用无障碍模式。涵盖代码审查中最常被标记的问题:缺失的表单标签、不正确的 ARIA 使用、非语义的交互元素和损坏的键盘导航。
React 和 Next.js 的实用无障碍模式。涵盖代码审查中最常被标记的问题:缺失的表单标签、不正确的 ARIA 使用、非语义的交互元素和损坏的键盘导航。
<input>、<select>、<textarea>)<div> 或 <span> 上使用 onClickaria-* 属性缺失的 htmlFor / id 配对和断开的错误消息是代码审查中最常被标记的问题。
// 错误:label 与 input 没有关联——屏幕阅读器无法将它们联系起来
<label>邮箱</label>
<input type="email" />
// 正确:htmlFor 匹配 input id
<label htmlFor="email">邮箱</label>
<input id="email" type="email" />
// 错误:仅视觉的星号对屏幕阅读器没有任何意义
<label htmlFor="email">邮箱 *</label>
<input id="email" type="email" />
// 正确:required 启用原生浏览器验证;aria-required 向屏幕阅读器发出信号
<label htmlFor="email">
邮箱 <span aria-hidden="true">*</span>
</label>
<input id="email" type="email" required aria-required="true" />
// 错误:错误文本在视觉上存在但未链接到输入框
<input id="email" type="email" />
<span className="error">邮箱地址无效</span>
// 正确:aria-describedby 将输入框连接到其错误消息
// aria-invalid 向屏幕阅读器发出无效状态信号
<input
id="email"
type="email"
aria-describedby="email-error"
aria-invalid={!!error}
/>
{error && (
<span id="email-error" role="alert">
{error}
</span>
)}
interface LoginFormProps {
onSubmit: (email: string, password: string) => void;
}
export function LoginForm({ onSubmit }: LoginFormProps) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState<{ email?: string; password?: string }>({});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const newErrors: typeof errors = {};
if (!email) newErrors.email = '邮箱是必填的';
if (!password) newErrors.password = '密码是必填的';
if (Object.keys(newErrors).length) {
setErrors(newErrors);
return;
}
onSubmit(email, password);
};
return (
<form onSubmit={handleSubmit} noValidate>
<div>
<label htmlFor="email">
邮箱 <span aria-hidden="true">*</span>
</label>
<input
id="email"
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
aria-required="true"
aria-describedby={errors.email ? 'email-error' : undefined}
aria-invalid={!!errors.email}
autoComplete="email"
/>
{errors.email && (
<span id="email-error" role="alert">
{errors.email}
</span>
)}
</div>
<div>
<label htmlFor="password">
密码 <span aria-hidden="true">*</span>
</label>
<input
id="password"
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
aria-required="true"
aria-describedby={errors.password ? 'password-error' : undefined}
aria-invalid={!!errors.password}
autoComplete="current-password"
/>
{errors.password && (
<span id="password-error" role="alert">
{errors.password}
</span>
)}
</div>
<button type="submit">登录</button>
</form>
);
}
使用与意图匹配的元素。屏幕阅读器和键盘用户依赖原生语义。
// 错误:div 没有角色、没有键盘支持、没有可访问名称
<div onClick={handleClick}>提交</div>
// 正确:button 可聚焦,在 Enter/Space 上激活,宣布为"按钮"
<button type="button" onClick={handleClick}>提交</button>
// 错误:非语义导航
<div onClick={() => navigate('/home')}>首页</div>
// 正确:锚点支持右键、中键和键盘导航
<a href="/home">首页</a>
// 错误:标题层级跳过(h1 到 h4)
<h1>仪表板</h1>
<h4>最近活动</h4>
// 正确:顺序的标题层级
<h1>仪表板</h1>
<h2>最近活动</h2>
仅当原生 HTML 语义不足时使用 ARIA。错误的 ARIA 比没有 ARIA 更糟糕。
// aria-label:内联字符串标签——当没有可见的标签文本时使用
<button aria-label="关闭模态框">
<XIcon />
</button>
// aria-labelledby:引用另一个元素的文本——当存在可见标签时使用
<section aria-labelledby="section-title">
<h2 id="section-title">最近订单</h2>
{/* 内容 */}
</section>
// 提供标签之外的补充描述
<button
aria-describedby="delete-warning"
onClick={handleDelete}
>
删除账户
</button>
<p id="delete-warning">此操作无法撤销。</p>
// 使用 aria-live 宣布在不重新加载页面的情况下更新的内容
// polite:等待用户完成当前操作后再宣布
// assertive:立即打断——仅用于紧急错误
export function StatusMessage({ message, isError }: { message: string; isError?: boolean }) {
return (
<div role="status" aria-live={isError ? 'assertive' : 'polite'} aria-atomic="true">
{message}
</div>
);
}
export function Accordion({ title, children }: { title: string; children: React.ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
const contentId = useId();
return (
<div>
<button aria-expanded={isOpen} aria-controls={contentId} onClick={() => setIsOpen(prev => !prev)}>
{title}
</button>
<div id={contentId} hidden={!isOpen}>
{children}
</div>
</div>
);
}
每个交互元素必须仅通过键盘就可到达和操作。
export function Dropdown({ options, onSelect }: { options: string[]; onSelect: (value: string) => void }) {
const [isOpen, setIsOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(0);
const listId = useId();
if (!options.length) return null;
const handleKeyDown = (e: React.KeyboardEvent) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setActiveIndex(i => Math.min(i + 1, options.length - 1));
break;
case 'ArrowUp':
e.preventDefault();
setActiveIndex(i => Math.max(i - 1, 0));
break;
case 'Enter':
case ' ':
e.preventDefault();
if (isOpen) onSelect(options[activeIndex]);
setIsOpen(prev => !prev);
break;
case 'Escape':
setIsOpen(false);
break;
}
};
return (
<div
role="combobox"
aria-expanded={isOpen}
aria-haspopup="listbox"
aria-controls={listId}
tabIndex={0}
onKeyDown={handleKeyDown}
onClick={() => setIsOpen(prev => !prev)}
>
<span>{options[activeIndex]}</span>
{isOpen && (
<ul id={listId} role="listbox">
{options.map((option, index) => (
<li
key={option}
role="option"
aria-selected={index === activeIndex}
onClick={() => {
onSelect(option);
setIsOpen(false);
}}
>
{option}
</li>
))}
</ul>
)}
</div>
);
}
当 UI 状态改变时焦点必须逻辑移动——特别是模态框和路由转换。
此示例涵盖初始焦点和恢复。对于完整的焦点陷阱(Tab/Shift+Tab 在模态框内循环),使用像
focus-trap-react这样的库,它处理动态内容和嵌套 portal 等边缘情况。
export function Modal({ isOpen, onClose, title, children }: { isOpen: boolean; onClose: () => void; title: string; children: React.ReactNode }) {
const modalRef = useRef<HTMLDivElement>(null);
const previousFocusRef = useRef<HTMLElement | null>(null);
useEffect(() => {
if (isOpen) {
// 保存当前聚焦的元素并将焦点移入模态框
previousFocusRef.current = document.activeElement as HTMLElement;
modalRef.current?.focus();
} else {
// 将焦点恢复到打开模态框的元素
previousFocusRef.current?.focus();
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div ref={modalRef} role="dialog" aria-modal="true" aria-labelledby="modal-title" tabIndex={-1} onKeyDown={e => e.key === 'Escape' && onClose()}>
<h2 id="modal-title">{title}</h2>
{children}
<button onClick={onClose}>关闭</button>
</div>
);
}
// 错误:装饰性图标被宣布为未标记的图像
<img src="/icon.svg" />
// 正确:装饰性图像对屏幕阅读器隐藏
<img src="/decoration.png" alt="" aria-hidden="true" />
// 正确:有意义的图像带描述性 alt 文本
<img src="/chart.png" alt="月收入从一月到三月增长了 23%" />
// 正确:带可访问标签的图标按钮
<button aria-label="删除项目">
<TrashIcon aria-hidden="true" />
</button>
尊重在其操作系统设置中请求减少动画的用户。
export function useReducedMotion(): boolean {
const [prefersReduced, setPrefersReduced] = useState(false);
useEffect(() => {
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
setPrefersReduced(mq.matches);
const handler = (e: MediaQueryListEvent) => setPrefersReduced(e.matches);
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, []);
return prefersReduced;
}
// 用法
export function AnimatedCard({ children }: { children: React.ReactNode }) {
const reduceMotion = useReducedMotion();
return (
<div
style={{
transition: reduceMotion ? 'none' : 'transform 300ms ease'
}}
>
{children}
</div>
);
}
// 错误:在非交互元素上使用 onClick 但没有键盘支持
<div onClick={handleClick}>点击我</div>
// 错误:在没有 role 的 div 上使用 aria-label
<div aria-label="导航">...</div>
// 错误:placeholder 用作 label 的替代
<input placeholder="输入您的邮箱" />
// 错误:正 tabIndex 创建不可预测的 tab 顺序
<button tabIndex={3}>提交</button>
// 错误:可聚焦元素上的 aria-hidden——键盘用户被困住
<button aria-hidden="true">打开</button>
// 错误:div 上的 role="button" 没有键盘处理器
<div role="button" onClick={handleClick}>提交</div>
// 缺少:tabIndex={0},Enter/Space 的 onKeyDown
在提交任何交互式组件进行审查之前:
<input>、<select> 和 <textarea> 通过 htmlFor/id 连接到 <label>aria-describedby 链接并标记为 role="alert"onClick 在 <div> 或 <span> 上而没有 role、tabIndex 和 onKeyDownaria-labelalt="" 和 aria-hidden="true"focus-trap-react 等库)aria-liveprefers-reduced-motionfrontend-patterns — 通用 React 组件和状态模式design-system — 设计令牌和组件一致性motion-ui — 带无障碍考虑的动画模式npx claudepluginhub aaione/everything-claude-code-zhAccessibility patterns for React and Next.js covering semantic HTML, ARIA attributes, form labeling, keyboard navigation, focus management, and screen reader support.
Implements MUI accessibility patterns including ARIA labels for IconButtons, TextFields, Dialogs; covers built-in features, keyboard navigation, and WCAG compliance.
Web accessibility discipline: semantic HTML first, ARIA only when needed, keyboard access always. Invoke whenever task involves any interaction with accessible web content -- writing, reviewing, refactoring, or debugging HTML/CSS/JS for WCAG compliance, ARIA usage, keyboard navigation, focus management, screen reader support, or accessible component patterns.