Accessibility
Inherited, not owed
The position this system was extracted from was that it "is not accessible by default — the consumer has to check". That is no longer the position. Adopting Ahimsa should mean inheriting accessibility rather than acquiring a debt.
Conformance
WCAG 2.2 AA, criterion by criterion
Target is WCAG 2.2 Level AA, the standard the US Department of Justice's
2024 ADA Title II rule adopts. Rows below marked Met are enforced
by a gate in tools/, by construction, or both. Rows that are
only partially met say so.
| SC | Criterion | Status | How |
|---|---|---|---|
1.4.1 | Use of color | Met | Every status has a text or shape channel. The pill dot never carries meaning alone. |
1.4.3 | Contrast (minimum) | Met | Machine-checked across both aesthetics and all 28 gradients by tools/check-contrast.mjs. Three inherited token values were raised to reach it — see below. |
1.4.4 | Resize text | Met | The whole type scale is rem. Nothing sets a px font-size on the root. |
1.4.10 | Reflow | Met for .ah-page | The document shell reflows to 320px. The fixed app shell is a deliberate trade — do not use it to hold an article. |
1.4.11 | Non-text contrast | Met | --border-control (a control's edge) clears 3:1. --border-strong is a separator and deliberately does not — it carries no information. |
1.4.12 | Text spacing | Met | No fixed line heights that clip, no max-height on text containers. |
1.4.13 | Content on hover or focus | Met | No hover-revealed content anywhere. The system has no tooltips. |
2.1.1 | Keyboard | Met | Every control is a real button or input. No div handlers. |
2.1.2 | No keyboard trap | Met | Escape closes the only modal the system has. |
2.2.1 | Timing adjustable | Met | There is no timing. No timeouts, no auto-advance, no session expiry. |
2.3.1 | Three flashes | Met | The ambient layer cannot animate faster than 0.1Hz; check-attention.mjs enforces a 2s floor. |
2.3.3 | Animation from interactions | Met | prefers-reduced-motion removes animation outright, without lowering durations. |
2.4.7 | Focus visible | Met | A :focus-visible ring on every focusable element. The blanket outline:none this was extracted from is gone. |
2.4.11 | Focus not obscured | Met | Nothing in the system is position:fixed over content. The site header is in the flow. |
2.5.1 | Pointer gestures | Met | No path-based or multipoint gesture is required anywhere. |
2.5.2 | Pointer cancellation | Met | Press feedback is :active and actions fire on up, so sliding off aborts. |
2.5.5 | Target size (AAA) | Met | .ah-hit gives a 44px target without moving a painted pixel. |
2.5.7 | Dragging movements | Met | Pagination dots are buttons. Any drag affordance must be paired with a single-pointer route. |
4.1.2 | Name, role, value | Met | Every snippet on the Components page ships its ARIA. |
1.4.3 (hints) | --text-faint | Partial, by declaration | At 2.14:1 it does not meet AA and is not intended to: it is for hints, no component uses it, and it must never be the sole carrier of meaning. |
The cost
Three inherited values had to change
Ahimsa Day is a restructure of a system already shipping in two apps, so the
rule was that no color value moves. Three had to. Each is declared in
tools/check-parity.mjs, which fails the build if a declared
deviation disappears or if any undeclared value moves.
| Token | Reason |
|---|---|
--text-secondary | WCAG 1.4.3: the original 65% alpha measures 3.86:1 on the darkest Day ground and fails AA for the descriptions and captions it exists to set. Raised to 76% (5.15:1) — the value a shipping consumer had already independently arrived at. |
--text-muted | WCAG 1.4.3: the original 45% alpha measures 2.39:1 and fails even the 3:1 large-text floor, for small-caps labels and metadata. Raised to 60% (3.40:1). |
--text-faint | The original 28% alpha measures 1.66:1. Raised to 40% (2.14:1) and redefined as hints only — it is now used by no component and must never be the sole carrier of meaning. |
Two of the three are not invented values: a shipping consumer had already independently arrived at exactly them, for an unrelated reason. That is a reasonable sign they are right.
The type scale also moved from px to rem at
identical computed sizes. Nothing renders differently at default settings,
but a px scale silently overrides a person's own browser font size, which
for this system is the wrong way round.
Honestly
What is still imperfect
- Some Day gradients cannot carry text. Day's indigo gradient spans too wide a lightness range for any ink to clear AA across it. It ships as surface-only, and text on it belongs in an opaque card. The generator lists exactly which ones.
--text-faintis 2.14:1 and does not meet AA. It is for hints, no component uses it, and the docs say plainly that it must never be the sole carrier of meaning. If you find yourself reaching for it to set real content, reach for--text-mutedinstead.- The fixed app shell does not reflow like a document.
.ah-appmeets 320px, but at 200% zoom a fixed-viewport application is inherently tighter than a flowing page. Use.ah-pagefor anything read rather than operated. - The two apps this was extracted from do not yet comply. Both ship a floating update bar that this system now refuses, and one has a drag-driven card deck with no single-pointer alternative. Those are theirs to fix, and naming them here is more useful than implying otherwise.
Verifying
Run the gates yourself
npm run check # parity, contrast, attention, offline
npm run check:a11y # axe-core through Playwright, both themes, 320/390/1280 + 200% zoom
The contrast gate composites translucent ink onto its actual ground before measuring, which most checkers do not — quiet text in this system is the ink color at low alpha, and a ratio computed without compositing is meaningless.