Skip to content
Ahimsa

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.

SCCriterionStatusHow
1.4.1Use of colorMetEvery status has a text or shape channel. The pill dot never carries meaning alone.
1.4.3Contrast (minimum)MetMachine-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.4Resize textMetThe whole type scale is rem. Nothing sets a px font-size on the root.
1.4.10ReflowMet for .ah-pageThe document shell reflows to 320px. The fixed app shell is a deliberate trade — do not use it to hold an article.
1.4.11Non-text contrastMet--border-control (a control's edge) clears 3:1. --border-strong is a separator and deliberately does not — it carries no information.
1.4.12Text spacingMetNo fixed line heights that clip, no max-height on text containers.
1.4.13Content on hover or focusMetNo hover-revealed content anywhere. The system has no tooltips.
2.1.1KeyboardMetEvery control is a real button or input. No div handlers.
2.1.2No keyboard trapMetEscape closes the only modal the system has.
2.2.1Timing adjustableMetThere is no timing. No timeouts, no auto-advance, no session expiry.
2.3.1Three flashesMetThe ambient layer cannot animate faster than 0.1Hz; check-attention.mjs enforces a 2s floor.
2.3.3Animation from interactionsMetprefers-reduced-motion removes animation outright, without lowering durations.
2.4.7Focus visibleMetA :focus-visible ring on every focusable element. The blanket outline:none this was extracted from is gone.
2.4.11Focus not obscuredMetNothing in the system is position:fixed over content. The site header is in the flow.
2.5.1Pointer gesturesMetNo path-based or multipoint gesture is required anywhere.
2.5.2Pointer cancellationMetPress feedback is :active and actions fire on up, so sliding off aborts.
2.5.5Target size (AAA)Met.ah-hit gives a 44px target without moving a painted pixel.
2.5.7Dragging movementsMetPagination dots are buttons. Any drag affordance must be paired with a single-pointer route.
4.1.2Name, role, valueMetEvery snippet on the Components page ships its ARIA.
1.4.3 (hints)--text-faintPartial, by declarationAt 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.

TokenReason
--text-secondaryWCAG 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-mutedWCAG 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-faintThe 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-faint is 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-muted instead.
  • The fixed app shell does not reflow like a document. .ah-app meets 320px, but at 200% zoom a fixed-viewport application is inherently tighter than a flowing page. Use .ah-page for 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.