Accessibility validator
Skill smicolon/ai-kit/packs/nextjs/skills/accessibility-validator
Convention packs for any AI coding tool - agents, skills, commands, and rules for 15 tools including Claude Code, Cursor, Windsurf, and Copilot
npx -y skills add smicolon/ai-kit --skill accessibility-validatorAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 6 stars6 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Automatically validate React/Next.js components meet WCAG 2.1 AA standards. Use when creating components, forms, buttons, modals, navigation, or any interactive UI elements.
SKILL.md
10.1 KB, as published. Nobody here has run it
Accessibility Validator
Auto-enforces WCAG 2.1 AA accessibility standards for all React/Next.js components.
Activation Triggers
This skill activates when:
- Creating React/Next.js components
- Building forms, buttons, modals, navigation
- Adding interactive elements
- Mentioning "component", "UI", "form", "button"
- Creating pages or layouts
- Working on user-facing features
WCAG 2.1 AA Requirements (MANDATORY)
All components MUST meet:
- ✅ Keyboard navigation - All interactive elements focusable and usable via keyboard
- ✅ Semantic HTML - Proper HTML elements and ARIA attributes
- ✅ Color contrast - 4.5:1 for normal text, 3:1 for large text
- ✅ Screen reader support - All content accessible to assistive technologies
- ✅ Focus management - Visible focus indicators, logical tab order
- ✅ Form accessibility - Labels, error announcements, help text
Auto-Validation Process
Step 1: Detect Component Creation
When detecting component being written:
// Component without accessibility
function LoginButton({ onClick }) {
return <div onClick={onClick}>Login</div> // ❌ Multiple issues!
}
Step 2: Run Accessibility Checklist
Verify:
- ❌ Using div instead of button (semantic HTML)
- ❌ No keyboard support (only onClick, no onKeyDown)
- ❌ Not focusable (div not in tab order)
- ❌ No ARIA attributes
- ❌ No focus styles
Step 3: Auto-Fix Violations
Before (Inaccessible):
function LoginButton({ onClick }) {
return <div onClick={onClick}>Login</div>
}
After (Accessible):
function LoginButton({ onClick }) {
return (
<button
onClick={onClick}
type="button"
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
Login
</button>
)
}
Step 4: Explain Fixes
Accessibility Violations Fixed
Changes:
- ✅ Changed
<div>to<button>(semantic HTML)- ✅ Added
type="button"(prevents form submission)- ✅ Button is now keyboard accessible (Enter/Space keys work)
- ✅ Automatically focusable (in tab order)
- ✅ Added visible focus ring (focus:ring-2)
Why:
- Buttons are natively accessible to keyboard and screen readers
- Focus ring shows keyboard users where they are
- Proper semantics help assistive technologies
Common Accessibility Patterns
Pattern 1: Buttons vs Links
// ❌ WRONG - div as button
<div onClick={handleClick}>Click me</div>
// ❌ WRONG - link as button
<a href="#" onClick={handleClick}>Click me</a>
// ✅ CORRECT - button for actions
<button onClick={handleClick} type="button">
Click me
</button>
// ✅ CORRECT - link for navigation
<Link href="/page">Go to page</Link>
Pattern 2: Form Labels
// ❌ WRONG - no label
<input type="email" placeholder="Email" />
// ❌ WRONG - placeholder as label (insufficient)
<input type="email" placeholder="Enter your email" />
// ✅ CORRECT - explicit label
<label htmlFor="email" className="block text-sm font-medium">
Email Address
</label>
<input
id="email"
type="email"
className="mt-1 block w-full rounded-md border-gray-300"
aria-required="true"
/>
// ✅ CORRECT - with error handling
<label htmlFor="email" className="block text-sm font-medium">
Email Address
</label>
<input
id="email"
type="email"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? "email-error" : undefined}
/>
{errors.email && (
<p id="email-error" className="mt-1 text-sm text-red-600" role="alert">
{errors.email.message}
</p>
)}
Pattern 3: Modal Dialogs
// ✅ CORRECT - Accessible modal
import { Dialog } from '@headlessui/react'
function Modal({ isOpen, onClose, title, children }) {
return (
<Dialog
open={isOpen}
onClose={onClose}
className="relative z-50"
>
{/* Backdrop */}
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
{/* Modal */}
<div className="fixed inset-0 flex items-center justify-center p-4">
<Dialog.Panel className="bg-white rounded-lg p-6 max-w-md">
<Dialog.Title className="text-lg font-semibold">
{title}
</Dialog.Title>
<div className="mt-4">
{children}
</div>
<button
onClick={onClose}
className="mt-4 px-4 py-2 bg-gray-200 rounded"
aria-label="Close dialog"
>
Close
</button>
</Dialog.Panel>
</div>
</Dialog>
)
}
Pattern 4: Icon Buttons
// ❌ WRONG - no accessible name
<button>
<XIcon />
</button>
// ✅ CORRECT - with aria-label
<button aria-label="Close" type="button">
<XIcon className="h-5 w-5" aria-hidden="true" />
</button>
// ✅ CORRECT - with visually hidden text
<button type="button" className="relative">
<span className="sr-only">Close</span>
<XIcon className="h-5 w-5" aria-hidden="true" />
</button>
Pattern 5: Loading States
// ❌ WRONG - no screen reader announcement
{isLoading && <Spinner />}
// ✅ CORRECT - with live region
{isLoading && (
<div role="status" aria-live="polite">
<Spinner />
<span className="sr-only">Loading...</span>
</div>
)}
Pattern 6: Images
// ❌ WRONG - no alt text
<img src="/logo.png" />
// ✅ CORRECT - with alt text
<img src="/logo.png" alt="Company Logo" />
// ✅ CORRECT - decorative image
<img src="/decoration.png" alt="" aria-hidden="true" />
Keyboard Navigation Checklist
For every component, verify:
- ✅ All interactive elements are focusable (button, a, input, etc.)
- ✅ Tab order is logical (follows visual flow)
- ✅ Focus is visible (outline, ring, or custom indicator)
- ✅ No keyboard traps (can escape from all UI)
- ✅ Enter/Space work on buttons
- ✅ Escape closes modals/dropdowns
- ✅ Arrow keys work in lists/menus
Color Contrast Requirements
Check all text meets minimum contrast:
// ❌ WRONG - insufficient contrast (2.5:1)
<p className="text-gray-400 bg-white">Low contrast text</p>
// ✅ CORRECT - good contrast (7:1)
<p className="text-gray-900 bg-white">High contrast text</p>
// ✅ CORRECT - large text can be 3:1
<h1 className="text-2xl text-gray-600 bg-white">Large heading</h1>
Minimum Ratios:
- Normal text (< 18pt): 4.5:1
- Large text (≥ 18pt or bold ≥ 14pt): 3:1
- UI components: 3:1
Screen Reader Support
Landmark Regions
// ✅ CORRECT - proper landmarks
<header role="banner">
<nav role="navigation" aria-label="Main navigation">
{/* nav items */}
</nav>
</header>
<main role="main">
{/* main content */}
</main>
<aside role="complementary" aria-label="Related content">
{/* sidebar */}
</aside>
<footer role="contentinfo">
{/* footer */}
</footer>
ARIA Labels
// ✅ CORRECT - descriptive labels
<button aria-label="Add item to cart">
<PlusIcon aria-hidden="true" />
</button>
<nav aria-label="Breadcrumb">
<ol>
<li><a href="/">Home</a></li>
<li aria-current="page">Products</li>
</ol>
</nav>
Live Regions
// ✅ CORRECT - announce changes
<div role="alert" aria-live="assertive">
Error: Please fill in all required fields
</div>
<div role="status" aria-live="polite">
5 items in cart
</div>
Focus Management
'use client'
import { useEffect, useRef } from 'react'
function Modal({ isOpen, title }) {
const titleRef = useRef<HTMLHeadingElement>(null)
// Focus title when modal opens
useEffect(() => {
if (isOpen && titleRef.current) {
titleRef.current.focus()
}
}, [isOpen])
return (
<div role="dialog" aria-modal="true">
<h2 ref={titleRef} tabIndex={-1} className="outline-none">
{title}
</h2>
{/* content */}
</div>
)
}
Testing Recommendations
Suggest automated tests:
import { render } from '@testing-library/react'
import { axe, toHaveNoViolations } from 'jest-axe'
expect.extend(toHaveNoViolations)
test('LoginButton has no accessibility violations', async () => {
const { container } = render(<LoginButton onClick={() => {}} />)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
Common Violations Checked
Missing Labels
- Form inputs without labels
- Icon buttons without aria-label
- Images without alt text
Poor Semantics
- Divs instead of buttons
- Links instead of buttons
- Missing heading hierarchy
Keyboard Issues
- Elements not in tab order
- Missing focus indicators
- Keyboard traps
Screen Reader Issues
- Missing ARIA labels
- Incorrect ARIA roles
- No live region announcements
Color Issues
- Insufficient contrast
- Color-only information
- Missing text alternatives
Integration with Tailwind
// Utility class for screen reader only text
<span className="sr-only">Accessible description</span>
// Focus ring utilities
<button className="focus:outline-none focus:ring-2 focus:ring-blue-500">
Click me
</button>
// High contrast text
<p className="text-gray-900 dark:text-gray-100">
Good contrast in both modes
</p>
Success Criteria
✅ All interactive elements keyboard accessible ✅ All images have alt text ✅ All forms have labels ✅ Color contrast ≥ 4.5:1 (normal text) ✅ Proper semantic HTML used ✅ ARIA attributes where needed ✅ Focus indicators visible ✅ Screen reader tested ✅ Automated axe tests pass
Behavior
Proactive enforcement:
- Check accessibility without being asked
- Fix violations immediately
- Add ARIA attributes automatically
- Explain WCAG criteria for each fix
- Suggest automated tests
Never:
- Require explicit "check accessibility" request
- Wait for accessibility audit
- Just warn without fixing
Block completion if:
- Buttons are divs
- Forms lack labels
- Color contrast fails
- Icon buttons lack accessible names
- Images missing alt text
This ensures every component is accessible from day one.