tailwindcss-accessibility by josiahsiegel/claude-plugin-marketplace
npx skills add https://github.com/josiahsiegel/claude-plugin-marketplace --skill tailwindcss-accessibilityWCAG 2.2 于 2023 年 10 月发布,是当前的 W3C 标准。与 Tailwind 相关的主要新增内容包括:
<!-- 默认焦点环 -->
<button class="focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2">
按钮
</button>
<!-- 仅对键盘用户显示焦点环 -->
<button class="focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500">
仅对键盘焦点显示环
</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;
}
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
<!-- 跳转到主要内容 -->
<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
"
>
跳转到主要内容
</a>
<header>导航...</header>
<main id="main-content" tabindex="-1">
主要内容
</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">模态框标题</h2>
<p>模态框内容</p>
<button class="focus-ring">关闭</button>
</div>
</div>
<!-- 视觉上隐藏但对屏幕阅读器可用 -->
<span class="sr-only">为屏幕阅读器提供的额外上下文</span>
<!-- 聚焦时显示 (跳过链接) -->
<a href="#main" class="sr-only focus:not-sr-only">跳转到主要内容</a>
<!-- 带无障碍标签的图标 -->
<button>
<svg aria-hidden="true">...</svg>
<span class="sr-only">关闭菜单</span>
</button>
<!-- 表单标签 -->
<label>
<span class="sr-only">搜索</span>
<input type="search" placeholder="搜索..." />
</label>
<!-- 用于播报的动态区域 -->
<div
role="status"
aria-live="polite"
aria-atomic="true"
class="sr-only"
>
<!-- 向屏幕阅读器播报的动态内容 -->
已添加 3 件商品到购物车
</div>
<!-- 重要消息的警告 -->
<div
role="alert"
aria-live="assertive"
class="bg-red-100 text-red-800 p-4 rounded-lg"
>
错误:请更正表单
</div>
<!-- 确保足够的对比度 -->
<p class="text-gray-700 bg-white">4.5:1 对比度比率</p>
<p class="text-gray-500 bg-white">可能不满足 WCAG AA 级 (大文本最低 3:1)</p>
<!-- 大文本 (18pt+) 需要 3:1 -->
<h1 class="text-4xl text-gray-600 bg-white">大文本 - 3:1 比率符合要求</h1>
<!-- 交互元素需要与相邻颜色有 3:1 对比度 -->
<button class="
bg-brand-500 text-white
border-2 border-brand-500
focus:ring-2 focus:ring-brand-500 focus:ring-offset-2
">
无障碍按钮
</button>
<!-- 在两种模式下都保持对比度 -->
<p class="text-gray-900 dark:text-gray-100">
高对比度文本
</p>
<p class="text-gray-600 dark:text-gray-400">
具有足够对比度的次要文本
</p>
<!-- 避免低对比度组合 -->
<p class="text-gray-400 dark:text-gray-600">
⚠️ 在深色模式下可能存在对比度问题
</p>
@theme {
/* 高对比度焦点环 */
--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)]
">
高对比度焦点
</button>
<!-- 尊重用户的动效偏好 -->
<div class="
animate-bounce
motion-reduce:animate-none
">
弹跳元素 (对动效敏感的用户显示静态)
</div>
<!-- 更安全的替代动画 -->
<div class="
transition-opacity duration-300
motion-reduce:transition-none
">
淡入 (对动效敏感的用户立即显示)
</div>
<!-- 使用透明度代替移动 -->
<div class="
transition-all
hover:scale-105 hover:shadow-lg
motion-reduce:hover:scale-100 motion-reduce:hover:shadow-md
">
悬停时缩放 (对动效敏感的用户仅显示阴影)
</div>
@layer components {
/* 尊重减少动效设置的动画 */
.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
">
加载旋转器
</div>
<div class="space-y-4">
<!-- 带标签的文本输入 -->
<div>
<label for="email" class="block text-sm font-medium text-gray-700">
邮箱地址
<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">
我们绝不会分享您的邮箱
</p>
<p id="email-error" class="mt-1 text-sm text-red-600 hidden" role="alert">
请输入有效的邮箱地址
</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">
我同意
<a href="/terms" class="text-brand-500 underline">条款和条件</a>
</label>
</div>
<!-- 单选按钮组 -->
<fieldset>
<legend class="text-sm font-medium text-gray-700">通知偏好</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">邮箱</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">短信</label>
</div>
</div>
</fieldset>
</div>
<!-- 带错误的输入 -->
<div>
<label for="password" class="block text-sm font-medium text-gray-700">
密码
</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">错误:</span>
密码必须至少 8 个字符
</p>
</div>
/* 基于 aria-invalid 属性的样式 */
@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">提交</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">加载中...</span>
</span>
</button>
<!-- 图标按钮 -->
<button
type="button"
aria-label="关闭对话框"
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">切换功能</span>
功能
</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"
>
选项
<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"
>
编辑
</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"
>
删除
</a>
</li>
</ul>
</div>
<div>
<div role="tablist" aria-label="账户设置" 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
"
>
个人资料
</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"
>
设置
</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"
>
个人资料内容
</div>
<div
role="tabpanel"
id="panel-2"
aria-labelledby="tab-2"
tabindex="0"
hidden
class="p-4"
>
设置内容
</div>
</div>
| 级别 | 要求 | 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 |
平台指南对比:
<!-- WCAG 2.2 AA 级最小尺寸 (24px) -->
<button class="min-h-6 min-w-6 p-1">
<svg class="h-4 w-4">...</svg>
</button>
<!-- 推荐尺寸 (44px) - 移动端首选 -->
<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">操作</span>
</button>
<!-- 主要操作的最佳尺寸 (48px) -->
<button class="min-h-12 min-w-12 px-6 py-3 text-base font-medium">
主要操作
</button>
<!-- 小可见链接带扩展点击区域 -->
<a href="#" class="relative inline-block text-sm">
小链接文本
<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">关闭菜单</span>
</button>
<!-- 带全表面点击目标的卡片 -->
<article class="relative p-4 rounded-lg border hover:shadow-md">
<h3>卡片标题</h3>
<p>描述文本</p>
<a href="/details" class="after:absolute after:inset-0">
<span class="sr-only">查看详情</span>
</a>
</article>
WCAG 2.2 要求 24px 间距或目标必须至少 24px:
<!-- 触摸目标之间有足够间距 (最小 12px 间隙) -->
<div class="flex gap-3">
<button class="min-h-11 px-4 py-2">按钮 1</button>
<button class="min-h-11 px-4 py-2">按钮 2</button>
</div>
<!-- 堆叠链接带足够高度和间距 -->
<nav class="flex flex-col">
<a href="#" class="py-3 px-4 min-h-11 border-b border-gray-100">链接 1</a>
<a href="#" class="py-3 px-4 min-h-11 border-b border-gray-100">链接 2</a>
<a href="#" class="py-3 px-4 min-h-11">链接 3</a>
</nav>
<!-- 带安全间距的按钮组 -->
<div class="flex flex-wrap gap-3">
<button class="min-h-11 px-4 py-2 border rounded-lg">取消</button>
<button class="min-h-11 px-4 py-2 bg-blue-600 text-white rounded-lg">确认</button>
</div>
目标可以小于 24x24 如果:
<!-- 正文文本有足够的行高 -->
<p class="leading-relaxed">
具有舒适行高的长篇幅内容
</p>
<!-- 限制行长以提高可读性 -->
<article class="max-w-prose">
<p class="leading-relaxed">
具有最佳行长 (45-75 个字符) 的内容
</p>
</article>
<!-- 足够的段落间距 -->
<div class="space-y-6">
<p>段落 1</p>
<p>段落 2</p>
</div>
<!-- 对文本使用相对单位 -->
<p class="text-base">随用户的字体大小偏好缩放</p>
<!-- 不要对文本使用固定像素值 -->
<p class="text-[14px]">⚠️ 不会随浏览器缩放而缩放</p>
<!-- 文本缩放时不会破坏的容器 -->
<div class="min-h-[auto]">
内容高度随文本大小调整
</div>
<body class="min-h-screen flex flex-col">
<header class="sticky top-0 bg-white shadow z-50">
<nav aria-label="主导航">...</nav>
</header>
<main id="main-content" class="flex-1">
<article>
<h1>页面标题</h1>
<section aria-labelledby="section-1">
<h2 id="section-1">章节标题</h2>
<p>内容...</p>
</section>
</article>
<aside aria-label="相关内容" class="hidden lg:block">
侧边栏内容
</aside>
</main>
<footer class="bg-gray-800 text-white">
<nav aria-label="页脚导航">...</nav>
</footer>
</body>
<article class="prose">
<h1 class="text-4xl font-bold">主标题 (H1)</h1>
<section>
<h2 class="text-2xl font-semibold">章节 (H2)</h2>
<section>
<h3 class="text-xl font-medium">子章节 (H3)</h3>
<p>内容...</p>
</section>
</section>
</article>
// axe-core 集成
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('组件是无障碍的', 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="
/* 触摸目标尺寸 (最小 44px) */
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>
每周安装量
129
代码仓库
GitHub 星标数
21
首次出现
2026年1月24日
安全审计
安装于
gemini-cli104
opencode104
codex102
github-copilot95
cursor93
claude-code92
WCAG 2.2 was released October 2023 and is the current W3C standard. Key additions relevant to Tailwind:
<!-- Default focus ring -->
<button class="focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2">
Button
</button>
<!-- Focus-visible for keyboard users only -->
<button class="focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500">
Only shows ring for keyboard focus
</button>
<!-- Focus-within for parent containers -->
<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>
<!-- Custom focus ring component -->
@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;
}
}
<!-- Skip to main content -->
<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>
<!-- Modal with focus management -->
<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>
<!-- Hidden visually but available to screen readers -->
<span class="sr-only">Additional context for screen readers</span>
<!-- Show on focus (skip links) -->
<a href="#main" class="sr-only focus:not-sr-only">Skip to main</a>
<!-- Icon with accessible label -->
<button>
<svg aria-hidden="true">...</svg>
<span class="sr-only">Close menu</span>
</button>
<!-- Form labels -->
<label>
<span class="sr-only">Search</span>
<input type="search" placeholder="Search..." />
</label>
<!-- Live region for announcements -->
<div
role="status"
aria-live="polite"
aria-atomic="true"
class="sr-only"
>
<!-- Dynamic content announced to screen readers -->
3 items added to cart
</div>
<!-- Alert for important messages -->
<div
role="alert"
aria-live="assertive"
class="bg-red-100 text-red-800 p-4 rounded-lg"
>
Error: Please correct the form
</div>
<!-- Ensure sufficient contrast -->
<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>
<!-- Large text (18pt+) needs 3:1 -->
<h1 class="text-4xl text-gray-600 bg-white">Large text - 3:1 ratio OK</h1>
<!-- Interactive elements need 3:1 against adjacent colors -->
<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>
<!-- Maintain contrast in both modes -->
<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>
<!-- Avoid low contrast combinations -->
<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>
<!-- Respect user's motion preferences -->
<div class="
animate-bounce
motion-reduce:animate-none
">
Bouncing element (static for motion-sensitive users)
</div>
<!-- Safer alternative animations -->
<div class="
transition-opacity duration-300
motion-reduce:transition-none
">
Fades in (instant for motion-sensitive)
</div>
<!-- Use opacity instead of movement -->
<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;
}
}
<!-- Allow users to pause animations -->
<div class="
animate-spin
hover:animate-pause
motion-reduce:animate-none
">
Loading spinner
</div>
<div class="space-y-4">
<!-- Text input with label -->
<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>
<!-- Checkbox with accessible label -->
<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>
<!-- Radio group -->
<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>
<!-- Input with error -->
<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 with loading state -->
<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>
<!-- Icon 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>
<!-- Toggle 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>
| Level | Requirement | Tailwind Class |
|---|---|---|
| AA (2.5.8) | 24x24 CSS pixels minimum | min-h-6 min-w-6 |
| Recommended | 44x44 CSS pixels | min-h-11 min-w-11 |
| AAA (2.5.5) | 44x44 CSS pixels | min-h-11 min-w-11 |
| Optimal | 48x48 CSS pixels | min-h-12 min-w-12 |
Platform guidelines comparison:
<!-- WCAG 2.2 Level AA minimum (24px) -->
<button class="min-h-6 min-w-6 p-1">
<svg class="h-4 w-4">...</svg>
</button>
<!-- Recommended size (44px) - preferred for mobile -->
<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>
<!-- Optimal for primary actions (48px) -->
<button class="min-h-12 min-w-12 px-6 py-3 text-base font-medium">
Primary Action
</button>
<!-- Small visible link with extended tap area -->
<a href="#" class="relative inline-block text-sm">
Small Link Text
<span class="absolute -inset-3" aria-hidden="true"></span>
</a>
<!-- Icon button with extended target -->
<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>
<!-- Card with full-surface tap target -->
<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 requires 24px spacing OR targets must be 24px minimum:
<!-- Adequate spacing between touch targets (12px gap minimum) -->
<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>
<!-- Stacked links with adequate height and spacing -->
<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>
<!-- Button group with safe spacing -->
<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>
Targets can be smaller than 24x24 if:
<!-- Adequate line height for body text -->
<p class="leading-relaxed">
Long form content with comfortable line height
</p>
<!-- Limit line length for readability -->
<article class="max-w-prose">
<p class="leading-relaxed">
Content with optimal line length (45-75 characters)
</p>
</article>
<!-- Adequate paragraph spacing -->
<div class="space-y-6">
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</div>
<!-- Use relative units for text -->
<p class="text-base">Scales with user's font size preferences</p>
<!-- Don't use fixed pixel values for text -->
<p class="text-[14px]">⚠️ Won't scale with browser zoom</p>
<!-- Container that doesn't break on text zoom -->
<div class="min-h-[auto]">
Content height adjusts with text size
</div>
<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>
// 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();
});
| Pattern | Implementation | WCAG Level |
|---|---|---|
| Focus visible | focus-visible:ring-2 focus-visible:ring-offset-2 | 2.4.7 (AA) |
| Screen reader only | sr-only | 1.3.1 (A) |
| Skip links | sr-only focus:not-sr-only focus:absolute | 2.4.1 (A) |
| Reduced motion | motion-reduce:animate-none motion-reduce:transition-none | 2.3.3 (AAA) |
| Touch targets (min) | min-h-6 min-w-6 (24px) |
<!-- Accessible, touch-friendly button component -->
<button
type="button"
class="
/* Touch target size (44px minimum) */
min-h-11 min-w-11 px-4 py-2.5
/* Typography */
text-sm md:text-base font-medium
/* Colors with sufficient contrast */
bg-blue-600 text-white
hover:bg-blue-700
/* Focus indicator (visible, not obscured) */
focus:outline-none
focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2
/* Shape */
rounded-lg
/* Disabled state */
disabled:opacity-50 disabled:cursor-not-allowed
/* Respect motion preferences */
transition-colors motion-reduce:transition-none
"
>
Button Text
</button>
Weekly Installs
129
Repository
GitHub Stars
21
First Seen
Jan 24, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
gemini-cli104
opencode104
codex102
github-copilot95
cursor93
claude-code92
agentation - AI智能体可视化UI反馈工具,连接人眼与代码的桥梁
5,400 周安装
| 2.5.8 (AA) |
| Touch targets (rec) | min-h-11 min-w-11 (44px) | 2.5.5 (AAA) |
| Touch spacing | gap-3 (12px minimum between targets) | 2.5.8 (AA) |
| Text contrast | 4.5:1 for normal, 3:1 for large text | 1.4.3 (AA) |
| Form errors | aria-invalid="true" + role="alert" | 3.3.1 (A) |
| Focus not obscured | Avoid z-index covering focused elements | 2.4.11 (AA) |