Accessibility
Accessibility conformance statement
An honest account of what Cooud UI targets, how that target is enforced, and where the gaps are. It is a self-assessment (ACR-style), not a certification.
Target standard
Cooud UI is built to meet WCAG 2.2 Level AA.
The design goal for every component is Web Content Accessibility Guidelines (WCAG) 2.2, Level AA. This statement describes our conformance effort and the checks behind it. It is a self-assessment produced by the maintainers — not a formal audit, and not a certification. Cooud UI is a component library, so the final accessibility of a shipped product also depends on how you compose these components, your content, and your app.
Self-assessment, not a certificate
How it is enforced
Conformance is not a one-time review — it is gated on every run by layered automated and manual checks.
Automated axe gate
e2e/a11y runs axe-core over every component and block route and fails the build on any serious or critical WCAG A/AA violation.
Keyboard e2e
e2e/flows/keyboard.spec.ts drives dialogs, menus, tabs, selects, sliders, switches, and accordions with real key events and asserts focus and state.
Unit axe layer
Component tests assert zero axe violations with vitest-axe through @cooud-ui/ui/testing, so regressions are caught before a route exists.
Reduced motion & RTL
Animated primitives honour prefers-reduced-motion by default, and layout uses logical properties so components mirror correctly under dir=rtl.
The automated gate scans every /components/* and /blocks/* route with axe-core and fails the build on any serious or critical violation. It runs the WCAG 2.0 and 2.1 Level A/AA rule sets — the subset that can be checked automatically — so it does not, on its own, prove every WCAG 2.2 criterion.
// e2e/a11y/core-routes.a11y.spec.ts — the automated gate.// Runs the WCAG 2.0 + 2.1 Level A/AA rule sets (the automatable subset)// over every component and block route, generated from routes.generated.json.const results = await new AxeBuilder({ page }).withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]).analyze();// serious/critical are hard failures; moderate/minor are reported, not blocked.const blocking = results.violations.filter((v) => v.impact === "serious" || v.impact === "critical",);expect(blocking).toEqual([]);
Alongside it, the keyboard end-to-end suite drives the highest-value widgets with real key events and asserts focus location, aria-expanded, aria-checked, selected values, and focus return — the operability that a static scan cannot see.
What is supported
These behaviors are represented in components, and covered by the automated and keyboard gates.
- Keyboard operability follows the WAI-ARIA authoring pattern for each primitive.
- Focus is managed and trapped in overlays, and returns to the trigger on dismiss.
- Interactive elements expose an accessible name, role, and state.
- One skip link and exactly one <main> landmark per route (bypass blocks).
- Focus is visible via focus-visible in both dark and light presets.
- Semantic token pairs are authored to meet WCAG AA text contrast.
Known limitations & not-yet-covered
The honest half of a conformance statement. These are real gaps, stated plainly.
- Only serious and critical axe findings block; moderate and minor findings are reported, triaged, and fixed but do not fail the build.
- Automated scans cover the automatable subset of WCAG. jsdom cannot compute rendered contrast, so contrast is validated by the browser-based axe project and by authoring token pairs — not by the unit layer.
- The full-route axe gate scans every component and block page in the default theme (aurora, dark), where zero serious/critical violations is enforced route-by-route. A comprehensive per-theme and light-mode contrast pass is in progress and not yet fully gated — the docs code viewer already ships a mode-aware syntax palette, but some chrome and the neutral light preset still have contrast to tighten.
- WCAG 2.2 adds success criteria (for example target size and focus-not-obscured) that have little automated coverage; those are addressed through the component patterns and manual review rather than a passing rule.
- Components that wrap third-party libraries (charts, some date primitives) inherit their upstream accessibility; we add names and text alternatives around them but do not rewrite their internals.
- No formal third-party accessibility audit has been performed yet, and no VPAT has been issued.
- Screen-reader behaviour is verified by manual passes, not by an automated gate, so coverage depends on the reviewer.
Contrast is browser-gated, not unit-gated
For teams adopting Cooud UI
The accessibility you get depends on how you use the components — here is how to keep it.
Whether you install through the registry or copy the source, you get the same component behavior — the accessibility work travels with the code, not a runtime service. To keep it as you compose and extend, run the same axe gate we do with @cooud-ui/ui/testing’s expectNoA11yViolations, and pair it with a keyboard and screen-reader pass on your own flows:
// your-component.test.tsx — the same axe gate we run, in your app.import { expectNoA11yViolations, renderWithCooud } from "@cooud-ui/ui/testing";import { screen } from "@testing-library/react";import userEvent from "@testing-library/user-event";import { it } from "vitest";it("stays accessible after interaction", async () => {const user = userEvent.setup();const { baseElement } = renderWithCooud(<YourComponent />);await user.click(screen.getByRole("button", { name: "Open" }));await expectNoA11yViolations(baseElement); // fails with the violations});
Provide accessible names for your content (labels for fields, aria-label for icon-only buttons), keep one <main> and a skip link per route, and move focus deliberately after navigation and async actions — the areas only your app can own.
Report an accessibility issue
Related
See the Accessibility overview for the per-component guidance, and RTL for the logical-properties and direction work referenced above.