-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Accessibility Checklist
The accessibility bar for Astryx components. Every new component — and any change to an interactive one — must clear this checklist before review. Reviewers block on it (see the blocking criteria in .github/copilot-instructions.md), and it is a hard requirement for a lab → core promotion (see packages/lab/README.md).
This page is the canonical, editable home of the checklist so accessibility experts can refine it without a code PR. It is referenced by CONTRIBUTING.md and consumed by the Night Watch Component Auditor.
This page owns what the bar is. Component Audit Rubric owns how each item is verified, how severe a failure is, and how it scores — §1 expands the operability requirements into A1–A21, while rendered contrast is verified by D7. Change the bar here; change how it is checked there.
⚠️ Keep in sync. The checklist items map to shared primitives and CI enforcement in the repo. If you add or reword an item, check whether the corresponding Component Audit Rubric check and the CONTRIBUTING pointer need a matching update.
Compose the shared primitives — VisuallyHidden, useAnnounce, useFocusTrap, and the focus hooks (useListFocus, useGridFocus, useTreeFocus) — rather than hand-rolling equivalents. They implement the WAI-ARIA APG patterns and are tested once; a bespoke reimplementation of one is a review reject.
Eleven items. Each has its own section below so a review or an audit can cite it precisely. For a promotion PR, copy all eleven and tick each one in the PR body.
- Every interactive element has an accessible name, and the widget itself is labeled (visible label,
aria-label, oraria-labelledby).
- The correct ARIA pattern from the WAI-ARIA APG is implemented; link the pattern you implemented in the PR description.
- Every state (selected, expanded, disabled, invalid, busy, …) is exposed via ARIA, never by color or shape alone.
- A loading action exposes
aria-busy, remains focusable, and blocks duplicate activation througharia-disabledplus guarded pointer and keyboard handlers. It remains actionable only when the API explicitly declares the operation interruptible. Busy editing controls also remain focusable so the user can continue or correct input. Do not use nativedisabledmerely to implement busy state.
- Full APG keyboard support for the pattern, including RTL arrow-key flipping via the shared focus hooks (they auto-detect direction from the container's computed
direction, read lazily on keydown — theisRtloverride is deprecated and must not be passed).
- Overlays trap focus while open and restore it on close via
useFocusTrap; focus is never dropped to<body>on a state change; the focus ring is visible in every theme.
- Async state changes (loading, results, errors) announce via
useAnnounce. Never render a live region that is born with content — assistive tech only announces changes to a live region, so it will be silent.
- Entry/exit animation goes through
useEntryAnimationor is guarded byprefers-reduced-motion.
- Every meaningful rendered foreground/background pair meets WCAG 2.2 AA in every supported color mode and theme under review: 4.5:1 for normal text, 3:1 for large text, and 3:1 for non-text UI and meaningful graphics.
- Decide whether each painted part is meaningful before scoring it: remove the part conceptually and ask whether content, a control, its state, or graphical information becomes harder to identify. Record decorative and conditional decisions with their reason, and reassess them for each state and composition.
- Verify every applicable component state — rest, hover, focus-visible, active/pressed, selected/current, loading, read-only, and each status value — including text, placeholders, icons, control boundaries, state indicators, and focus indicators. Do not infer a pass from token names or from one mode; measure the resolved rendered colors and composite alpha colors over their actual backdrop.
- Record any WCAG exception explicitly. Incidental or decorative content, logotypes, and genuinely inactive controls may be exempt; a low-contrast pair is never accepted merely because it is subtle, secondary, or inherited from a theme.
For a component review, test light and dark plus the non-default theme required by the Component Audit Rubric. For a theme-elevation review, test the theme's full supported component/state matrix: the theme owns every failing pair and cannot inherit the component-audit carve-out for a known systemic token failure. See D7 for the measurement and evidence protocol.
Contrast is component-specific even though the thresholds are universal. Every component that paints text, icons, boundaries, state indicators, progress, or nested content must maintain a contrast profile in its component accessibility documentation, surfaced by the CLI's accessibility section. The profile is an inventory of relationships to measure—not a list of theme hex values or a claim that the current theme passes.
Each profile records:
- the rendered part and whether it is meaningful, redundant/decorative, or conditionally meaningful;
- the foreground and the actual adjacent/composited backdrop it must be measured against;
- every applicable variant and state, including nested or slotted content;
- the WCAG threshold, or the exact exception and why removing that visual would not change understanding;
- whether disabled, busy, or read-only treatment changes applicability; and
- which shared semantic token relationship the component is expected to reuse.
Use these established profiles as the baseline:
| Component | Required relationships and exceptions |
|---|---|
| Button | Label in rest/hover/pressed; meaningful and icon-only glyphs; focus; loading arc; badge/count/custom end content. Boundary only when needed for identification. Disabled treatment is exempt; busy progress is not. |
| Badge / Token | Label against the fully composited pill surface; meaningful icons at 3:1. A label-redundant icon and pill-to-parent separation are decorative. Interactive Token states are measured through hover/pressed; disabled is exempt. |
| Banner | Title, description, and action labels against the composited status surface; meaningful dismiss/disclosure/action glyphs; nested action states and focus. A status icon repeated by visible status text may be decorative. Expanded content is measured on its own content surface. |
| FieldStatus | Message text against the composited muted status surface. The detached leading icon is decorative when the message names the status. The attached input border/on-field icon belongs to the owning input profile. Validation supports success/warning/error—not info—and reuses Banner's corresponding semantic surface/foreground families. |
| Card / ClickableCard / SelectableCard | Card content is measured against the rendered Card surface. A static Card background, border, or elevation is decorative when spacing and content already communicate grouping. A ClickableCard boundary is measured at 3:1 only when needed to identify the interactive target; focus is always measured separately. A SelectableCard's selection ring or equivalent visible state cue is meaningful and needs a perceptible 3:1 relationship with the adjacent surface it relies on. A color-only category or status fails the non-color-cue requirement; after an equivalent visible cue is added, reassess whether the colored surface remains meaningful or becomes decorative. Disabled interactive cards are exempt. |
| TextInput / ChatComposer / shared bordered inputs | Label or identifying context, description, optional/required marker, value, read-only value, and placeholder text; default/status boundaries; meaningful start, clear, send, info, and tooltip-status icons; loading arc; focus indicator. For ChatComposer, also measure warning/error message text and meaningful icons against the composited status surface, plus the send control in every applicable state. Measure the input or composer surface against every parent surface it can touch. A boundary is required only when it identifies the field; a supplemental hover inset does not replace that persistent cue. Disabled is exempt; read-only and active loading are not. |
| Checkbox / Radio / Switch | Label and description text; unchecked boundary; checked/selected fill and mark; Switch off/on track and thumb; focus indicator; and meaningful loading arc. The indicator may be aria-hidden yet remain visually meaningful because it is the visible control/state representation. Measure rest and hover against every supported parent surface. Disabled is exempt; Checkbox read-only is not. Status messages reuse the FieldStatus profile. |
| Spinner | The moving arc against its actual contextual background at 3:1; any visible label at 4.5:1. The static track may be decorative when the arc and status semantics independently communicate loading. inherit and onMedia are context contracts and must be measured in each real consuming surface, not once in isolation. |
| ProgressBar | Label/value text; determinate fill or indeterminate moving segment against its actual track/backdrop when the graphic carries progress information; total-range cue for a standalone graphic; target marks against either fill or track; and each focusable mark's focus indicator where it is visibly drawn. A standalone bar with no equivalent visible value needs a 3:1 graphical distinction between completed and remaining progress, plus an unambiguous total extent. Astryx may supply that extent with a contrasting track, endpoint, or another tested cue; the endpoint technique is an Astryx design decision, not a WCAG-prescribed pattern. When nearby visible text communicates the current value or an equivalent progress state, the graphical treatment may be supplemental or decorative; a name-only label such as “Uploading” is not equivalent. Genuinely disabled controls may use the inactive-control exception. A canceled or indeterminate progress graphic is not automatically exempt; apply the removal test to determine whether visible text makes it redundant. |
If a profile is missing, the audit still derives the complete rendered pair matrix; missing documentation never makes a pair not applicable. Add the derived profile so the next theme or component audit starts from the same decisions instead of rediscovering them.
Use these decisions for Button and for any control that combines text, icons, loading indicators, badges, or other end content:
- Labels: measure visible normal-size text against the button's actual composited background at rest, hover, and pressed; each pair must be at least 4.5:1 under SC 1.4.3 Contrast (Minimum). Do not round a value up to the threshold. The 3:1 large-text threshold applies only when the rendered label is at least 24 CSS px regular or 18.66 CSS px bold.
- Icons: an icon-only button, or an icon that uniquely communicates an action or state, is meaningful non-text content and must be at least 3:1 against its background under SC 1.4.11 Non-text Contrast. An icon that merely repeats a visible label is decorative and needs no separate contrast result. Removal is the test: if removing it changes the user's understanding of the action or state, it is meaningful.
- Boundaries: a button outline or boundary needs 3:1 only when it is required to identify the control. Sufficiently contrasting visible text or a meaningful icon can identify the control without a contrasting outer boundary. This does not remove the separate focus-indicator requirement.
- Focus: measure the visible portion that communicates focus at 3:1 against its adjacent background under WCAG 2.2 AA non-text contrast. An indicator entirely inside or outside a control is measured against that surface. When an indicator straddles a boundary, a sufficiently visible portion may establish focus without every side contrasting against both surfaces. The two-CSS-pixel perimeter-area rule belongs to SC 2.4.13 Focus Appearance at AAA; do not report it as an AA requirement.
- Loading: measure the spinner's active arc against the button background at 3:1. A faint circular track may be treated as decorative only when removing it leaves the loading state fully understandable; do not require arc-to-track contrast. A loading indicator remains meaningful even when the action is temporarily non-interactive, so do not dim it with the ordinary disabled opacity. Expose and announce the busy state separately under SC 4.1.3 Status Messages.
- End content: measure visible badge/count text against its fully composited badge surface at 4.5:1. The badge surface does not need 3:1 against the button when the visible text carries the meaning; an unlabeled status dot or color-only mark is meaningful and must pass 3:1 as well as the no-color-alone rule. Arbitrary custom content is evaluated from the final rendered result—the fact that a consumer supplied it is not a WCAG exception.
-
Inactive controls: genuinely disabled controls are exempt from WCAG 1.4.3 and 1.4.11. Apply that exception only to the inactive control treatment, not to a meaningful graphical progress indicator that communicates an operation is still running. Native
disabledis an implementation detail, not permission to hide an active status.
Apply these decisions to Card, ClickableCard, and SelectableCard in every background variant and supported color mode:
- Rendered content: measure text against the fully rendered Card background at 4.5:1 for normal text or 3:1 for qualifying large text. Meaningful icons and graphics require 3:1 against that same background; label-redundant visuals may be decorative.
- Static Card background, border, and elevation: these are decorative when spacing, headings, and content still communicate the grouping after the surface treatment is removed. Record their ratios for design review, but do not report a WCAG failure merely because the Card does not reach 3:1 against the page. WCAG explicitly allows an optional border or drop shadow to be ignored when it is not required to identify the component.
- Color variants: a tinted background may support categorization, but color cannot be the only identifying cue. If removing the color changes understanding, the first result is a failure of SC 1.4.1 Use of Color; add visible category/status text, a meaningful icon or shape, or another non-color treatment. Then run the removal test again. If the new cue preserves the meaning without the tint, the surface may be decorative and needs no Card-to-page ratio; if the surface still carries required graphical information, measure that required relationship at 3:1.
- ClickableCard: its resting boundary needs 3:1 only when users need that boundary to identify the whole Card as an interactive target. Hover and pressed differences do not need to contrast with the prior state, but the target must remain identifiable in each state. Measure the portion of the focus indicator that communicates focus at 3:1 against the adjacent surface on which it is drawn.
- SelectableCard: the selection ring, border, or other visible selected-state cue is meaningful and requires a perceptible 3:1 relationship with the adjacent surface it relies on. If the indicator is split across inside and outside surfaces, measure the portion that actually communicates selection rather than automatically requiring every segment to contrast with both. Programmatic selected state does not exempt the visual indicator, and using a different hue alone does not satisfy the requirement.
- Compositing and disabled state: resolve alpha backgrounds, borders, overlays, and shadows over the actual parent surface before measuring. Genuinely disabled ClickableCard and SelectableCard treatments are exempt from 1.4.3 and 1.4.11; read-only or selected cards are not.
Use the same contrast decision process for TextInput, ChatComposer, and components built on the shared bordered-input anatomy. Component-specific controls and status regions extend this baseline rather than replacing it.
| Part | Rendered relationship | Requirement |
|---|---|---|
| Label, value, description, counter, and visible status message | Text against its fully composited field, composer, page, or semantic-status surface | 4.5:1 for normal text; 3:1 only for qualifying large text. |
| Placeholder | Placeholder text against the editable surface | 4.5:1. A placeholder never substitutes for the accessible or visible identifying context. |
| Resting or status boundary | Boundary against every parent surface the control can touch | 3:1 only when the boundary is needed to identify the field or its state. A visible label does not by itself identify the editable region's extent. |
| Start/end icons and actions | Meaningful icon against its immediate surface; nested control label/icon against its own background | Meaningful and icon-only graphics need 3:1. Label-redundant icons may be decorative. Interactive clear, send, attachment, and similar controls keep their own state and focus checks. |
| Warning/error region | Message text and meaningful icon against the fully composited semantic surface | Text needs 4.5:1; meaningful graphics need 3:1. A status icon repeated by explicit message text may be decorative, and the status surface need not contrast with the parent when the text carries the status. |
| Focus-visible indicator | Visible focus cue against the adjacent surface on which it is drawn | 3:1. Audit keyboard focus presentation; a pointer-only shadow or elevation change is not a substitute for it. |
| Disabled, read-only, and loading | Re-evaluate every relationship in the rendered state | Genuinely disabled controls are exempt. Read-only content remains subject to text and meaningful non-text contrast. Active loading indicators remain meaningful even when duplicate activation is blocked. |
Classify a ProgressBar presentation before measuring it:
- Standalone: no nearby visible text communicates the current value or an equivalent progress state. A name-only label such as “Uploading” identifies the operation but does not communicate its progress. The graphic therefore carries the information, so completed versus remaining progress needs a 3:1 distinction and the total extent must be unambiguous. Astryx may provide that extent with a contrasting track, endpoint, or another tested cue; an endpoint is an Astryx technique rather than a pattern prescribed by WCAG, and it is sufficient only when the rendered fill and endpoint make the current and total positions clear.
- Supplemental: nearby visible text communicates the current value or an equivalent progress state, such as “65%” or “3 of 5.” Measure that text normally. The graphical fill/track treatment may be supplemental or decorative when removing it does not change understanding; do not fail an informational fill-to-track or track-to-surface ratio merely because it is below 3:1.
- Determinate: measure the fill against the remaining track when the visual proportion carries progress. Meaningful target marks are measured against whichever fill or track they overlap.
- Indeterminate: an active operation does not qualify for the inactive-control exception merely because duplicate activation is blocked. If the moving segment is the visible loading cue, measure it against the track or surface directly behind it. If visible text independently communicates loading, the animation may instead be redundant or decorative; record that applicability decision. The static track is likewise conditional.
- Semantic color: success, warning, error, and accent fills may reuse the corresponding semantic Badge family, but color cannot be the only status cue. A standalone warning fill may use a darker palette stop when required to preserve the 3:1 fill/track distinction.
- Disabled/canceled: the inactive-control exception applies only to a genuinely inactive UI control. A canceled progress graphic is not automatically exempt; it may be decorative when equivalent visible text communicates the canceled state, otherwise its meaningful parts remain in scope. Active indeterminate progress is not inactive, although its animation may be redundant when an equivalent visible loading message exists.
- Painted state (selection fills, custom checkmarks, focus indicators) has system-color fallbacks so it survives
forced-colors: active.
- Every AT-facing string (labels, announcements, hints) goes through
useTranslator()/ the i18n key system, same as visible text.
- Interactive targets meet WCAG 2.5.8 (AA): a 24×24 CSS-px square fits inside the target — or one of the five exceptions holds. Under 24px is not an automatic fail.
-
Spacing — an undersized target passes if a 24px-diameter circle centered on its bounding box clears every neighboring target and every other undersized target's circle (two 20×20 targets 4px apart pass; touching, they fail; equal targets of side
sneed24 − spx of clearance). - Inline — the target sits in a sentence or is sized by surrounding line-height (prose links); reflow makes its position unknowable, so it is exempt.
- Equivalent — the same function is reachable through a ≥24px control on the same page.
- User-agent — native rendering the author has not resized (native date picker, scrollbars, number spin buttons).
- Essential — the presentation is required or legally mandated (dense data-viz points, map pins).
-
Spacing — an undersized target passes if a 24px-diameter circle centered on its bounding box clears every neighboring target and every other undersized target's circle (two 20×20 targets 4px apart pass; touching, they fail; equal targets of side
- The rule does not apply to a target while it is obscured by interaction-triggered content (open dropdown, modal, cookie banner); a spatially-selected control (Slider thumb, color-picker gradient, cursor-positioning area) counts as one target. Pointer modality does not raise the AA minimum: coarse pointers follow the same 2.5.8 rule. The 44×44 size in 2.5.5 Target Size (Enhanced) is AAA, not a requirement here.
CI is the enforcement layer, not a replacement for the checklist: the pr-a11y workflow runs an axe audit on every PR, a weekly workflow scans the full component surface, and the useAnnounce lint rule rejects hand-wired aria-live regions. axe only catches a subset of static, DOM-level issues — it cannot settle keyboard behavior, focus management, announcement timing, or every contrast pair in its rendered theme/state context. Those are exactly what this checklist, the rubric's manual evidence, and the component's tests cover.
Start here Astryx Philosophy Contributing with AI Assistants Contributing
Architecture System Architecture Architecture Cheat Sheet Theming Infrastructure Distribution
Building a component Component Lifecycle Component Authoring Guide API Conventions Design Conventions
Quality Component Audit Rubric Accessibility Checklist
Operations Release Process Night Watch Overview