Skip to content
Troopside
Troopside

Accessibility Conformance Report

WCAG 2.1 Level A and AA, criterion by criterion: what was tested, how, and what was found. Dated 9 September 2026.

Counsel review owed. This report was prepared by the platform's engineering team from automated scans, authoring-time checks and a code-level review, not by counsel and not by a third-party auditor. Counsel review of the wording is owed before it is relied on outside the platform, and remains open on issue #1085. It is a description of what was tested and found on the date above, not a warranty.

Structured after the VPAT 2.5 template, WCAG edition (VPAT is a registered service mark of the Information Technology Industry Council). The same report is kept in the repository as docs/accessibility-conformance-report.md. Back to the accessibility statement.

About this report

Name of product and version
Troopside (web application: a unit's public website and its member hub). The code as merged to the staging branch on 9 September 2026, promoted to production with the next release (the report follows the code: a change that alters a row regenerates it and moves this date).
Report date
9 September 2026
Product description
Troopside is software for running a Scout troop or Cub Scout pack: each unit gets a public website on its own domain (the pages a family reaches with no sign-in) and a member hub behind a sign-in (roster, calendar and outings, sign-ups, payments, documents, health forms). One platform serves every unit; the platform builds and tests the pages, and each unit posts its own content into them.
Contact
team@troopside.com (a role address people read and reply to)
Scope
In scope: the public unit site (every page a signed-out visitor can reach) and the member hub (the pages behind sign-in), as the platform builds them. Out of scope: documents, photos and text a unit uploads or types into the platform. Those are the unit's own content; the platform cannot author or repair them, and since issue #1082 it checks every uploaded PDF for the cheap accessibility signals, tells the leader in plain words, and offers a place for an accessible alternative beside the download.

Notes

  • The structure is the VPAT 2.5 WCAG edition (tables 1 and 2). Level AAA was not evaluated and is not claimed.
  • Conformance levels follow the standard terms below. Where the platform has not tested a criterion by every relevant means, the level is Partially Supports and the remark says what was not looked at, so the report understates rather than overstates.
  • The member hub has not been scanned by the automated tool, which reaches only what a signed-out visitor reaches; its levels rest on the authoring-time rules and the code review. An authenticated scan is a separate piece of work.
  • No assistive-technology session (a screen reader driven by a person) was part of this evaluation. That is named on the rows it would have informed.
  • The platform's accessibility statements (one per unit at /accessibility on the unit's own site, and the platform's at troopside.com/accessibility) make no conformance claim; this report is the place where the criterion-by-criterion answer lives.

Evaluation methods used

  • Automated scan, on every deploy and every pull request. scripts/a11y-scan.mjs drives a real Chromium (Playwright) with axe-core 4.13 over the sixteen routes a signed-out visitor can reach on a unit's host (/, /advancement, /contact, /e-blast, /e-blast/subscribe, /faq, /forgot-password, /gallery, /join, /join/apply, /login, /new-scouts, /outings, /positions, /reset-password, /resources) at two viewports (1280 by 900 and 375 by 812), against the axe rule set tagged wcag2a, wcag2aa, wcag21a and wcag21aa. Serious and critical violations fail the run; moderate and minor are reported. It runs after every staging and production deploy (the Smoke workflow, since issue #1081) and, since issue #1099, on every pull request against that pull request's own preview deployment. Each run keeps a dated JSON report for 90 days, which is the record behind "last checked" on the statements.
  • Authoring-time rules, on every pull request. ESLint with eslint-plugin-jsx-a11y runs over the whole application in CI (issue #1080). Every rule the configuration names is an error that fails the build or is off with its reason recorded; none reports as a warning (issue #1169, 9 September 2026). Among the errors: alt-text, anchor-has-content, anchor-is-valid, aria-props, aria-proptypes, aria-role, aria-unsupported-elements, autocomplete-valid, click-events-have-key-events, heading-has-content, html-has-lang, iframe-has-title, img-redundant-alt, interactive-supports-focus, label-has-associated-control, media-has-caption, mouse-events-have-key-events, no-access-key, no-autofocus, no-distracting-elements, no-noninteractive-element-to-interactive-role, no-redundant-roles, role-has-required-aria-props, role-supports-aria-props, scope, tabindex-no-positive, lang, no-aria-hidden-on-focusable and anchor-ambiguous-text. The one stated exception is the native modal dialog's click-outside-to-dismiss shortcut, kept in a single shared component with its reason (the dialog answers Escape natively and carries a close button).
  • Code-level review, 8 September 2026. A read of the application source for this report: image alternatives, form labels and grouping, roles and states on custom widgets, focus visibility rules, the colour tokens in the stylesheet with their contrast ratios computed, keyboard reachability of every handler, motion, the language attribute, page titles, error identification, status messages, time limits, and confirmation before destructive actions. Its findings are the remarks below; the gaps it found are filed and listed under Tracking.

Applicable standards

Web Content Accessibility Guidelines 2.1: Level A, yes; Level AA, yes; Level AAA, not evaluated.

Terms

Supports
The functionality of the product has at least one method that meets the criterion without known defects or meets with equivalent facilitation.
Partially Supports
Some functionality of the product does not meet the criterion, or the criterion has not been tested by every relevant means and the remark says what was not looked at.
Does Not Support
The majority of product functionality does not meet the criterion.
Not Applicable
The criterion is not relevant to the product.
Not Evaluated
The product has not been evaluated against the criterion. Used only for Level AAA.

WCAG 2.1 report

Table 1: Success Criteria, Level A
CriteriaConformance LevelRemarks and Explanations
1.1.1 Non-text ContentSupportsEvery img element carries an alt attribute, and a new one without alt fails the build (jsx-a11y alt-text is an error). Decorative images (a unit's crest beside its name, a 24px patrol patch beside its name) carry alt=""; gallery photos carry the caption as alt. The 65 inline SVG icons are all decorative, each beside the text it decorates (a tab label, a tile title, a button's own text), and every one carries aria-hidden="true"; a test reads every component file and fails on an inline SVG that is neither hidden nor named as an image (closed 9 September 2026, issue #1187). The scan's image-alt, role-img-alt and svg-img-alt rules pass on the sixteen public routes at both viewports. Unit-uploaded documents are out of scope and are flagged on upload since issue #1082.
1.2.1 Audio-only and Video-only (Prerecorded)Not ApplicableThe platform publishes no audio or video: no video, audio or autoplay element exists in the source.
1.2.2 Captions (Prerecorded)Not ApplicableNo prerecorded media.
1.2.3 Audio Description or Media Alternative (Prerecorded)Not ApplicableNo prerecorded media.
1.3.1 Info and RelationshipsSupportsForm controls are labelled by a wrapping label element (the house pattern; jsx-a11y label-has-associated-control is an error), radio and checkbox groups sit in fieldset with legend (53 fieldsets, 48 legends in the source), headings and lists are semantic elements, and status is conveyed as text. Data tables use th cells, and every one of the 66 header cells carries scope (column headers scope="col", row headers scope="row"); a test reads every component file and fails on a th without it (closed 9 September 2026, issue #1187). Every public page opens with an h1: the single e-blast issue the review had listed without one renders its subject as the h1 through the shared issue view, which the review's file-by-file read missed, and the same test pins it.
1.3.2 Meaningful SequenceSupportsContent is authored in reading order and the phone layout reflows the same document order; no CSS reordering of content. Checked by the code review and by the scan rendering each route at 375 by 812.
1.3.3 Sensory CharacteristicsSupportsInstructions name controls by their label rather than by shape, colour or position (for example "Tap a document's name to read it. Save or print opens your share sheet").
1.4.1 Use of ColorSupportsStatus chips carry their meaning as text ("Paid", "Public", "Going") as well as colour; links inside paragraphs are underlined (the contact-link style) as well as coloured; form errors are text in an alert region, not a red outline alone.
1.4.2 Audio ControlNot ApplicableNo audio plays.
2.1.1 KeyboardSupportsEvery control is a native button, link, input, select or dialog. The nine click handlers on non-interactive elements in the source are all on native dialog elements (click outside to dismiss); each dialog also closes on Escape and carries a close button, so the pointer shortcut takes nothing from keyboard users. The one custom listbox (the family picker on an RSVP) moves its highlight with the arrow keys and selects with Enter.
2.1.2 No Keyboard TrapSupportsModals are native dialog elements opened with showModal, which contains focus while open and releases it on Escape or the close button; there are no custom focus traps in the source.
2.1.4 Character Key ShortcutsNot ApplicableNo single-character keyboard shortcuts are implemented.
2.2.1 Timing AdjustableSupportsNo interaction is timed. The signed-in session lasts days (the authentication library's default), past the twenty-hour exception; links that expire (password reset, a file view pass for a home-screen install) can be issued again by the person who needs them.
2.2.2 Pause, Stop, HideSupportsNothing moves, blinks, scrolls or auto-updates for longer than five seconds. The one CSS animation in the application stylesheet is a 0.7 second one-shot highlight on a declined request; the platform site's two animations honour prefers-reduced-motion. There are no carousels or tickers.
2.3.1 Three Flashes or Below ThresholdSupportsThe one animation is a single 0.7 second box-shadow transition; nothing flashes more than three times in any second.
2.4.1 Bypass BlocksSupports"Skip to content" is the first focusable element on the public site, the member hub and the platform site (issue #1077), and targets the main content wrapper; page structure uses landmark elements (header, nav, main, footer) and headings.
2.4.2 Page TitledPartially SupportsEvery page has a title: the root layout titles by the unit's name, and 67 of 147 pages set their own descriptive title ("Contact Us | <the unit's name>", "Forms & Documents | <the unit's name>"). The scan's document-title rule passes on the public routes. Gap: the remaining pages, mostly in the member hub, carry the unit's name alone rather than a title naming the page.
2.4.3 Focus OrderSupportsFocus follows document order (no positive tabindex; jsx-a11y tabindex-no-positive is an error). Dialogs take focus when opened and return it to the opening control when closed, as native dialog elements do.
2.4.4 Link Purpose (In Context)SupportsLink text names its destination; a stored file's link carries the file's title and the Save or print button beside it carries the filename in its accessible name. No "click here" or "read more" links exist in the source; jsx-a11y anchor-ambiguous-text is an error.
2.5.1 Pointer GesturesSupportsNo multipoint or path-based gestures; every action is a single tap or click.
2.5.2 Pointer CancellationSupportsControls act on the up-event: native buttons and links, and the one custom control the review found acting on pointerdown (the family picker on an RSVP) now only keeps focus in its text box on the down-event and selects on the click, so a finger that slides off the option before lifting selects nothing. Pinned by a test against the component (closed 9 September 2026, issue #1187).
2.5.3 Label in NameSupportsWhere a control carries an aria-label, it begins with the visible text (for example "Save or print: Permission slip" on a button that reads "Save or print"). Checked by the code review; not exercised with a speech-input tool.
2.5.4 Motion ActuationNot ApplicableNo function responds to device motion.
3.1.1 Language of PageSupportsThe root layout renders html lang="en" on every page; the scan's html-has-lang and html-lang-valid rules pass.
3.2.1 On FocusSupportsReceiving focus changes nothing: no control opens, submits or navigates on focus.
3.2.2 On InputSupportsSelects, checkboxes and text fields change page state in place (the fields they reveal or the totals they update); every form submits on an explicit button, and forms are server-action forms that work the same before scripts load.
3.3.1 Error IdentificationSupportsErrors are text in a role="alert" region that names what is wrong and which fields it concerns (for example the contact form's "Please share your name, an email, and a short message"); 247 alert regions exist in the source. Every form ties the message to the fields it concerns with aria-describedby: the alert carries an id, the fields the message can be about point at it, the shared outcome panel takes the id as a prop and puts it on its line, and a field is marked invalid where the message is about that one field. The forms a visitor or a family fills in were wired in issue #1187 and the leader side (settings, imports, reviews, the composer) in issue #1204, closed 9 September 2026. A test reads every component and fails on any alert inside a form that no field of the form points at, so a form added later is held to the same rule. An alert outside every form is a page notice with no field to reference (a refusal, a saved-message read from the address); a form with no field at all (one button that confirms, sends or pays) is listed in that test with its reason.
3.3.2 Labels or InstructionsSupportsEvery control has a visible label (the wrapping label pattern), and fields that need it carry a hint underneath (upload limits, formats, what a field feeds). A required control's label carries a visible Required badge, from one stylesheet rule on the labelled-field pattern (and its two cousins, the platform site's field and the sign-in row) rather than a marker typed into each label, so a new required field is marked by being required; the control's own required attribute is what assistive technology reads. Single-box controls with no wrapping label (the roster search, the assistant's question box) carry an aria-label and no badge, and need none. Pinned by a test that reads the rule (closed 9 September 2026, issue #1187).
4.1.1 ParsingSupportsPages are rendered by React, which emits well-formed markup with unique ids where ids are generated; the scan's duplicate-id rules pass on the public routes.
4.1.2 Name, Role, ValuePartially SupportsNative elements carry their own names, roles and states throughout. Custom widgets carry theirs: the navigation menus set aria-expanded on their buttons, the calendar's clickable rows carry role="button", and jsx-a11y's aria-props, aria-proptypes, aria-role, role-has-required-aria-props and role-supports-aria-props are errors. Gap: the family picker on an RSVP is built as a list (ul and li) with listbox and option roles, which jsx-a11y warns about and issue #1169 tracks.
Table 2: Success Criteria, Level AA
CriteriaConformance LevelRemarks and Explanations
1.2.4 Captions (Live)Not ApplicableNo live media.
1.2.5 Audio Description (Prerecorded)Not ApplicableNo prerecorded media.
1.3.4 OrientationSupportsNo orientation is locked; layouts reflow in portrait and landscape.
1.3.5 Identify Input PurposePartially SupportsSign-in, password and join forms carry autocomplete tokens (email, current-password, name, tel); 45 inputs in the source set one, and jsx-a11y autocomplete-valid is an error. Gap: not every field that collects a person's own details on the profile forms has been audited for a token.
1.4.3 Contrast (Minimum)SupportsRatios computed for this report from the stylesheet's colour tokens: body text on the page background 14.3:1; muted text 5.3:1 on the page and 5.9:1 on white panels; white on navy 9.8:1; sand on navy 7.4:1; sky on navy 5.4:1; white on scout red 5.8:1; ink on gold 7.6:1 (chosen after white on gold failed at 2.6:1); success text 5.3:1, warning text 5.3:1 and danger text 6.6:1 on their tints; the platform site 12.0:1, 15.5:1 and 6.0:1. The success chip ("Paid", "Going") was 4.39:1 on 8 September 2026 and its green was darkened on 9 September (issue #1187); a test computes the three status pairs from the tokens and fails under 4.5:1. The scan's color-contrast rule runs against the deployed palette on every deploy and passes at both viewports. Units may choose a palette preset, and the scan is what checks the palette actually deployed.
1.4.4 Resize TextSupportsEvery font size in the stylesheet is in rem (492 declarations, none in px); text resizes to 200 percent with browser zoom and the layout reflows rather than clipping.
1.4.5 Images of TextSupportsText is rendered as text; the unit number in the header and footer is styled spans, not an image. Crests and logos are the only images containing text, which the criterion exempts.
1.4.10 ReflowSupportsThe platform is built phone-first and every change is checked at 375px before it ships (house rule); the scan renders each route at 375 by 812. Two-dimensional content (data tables, this report included) scrolls inside its own container, never the page.
1.4.11 Non-text ContrastPartially SupportsFocus rings are a 2px navy outline (8.8:1 on the page background); buttons are filled navy or scout red on the page background. Gap: input borders use the hairline token, whose contrast against a white panel has not been measured, and the ratio of every icon was not computed.
1.4.12 Text SpacingPartially SupportsType is sized in rem with no fixed-height text containers found in the review, so increased spacing reflows rather than clips. Gap: not exercised with a text-spacing override tool on every page.
1.4.13 Content on Hover or FocusSupportsNo content appears on hover alone. Navigation menus open on activation (click, Enter or Space) with aria-expanded and stay open until dismissed; there are no tooltips.
2.4.5 Multiple WaysSupportsPublic pages are reachable from the header navigation, the footer's link columns and the home page's sections; hub pages from the sidebar, the phone tab bar and the Home page's tiles.
2.4.6 Headings and LabelsSupportsPages open with an h1 naming the page and use h2 for each section; labels say what a field is for. The one public page the review had listed without an h1 (a single e-blast issue) has one: the shared issue view renders the subject as the h1, which a file-by-file read of the page missed, and a test pins it across both files (issue #1187). jsx-a11y heading-has-content is an error, so no heading is empty.
2.4.7 Focus VisibleSupportsNo global suppression of the focus outline. Seventeen components add a high-contrast focus-visible ring where the browser default would be lost against a dark fill (calendar controls, tabs, search and amount inputs), and the browser default stands everywhere else. The one input that removes its own outline sits inside a wrapper that draws a 2px navy ring on focus-within.
3.1.2 Language of PartsSupportsThe platform's copy is English throughout and no passage in another language exists in it. A unit's own content is the unit's.
3.2.3 Consistent NavigationSupportsThe public header and footer are derived from one route table per unit type, so the same links appear in the same order on every public page; the hub's sidebar, tab bar and Manage console derive from one console table.
3.2.4 Consistent IdentificationSupportsShared components (file links, status chips, forms, dialogs) give the same function the same label and behaviour wherever it appears.
3.3.3 Error SuggestionSupportsError text says what to change: "Choose a file to upload", "Give the document a title", the upload size limit and accepted types by name, "Please share your name, an email, and a short message so we can reply".
3.3.4 Error Prevention (Legal, Financial, Data)SupportsPayments run through Stripe Checkout, which shows a review step before any charge; sign-ups and RSVPs can be changed after they are submitted. Deleting or removing what somebody entered (a document, a photo, a draft e-blast, a footer line, a subscriber, a sign-up shift, a towing request, a position or its area, a handbook page, a merit badge class, a training assignment, a scheduled board of review, a bulk archive, a member merge) takes two presses: the red button opens a panel that names the item and says what is lost or kept, and the confirming press sits inside it (ConfirmDanger, a native disclosure that works before scripts load). The roster and audience removals keep their own armed panels, the calendar clean-up its ticked list, the demo panel its browser confirm. Review decisions on a request (approve, reject, decline) act on one press and are reversible: a status a leader can change again, or a request the family can make again; taking a site down is undone by publishing it. A test reads every red button and refuses one outside a confirm step unless its file is listed with the reason (closed 9 September 2026, issue #1187).
4.1.3 Status MessagesSupportsForm results and notices render in role="status" or role="alert" regions (126 status or live regions and 242 alert regions in the source), so they are announced without moving focus.

Table 3: Success Criteria, Level AAA

Not evaluated.

Tracking

  • Issue #1169 (closed 9 September 2026): the counted lint warnings worked to zero and every rule promoted to an error; the nine native dialogs' click-to-dismiss now lives in one shared component with its reason, and the RSVP family picker's list markup is allowed by the rule's own options.
  • Issue #1082 (shipped 8 September 2026): unit-uploaded PDFs checked on upload for text, language and tag structure; the leader told in plain words; an accessible alternative beside the download.
  • Issue #1099 (shipped 8 September 2026): the automated scan as a pull-request gate against each pull request's preview; armed once PREVIEW_TENANT_SLUG is set for all preview branches and the check is required.
  • Issue #1187, from this report's code review, closed on 9 September 2026 with each fix pinned by a test against the source: the success chip's 4.39:1 contrast, decorative inline SVGs without aria-hidden, the five data tables without header scope, the e-blast issue page's h1 (present through its shared view; the review's read had missed it), required fields marked in the label, deletions behind a confirm step, the family picker selecting on the click, and errors tied to their fields on the forms a visitor or a family fills in.
  • Issue #1204 (closed 9 September 2026): the leader-side forms tie their error message to the fields it concerns, the rest of 3.3.1, through the shared outcome panel carrying an id and a scan over every component that fails on an alert inside a form no field points at.