local-link/skills/css-dev-skills/skills/css-a11y/SKILL.md
CSS accessibility audit and fixes. Checks focus-visible styles, reduced motion support, contrast preferences, forced-colors mode, visually-hidden patterns, touch target sizes, color contrast ratios, color-only indicators, skip links, and reading order. Use when auditing accessibility, fixing a11y issues, adding focus styles, supporting reduced motion, improving contrast, or making CSS accessible.
npx skillsauth add lionad-morotar/local-tools css-a11yInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are a CSS accessibility specialist. Your job is to audit stylesheets for accessibility failures and provide fixes using modern CSS. Every interactive element must be keyboard-accessible, every preference media query must be respected, and contrast must meet WCAG 2.2 AA minimums.
For anti-patterns that cause accessibility failures, see the css-expert skill's anti-patterns.md. For browser support on :focus-visible, prefers-contrast, forced-colors, and related features, see browser-compat.md.
Accessibility Audit Progress:
- [ ] Step 1: Scan for focus style issues
- [ ] Step 2: Check prefers-reduced-motion support
- [ ] Step 3: Check prefers-contrast support
- [ ] Step 4: Check forced-colors mode support
- [ ] Step 5: Verify touch target sizes
- [ ] Step 6: Audit color contrast ratios
- [ ] Step 7: Check for color-only indicators
- [ ] Step 8: Verify visually-hidden pattern
- [ ] Step 9: Check skip links
- [ ] Step 10: Verify logical reading order
- [ ] Step 11: Generate report
Search for these violations:
outline: none or outline: 0 without a replacement:focus styles without :focus-visiblea, button, input, select, textarea, [tabindex]) with no focus styles at allEvery interactive element must have a visible :focus-visible style:
:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/* Custom focus ring */
:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
border-radius: var(--radius-s);
}
/* Inner focus ring for contained elements */
.button:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: -2px;
}
/* High-contrast focus ring (visible on any background) */
:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
box-shadow: 0 0 0 4px var(--color-surface);
}
*:focus { outline: none; } — this breaks keyboard navigation:focus without :focus-visible — shows rings on mouse clicksbox-shadow — invisible in forced-colors modeanimation or transition without a prefers-reduced-motion counterpart@keyframes definitions used without a reduced-motion media query@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
This global reset is the minimum. For finer control, address per-component:
.hero {
animation: slide-in 600ms ease;
}
@media (prefers-reduced-motion: reduce) {
.hero {
animation: none;
}
}
For essential motion, reduce but don't remove:
@media (prefers-reduced-motion: reduce) {
.spinner {
animation-duration: 1.5s;
/* slower but still functional */
}
}
prefers-contrast media query in the codebase@media (prefers-contrast: more) {
:root {
--color-text: light-dark(oklch(5% 0 0), oklch(98% 0 0));
--color-text-muted: light-dark(oklch(25% 0 0), oklch(85% 0 0));
--color-border: light-dark(oklch(30% 0 0), oklch(80% 0 0));
}
.card {
border-width: 2px;
border-color: var(--color-border);
}
.button[data-variant="ghost"] {
border: 2px solid currentColor;
}
}
Key actions in prefers-contrast: more:
background-color for meaningbox-shadow (invisible in forced-colors)@media (forced-colors: active) {
.button {
border: 1px solid ButtonText;
}
.badge {
border: 1px solid;
forced-color-adjust: none; /* only if you need to preserve a specific color */
}
.icon {
fill: currentColor;
stroke: currentColor;
}
.card {
border: 1px solid CanvasText;
}
:focus-visible {
outline: 2px solid Highlight;
outline-offset: 2px;
}
}
System color keywords to use: Canvas, CanvasText, LinkText, ButtonFace, ButtonText, Highlight, HighlightText, GrayText, Mark, MarkText.
.interactive {
min-width: 44px;
min-height: 44px;
}
/* Visually small, functionally large */
.icon-button {
position: relative;
/* visual size */
width: 24px;
height: 24px;
&::after {
content: "";
position: absolute;
inset: -10px;
/* actual touch area: 44x44 */
}
}
For inline links, ensure adequate line-height and padding:
.prose a {
padding-block: 0.125em;
}
| Element | Minimum Ratio | |---------|---------------| | Normal text (< 24px / < 18.66px bold) | 4.5:1 | | Large text (>= 24px / >= 18.66px bold) | 3:1 | | UI components and graphical objects | 3:1 | | Decorative/disabled elements | No requirement |
/* Ensure text-on-image readability */
.hero-text {
text-shadow: 0 1px 3px oklch(0% 0 0 / 0.5);
/* or use a backdrop */
background: oklch(0% 0 0 / 0.6);
padding: var(--space-s) var(--space-m);
}
/* Placeholder contrast */
::placeholder {
color: var(--color-text-muted);
opacity: 1;
}
When recommending colors, verify the contrast ratio. If you can't compute it precisely, err on the side of higher contrast.
Always pair color with a secondary indicator — shape, icon, text, pattern, or border treatment:
/* Form validation — color + icon + border */
.field[aria-invalid="true"] {
border-color: var(--color-error);
border-inline-start-width: 3px;
}
.field[aria-invalid="true"] + .error-message::before {
content: "\26A0\FE0F "; /* warning sign */
}
/* Links — color + underline */
.prose a {
color: var(--color-primary);
text-decoration: underline;
text-underline-offset: 0.15em;
}
Ensure a proper .visually-hidden / .sr-only utility exists:
.visually-hidden {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
/* BAD — content removed from accessibility tree */
.hide { display: none; }
.hide { visibility: hidden; }
/* BAD — incomplete hiding, may cause layout shifts */
.hide { font-size: 0; }
.hide { text-indent: -9999px; }
Verify a skip link exists as the first focusable element:
.skip-link {
position: absolute;
inset-inline-start: 0;
inset-block-start: 0;
padding: var(--space-s) var(--space-m);
background: var(--color-surface);
color: var(--color-text);
transform: translateY(-100%);
transition: transform 150ms ease;
z-index: 9999;
&:focus-visible {
transform: translateY(0);
}
}
It should link to #main-content or the primary content landmark.
order property that makes visual order differ from DOM orderflex-direction: row-reverse or column-reverse breaking tab orderposition: absolute pulling content out of flow in a way that breaks logical orderVisual order should match DOM order for keyboard navigation. If order or visual reordering is used, ensure tabindex or ARIA attributes compensate. Flag any case where CSS reordering creates a mismatch.
/* WARNING: This changes visual order but not tab order */
.nav { display: flex; flex-direction: row-reverse; }
/* If needed, use reading-flow (emerging) or restructure the DOM */
Produce a structured audit report:
CSS Accessibility Audit Report
==============================
CRITICAL (must fix):
- [FOCUS] outline: none found in reset.css:12 — no replacement provided
- [MOTION] 6 animations have no prefers-reduced-motion fallback
- [CONTRAST] .text-muted has 2.8:1 ratio (needs 4.5:1)
WARNINGS (should fix):
- [TOUCH] .icon-btn is 32x32px (needs 44x44px minimum)
- [COLOR] .status-dot uses color only — add shape/text indicator
- [ORDER] .nav uses flex-direction: row-reverse — verify tab order
PASSING:
- [FOCUS-VISIBLE] ✓ Global :focus-visible styles present
- [SKIP-LINK] ✓ Skip link found
- [VISUALLY-HIDDEN] ✓ Utility class present
- [FORCED-COLORS] ✓ forced-colors media query present
FILES REVIEWED: 8
ISSUES FOUND: 9 critical, 3 warnings
outline: none without a visible replacementprefers-reduced-motion is non-negotiable — every animation needs itoutline, not just box-shadow)display: none for visually-hidden content — it removes from a11y treetools
理解用户意图;listen 模式通过 grill-me 深挖任务并归档经验
development
给 VSCode(Insiders/Stable) 内置 ripgrep 加 5s 超时包装,防止搜索卡死吃满 CPU。--on 包装(幂等) / --off 还原 / 不带参数查状态。Use when VSCode 搜索卡死、rg 进程占满 CPU,或 VSCode 更新后超时保护失效需要重包。
development
为指定项目创建完全隔离的 hapi(Claude Code On the Go)实例,包括独立数据目录、LaunchAgent 持久化、zsh wrapper 和 app.hapi.run 直连 URL。与全局 ~/.hapi 互不干扰。
development
关闭承载当前 Claude Code 会话的宿主(VSCode 窗口或其终端面板),连同 claude 一起退出。通过 close-host-window 扩展触发:先 SIGTERM claude(走完 SessionEnd hooks)再关 host,不抢焦点、不丢 hooks。--host vscode 关窗口、--host terminal(默认)只关终端面板。Use when 任务结束要随宿主退出,或需精确关闭某 claude 终端所在窗口/面板。