关于 tailwindcss-accessibility
tailwindcss-accessibility 是使用 Tailwind CSS 实现无障碍模式的参考技能,以 WCAG 2.2 为依据,涵盖焦点、屏幕阅读器、对比度、动效、表单与测试。
主要功能
- 01焦点指示器与跳转链接
- 02屏幕阅读器内容模式
- 03对比度与减少动效指导
- 04无障碍表单与触控目标
- 05手动及自动化测试清单
适用场景
- 01为界面组件添加键盘焦点样式
- 02检查表单和控件的无障碍性
- 03依据 WCAG 模式检查 Tailwind 界面
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ josiahsiegel/ claude-plugin-marketplace --skill tailwindcss-accessibility安装后在 Claude Code 中输入/tailwindcss-accessibility确认已加载。
详细介绍
Tailwind CSS 无障碍设计模式(WCAG 2.2 - 2025/2026)
WCAG 2.2 概览(当前标准)
WCAG 2.2 是当前的 W3C 标准。与 Tailwind 相关的主要新增内容:
- 2.5.8 目标尺寸(AA 级):最小为 24x24 CSS 像素,建议为 44x44
- 2.4.11 焦点不被遮挡:焦点指示器必须清晰可见
- 2.4.13 焦点外观:对焦点指示器提出更严格的要求
- 3.3.7 重复输入:不要要求重复输入信息
- 3.2.6 一致的帮助:帮助机制应位于一致的位置
焦点管理
焦点环工具类
<button class="focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2">
Button
</button>
<button class="focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500">
Only shows ring for keyboard focus
</button>
<div class="focus-within:ring-2 focus-within:ring-brand-500 rounded-lg p-1">
<input type="text" class="border-none focus:outline-none" />
</div>
@layer components {
.focus-ring {
@apply focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-2;
}
.focus-ring-inset {
@apply focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-inset;
}
}
跳过链接
<a
href="#main-content"
class="
sr-only focus:not-sr-only
focus:absolute focus:top-4 focus:left-4 focus:z-50
focus:bg-white focus:px-4 focus:py-2 focus:rounded-md focus:shadow-lg
focus:ring-2 focus:ring-brand-500
"
>
Skip to main content
</a>
<header>Navigation...</header>
<main id="main-content" tabindex="-1">
Main content
</main>
焦点陷阱模式
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
class="fixed inset-0 z-50 flex items-center justify-center"
>
<div class="fixed inset-0 bg-black/50" aria-hidden="true"></div>
<div
class="relative bg-white rounded-xl p-6 max-w-md w-full"
role="document"
>
<h2 id="modal-title" class="text-lg font-semibold">Modal Title</h2>
<p>Modal content</p>
<button class="focus-ring">Close</button>
</div>
</div>
屏幕阅读器工具类
视觉隐藏内容
<span class="sr-only">Additional context for screen readers</span>
<a href="#main" class="sr-only focus:not-sr-only">Skip to main</a>
<button>
<svg aria-hidden="true">...</svg>
<span class="sr-only">Close menu</span>
</button>
<label>
<span class="sr-only">Search</span>
<input type="search" placeholder="Search..." />
</label>
宣告动态内容
<div
role="status"
aria-live="polite"
aria-atomic="true"
class="sr-only"
>
3 items added to cart
</div>
<div
role="alert"
aria-live="assertive"
class="bg-red-100 text-red-800 p-4 rounded-lg"
>
Error: Please correct the form
</div>
色彩对比度
高对比度模式
<p class="text-gray-700 bg-white">4.5:1 contrast ratio</p>
<p class="text-gray-500 bg-white">May not meet WCAG AA (3:1 min for large text)</p>
<h1 class="text-4xl text-gray-600 bg-white">Large text - 3:1 ratio OK</h1>
<button class="
bg-brand-500 text-white
border-2 border-brand-500
focus:ring-2 focus:ring-brand-500 focus:ring-offset-2
">
Accessible Button
</button>
深色模式对比度
<p class="text-gray-900 dark:text-gray-100">
High contrast text
</p>
<p class="text-gray-600 dark:text-gray-400">
Secondary text with adequate contrast
</p>
<p class="text-gray-400 dark:text-gray-600">
⚠️ May have contrast issues in dark mode
</p>
焦点指示器对比度
@theme {
/* High contrast focus ring */
--color-focus: oklch(0.55 0.25 250);
--color-focus-offset: oklch(1 0 0);
}
<button class="
focus:outline-none
focus-visible:ring-2
focus-visible:ring-[var(--color-focus)]
focus-visible:ring-offset-2
focus-visible:ring-offset-[var(--color-focus-offset)]
">
High contrast focus
</button>
动效与动画
减少动态效果
<div class="
animate-bounce
motion-reduce:animate-none
">
Bouncing element (static for motion-sensitive users)
</div>
<div class="
transition-opacity duration-300
motion-reduce:transition-none
">
Fades in (instant for motion-sensitive)
</div>
<div class="
transition-all
hover:scale-105 hover:shadow-lg
motion-reduce:hover:scale-100 motion-reduce:hover:shadow-md
">
Scales on hover (shadow only for motion-sensitive)
</div>
安全的动画模式
@layer components {
/* Animations that respect reduced motion */
.animate-fade-in {
@apply animate-in fade-in duration-300;
@apply motion-reduce:animate-none motion-reduce:opacity-100;
}
.animate-slide-up {
@apply animate-in slide-in-from-bottom-4 duration-300;
@apply motion-reduce:animate-none motion-reduce:translate-y-0;
}
}
悬停时暂停动画
<div class="
animate-spin
hover:animate-pause
motion-reduce:animate-none
">
Loading spinner
</div>
表单无障碍设计
无障碍表单字段
<div class="space-y-4">
<div>
<label for="email" class="block text-sm font-medium text-gray-700">
Email address
<span class="text-red-500" aria-hidden="true">*</span>
</label>
<input
type="email"
id="email"
name="email"
required
aria-required="true"
aria-describedby="email-hint email-error"
class="
mt-1 block w-full rounded-md border-gray-300
focus:border-brand-500 focus:ring-brand-500
aria-invalid:border-red-500 aria-invalid:ring-red-500
"
/>
<p id="email-hint" class="mt-1 text-sm text-gray-500">
We'll never share your email
</p>
<p id="email-error" class="mt-1 text-sm text-red-600 hidden" role="alert">
Please enter a valid email
</p>
</div>
<div class="flex items-start gap-3">
<input
type="checkbox"
id="terms"
name="terms"
class="
h-4 w-4 rounded border-gray-300 text-brand-500
focus:ring-brand-500
"
/>
<label for="terms" class="text-sm text-gray-700">
I agree to the
<a href="/terms" class="text-brand-500 underline">terms and conditions</a>
</label>
</div>
<fieldset>
<legend class="text-sm font-medium text-gray-700">Notification preference</legend>
<div class="mt-2 space-y-2">
<div class="flex items-center gap-3">
<input type="radio" id="email-pref" name="notification" value="email" class="h-4 w-4" />
<label for="email-pref">Email</label>
</div>
<div class="flex items-center gap-3">
<input type="radio" id="sms-pref" name="notification" value="sms" class="h-4 w-4" />
<label for="sms-pref">SMS</label>
</div>
</div>
</fieldset>
</div>
错误状态
<div>
<label for="password" class="block text-sm font-medium text-gray-700">
Password
</label>
<input
type="password"
id="password"
aria-invalid="true"
aria-describedby="password-error"
class="
mt-1 block w-full rounded-md
border-red-500 text-red-900
focus:border-red-500 focus:ring-red-500
"
/>
<p id="password-error" class="mt-1 text-sm text-red-600" role="alert">
<span class="sr-only">Error:</span>
Password must be at least 8 characters
</p>
</div>
表单验证反馈
/* Style based on aria-invalid attribute */
@custom-variant aria-invalid (&[aria-invalid="true"]);
<input
class="
border-gray-300
aria-invalid:border-red-500
aria-invalid:text-red-900
aria-invalid:focus:ring-red-500
"
aria-invalid="true"
/>
交互组件
无障碍按钮
<button
type="submit"
aria-busy="true"
aria-disabled="true"
class="
relative
aria-busy:cursor-wait
aria-disabled:opacity-50 aria-disabled:cursor-not-allowed
"
>
<span class="aria-busy:invisible">Submit</span>
<span class="absolute inset-0 flex items-center justify-center aria-busy:visible invisible">
<svg class="animate-spin h-5 w-5" aria-hidden="true">...</svg>
<span class="sr-only">Loading...</span>
</span>
</button>
<button
type="button"
aria-label="Close dialog"
class="rounded-full p-2 hover:bg-gray-100 focus-ring"
>
<svg aria-hidden="true" class="h-5 w-5">...</svg>
</button>
<button
type="button"
aria-pressed="false"
class="
px-4 py-2 rounded-lg border
aria-pressed:bg-brand-500 aria-pressed:text-white aria-pressed:border-brand-500
"
>
<span class="sr-only">Toggle feature</span>
Feature
</button>
无障碍下拉菜单
<div class="relative">
<button
type="button"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="dropdown-menu"
class="flex items-center gap-2 px-4 py-2 rounded-lg border focus-ring"
>
Options
<svg aria-hidden="true" class="h-4 w-4">...</svg>
</button>
<ul
id="dropdown-menu"
role="menu"
aria-labelledby="dropdown-button"
class="
absolute top-full mt-1 w-48 rounded-lg bg-white shadow-lg border
hidden aria-expanded:block
"
>
<li role="none">
<a
href="#"
role="menuitem"
tabindex="-1"
class="block px-4 py-2 hover:bg-gray-100 focus:bg-gray-100 focus:outline-none"
>
Edit
</a>
</li>
<li role="none">
<a
href="#"
role="menuitem"
tabindex="-1"
class="block px-4 py-2 hover:bg-gray-100 focus:bg-gray-100 focus:outline-none"
>
Delete
</a>
</li>
</ul>
</div>
无障碍标签页
<div>
<div role="tablist" aria-label="Account settings" class="flex border-b">
<button
role="tab"
aria-selected="true"
aria-controls="panel-1"
id="tab-1"
class="
px-4 py-2 border-b-2
aria-selected:border-brand-500 aria-selected:text-brand-500
hover:text-gray-700
focus-visible:ring-2 focus-visible:ring-inset
"
>
Profile
</button>
<button
role="tab"
aria-selected="false"
aria-controls="panel-2"
id="tab-2"
tabindex="-1"
class="px-4 py-2 border-b-2 border-transparent"
>
Settings
</button>
</div>
<div
role="tabpanel"
id="panel-1"
aria-labelledby="tab-1"
tabindex="0"
class="p-4 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset"
>
Profile content
</div>
<div
role="tabpanel"
id="panel-2"
aria-labelledby="tab-2"
tabindex="0"
hidden
class="p-4"
>
Settings content
</div>
</div>
触控目标(WCAG 2.2 - 2025/2026 年的关键要求)
WCAG 2.2 目标尺寸要求
| 级别 | 要求 | Tailwind 类 |
|---|---|---|
| AA(2.5.8) | 最小为 24x24 CSS 像素 | min-h-6 min-w-6 |
| 建议值 | 44x44 CSS 像素 | min-h-11 min-w-11 |
| AAA(2.5.5) | 44x44 CSS 像素 | min-h-11 min-w-11 |
| 最佳值 | 48x48 CSS 像素 | min-h-12 min-w-12 |
平台指南比较:
- Apple iOS:最小为 44x44 点
- Google Android:最小为 48x48 dp
- Microsoft Fluent:最小为 44x44 像素
最小触控目标尺寸
<button class="min-h-6 min-w-6 p-1">
<svg class="h-4 w-4">...</svg>
</button>
<button class="min-h-11 min-w-11 p-2.5">
<svg class="h-6 w-6" aria-hidden="true">...</svg>
<span class="sr-only">Action</span>
</button>
<button class="min-h-12 min-w-12 px-6 py-3 text-base font-medium">
Primary Action
</button>
将触控目标扩展至可见元素之外
<a href="#" class="relative inline-block text-sm">
Small Link Text
<span class="absolute -inset-3" aria-hidden="true"></span>
</a>
<button class="relative p-2 -m-2 rounded-lg hover:bg-gray-100">
<svg class="h-5 w-5" aria-hidden="true">...</svg>
<span class="sr-only">Close menu</span>
</button>
<article class="relative p-4 rounded-lg border hover:shadow-md">
<h3>Card Title</h3>
<p>Description text</p>
<a href="/details" class="after:absolute after:inset-0">
<span class="sr-only">View details</span>
</a>
</article>
交互元素之间的间距
WCAG 2.2 要求间距为 24px,或目标尺寸至少为 24px:
<div class="flex gap-3">
<button class="min-h-11 px-4 py-2">Button 1</button>
<button class="min-h-11 px-4 py-2">Button 2</button>
</div>
<nav class="flex flex-col">
<a href="#" class="py-3 px-4 min-h-11 border-b border-gray-100">Link 1</a>
<a href="#" class="py-3 px-4 min-h-11 border-b border-gray-100">Link 2</a>
<a href="#" class="py-3 px-4 min-h-11">Link 3</a>
</nav>
<div class="flex flex-wrap gap-3">
<button class="min-h-11 px-4 py-2 border rounded-lg">Cancel</button>
<button class="min-h-11 px-4 py-2 bg-blue-600 text-white rounded-lg">Confirm</button>
</div>
触控目标例外情况(WCAG 2.2)
以下目标可以小于 24x24:
- 句子中的行内文本链接
- 浏览器提供的控件(滚动条)
- 尺寸对信息展示至关重要
- 同一页面上存在更大的等效目标
文本无障碍设计
易读文本
<p class="leading-relaxed">
Long form content with comfortable line height
</p>
<article class="max-w-prose">
<p class="leading-relaxed">
Content with optimal line length (45-75 characters)
</p>
</article>
<div class="space-y-6">
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</div>
文本缩放
<p class="text-base">Scales with user's font size preferences</p>
<p class="text-[14px]">⚠️ Won't scale with browser zoom</p>
<div class="min-h-[auto]">
Content height adjusts with text size
</div>
使用 Tailwind 编写语义化 HTML
地标区域
<body class="min-h-screen flex flex-col">
<header class="sticky top-0 bg-white shadow z-50">
<nav aria-label="Main navigation">...</nav>
</header>
<main id="main-content" class="flex-1">
<article>
<h1>Page Title</h1>
<section aria-labelledby="section-1">
<h2 id="section-1">Section Title</h2>
<p>Content...</p>
</section>
</article>
<aside aria-label="Related content" class="hidden lg:block">
Sidebar content
</aside>
</main>
<footer class="bg-gray-800 text-white">
<nav aria-label="Footer navigation">...</nav>
</footer>
</body>
标题层级
<article class="prose">
<h1 class="text-4xl font-bold">Main Title (H1)</h1>
<section>
<h2 class="text-2xl font-semibold">Section (H2)</h2>
<section>
<h3 class="text-xl font-medium">Subsection (H3)</h3>
<p>Content...</p>
</section>
</section>
</article>
无障碍测试
浏览器开发者工具检查清单
- 色彩对比度:使用对比度检查工具
- 焦点顺序:按 Tab 键遍历页面
- 缩放:测试 200% 缩放
- 减少动态效果:在操作系统设置中启用
自动化测试
// axe-core integration
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('component is accessible', async () => {
const { container } = render(<Component />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
最佳实践总结
| 模式 | 实现方式 | WCAG 级别 |
|---|---|---|
| 焦点可见 | focus-visible:ring-2 focus-visible:ring-offset-2 | 2.4.7(AA) |
| 仅供屏幕阅读器使用 | sr-only | 1.3.1(A) |
| 跳过链接 | sr-only focus:not-sr-only focus:absolute | 2.4.1(A) |
| 减少动态效果 | motion-reduce:animate-none motion-reduce:transition-none | 2.3.3(AAA) |
| 触控目标(最小值) | min-h-6 min-w-6(24px) | 2.5.8(AA) |
| 触控目标(建议值) | min-h-11 min-w-11(44px) | 2.5.5(AAA) |
| 触控间距 | gap-3(目标之间至少间隔 12px) | 2.5.8(AA) |
| 文本对比度 | 普通文本为 4.5:1,大号文本为 3:1 | 1.4.3(AA) |
| 表单错误 | aria-invalid="true" + role="alert" | 3.3.1(A) |
| 焦点不被遮挡 | 避免使用 z-index 覆盖聚焦元素 | 2.4.11(AA) |
适合触控的按钮
<button
type="button"
class="
min-h-11 min-w-11 px-4 py-2.5
text-sm md:text-base font-medium
bg-blue-600 text-white hover:bg-blue-700
focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2
rounded-lg disabled:opacity-50 disabled:cursor-not-allowed
transition-colors motion-reduce:transition-none
"
>
Button Text
</button>
评论
支持 Markdown