Agent skill
accessibility
Accessibility (a11y) patterns for this Next.js application. Covers WCAG 2.1 AA compliance, ARIA attributes, keyboard navigation, focus management, and screen reader support. Use this skill when implementing accessible UI components or validating accessibility requirements.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/accessibility-nextspark-js-nextspark
SKILL.md
Accessibility Skill
Patterns for implementing WCAG 2.1 Level AA compliant components with proper ARIA attributes, keyboard navigation, and screen reader support.
Architecture Overview
ACCESSIBILITY LAYERS:
Semantic HTML Foundation:
├── <button> for buttons (not <div>)
├── <nav> for navigation regions
├── <form> and <fieldset> for forms
├── <a> for links
└── Heading hierarchy (h1-h6)
ARIA Enhancement:
├── Landmark roles (navigation, main, complementary)
├── State attributes (aria-expanded, aria-current)
├── Relationship attributes (aria-labelledby, aria-describedby)
└── Live regions (aria-live, role="alert")
Visual Accessibility:
├── Color contrast (4.5:1 minimum)
├── Focus indicators (focus-visible:ring-2)
├── Motion reduction (prefers-reduced-motion)
└── Text scaling support
Keyboard Accessibility:
├── Tab order management
├── Focus trapping in modals
├── Escape key handling
└── Arrow key navigation (where appropriate)
When to Use This Skill
- Implementing interactive UI components
- Adding ARIA attributes to custom elements
- Validating accessibility requirements
- Ensuring keyboard navigation works
- Testing with screen readers
WCAG Compliance Standards
Level AA Requirements (Minimum)
| Criterion | Requirement | Implementation |
|---|---|---|
| 1.4.3 Color Contrast | 4.5:1 for normal text, 3:1 for large text | Use semantic color tokens |
| 1.4.11 Non-text Contrast | 3:1 for UI components | Focus rings, borders |
| 2.1.1 Keyboard | All functionality via keyboard | Tab navigation, Enter/Space |
| 2.4.3 Focus Order | Logical focus sequence | DOM order, tabIndex |
| 2.4.7 Focus Visible | Visible focus indicator | focus-visible:ring-2 |
| 4.1.2 Name, Role, Value | Accessible names for elements | Labels, aria-label |
POUR Principles
- Perceivable: Content visible to all users (contrast, alt text)
- Operable: Interface usable via keyboard and assistive tech
- Understandable: Clear, predictable interface
- Robust: Compatible with current and future assistive tech
ARIA Attribute Patterns
Landmark Roles
// Navigation with accessible label
<nav role="navigation" aria-label="pagination">
<ul>
<li><a href="/page/1">1</a></li>
<li><a href="/page/2" aria-current="page">2</a></li>
</ul>
</nav>
// Breadcrumb navigation
<nav aria-label="breadcrumb">
<ol>
<li><a href="/">Home</a></li>
<li aria-current="page">Products</li>
</ol>
</nav>
// Main content area
<main role="main" id="main-content">
{/* Page content */}
</main>
Dynamic State Attributes
// Accordion/Collapsible
<button
aria-expanded={isOpen}
aria-controls={`content-${id}`}
onClick={() => setIsOpen(!isOpen)}
>
Section Title
</button>
<div
id={`content-${id}`}
aria-hidden={!isOpen}
hidden={!isOpen}
>
{/* Collapsible content */}
</div>
// Tabs
<div role="tablist" aria-label="Settings tabs">
<button
role="tab"
aria-selected={activeTab === 'general'}
aria-controls="panel-general"
tabIndex={activeTab === 'general' ? 0 : -1}
>
General
</button>
</div>
<div
role="tabpanel"
id="panel-general"
aria-labelledby="tab-general"
hidden={activeTab !== 'general'}
>
{/* Tab content */}
</div>
// Menu/Dropdown
<button
aria-haspopup="menu"
aria-expanded={isOpen}
aria-controls="menu-items"
>
Options
</button>
<div
id="menu-items"
role="menu"
aria-hidden={!isOpen}
>
<button role="menuitem">Edit</button>
<button role="menuitem">Delete</button>
</div>
Form Accessibility
// core/components/ui/form.tsx pattern
<FormItem>
<FormLabel htmlFor={formItemId}>
Email
</FormLabel>
<FormControl>
<Input
id={formItemId}
aria-describedby={
!error
? formDescriptionId
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
aria-required={required}
/>
</FormControl>
<FormDescription id={formDescriptionId}>
We'll never share your email.
</FormDescription>
{error && (
<FormMessage id={formMessageId} role="alert">
{error.message}
</FormMessage>
)}
</FormItem>
Live Regions
// Status announcements (polite)
<div aria-live="polite" aria-atomic="true" className="sr-only">
{statusMessage}
</div>
// Error alerts (assertive)
<div role="alert" className="text-destructive">
{errorMessage}
</div>
// Toast notifications
<div
role="status"
aria-live="polite"
aria-atomic="true"
>
{notification}
</div>
Keyboard Navigation Patterns
Focus Indicators
All interactive elements use the standardized focus ring:
// Button focus pattern
<button
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
Click me
</button>
// Input focus pattern
<input
className="focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
/>
// Custom focus indicator
<div
tabIndex={0}
className="focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
handleAction()
}
}}
>
Interactive element
</div>
Tab Order Management
// Logical tab order (follows DOM)
<header>
<nav tabIndex={0}>...</nav> {/* First */}
</header>
<main tabIndex={0}>...</main> {/* Second */}
<aside tabIndex={0}>...</aside> {/* Third */}
// Remove from tab order
<div tabIndex={-1}>
{/* Programmatically focusable but not in tab order */}
</div>
// Skip link pattern
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:bg-background focus:px-4 focus:py-2"
>
Skip to main content
</a>
Focus Trap (Modals)
Radix UI handles focus trapping automatically in dialogs:
// core/components/ui/dialog.tsx
import * as DialogPrimitive from '@radix-ui/react-dialog'
// Radix Dialog automatically:
// - Traps focus within modal
// - Returns focus on close
// - Handles Escape key
// - Manages aria-hidden on background
<DialogPrimitive.Root>
<DialogPrimitive.Trigger>Open</DialogPrimitive.Trigger>
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay />
<DialogPrimitive.Content>
{/* Focus is trapped here */}
<DialogPrimitive.Close>
<span className="sr-only">Close</span>
<X aria-hidden="true" />
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
</DialogPrimitive.Root>
Screen Reader Support
Screen Reader Only Text
// Visually hidden but announced by screen readers
<span className="sr-only">Close dialog</span>
// Icon button with accessible name
<Button variant="ghost" size="icon">
<X aria-hidden="true" />
<span className="sr-only">Close</span>
</Button>
// More pages indicator
<span aria-hidden="true">...</span>
<span className="sr-only">More pages</span>
Decorative Icons
// Hide decorative icons from screen readers
<ChevronRight aria-hidden="true" className="h-4 w-4" />
// Separator in breadcrumb
<span role="presentation" aria-hidden="true">/</span>
// Loading spinner
<Loader2
aria-hidden="true"
className="h-4 w-4 animate-spin"
/>
<span className="sr-only">Loading...</span>
Accessible Names Strategy
// 1. Visible text (preferred)
<button>Delete Item</button>
// 2. aria-label for icon-only buttons
<button aria-label="Delete item">
<TrashIcon aria-hidden="true" />
</button>
// 3. aria-labelledby for complex labels
<div id="dialog-title">Confirm Deletion</div>
<div aria-labelledby="dialog-title">
{/* Content described by title */}
</div>
// 4. Form labels
<Label htmlFor="email">Email address</Label>
<Input id="email" type="email" />
Color Contrast
OKLCH Color System
The theme uses OKLCH color space for perceptually uniform colors:
/* contents/themes/default/styles/globals.css */
:root {
/* High contrast pairs */
--primary: oklch(0.2050 0 0); /* Dark */
--primary-foreground: oklch(0.9850 0 0); /* Light - 4.5:1+ contrast */
--destructive: oklch(0.5770 0.2450 27.3250);
--destructive-foreground: oklch(1 0 0);
/* Focus ring with sufficient contrast */
--ring: oklch(0.7080 0 0);
}
.dark {
/* Inverted for dark mode */
--primary: oklch(0.9220 0 0);
--primary-foreground: oklch(0.2050 0 0);
}
Semantic Color Usage
// ✅ CORRECT - Use semantic tokens that guarantee contrast
<p className="text-foreground">Primary text</p>
<p className="text-muted-foreground">Secondary text</p>
<button className="bg-primary text-primary-foreground">
Action
</button>
// ❌ WRONG - Arbitrary colors may not have proper contrast
<p className="text-gray-400">Low contrast text</p>
<button className="bg-blue-300 text-blue-100">
Poor contrast
</button>
Component Patterns
Slider/Range Component
// core/components/ui/double-range.tsx
<div
ref={thumbMinRef}
role="slider"
aria-label="Minimum value"
aria-valuemin={min}
aria-valuemax={max}
aria-valuenow={localValue[0]}
tabIndex={disabled ? -1 : 0}
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
onKeyDown={handleKeyDown} // Arrow keys to adjust
/>
Pagination Component
// core/components/ui/pagination.tsx
<nav role="navigation" aria-label="pagination">
<PaginationPrevious aria-label="Go to previous page" />
{pages.map((page) => (
<PaginationLink
aria-current={page === current ? 'page' : undefined}
>
{page}
</PaginationLink>
))}
<PaginationNext aria-label="Go to next page" />
</nav>
Alert Component
// core/components/ui/alert.tsx
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
Testing Accessibility
Tools
- ESLint Plugin:
eslint-plugin-jsx-a11yfor linting - Jest:
jest-axefor unit testing - Cypress:
cypress-axefor E2E testing - Manual: Keyboard navigation, screen reader testing
Jest-axe Example
import { axe, toHaveNoViolations } from 'jest-axe'
expect.extend(toHaveNoViolations)
test('should have no accessibility violations', async () => {
const { container } = render(<MyComponent />)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
Manual Testing Checklist
-
Keyboard Navigation:
- Tab through all interactive elements
- Verify logical focus order
- Escape key closes modals
- Enter/Space activates buttons
-
Screen Reader:
- VoiceOver (Mac) or NVDA (Windows)
- Verify content is announced correctly
- Check form labels and error messages
- Verify live regions announce updates
-
Visual:
- Check focus indicators are visible
- Verify contrast with browser devtools
- Test with zoom (200%)
Anti-Patterns
// ❌ NEVER: Div as button
<div onClick={handleClick}>Click me</div>
// ✅ CORRECT: Use semantic button
<button onClick={handleClick}>Click me</button>
// ❌ NEVER: Icon-only button without accessible name
<button><TrashIcon /></button>
// ✅ CORRECT: Add aria-label or sr-only text
<button aria-label="Delete item">
<TrashIcon aria-hidden="true" />
</button>
// ❌ NEVER: Remove focus indicator completely
<button className="outline-none focus:outline-none">
No focus indicator
</button>
// ✅ CORRECT: Custom focus indicator
<button className="focus-visible:ring-2 focus-visible:ring-ring">
Visible focus
</button>
// ❌ NEVER: Low contrast text
<p className="text-gray-300 bg-white">Hard to read</p>
// ✅ CORRECT: Use semantic tokens
<p className="text-muted-foreground bg-background">Readable</p>
// ❌ NEVER: Missing form labels
<input type="email" placeholder="Email" />
// ✅ CORRECT: Proper label association
<Label htmlFor="email">Email</Label>
<input id="email" type="email" />
// ❌ NEVER: Images without alt text
<img src="/logo.png" />
// ✅ CORRECT: Descriptive alt or empty for decorative
<img src="/logo.png" alt="Company Logo" />
<img src="/decorative.png" alt="" aria-hidden="true" />
// ❌ NEVER: Auto-playing media without controls
<video autoPlay src="/video.mp4" />
// ✅ CORRECT: User controls and captions
<video controls>
<source src="/video.mp4" />
<track kind="captions" src="/captions.vtt" />
</video>
Checklist
Before finalizing component accessibility:
- Semantic HTML elements used (button, nav, form, etc.)
- All interactive elements keyboard accessible
- Focus indicators visible (focus-visible:ring-2)
- ARIA attributes correct (aria-expanded, aria-controls, etc.)
- Form fields have associated labels
- Error messages use role="alert"
- Icons have aria-hidden="true"
- Icon-only buttons have aria-label or sr-only text
- Color contrast meets 4.5:1 (text) / 3:1 (UI)
- Screen reader text (.sr-only) where needed
- Tab order is logical
- Modals trap focus and return it on close
- Live regions announce dynamic content
Related Skills
shadcn-components- Accessible UI component patternsreact-patterns- Component architecturetailwind-theming- Color contrast tokenscypress-e2e- Accessibility testing
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?