web-accessibility
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWeb Accessibility (A11y)
Web可访问性(A11y)
When to use this skill
适用场景
- 새 UI 컴포넌트 개발: 접근 가능한 컴포넌트 설계
- 접근성 감사: 기존 사이트의 접근성 문제 식별 및 수정
- 폼 구현: 스크린 리더 친화적인 폼 작성
- 모달/드롭다운: 포커스 관리 및 키보드 트랩 방지
- WCAG 준수: 법적 요구사항 또는 표준 준수
- 新UI组件开发:设计无障碍组件
- 可访问性审计:识别并修复现有网站的可访问性问题
- 表单实现:创建适配屏幕阅读器的表单
- 模态框/下拉菜单:焦点管理及防止键盘陷阱
- WCAG合规:满足法律要求或标准规范
입력 형식 (Input Format)
输入格式
필수 정보
必填信息
- 프레임워크: React, Vue, Svelte, Vanilla JS 등
- 컴포넌트 유형: Button, Form, Modal, Dropdown, Navigation 등
- WCAG 레벨: A, AA, AAA (기본값: AA)
- 框架:React, Vue, Svelte, Vanilla JS等
- 组件类型:Button, Form, Modal, Dropdown, Navigation等
- WCAG级别:A, AA, AAA(默认值:AA)
선택 정보
可选信息
- 스크린 리더: NVDA, JAWS, VoiceOver (테스트용)
- 자동 테스트 도구: axe-core, Pa11y, Lighthouse (기본값: axe-core)
- 브라우저: Chrome, Firefox, Safari (기본값: Chrome)
- 屏幕阅读器:NVDA, JAWS, VoiceOver(用于测试)
- 自动化测试工具:axe-core, Pa11y, Lighthouse(默认值:axe-core)
- 浏览器:Chrome, Firefox, Safari(默认值:Chrome)
입력 예시
输入示例
React 모달 컴포넌트를 접근 가능하게 만들어줘:
- 프레임워크: React + TypeScript
- WCAG 레벨: AA
- 요구사항:
- 포커스 트랩 (모달 내부에만 포커스)
- ESC 키로 닫기
- 배경 클릭으로 닫기
- 스크린 리더에서 제목/설명 읽기请将React模态框组件改造为无障碍版本:
- 框架:React + TypeScript
- WCAG级别:AA
- 需求:
- 焦点陷阱(仅在模态框内部移动焦点)
- ESC键关闭
- 点击背景关闭
- 屏幕阅读器可读取标题/描述Instructions
操作步骤
Step 1: Semantic HTML 사용
步骤1:使用语义化HTML
의미있는 HTML 요소를 사용하여 구조를 명확히 합니다.
작업 내용:
- ,
<button>,<nav>,<main>,<header>등 시맨틱 태그 사용<footer> - ,
<div>남용 지양<span> - 제목 계층 구조 (~
<h1>) 올바르게 사용<h6> - 과
<label>연결<input>
예시 (❌ 나쁜 예 vs ✅ 좋은 예):
html
<!-- ❌ 나쁜 예: div와 span만 사용 -->
<div class="header">
<span class="title">My App</span>
<div class="nav">
<div class="nav-item" onclick="navigate()">Home</div>
<div class="nav-item" onclick="navigate()">About</div>
</div>
</div>
<!-- ✅ 좋은 예: 시맨틱 HTML -->
<header>
<h1>My App</h1>
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
</header>폼 예시:
html
<!-- ❌ 나쁜 예: label 없음 -->
<input type="text" placeholder="Enter your name">
<!-- ✅ 좋은 예: label 연결 -->
<label for="name">Name:</label>
<input type="text" id="name" name="name" required>
<!-- 또는 label로 감싸기 -->
<label>
Email:
<input type="email" name="email" required>
</label>使用具有语义的HTML元素明确页面结构。
工作内容:
- 使用,
<button>,<nav>,<main>,<header>等语义化标签<footer> - 避免滥用,
<div><span> - 正确使用标题层级结构(~
<h1>)<h6> - 关联与
<label><input>
示例(❌ 错误示例 vs ✅ 正确示例):
html
<!-- ❌ 错误示例:仅使用div和span -->
<div class="header">
<span class="title">My App</span>
<div class="nav">
<div class="nav-item" onclick="navigate()">Home</div>
<div class="nav-item" onclick="navigate()">About</div>
</div>
</div>
<!-- ✅ 正确示例:语义化HTML -->
<header>
<h1>My App</h1>
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
</header>表单示例:
html
<!-- ❌ 错误示例:无label -->
<input type="text" placeholder="Enter your name">
<!-- ✅ 正确示例:关联label -->
<label for="name">姓名:</label>
<input type="text" id="name" name="name" required>
<!-- 或用label包裹 -->
<label>
邮箱:
<input type="email" name="email" required>
</label>Step 2: 키보드 네비게이션 구현
步骤2:实现键盘导航
마우스 없이도 모든 기능 사용 가능하도록 합니다.
작업 내용:
- Tab, Shift+Tab으로 포커스 이동
- Enter/Space로 버튼 활성화
- 화살표 키로 리스트/메뉴 탐색
- ESC로 모달/드롭다운 닫기
- 적절히 사용
tabindex
판단 기준:
- 인터랙티브 요소 → (포커스 가능)
tabindex="0" - 포커스 제외 → (프로그래밍 방식 포커스만)
tabindex="-1" - 포커스 순서 변경 금지 → 사용 지양
tabindex="1+"
예시 (React 드롭다운):
typescript
import React, { useState, useRef, useEffect } from 'react';
interface DropdownProps {
label: string;
options: { value: string; label: string }[];
onChange: (value: string) => void;
}
function AccessibleDropdown({ label, options, onChange }: DropdownProps) {
const [isOpen, setIsOpen] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
const buttonRef = useRef<HTMLButtonElement>(null);
const listRef = useRef<HTMLUListElement>(null);
// 키보드 핸들러
const handleKeyDown = (e: React.KeyboardEvent) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
if (!isOpen) {
setIsOpen(true);
} else {
setSelectedIndex((prev) => (prev + 1) % options.length);
}
break;
case 'ArrowUp':
e.preventDefault();
if (!isOpen) {
setIsOpen(true);
} else {
setSelectedIndex((prev) => (prev - 1 + options.length) % options.length);
}
break;
case 'Enter':
case ' ':
e.preventDefault();
if (isOpen) {
onChange(options[selectedIndex].value);
setIsOpen(false);
buttonRef.current?.focus();
} else {
setIsOpen(true);
}
break;
case 'Escape':
e.preventDefault();
setIsOpen(false);
buttonRef.current?.focus();
break;
}
};
return (
<div className="dropdown">
<button
ref={buttonRef}
onClick={() => setIsOpen(!isOpen)}
onKeyDown={handleKeyDown}
aria-haspopup="listbox"
aria-expanded={isOpen}
aria-labelledby="dropdown-label"
>
{label}
</button>
{isOpen && (
<ul
ref={listRef}
role="listbox"
aria-labelledby="dropdown-label"
onKeyDown={handleKeyDown}
tabIndex={-1}
>
{options.map((option, index) => (
<li
key={option.value}
role="option"
aria-selected={index === selectedIndex}
onClick={() => {
onChange(option.value);
setIsOpen(false);
}}
>
{option.label}
</li>
))}
</ul>
)}
</div>
);
}确保无需鼠标即可使用所有功能。
工作内容:
- 通过Tab、Shift+Tab移动焦点
- 通过Enter/Space激活按钮
- 通过方向键导航列表/菜单
- 通过ESC关闭模态框/下拉菜单
- 合理使用
tabindex
判断标准:
- 交互元素 → (可获取焦点)
tabindex="0" - 排除焦点 → (仅允许编程式焦点)
tabindex="-1" - 禁止修改焦点顺序 → 避免使用
tabindex="1+"
示例(React下拉菜单):
typescript
import React, { useState, useRef, useEffect } from 'react';
interface DropdownProps {
label: string;
options: { value: string; label: string }[];
onChange: (value: string) => void;
}
function AccessibleDropdown({ label, options, onChange }: DropdownProps) {
const [isOpen, setIsOpen] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
const buttonRef = useRef<HTMLButtonElement>(null);
const listRef = useRef<HTMLUListElement>(null);
// 键盘事件处理器
const handleKeyDown = (e: React.KeyboardEvent) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
if (!isOpen) {
setIsOpen(true);
} else {
setSelectedIndex((prev) => (prev + 1) % options.length);
}
break;
case 'ArrowUp':
e.preventDefault();
if (!isOpen) {
setIsOpen(true);
} else {
setSelectedIndex((prev) => (prev - 1 + options.length) % options.length);
}
break;
case 'Enter':
case ' ':
e.preventDefault();
if (isOpen) {
onChange(options[selectedIndex].value);
setIsOpen(false);
buttonRef.current?.focus();
} else {
setIsOpen(true);
}
break;
case 'Escape':
e.preventDefault();
setIsOpen(false);
buttonRef.current?.focus();
break;
}
};
return (
<div className="dropdown">
<button
ref={buttonRef}
onClick={() => setIsOpen(!isOpen)}
onKeyDown={handleKeyDown}
aria-haspopup="listbox"
aria-expanded={isOpen}
aria-labelledby="dropdown-label"
>
{label}
</button>
{isOpen && (
<ul
ref={listRef}
role="listbox"
aria-labelledby="dropdown-label"
onKeyDown={handleKeyDown}
tabIndex={-1}
>
{options.map((option, index) => (
<li
key={option.value}
role="option"
aria-selected={index === selectedIndex}
onClick={() => {
onChange(option.value);
setIsOpen(false);
}}
>
{option.label}
</li>
))}
</ul>
)}
</div>
);
}Step 3: ARIA 속성 추가
步骤3:添加ARIA属性
스크린 리더에게 추가 컨텍스트를 제공합니다.
작업 내용:
- : 요소의 이름 정의
aria-label - : 다른 요소를 라벨로 참조
aria-labelledby - : 추가 설명 제공
aria-describedby - : 동적 콘텐츠 변경 알림
aria-live - : 스크린 리더에서 숨기기
aria-hidden
확인 사항:
- 모든 인터랙티브 요소에 명확한 라벨
- 버튼 목적이 명확 (예: "Submit form" not "Click")
- 상태 변화 알림 (aria-live)
- 장식용 이미지는 alt="" 또는 aria-hidden="true"
예시 (모달):
tsx
function AccessibleModal({ isOpen, onClose, title, children }) {
const modalRef = useRef<HTMLDivElement>(null);
// 모달 열릴 때 포커스 트랩
useEffect(() => {
if (isOpen) {
modalRef.current?.focus();
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-describedby="modal-description"
ref={modalRef}
tabIndex={-1}
onKeyDown={(e) => {
if (e.key === 'Escape') {
onClose();
}
}}
>
<div className="modal-overlay" onClick={onClose} aria-hidden="true" />
<div className="modal-content">
<h2 id="modal-title">{title}</h2>
<div id="modal-description">
{children}
</div>
<button onClick={onClose} aria-label="Close modal">
<span aria-hidden="true">×</span>
</button>
</div>
</div>
);
}aria-live 예시 (알림):
tsx
function Notification({ message, type }: { message: string; type: 'success' | 'error' }) {
return (
<div
role="alert"
aria-live="assertive" // 즉시 알림 (error), "polite"는 순서대로 알림
aria-atomic="true" // 전체 내용 읽기
className={`notification notification-${type}`}
>
{type === 'error' && <span aria-label="Error">⚠️</span>}
{type === 'success' && <span aria-label="Success">✅</span>}
{message}
</div>
);
}为屏幕阅读器提供额外上下文信息。
工作内容:
- : 定义元素名称
aria-label - : 引用其他元素作为标签
aria-labelledby - : 提供额外说明
aria-describedby - : 通知动态内容变更
aria-live - : 隐藏屏幕阅读器无需读取的内容
aria-hidden
检查事项:
- 所有交互元素都有明确标签
- 按钮用途清晰(例如:"提交表单"而非"点击")
- 状态变更使用aria-live通知
- 装饰性图片设置alt=""或aria-hidden="true"
示例(模态框):
tsx
function AccessibleModal({ isOpen, onClose, title, children }) {
const modalRef = useRef<HTMLDivElement>(null);
// 模态框打开时设置焦点陷阱
useEffect(() => {
if (isOpen) {
modalRef.current?.focus();
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-describedby="modal-description"
ref={modalRef}
tabIndex={-1}
onKeyDown={(e) => {
if (e.key === 'Escape') {
onClose();
}
}}
>
<div className="modal-overlay" onClick={onClose} aria-hidden="true" />
<div className="modal-content">
<h2 id="modal-title">{title}</h2>
<div id="modal-description">
{children}
</div>
<button onClick={onClose} aria-label="关闭模态框">
<span aria-hidden="true">×</span>
</button>
</div>
</div>
);
}aria-live示例(通知):
tsx
function Notification({ message, type }: { message: string; type: 'success' | 'error' }) {
return (
<div
role="alert"
aria-live="assertive" // 立即通知(错误场景),"polite"为按顺序通知
aria-atomic="true" // 读取全部内容
className={`notification notification-${type}`}
>
{type === 'error' && <span aria-label="错误">⚠️</span>}
{type === 'success' && <span aria-label="成功">✅</span>}
{message}
</div>
);
}Step 4: 색상 대비 및 시각적 접근성
步骤4:色彩对比与视觉可访问性
시각 장애인을 위한 충분한 대비율을 보장합니다.
작업 내용:
- WCAG AA: 텍스트 4.5:1, 큰 텍스트 3:1
- WCAG AAA: 텍스트 7:1, 큰 텍스트 4.5:1
- 색상만으로 정보 전달 금지 (아이콘, 패턴 병행)
- 포커스 표시 명확히 (outline)
예시 (CSS):
css
/* ✅ 충분한 대비 (텍스트 #000 on #FFF = 21:1) */
.button {
background-color: #0066cc;
color: #ffffff; /* 대비율 7.7:1 */
}
/* ✅ 포커스 표시 */
button:focus,
a:focus {
outline: 3px solid #0066cc;
outline-offset: 2px;
}
/* ❌ outline: none 금지! */
button:focus {
outline: none; /* 절대 사용 금지 */
}
/* ✅ 색상 + 아이콘으로 상태 표시 */
.error-message {
color: #d32f2f;
border-left: 4px solid #d32f2f;
}
.error-message::before {
content: '⚠️';
margin-right: 8px;
}确保为视障人士提供足够的对比度。
工作内容:
- WCAG AA标准:普通文本4.5:1,大文本3:1
- WCAG AAA标准:普通文本7:1,大文本4.5:1
- 禁止仅通过颜色传递信息(需配合图标、图案)
- 清晰显示焦点状态(outline)
示例(CSS):
css
/* ✅ 足够对比度(文本#000在#FFF上=21:1) */
.button {
background-color: #0066cc;
color: #ffffff; /* 对比度7.7:1 */
}
/* ✅ 焦点显示 */
button:focus,
a:focus {
outline: 3px solid #0066cc;
outline-offset: 2px;
}
/* ❌ 禁止使用outline: none! */
button:focus {
outline: none; /* 绝对禁止 */
}
/* ✅ 通过颜色+图标显示状态 */
.error-message {
color: #d32f2f;
border-left: 4px solid #d32f2f;
}
.error-message::before {
content: '⚠️';
margin-right: 8px;
}Step 5: 접근성 테스트
步骤5:可访问性测试
자동 및 수동 테스트로 접근성을 검증합니다.
작업 내용:
- axe DevTools로 자동 스캔
- Lighthouse Accessibility 점수 확인
- 키보드만으로 전체 기능 테스트
- 스크린 리더 테스트 (NVDA, VoiceOver)
예시 (Jest + axe-core):
typescript
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import AccessibleButton from './AccessibleButton';
expect.extend(toHaveNoViolations);
describe('AccessibleButton', () => {
it('should have no accessibility violations', async () => {
const { container } = render(
<AccessibleButton onClick={() => {}}>
Click Me
</AccessibleButton>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('should be keyboard accessible', () => {
const handleClick = jest.fn();
const { getByRole } = render(
<AccessibleButton onClick={handleClick}>
Click Me
</AccessibleButton>
);
const button = getByRole('button');
// Enter 키
button.focus();
fireEvent.keyDown(button, { key: 'Enter' });
expect(handleClick).toHaveBeenCalled();
// Space 키
fireEvent.keyDown(button, { key: ' ' });
expect(handleClick).toHaveBeenCalledTimes(2);
});
});通过自动化和手动测试验证可访问性。
工作内容:
- 使用axe DevTools进行自动化扫描
- 检查Lighthouse可访问性得分
- 仅使用键盘测试所有功能
- 使用屏幕阅读器测试(NVDA、VoiceOver)
示例(Jest + axe-core):
typescript
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import AccessibleButton from './AccessibleButton';
expect.extend(toHaveNoViolations);
describe('AccessibleButton', () => {
it('应无任何可访问性违规', async () => {
const { container } = render(
<AccessibleButton onClick={() => {}}>
Click Me
</AccessibleButton>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('应支持键盘访问', () => {
const handleClick = jest.fn();
const { getByRole } = render(
<AccessibleButton onClick={handleClick}>
Click Me
</AccessibleButton>
);
const button = getByRole('button');
// Enter键
button.focus();
fireEvent.keyDown(button, { key: 'Enter' });
expect(handleClick).toHaveBeenCalled();
// Space键
fireEvent.keyDown(button, { key: ' ' });
expect(handleClick).toHaveBeenCalledTimes(2);
});
});Output format
输出格式
기본 체크리스트
基础检查表
markdown
undefinedmarkdown
undefinedAccessibility Checklist
可访问性检查表
Semantic HTML
语义化HTML
- 시맨틱 HTML 태그 사용 (,
<button>,<nav>등)<main> - 제목 계층 구조 올바름 (h1 → h2 → h3)
- 폼 라벨 모두 연결됨
- 使用语义化HTML标签(,
<button>,<nav>等)<main> - 标题层级结构正确(h1 → h2 → h3)
- 所有表单标签已关联
Keyboard Navigation
键盘导航
- Tab으로 모든 인터랙티브 요소 접근 가능
- Enter/Space로 버튼 활성화
- ESC로 모달/드롭다운 닫기
- 포커스 표시 명확 (outline)
- 所有交互元素可通过Tab访问
- 可通过Enter/Space激活按钮
- 可通过ESC关闭模态框/下拉菜单
- 焦点状态显示清晰(outline)
ARIA
ARIA属性
- 적절히 사용
role - 또는
aria-label제공aria-labelledby - 동적 콘텐츠에 사용
aria-live - 장식용 요소
aria-hidden="true"
- 合理使用
role - 提供或
aria-labelaria-labelledby - 动态内容使用
aria-live - 装饰性元素设置
aria-hidden="true"
Visual
视觉设计
- 색상 대비 WCAG AA 준수 (4.5:1)
- 색상만으로 정보 전달 안 함
- 텍스트 크기 조절 가능
- 반응형 디자인
- 色彩对比符合WCAG AA标准(4.5:1)
- 未仅通过颜色传递信息
- 文本大小可调节
- 响应式设计
Testing
测试验证
- axe DevTools 위반 사항 0
- Lighthouse Accessibility 90+ 점수
- 키보드 테스트 통과
- 스크린 리더 테스트 완료
undefined- axe DevTools无违规
- Lighthouse可访问性得分90+
- 键盘测试通过
- 屏幕阅读器测试完成
undefinedConstraints
约束规则
필수 규칙 (MUST)
必须遵守的规则(MUST)
-
키보드 접근성: 모든 기능은 마우스 없이 사용 가능해야 함
- Tab, Enter, Space, 화살표, ESC 지원
- 포커스 트랩 구현 (모달)
-
대체 텍스트: 모든 이미지에속성
alt- 의미 있는 이미지: 설명적 alt text
- 장식용 이미지: (스크린 리더 무시)
alt=""
-
명확한 라벨: 모든 폼 입력에 연결된 라벨
- 또는
<label for="...">aria-label - 플레이스홀더만으로 라벨 대체 금지
-
键盘可访问性:所有功能必须无需鼠标即可使用
- 支持Tab、Enter、Space、方向键、ESC
- 模态框需实现焦点陷阱
-
替代文本:所有图片必须设置属性
alt- 有意义的图片:使用描述性alt文本
- 装饰性图片:设置(屏幕阅读器忽略)
alt=""
-
清晰标签:所有表单输入必须关联标签
- 使用或
<label for="...">aria-label - 禁止仅使用占位符作为标签
- 使用
금지 사항 (MUST NOT)
禁止事项(MUST NOT)
-
outline 제거 금지:절대 사용 금지
outline: none- 키보드 사용자에게 치명적
- 커스텀 포커스 스타일 제공 필요
-
tabindex > 0 사용 금지: 포커스 순서 변경 지양
- DOM 순서를 논리적으로 유지
- 예외: 특별한 이유가 있는 경우만
-
색상만으로 정보 전달 금지: 아이콘, 텍스트 병행
- 색맹 사용자 고려
- 예: "빨간색 항목 클릭" → "⚠️ Error 항목 클릭"
-
禁止移除outline:绝对禁止使用
outline: none- 对键盘用户至关重要
- 如需自定义可替换为其他焦点样式
-
禁止使用tabindex > 0:避免修改焦点顺序
- 保持DOM顺序的逻辑性
- 仅在特殊场景下可例外
-
禁止仅通过颜色传递信息:需配合图标、文本
- 考虑色弱用户需求
- 示例:将"点击红色项"改为"点击⚠️错误项"
Examples
示例
예시 1: 접근 가능한 폼
示例1:无障碍表单
tsx
function AccessibleContactForm() {
const [errors, setErrors] = useState<Record<string, string>>({});
const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle');
return (
<form onSubmit={handleSubmit} noValidate>
<h2 id="form-title">Contact Us</h2>
<p id="form-description">Please fill out the form below to get in touch.</p>
{/* 이름 */}
<div className="form-group">
<label htmlFor="name">
Name <span aria-label="required">*</span>
</label>
<input
type="text"
id="name"
name="name"
required
aria-required="true"
aria-invalid={!!errors.name}
aria-describedby={errors.name ? 'name-error' : undefined}
/>
{errors.name && (
<span id="name-error" role="alert" className="error">
{errors.name}
</span>
)}
</div>
{/* 이메일 */}
<div className="form-group">
<label htmlFor="email">
Email <span aria-label="required">*</span>
</label>
<input
type="email"
id="email"
name="email"
required
aria-required="true"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : 'email-hint'}
/>
<span id="email-hint" className="hint">
We'll never share your email.
</span>
{errors.email && (
<span id="email-error" role="alert" className="error">
{errors.email}
</span>
)}
</div>
{/* 제출 버튼 */}
<button type="submit" disabled={submitStatus === 'loading'}>
{submitStatus === 'loading' ? 'Submitting...' : 'Submit'}
</button>
{/* 성공/실패 메시지 */}
{submitStatus === 'success' && (
<div role="alert" aria-live="polite" className="success">
✅ Form submitted successfully!
</div>
)}
{submitStatus === 'error' && (
<div role="alert" aria-live="assertive" className="error">
⚠️ An error occurred. Please try again.
</div>
)}
</form>
);
}tsx
function AccessibleContactForm() {
const [errors, setErrors] = useState<Record<string, string>>({});
const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle');
return (
<form onSubmit={handleSubmit} noValidate>
<h2 id="form-title">联系我们</h2>
<p id="form-description">请填写以下表单与我们取得联系。</p>
{/* 姓名 */}
<div className="form-group">
<label htmlFor="name">
姓名 <span aria-label="必填">*</span>
</label>
<input
type="text"
id="name"
name="name"
required
aria-required="true"
aria-invalid={!!errors.name}
aria-describedby={errors.name ? 'name-error' : undefined}
/>
{errors.name && (
<span id="name-error" role="alert" className="error">
{errors.name}
</span>
)}
</div>
{/* 邮箱 */}
<div className="form-group">
<label htmlFor="email">
邮箱 <span aria-label="必填">*</span>
</label>
<input
type="email"
id="email"
name="email"
required
aria-required="true"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : 'email-hint'}
/>
<span id="email-hint" className="hint">
我们绝不会分享您的邮箱。
</span>
{errors.email && (
<span id="email-error" role="alert" className="error">
{errors.email}
</span>
)}
</div>
{/* 提交按钮 */}
<button type="submit" disabled={submitStatus === 'loading'}>
{submitStatus === 'loading' ? '提交中...' : '提交'}
</button>
{/* 成功/失败提示 */}
{submitStatus === 'success' && (
<div role="alert" aria-live="polite" className="success">
✅ 表单提交成功!
</div>
)}
{submitStatus === 'error' && (
<div role="alert" aria-live="assertive" className="error">
⚠️ 发生错误,请重试。
</div>
)}
</form>
);
}예시 2: 접근 가능한 탭 UI
示例2:无障碍标签页UI
tsx
function AccessibleTabs({ tabs }: { tabs: { id: string; label: string; content: React.ReactNode }[] }) {
const [activeTab, setActiveTab] = useState(0);
const handleKeyDown = (e: React.KeyboardEvent, index: number) => {
switch (e.key) {
case 'ArrowRight':
e.preventDefault();
setActiveTab((index + 1) % tabs.length);
break;
case 'ArrowLeft':
e.preventDefault();
setActiveTab((index - 1 + tabs.length) % tabs.length);
break;
case 'Home':
e.preventDefault();
setActiveTab(0);
break;
case 'End':
e.preventDefault();
setActiveTab(tabs.length - 1);
break;
}
};
return (
<div>
{/* Tab List */}
<div role="tablist" aria-label="Content sections">
{tabs.map((tab, index) => (
<button
key={tab.id}
role="tab"
id={`tab-${tab.id}`}
aria-selected={activeTab === index}
aria-controls={`panel-${tab.id}`}
tabIndex={activeTab === index ? 0 : -1}
onClick={() => setActiveTab(index)}
onKeyDown={(e) => handleKeyDown(e, index)}
>
{tab.label}
</button>
))}
</div>
{/* Tab Panels */}
{tabs.map((tab, index) => (
<div
key={tab.id}
role="tabpanel"
id={`panel-${tab.id}`}
aria-labelledby={`tab-${tab.id}`}
hidden={activeTab !== index}
tabIndex={0}
>
{tab.content}
</div>
))}
</div>
);
}tsx
function AccessibleTabs({ tabs }: { tabs: { id: string; label: string; content: React.ReactNode }[] }) {
const [activeTab, setActiveTab] = useState(0);
const handleKeyDown = (e: React.KeyboardEvent, index: number) => {
switch (e.key) {
case 'ArrowRight':
e.preventDefault();
setActiveTab((index + 1) % tabs.length);
break;
case 'ArrowLeft':
e.preventDefault();
setActiveTab((index - 1 + tabs.length) % tabs.length);
break;
case 'Home':
e.preventDefault();
setActiveTab(0);
break;
case 'End':
e.preventDefault();
setActiveTab(tabs.length - 1);
break;
}
};
return (
<div>
{/* 标签列表 */}
<div role="tablist" aria-label="内容分区">
{tabs.map((tab, index) => (
<button
key={tab.id}
role="tab"
id={`tab-${tab.id}`}
aria-selected={activeTab === index}
aria-controls={`panel-${tab.id}`}
tabIndex={activeTab === index ? 0 : -1}
onClick={() => setActiveTab(index)}
onKeyDown={(e) => handleKeyDown(e, index)}
>
{tab.label}
</button>
))}
</div>
{/* 标签面板 */}
{tabs.map((tab, index) => (
<div
key={tab.id}
role="tabpanel"
id={`panel-${tab.id}`}
aria-labelledby={`tab-${tab.id}`}
hidden={activeTab !== index}
tabIndex={0}
>
{tab.content}
</div>
))}
</div>
);
}Best practices
最佳实践
-
시맨틱 HTML 우선: ARIA는 마지막 수단
- 올바른 HTML 요소 사용하면 ARIA 불필요
- 예: vs
<button><div role="button">
-
포커스 관리: SPA에서 페이지 전환 시 포커스 관리
- 새 페이지 로드 시 메인 콘텐츠로 포커스 이동
- Skip links 제공 ("Skip to main content")
-
에러 메시지: 명확하고 도움이 되는 에러 메시지
- "Invalid input" ❌ → "Email must be in format: example@domain.com" ✅
-
优先使用语义化HTML:ARIA仅作为最后手段
- 使用正确的HTML元素可避免使用ARIA
- 示例:vs
<button><div role="button">
-
焦点管理:单页应用切换页面时管理焦点
- 页面加载后将焦点移至主要内容
- 提供跳转链接("跳转到主要内容")
-
错误提示:使用清晰且有帮助的错误信息
- "无效输入" ❌ → "邮箱格式错误,例如:example@domain.com" ✅
References
参考资料
Metadata
元数据
버전
版本
- 현재 버전: 1.0.0
- 최종 업데이트: 2025-01-01
- 호환 플랫폼: Claude, ChatGPT, Gemini
- 当前版本:1.0.0
- 最后更新:2025-01-01
- 兼容平台:Claude, ChatGPT, Gemini
관련 스킬
相关技能
- ui-component-patterns: UI 컴포넌트 구현
- responsive-design: 반응형 디자인
- ui-component-patterns:UI组件实现
- responsive-design:响应式设计
태그
标签
#accessibility#a11y#WCAG#ARIA#screen-reader#keyboard-navigation#frontend#accessibility#a11y#WCAG#ARIA#screen-reader#keyboard-navigation#frontend