cosmicstack-labs/accessibility
Achieve and maintain WCAG compliance through inclusive design practices, proper ARIA usage, and comprehensive testing methodologies.
npx skills add https://github.com/cosmicstack-labs/mercury-agent-skills --skill accessibility
Digital accessibility is not a compliance checkbox — it's about ensuring everyone can use the web regardless of ability. Approximately 15% of the world's population experiences some form of disability. Building accessible products is the ethical choice, not just a legal requirement.
Accessibility must be considered from the first wireframe, not discovered in a last-minute audit before launch. Retrofitting accessibility costs significantly more than building it in from the start. Include a11y in design critiques, sprint planning, and definition of done.
Design with, not for, people with disabilities. Test with real users who rely on assistive technologies. Automated tools catch only 30-40% of accessibility issues — human testing catches the rest. Include disabled people in your user research and usability testing.
Curb cuts benefit everyone, not just wheelchair users. Captions help people in noisy environments. High contrast helps on sunny days. Keyboard navigation helps power users. Accessibility features often become mainstream UX improvements.
Level AAA compliance is aspirational and not always achievable. Level AA is the practical target for most products. Don't let perfect be the enemy of good — ship AA-compliant work and document remaining issues for future iterations.
Accessibility is not considered at all. No guidelines, no testing, no training. Accessibility issues are found only when legal complaints arise. This is the starting point — and a liability.
Accessibility is addressed reactively — when a bug is filed, an audit is demanded, or a legal threat emerges. Fixes are firefighting exercises. No systemic improvements occur. Burnout is high among any team members advocating for a11y.
Basic accessibility practices are followed. Designers check color contrast. Developers use semantic HTML. Automated tools (axe, Lighthouse) run in CI. But there's no dedicated owner or formal process. Consistency varies across teams.
Accessibility has a dedicated owner or team. WCAG 2.1 AA is the standard for all new work. Manual testing with screen readers is part of QA. Accessibility is in the definition of done. Training is provided to all designers and developers.
Accessibility is part of organizational culture. Every role owns accessibility — product managers, designers, engineers, QA, content writers. Users with disabilities are part of the research and testing process. The organization contributes back to the accessibility community.
WCAG 2.1 is organized around four principles. If any principle is broken, users with certain disabilities cannot access the content at all.
Users must be able to perceive the information being presented. It cannot be invisible to all of their senses.
Guideline 1.1: Text Alternatives
<img src="chart.png" alt="Sales increased 25% in Q4 2025"><img src="divider.png" alt="" role="presentation"><img src="chart.png">Guideline 1.2: Time-based Media
Guideline 1.3: Adaptable
<nav>, <main>, <aside>, <header>, <footer> — don't use <div class="nav">Guideline 1.4: Distinguishable
User interface components and navigation must be operable. The interface cannot require interaction that a user cannot perform.
Guideline 2.1: Keyboard Accessible
Guideline 2.4: Navigable
Guideline 2.5: Input Modalities
Information and the operation of the user interface must be understandable.
Guideline 3.1: Readable
<html lang="en">Guideline 3.2: Predictable
Guideline 3.3: Input Assistance
Content must be robust enough that it can be interpreted reliably by a wide variety of user agents, including assistive technologies.
Guideline 4.1: Compatible
aria-live)| Level | Standard | What's Expected |
|---|---|---|
| A | Minimum | Essential support — removes major barriers. All Level A criteria must be met. |
| AA | Target | Acceptable for most organizations and legal requirements. All Level A + AA criteria. |
| AAA | Aspirational | Highest level — may not be achievable for all content. All Level A + AA + AAA criteria. |
Legal context: Most accessibility lawsuits and regulations (Section 508, EN 301 549, ADA) reference WCAG 2.1 Level AA as the standard.
Use tools like WebAIM Contrast Checker, Stark (Figma), or @axe-core/cli to verify.
/* Good contrast */
.button-primary {
background: #0052CC; /* Blue */
color: #FFFFFF; /* White — 5.2:1 ratio ✅ */
}
/* Insufficient contrast */
.button-primary {
background: #6B9FFF; /* Light blue */
color: #FFFFFF; /* White — 2.8:1 ratio ❌ */
}
Every interactive element must be reachable and operable via keyboard:
| Key | Expected Behavior |
|---|---|
| Tab | Move focus forward through interactive elements |
| Shift + Tab | Move focus backward |
| Enter / Space | Activate buttons, links, and controls |
| Arrow keys | Navigate within components (menus, tabs, lists, sliders) |
| Escape | Close modals, dropdowns, dismiss popups |
Focus indicators must never be removed — unless a custom focus style with sufficient contrast is provided. Never use outline: none without a replacement.
/* Acceptable custom focus style */
:focus-visible {
outline: 2px solid #0052CC;
outline-offset: 2px;
}
/* Never do this */
*:focus {
outline: none; /* ❌ Invisible focus — unusable for keyboard users */
}
aria-label or aria-labelledby when visual labels aren't sufficientaria-describedby for additional contextaria-hidden="true" or role="presentation"aria-live regions<!-- Accessible loading button -->
<button aria-busy="true" aria-label="Submitting form, please wait">
<span class="sr-only">Submitting...</span>
<span aria-hidden="true">⏳</span>
</button>
Respect the user's system preference for reduced motion:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
This should not disable essential motion (like a loading spinner that indicates progress) — provide alternative non-moving indicators.
ARIA should be used only when native HTML semantics are insufficient.
The First Rule of ARIA: If you can use a native HTML element or attribute that has the semantics and behavior you need, use it instead of repurposing an element and adding ARIA.
<!-- ❌ Bad: div with ARIA instead of native button -->
<div role="button" tabindex="0" onclick="submit()">Submit</div>
<!-- ✅ Good: Native button (free keyboard support, form behavior, semantics) -->
<button type="submit">Submit</button>
Roles define what an element is or does:
navigation, main, complementary, contentinfo, banner, searchbutton, link, tab, tabpanel, dialog, tooltip, progressbarheading, list, listitem, table, row, cellalert, status, log, marquee, timeraria-expanded: Indicates whether a collapsible element is open (boolean)aria-pressed: Indicates toggle button state (tri-state)aria-current: Indicates the current item in a set (page, step, location, date, time, true)aria-selected: Indicates current selected item in a list/gridaria-hidden: Hides elements from the accessibility treearia-live: Announces dynamic content changes (off, polite, assertive)aria-atomic: Specifies whether to announce the entire live region or just changed partsaria-relevant: What types of changes are relevant (additions, removals, text, all)Modal Dialog:
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title" aria-describedby="dialog-desc">
<h2 id="dialog-title">Confirm Delete</h2>
<p id="dialog-desc">This action cannot be undone.</p>
<button>Cancel</button>
<button>Delete</button>
</div>
Tab Panel:
<div role="tablist" aria-label="Documentation">
<button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1" tabindex="0">Overview</button>
<button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2" tabindex="-1">API</button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1" tabindex="0">
<!-- Content -->
</div>
Automated tools catch ~30-40% of accessibility issues. They're great for catching low-hanging fruit but cannot replace manual testing.
| Tool | What It Checks | Use Case |
|---|---|---|
| axe-core (axe DevTools) | WCAG violations, best practices | CI/CD pipeline, local dev |
| WAVE | Visual overlay of a11y issues | Quick page audits |
| Lighthouse | Performance + a11y score | Reporting, regression |
| Pa11y | CI/CD integration | Automated regression testing |
| Accessibility Insights | Automated + guided manual tests | Comprehensive audits |
CI Integration Example (using axe-core with Playwright):
const { injectAxe, checkAxe } = require('axe-playwright');
test('Homepage has no accessibility violations', async ({ page }) => {
await page.goto('/');
await injectAxe(page);
const results = await checkAxe(page);
expect(results.violations).toHaveLength(0);
});
Automated tools miss context-dependent issues. Manual testing catches the rest.
Checklist for Manual Testing:
Test with real screen readers — each behaves differently:
| Screen Reader | Platform | Browser |
|---|---|---|
| VoiceOver | macOS, iOS | Safari |
| NVDA | Windows | Firefox |
| JAWS | Windows | Chrome, Edge |
| TalkBack | Android | Chrome |
| Orca | Linux | Firefox |
Testing checklist:
Allow keyboard users to bypass repetitive navigation:
<a href="#main-content" class="skip-link">Skip to main content</a>
/* Style */
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #0052CC;
color: white;
padding: 8px;
z-index: 9999;
}
.skip-link:focus {
top: 0;
}
When content changes dynamically (modals, navigation, async updates), manage focus intentionally:
function openModal(modalElement) {
modalElement.showModal(); // Using native <dialog> — focus management is built-in
// For custom modals:
focusTrap(modalElement); // Trap focus within modal
modalElement.querySelector('[autofocus]')?.focus();
}
function closeModal(modalElement, triggerButton) {
modalElement.close();
triggerButton.focus(); // Return focus to the element that opened it
}
Announce dynamic content changes without moving the user's focus:
<div aria-live="polite" aria-atomic="true" class="sr-only">
<!-- Screen reader announces this content when it changes -->
</div>
aria-live="polite" — Announce when idle (for non-critical updates)aria-live="assertive" — Announce immediately (for errors, critical alerts)<form novalidate>
<label for="email">Email address</label>
<input
type="email"
id="email"
aria-describedby="email-error"
aria-invalid="true"
required
/>
<span id="email-error" role="alert">
Please enter a valid email address (e.g., [email protected])
</span>
</form>
| Criteria | Novice (1) | Competent (2) | Proficient (3) | Expert (4) |
|---|---|---|---|---|
| WCAG Knowledge | Unaware of WCAG | Knows A vs. AA | Knows POUR principles | Can recite specific success criteria |
| Color Contrast | Not checked | Manual checks some text | Verified all text + UI components | Token-based contrast enforcement |
| Keyboard Support | Mouse-only interactions | Basic Tab navigation | Full keyboard patterns + focus visible | Advanced patterns (arrow nav, typeahead) |
| Screen Reader | Not tested | Basic alt text | Navigates with headings/landmarks | Full ARIA patterns + live regions |
| ARIA Usage | No ARIA used | ARIA on everything (overused) | ARIA used appropriately | ARIA used only when HTML insufficient |
| Testing | No testing | Automated tools only | Automated + manual keyboard | Full suite: auto + manual + screen reader + real users |
| Documentation | None | Basic checklist | WCAG alignment documented | Full VPAT / ACR |
WCAG success criteria are minimum standards, not a complete definition of accessibility. Checking boxes doesn't guarantee a usable experience. Real accessibility requires empathy, testing with real users, and ongoing commitment.
The most common and most damaging mistake. outline: none on focus removes the only way keyboard users can track their position on the page. Always provide a visible, high-contrast focus indicator.
Using ARIA where native HTML would suffice. Adding role="button" to a <div> instead of using <button>. Native elements come with built-in keyboard support, form semantics, and accessibility tree mappings. Don't reinvent what browsers already provide.
Automated tools catch syntax and structural issues but miss context-dependent problems. A form can pass all automated checks and still be unusable with a screen reader because instructions are unclear. Always test manually.
Using color alone to convey status, error states, or active states. Users with color blindness cannot distinguish red/green indicators. Always use icons, text, or patterns alongside color.
<!-- ❌ Color only -->
<span style="color: red">Error</span>
<!-- ✅ Color + icon + text -->
<span style="color: red">
<span aria-hidden="true">⚠</span>
<span>Error: Connection failed</span>
</span>
Adding animations without respecting prefers-reduced-motion. This can cause nausea, dizziness, and vestibular issues for users with motion sensitivity. Always provide a reduced motion alternative.
Using placeholder as a substitute for <label>. Placeholders disappear when the user types, have poor contrast by default, and are not reliably announced by screen readers. Always use explicit <label> elements.
<!-- ❌ Placeholder as label -->
<input type="email" placeholder="Enter your email" />
<!-- ✅ Proper label -->
<label for="email">Email address</label>
<input type="email" id="email" />
Simulating a screen reader experience does not replicate the real thing. A VoiceOver user navigates differently than an NVDA user. Test with at least two different screen reader + browser combinations.
Hiding skip links so they're never keyboard-reachable. Many implementations hide skip links permanently or collapse them to 0 height. Skip links must be accessible when focused via keyboard.
Focusing exclusively on visual and motor disabilities while ignoring cognitive accessibility. Use plain language, consistent navigation, clear error messages, and give users enough time to complete tasks. Cognitive accessibility affects the largest segment of users with disabilities.
Take cosmicstack-labs/accessibility from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.