Skip to content

Accessibility Conformance Report — WCAG 2.1 A/AA (VPAT-style)

Product: SHS Digital Wellness Passport (student + admin web app) Report date: 2026-07-19 Standard: WCAG 2.1, Levels A and AA (all 50 A/AA success criteria) Prepared for: CSU Accessible Technology Initiative procurement review (US-43 / PR-F1)

This report follows the structure of an ITI VPAT (Voluntary Product Accessibility Template) "WCAG Edition" conformance table — one row per WCAG 2.1 A/AA success criterion, a conformance level, and remarks — without reproducing the VPAT's copyrighted boilerplate sections verbatim. Conformance Level is one of:

  • Supports — the criterion is fully met, with evidence cited in Remarks.
  • Partially Supports — some but not all content/functionality meets the criterion; Remarks carry an EEAAP (equally-effective alternate access to the product) note describing how a user is not blocked in the meantime, plus a remediation date.
  • Does Not Support — no content/functionality meets the criterion.
  • Not Applicable — the criterion's subject matter does not exist in this product (e.g. no video content exists, so captioning criteria do not apply).

Evaluation methods

  • Automated: axe-core (WCAG 2.1 A/AA rule tags) run over every key student surface (SignIn, Landing, Passport, KnowledgeCheck, Guide, CrisisResources) and admin surface (ChallengeBuilder, Reports, LiveOps) — frontend/src/a11y.test.tsx, gating every make test-unit / CI run. color-contrast is disabled in that suite (jsdom performs no real layout/paint), which is why contrast-related criteria below are evaluated manually instead and, where not yet run through a tool, marked as a residual.
  • Manual keyboard pass: the full core student journey (sign-in → passport → check-in → assessment) driven keyboard-only at a 390×844 phone viewport, with live re-verification after each fix. Full log: docs/a11y-manual-pass.md.
  • Accessibility-tree inspection: Playwright ariaSnapshot() captures (a role/name/state tree equivalent to what a screen reader's accessibility API exposes) stood in for a real NVDA/JAWS/VoiceOver session, which was not available in this environment — logged as residual finding #4, not claimed as heard.
  • Reflow / zoom: live checks at 320px CSS width and 200% text zoom on the sign-in, passport, and admin challenge-detail screens (the widest, most button-dense layout in the app) — zero horizontally-overflowing elements found.
  • Source review: every custom interactive pattern (radio groups, tabbed panels, dialogs, live regions) read directly against its markup, not assumed from behavior alone.

Three defects were found and fixed as part of producing this report (all in frontend/src): the week-detail, offline-notice, and scan-result sheets in Passport.tsx, and the scanner overlay in QrScanner.tsx, asserted aria-modal="true" without moving keyboard focus into the dialog on open; and all ten inline error messages in ChallengeBuilder.tsx lacked role="alert" (present already on the equivalent messages in Reports.tsx, LiveOps.tsx, Passport.tsx, and KnowledgeCheck.tsx). Both are cited below and detailed in docs/a11y-manual-pass.md.

Success Criteria, Level A

Criterion Conformance Level Remarks
1.1.1 Non-text Content Supports Every icon (components/icons.tsx) is aria-hidden="true" next to visible text or inside a labelled control; the Live Ops event QR (LiveOps.tsx) carries role="img", an aria-label, and a title on the <svg> (fixed this story — previously an unlabelled aria-label on a plain <div> and an untitled <svg role="img">, both caught by the automated axe sweep).
1.2.1 Audio-only and Video-only (Prerecorded) Not Applicable The product contains no audio-only or video-only content.
1.2.2 Captions (Prerecorded) Not Applicable No prerecorded video content exists.
1.2.3 Audio Description or Media Alternative (Prerecorded) Not Applicable No prerecorded video content exists.
1.3.1 Info and Relationships Supports Semantic structure throughout: <fieldset>/<legend> for the MCQ radio group, <dl>/<dt>/<dd> for week metadata, <label htmlFor> on every form field in ChallengeBuilder.tsx, headings (<h1><h3>) on every screen (verified via ariaSnapshot() captures in docs/a11y-manual-pass.md).
1.3.2 Meaningful Sequence Supports Single-column, mobile-first layouts (CLAUDE.md); DOM order matches visual/reading order on every screen reviewed.
1.3.3 Sensory Characteristics Supports Week status ("Available"/"Locked"/"Complete") and report data are always given as text alongside color/icon, never color or shape alone.
1.4.1 Use of Color Supports Same evidence as 1.3.3; attendance/engagement/outcome report bars pair color with a numeric or text value.
1.4.2 Audio Control Not Applicable No audio auto-plays anywhere in the product.
2.1.1 Keyboard Supports The full core student journey (sign-in, passport, check-in, the MCQ and reflection assessment) was driven live, keyboard-only, start to finish — docs/a11y-manual-pass.md.
2.1.2 No Keyboard Trap Supports Every dialog/sheet closes on Escape; none traps focus indefinitely (verified live).
2.1.4 Character Key Shortcuts Supports The product implements no single-character keyboard shortcuts.
2.2.1 Timing Adjustable Not Applicable No task carries a user-facing time limit; the session token's expiry is a security control the user is never made to race against mid-task.
2.2.2 Pause, Stop, Hide Partially Supports The Live Ops check-in count (LiveOps.tsx) auto-refreshes every 5 seconds indefinitely with no pause/stop/hide control. EEAAP: the same check-in totals are available without auto-refresh on the static Reports dashboard (Reports.tsx, manual "Refresh" button only), and nothing on the Live Ops screen depends on catching an update within the 5-second window — a stale count simply becomes current on the next tick or a page reload. Remediation: add a pause control, by 2026-11-30.
2.3.1 Three Flashes or Below Threshold Supports No flashing or strobing content exists anywhere in the product.
2.4.1 Bypass Blocks Supports Each route (App.tsx) renders one self-contained screen; there is no repeated navigation block preceding main content on any page for a user to need to bypass.
2.4.2 Page Titled Partially Supports index.html sets one static <title>SHS Wellness Passport</title>; no route updates document.title per screen. EEAAP: every screen carries its own clear on-page <h1>/<h2> heading (confirmed in the ariaSnapshot() captures in docs/a11y-manual-pass.md), so both sighted and assistive-technology users can identify the current screen without relying on the browser tab title. Remediation: set document.title per route, by 2026-09-30.
2.4.3 Focus Order Supports Fixed this story: the week-detail, offline-notice, and scan-result sheets (Passport.tsx) and the QR scanner overlay (QrScanner.tsx) now move focus to their own Close button on open, instead of leaving focus on a tile behind the backdrop — re-verified live (docs/a11y-manual-pass.md).
2.4.4 Link Purpose (In Context) Supports Every link found ("Ask the guide", the tel: crisis-resource links, "Open full-size diagram") carries descriptive text or aria-label in context; none reads as a bare "click here".
2.5.1 Pointer Gestures Supports No multipoint or path-based gestures exist; the camera QR scanner opens and closes with a single tap.
2.5.2 Pointer Cancellation Supports All actions fire on the click (up) event, never on pointer-down/touchstart.
2.5.3 Label in Name Supports Every aria-label reviewed contains its element's visible text as a substring (e.g. the tile label "Week 4: Signal Distortion Check, Locked" contains the visible "Week 4").
2.5.4 Motion Actuation Not Applicable No control is triggered by device motion.
3.1.1 Language of Page Supports index.html sets <html lang="en">.
3.2.1 On Focus Supports Verified live: tabbing through sign-in, the passport grid, and every dialog never itself triggered navigation or opened new content — only an explicit activation (Enter/Space/click) does.
3.2.2 On Input Supports Form fields only submit on an explicit action (Submit click/Enter); selecting an MCQ option or typing in a field never auto-submits.
3.3.1 Error Identification Supports Every error path reviewed (check-in, MCQ/reflection submission, admin save/duplicate/publish) renders the server's own descriptive message as visible text.
3.3.2 Labels or Instructions Supports Every form input carries a <label htmlFor> (verified across every ChallengeBuilder.tsx form) or an aria-label (Passport/KnowledgeCheck).
4.1.1 Parsing Supports Markup is React/JSX-rendered with no hand-built HTML string concatenation in any surface reviewed, so ids are unique and elements are well-formed by construction.
4.1.2 Name, Role, Value Partially Supports The aria-modal="true" defect fixed this story (see 2.4.3) affected the student-facing sheets; the identical pattern (a dialog that does not move focus on open) remains on the seven admin modals in ChallengeBuilder.tsx (challenge/task/theme forms, duplicate, assessment items, score override, check-in override). EEAAP: every one of those modals is still fully reachable field-by-field via Tab and closes via its own Cancel/Done button or Escape — no control is unreachable, only the initial focus placement lags the now-fixed student-facing pattern. Remediation: apply the same fix across all seven, by 2026-09-30 (tracked as docs/a11y-manual-pass.md finding #3).

Success Criteria, Level AA

Criterion Conformance Level Remarks
1.2.4 Captions (Live) Not Applicable No live audio/video content exists.
1.2.5 Audio Description (Prerecorded) Not Applicable No prerecorded video content exists.
1.3.4 Orientation Supports No CSS orientation lock exists anywhere; layouts are fluid (CLAUDE.md mobile-first rule) rather than fixed to portrait.
1.3.5 Identify Input Purpose Not Applicable The product collects no self-identifying profile data through a form — identity comes from campus SSO (FR-A1/A2), never typed. The one free-text "student SSO subject" field (ManualCheckInForm in ChallengeBuilder.tsx) identifies a different person than the admin filling in the form, so the recognized input-purpose taxonomy (which targets a user's own data) does not apply.
1.4.3 Contrast (Minimum) Partially Supports Not run through a contrast-analyzer tool in this story — jsdom performs no real layout/paint, so axe-core's color-contrast rule is disabled in the automated sweep (frontend/src/a11y.test.tsx). EEAAP: the design system's contrast-pair tokens (--wp-on-* in theme/tokens.css, applied by ThemeProvider.tsx) were authored to the frontend design brief's stated contrast targets; every status/label pairing also carries redundant text (see 1.4.1), so meaning does not depend on the color contrast alone even where a specific pairing is later found short. Remediation: run an automated contrast-analyzer tool pass across all theme palettes, by 2026-08-15 (docs/a11y-manual-pass.md finding #5).
1.4.4 Resize Text Supports Verified live: forcing documentElement.style.fontSize to 200% on the passport screen at a 390px viewport produced zero horizontally-overflowing elements.
1.4.5 Images of Text Supports All copy (titles, captions, labels, buttons) reviewed is real text; no image-of-text was found on any surface.
1.4.10 Reflow Supports Verified live at 320px CSS width (sign-in, passport) and at 390px on the admin challenge-detail screen's five-button task action row (its densest layout): zero horizontally-overflowing elements in all three.
1.4.11 Non-text Contrast Partially Supports Same residual as 1.4.3 — control-boundary and icon contrast was not run through a tool in this story. EEAAP: interactive controls are sized to the 44×44px touch-target minimum (CLAUDE.md) and carry visible borders/fills beyond a bare outline, reducing reliance on any one contrast pairing. Remediation: same 2026-08-15 tooling pass as 1.4.3.
1.4.12 Text Spacing Partially Supports Not verified against the WCAG text-spacing bookmarklet (1.5× line height, 2× paragraph spacing, 0.12×/0.16× letter/word spacing) in this story. EEAAP: all typography uses relative units (rem/em), which is what lets a user- or browser-applied text-spacing override take effect at all — nothing is pinned to a fixed pixel line-height that would clip overridden spacing. Remediation: run the bookmarklet check, by 2026-08-15.
1.4.13 Content on Hover or Focus Supports The product has no tooltips or hover-revealed content; there is nothing that could fail to be dismissible, hoverable, or persistent under this criterion.
2.4.5 Multiple Ways Not Applicable Each screen is a single-purpose destination reached by one explicit action (the SSO redirect, a nav button, the "Ask the guide" link); there is no multi-page content collection that would need more than one way to locate a given page.
2.4.6 Headings and Labels Supports Every screen captured in docs/a11y-manual-pass.md's ariaSnapshot() evidence exposes a descriptive heading and unambiguous control labels.
2.4.7 Focus Visible Supports Fixed this story (see 2.4.3) and verified live: every interactive element reached by Tab across sign-in, passport, the check-in dialog, and the assessment form showed a visible focus ring — a custom 3px :focus-visible style, or the browser's native ring reinforced by .option:focus-within on the MCQ radios.
3.1.2 Language of Parts Supports The product is English-only with no foreign-language passages; nothing requires a lang override on any element.
3.2.3 Consistent Navigation Not Applicable There is no repeated navigation menu appearing across multiple screens to be consistent or inconsistent (see 2.4.1); each screen family's own local controls (e.g. every admin screen's back control) already read and sit consistently within that family.
3.2.4 Consistent Identification Supports Recurring components — the Close (✕) button, role="alert" error text, the "Get help now" crisis affordance — use the same label and icon everywhere they appear.
3.3.3 Error Suggestion Supports Errors are the server's own actionable text (e.g. "That student is already checked in for this task — use Remove, then re-add.", "Answer key must match one of the options") rather than a bare failure code.
3.3.4 Error Prevention (Legal, Financial, Data) Not Applicable The product has no legal or financial transaction. Its closest analog — removing a check-in or overriding a score — requires a typed reason and is itself an audited, reversible correction mechanism (FR-D6), not the kind of one-way legal/financial commitment this criterion targets.
4.1.3 Status Messages Partially Supports Fixed this story for the ChallengeBuilder.tsx admin modals: all ten inline error messages now carry role="alert", matching the pattern already used in Reports.tsx, LiveOps.tsx, Passport.tsx, and KnowledgeCheck.tsx. The Guide chat's transcript (Guide.tsx) already uses aria-live="polite" so new replies are announced without moving focus. EEAAP: every status message reviewed is marked up correctly and was confirmed via the browser's accessibility tree; marked Partially Supports rather than Supports only because this was not confirmed with a real screen reader (see below), so "announced" is inferred from correct markup rather than heard. Remediation: real AT confirmation, by 2026-10-31.

Residual gaps carrying equally-effective-alternate-access notes

Every row above marked Partially Supports carries its own EEAAP note and remediation date in the Remarks column. The full evidence trail, live verification steps, and the complete residual-findings list (including the two found via source review that need real assistive-technology hardware to close out) are in docs/a11y-manual-pass.md.