web-accessibility
SKILL.md
Web Accessibility
Build interfaces that work for everyone. These are not optional enhancements — they are baseline quality.
Semantic HTML
Use the right element for the job. Never simulate interactive elements with <div>.
// BAD: div with click handler
<div onClick={handleClick} className="button">Submit</div>
// GOOD: semantic button
<button onClick={handleClick}>Submit</button>
// BAD: div as link
<div onClick={() => router.push('/about')}>About</div>
// GOOD: anchor/Link for navigation
<Link href="/about">About</Link>
Element Selection Guide
| Purpose | Element | Not |
|---|---|---|
| Action (submit, toggle, delete) | <button> |
<div onClick> |
| Navigation to URL | <a> / <Link> |
<button onClick={navigate}> |
| Form input | <input>, <select>, <textarea> |
Custom div-based inputs |
| Section heading | <h1>–<h6> (sequential) |
<div className="heading"> |
| List of items | <ul> / <ol> + <li> |
Repeated <div> |
| Navigation group | <nav> |
<div className="nav"> |
| Main content | <main> |
<div id="content"> |
Keyboard Navigation
Every interactive element must be keyboard accessible.
Focus Management
/* NEVER remove focus indicators without replacement */
/* BAD */
*:focus { outline: none; }
/* GOOD: Visible focus only on keyboard navigation */
.interactive:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
/* Group focus for compound controls */
.input-group:focus-within {
outline: 2px solid var(--color-accent);
}
Keyboard Event Handling
// Interactive custom elements need keyboard support
function CustomButton({ onClick, children }: { onClick: () => void; children: React.ReactNode }) {
return (
<div
role="button"
tabIndex={0}
onClick={onClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClick();
}
}}
>
{children}
</div>
);
}
// Better: just use <button> and avoid all of the above
Skip Links
Provide skip navigation for keyboard users.
<a href="#main-content" className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50">
Skip to main content
</a>
Headings used as scroll targets need offset for fixed headers:
[id] { scroll-margin-top: 5rem; }
ARIA Patterns
Icon Buttons
// Icon-only buttons MUST have aria-label
<button aria-label="Close dialog" onClick={onClose}>
<XIcon aria-hidden="true" />
</button>
// Decorative icons are hidden from screen readers
<span aria-hidden="true">🔒</span> Secure connection
Live Regions
Announce dynamic content changes to screen readers.
// Toast notifications
<div role="status" aria-live="polite">
{notification && <p>{notification.message}</p>}
</div>
// Error alerts
<div role="alert" aria-live="assertive">
{error && <p>{error.message}</p>}
</div>
Loading States
<button disabled={isLoading} aria-busy={isLoading}>
{isLoading ? 'Saving\u2026' : 'Save'} {/* proper ellipsis character */}
</button>
// Skeleton screens
<div aria-busy="true" aria-label="Loading content">
<Skeleton />
</div>
Forms
Labels
Every input must have an associated label.
// GOOD: Explicit association
<label htmlFor="email">Email</label>
<input id="email" type="email" autoComplete="email" />
// GOOD: Wrapping (clickable label, no htmlFor needed)
<label>
Email
<input type="email" autoComplete="email" />
</label>
// GOOD: Visually hidden but accessible
<label htmlFor="search" className="sr-only">Search</label>
<input id="search" type="search" placeholder="Search..." />
Input Types and Autocomplete
Use semantic input types to get the right mobile keyboard and browser behavior.
<input type="email" autoComplete="email" />
<input type="tel" autoComplete="tel" />
<input type="url" autoComplete="url" />
<input type="password" autoComplete="current-password" />
<input type="password" autoComplete="new-password" />
Validation and Errors
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && (
<p id="email-error" role="alert" className="text-red-600">
{errors.email}
</p>
)}
</div>
Form Behavior Rules
- Never prevent paste on any input
- Disable spellcheck on emails and codes:
spellCheck={false} - Submit button stays enabled until request starts; show spinner during loading
- Focus first error on submit failure
- Checkboxes and radio buttons: single hit target, no dead zones between label and input
Images and Media
// Informative images: descriptive alt
<img src="chart.png" alt="Revenue grew 40% from Q1 to Q3 2025" />
// Decorative images: empty alt
<img src="divider.svg" alt="" />
// Prevent layout shift: always set dimensions
<img src="photo.jpg" width={800} height={600} alt="Team photo" />
// Below fold: lazy load
<img src="photo.jpg" loading="lazy" alt="..." />
// Critical: prioritize
<img src="hero.jpg" fetchPriority="high" alt="..." />
Touch and Mobile
Touch Targets
Minimum 44x44px for all interactive elements (WCAG 2.5.5).
.touch-target {
min-height: 44px;
min-width: 44px;
}
Safe Areas
Handle device notches and home indicators.
.full-bleed {
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}
Touch Behavior
/* Prevent 300ms delay and highlight flash */
.interactive {
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
/* Prevent scroll chaining on modals */
.modal { overscroll-behavior: contain; }
Internationalization
// BAD: Hardcoded formats
const date = `${month}/${day}/${year}`;
const price = `$${amount.toFixed(2)}`;
// GOOD: Locale-aware formatting
const date = new Intl.DateTimeFormat(locale).format(new Date());
const price = new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'USD',
}).format(amount);
// Detect language
const lang = request.headers.get('accept-language')?.split(',')[0] ?? 'en';
Performance for Accessibility
- Lists > 50 items: virtualize
- Critical fonts:
<link rel="preload" as="font">withfont-display: swap - Avoid layout reads during render (causes jank for screen reader users too)
- Uncontrolled inputs perform better than controlled for large forms
Checklist
Use this for review:
- All interactive elements are keyboard accessible
- Focus indicators are visible on
:focus-visible - Color contrast meets WCAG AA (4.5:1 body, 3:1 large text)
- Images have appropriate alt text
- Form inputs have labels
- Error messages are associated with inputs via
aria-describedby - Icon-only buttons have
aria-label - Dynamic content uses
aria-liveregions - Touch targets are minimum 44x44px
-
prefers-reduced-motionis respected - No
outline: nonewithout replacement focus style - Heading hierarchy is sequential (no skipped levels)
Weekly Installs
9
Repository
s-hiraoku/synapse-a2aFirst Seen
9 days ago
Security Audits
Installed on
amp9
gemini-cli9
claude-code9
github-copilot9
codex9
kimi-cli9