Skip to main content

Accessibility audit record, 2026-10-03

Filled from the record template, following the audit plan.

Outcome: partial. We scanned every route with axe in both color schemes. We walked each with Tab in Chromium, Firefox, and WebKit. We ran three complete processes by keyboard alone. Nobody has run NVDA or VoiceOver, so every screen-reader cell below is Not tested and this record supports no WCAG 2.2 AA conformance claim. The accessibility statement says so.

The evaluation found 16 barriers (F1 to F16) and one gap in the test coverage (F17). The same change fixes each one. Each has a story, test, or accessibility-spec check that fails without the fix. The evaluation records these barriers in #1601.

Audit​

FieldValue
Dates2026-10-03
Revision and releasemain at 20dd2fca7, which includes the instance overview readiness card (#2416), plus this change. The code evaluated is commit 83a841624. No release: the version string is 0.0.0-development.
Scope and sample rationaleEvery route in the generated route tree that renders a page of its own, 51 in all. Redirect-only routes (/landing, /w/:slug, /w/:slug/user/*, /w/:slug/admin/onboarding) and the transient /auth/callback have no page to evaluate. Thus, the structured sample covers the whole route tree without a random sample. The table below marks states without available deployment data as Not tested.
Deployment and test dataThe SPA production build (vp build, served by vp preview) against the server with the e2e profile. An empty PostgreSQL database uses webapp/e2e/seed.sql. It has one workspace and the shipped practice catalog. It has no synced work, review runs, or configured language-model provider. Accounts: e2e (instance administrator and workspace administrator) and e2e-plain (workspace member).
EvaluatorScripted checks only: webapp/e2e/accessibility.spec.ts, the Storybook suite and a one-off member-role scan. No person used an assistive technology.
WCAG-EM reportNot produced. The report needs screen-reader results for its sample, and there are none.
Storybook report, revision and browservp run --filter webapp test:storybook at 83a841624, Chromium 153.0.8010.12 with reduced motion: 313 files and 2,155 tests pass. axe-core 4.13.0 at error for every story.
Finding queryIssues and pull requests that mention #1601.

Environments​

IDOperating systemBrowserAssistive technologyVersions and relevant settingsRun
E1Ubuntu 24.04, Linux 6.8Chromium 153.0.8010.12 (Playwright 1.63.0), 1280×720Noneaxe-core 4.13.0 through @axe-core/playwright 4.13.0, tags wcag2a, wcag2aa, wcag21a, wcag21aa, wcag22aa, best-practice. Light then dark scheme. The Storybook suite runs with prefers-reduced-motion: reduceAutomated, keyboard
E2Ubuntu 24.04, Linux 6.8Firefox 155.0 (Playwright 1.63.0), 1280×720NoneThe same spec and the same axe configurationAutomated, keyboard
E3Ubuntu 24.04, Linux 6.8WebKit 26.6 (Playwright 1.63.0), 1280×720NoneThe same spec and the same axe configuration. This is the WebKit engine, not Safari on macOS.Automated, keyboard
E4WindowsFirefoxNVDANot set upNot tested: no Windows machine or NVDA tester was available
E5macOSSafariVoiceOverNot set upNot tested: no macOS machine or VoiceOver tester was available

Findings​

Each row is a failure the evaluation found. The Fix column names the code that changed. The Coverage column names what fails without it. Everything is checked against the callers of a shared component, listed where the component is shared.

FindingSurfaceCriterionBarrierFixCoverage
F118 routes. These include landing, about, imprint, privacy, both sign-in pages, sign-in problem, consent, and callback. They also include create workspace (3), user settings, integration callback, teams, mentor, choose your AI, and not found2.4.2 Page Titled (A)The tab, the history and a screen reader's page announcement all said only "Hephaestus". The unsubscribe page used a different separator from every other title.pageHead in webapp/src/lib/page-title.ts on each route. The unmatched-address page is now the routes/$.tsx route, because only a route can set a titleEvery surface test asserts its exact title
F2Every page of the signed-in application2.4.7 Focus Visible (AA), 2.4.11 Focus Not Obscured (AA), 2.4.1 Bypass Blocks (A)The skip link used sr-only focus:not-sr-only, which resets position, so a focused link fell into the page flow behind the fixed sidebar. A keyboard user pressed Tab and saw nothing.SkipToContent.tsx stays above the viewport and moves down on focusStory ShownAboveTheSidebar. The skip-link test and the Tab walk on every surface
F3Account menu in the header4.1.2 Name, Role, Value (A), 1.3.1 Info and Relationships (A)Activity and Settings menu items sat inside links, so axe reported aria-required-parent and aria-required-children and a screen reader met a menu whose items were not its children.Header.tsx renders each menu item as the link (render=) instead of wrapping itStory AccountMenu. The menu test
F4Practice setup, with 13 groups4.1.2 (A), 2.5.8 Target Size (AA)A tooltip trigger, itself a button, sat inside each accordion trigger. Nested buttons are invalid, the badge was 20px tall, and its explanation was reachable by hover only.CatalogOriginBadge.tsx is plain text. CatalogOriginNote states the sentence, on the practice's panel and in the group's edit dialog. The badge has callers in PracticeCatalog and WorkspacePracticePanel, both checked.Stories WithCatalogProvenance, FromTheCatalog and the group dialog's FromTheCatalog. The Practices surface test
F5Every page with the sidebar (an axe region finding on every signed-in route)1.3.1 (A), 2.4.1 (A)The sidebar was a stack of divs, so a screen reader's landmark list held no navigation.ui/sidebar.tsx renders the panel as nav named "Primary", on desktop and in the mobile sheet. Its one caller is AppSidebar.Story RegularUser asserts the landmark. Every signed-in surface test runs axe with region on
F6Practice setup, practice catalog, person data, instance overview (the readiness card from #2416, merged while this change was open, had the same defect)1.3.1 (A)An accordion trigger is a heading, and Base UI fixes it at level 3, so each sat directly under the page's level-1 heading. The shared empty state fixed its title at level 3 as well.AccordionTrigger takes headingLevel. EmptyState and NoWorkspace take a required headingLevel. Callers: SortableCatalogTree (practice setup and instance catalog), the readiness card, InstancePersonDataPage, the mentor routes and the six no-workspace pages.Stories Populated and ActionRequired assert the levels. The surface tests run axe heading-order
F7No-workspace page, conversation page1.3.1 (A), 2.4.6 Headings and Labels (AA)The page had no heading at all.NoWorkspace is a level-1 heading on a page of its own. The conversation routes carry a visually hidden level-1 headingStory Default of NoWorkspace. The mentor thread route tests. The sign-in process test runs axe on the no-workspace page
F8Practice profile, instance audit log (any tab panel with nothing focusable inside)2.4.7 Focus Visible (AA)Base UI makes such a panel a tab stop, and upstream's outline-none left it with no sign of focus.ui/tabs.tsx draws a focus ring on TabsContent. Callers: seven components in the practice profile, review and feedback screens, all checked.Story KeyboardReachesThePanel. The Tab walk on every surface
F9Every signed-in page (sidebar mark), the landing page2.2.2 Pause, Stop, Hide (A)The Hephaestus mark in the sidebar floated, wobbled and blinked without end, and the landing figure beside its text floated without end.The idle motion in HephIcon.module.css and LandingVisuals.module.css plays once and ends inside five seconds. Only the streaming ping, which says Hephaestus is working, repeats.Every surface test reads document.getAnimations() for a loop that is not a spinner or skeleton
F10Every dialog with a scrolling body (create group, edit group and others)2.4.3 Focus Order (A)Focus opened on the body, an unnamed scroll box and the dialog's first tab stop, instead of on the first field.ui/dialog.tsx opens DialogContent on the first control in the body. A body with none keeps the defaultStory OpensOnTheName. The dialog test and the group process test
F11Members1.4.10 Reflow (AA), checked at 640px as well as 320pxThe search, team-filter, and columns toolbar ran 37px past the window at 640 CSS pixels. This is a 1280px window at 200% zoom.WorkspaceMembersTable.tsx lets the toolbar wrapStory AtTwoHundredPercentZoom. Every surface test checks 640px
F12Every signed-in page, review observations1.4.10 Reflow (AA)In Firefox the header ran 3px past a 320px window, and the filter toolbar's actions ran 7px past it. Chromium fitted both exactly.Header.tsx narrows its gaps at small widths. FilterToolbar.tsx lets its actions wrap. The toolbar has eight callers.Stories Mobile of Header (room to spare) and ActionsAtReflowWidth. The Firefox run of the spec
F13Sidebar workspace switcher1.4.12 Text Spacing (AA)At the spacing the criterion sets, a workspace name ended in an ellipsis in the only place it is shown.WorkspaceSwitcher.tsx lets the name wrapStory LongWorkspaceName
F14Review settings, landing page, dark theme1.4.3 Contrast (AA)The chosen autonomy rung's description was 4.19:1 on its tint, and the landing page's "Merged" pill was 3.88:1.AutonomyLadder.tsx and LandingVisuals.tsxStories FullInDarkMode and WorkStatesInDarkMode. The Review settings surface test in dark
F15Every pressed outline button in the dark theme1.4.1 Use of Color (A), 1.4.11 Non-text Contrast (AA)dark:bg-input/30 outranked aria-pressed:bg-muted, so a pressed button looked like an unpressed one.ui/button.tsxStory PressedTogglesLookSelectedInDarkMode
F16Every page change4.1.3 Status Messages (AA), not confirmedThe page changes without a load, so a screen reader said nothing when the user followed a link. This is a barrier by design review: no screen reader has confirmed it.RouteAnnouncer.tsx speaks the new page's title through a polite live region and keeps focus where it is. It clears the region first, so it speaks both pages even with identical titles. It stays silent for the first page and for a change of search.Unit test RouteAnnouncer.test.tsx. The announcer test in the spec. A tester must confirm what NVDA and VoiceOver say.
F17Dark theme, whole Storybook suite1.4.3 (AA)Stories run in the light theme only, so a dark-theme defect passes unless a story asks for it.The dark stories of F14 and F15. We also ran every story once in the dark theme. We temporarily added globals: { theme: "dark" } to each file's meta, then removed the edit. All 313 files and 2,155 tests pass. A first attempt through initialGlobals failed 79 stories. The 54 failures unrelated to contrast or F15 came from story remounts when the theme global changes. Each passes with per-story globals.The dark stories above

One more result is a tool's, not the application's. axe 4.13.0 reports scrollable-region-focusable for an ARIA menu taller than the window, because its items are tabindex="-1" and reached by arrow keys. axe fixed this after 4.13.0 (pull request 5364). The spec filters that one result for role="menu" and says why beside the filter. Remove the filter when the axe pin moves.

Surfaces​

Automated is the axe run in the light and dark scheme, plus the page title, reflow at 320 and 640 CSS pixels, and looping motion. Keyboard is a Tab walk of the surface: order, focus indicator, focus not entirely hidden, and no trap. Pass means it passed in E1, E2 and E3, the environments that ran it.

A cell that names an environment, such as Pass (E1), ran in that environment only.

The generated rows ran as the instance administrator, who is also a workspace administrator. The database had no synced work, so pages that list work showed their empty state. The evidence for each row is its test in webapp/e2e/accessibility.spec.ts.

SurfaceRoute, state and roleAutomatedKeyboardNVDAVoiceOverEvidence or issue
Landing/, signed outPassPassNot testedNot testedSurface test Landing
About/about, signed outPassPassNot testedNot testedSurface test About
Imprint/imprint, signed outPassPassNot testedNot testedSurface test Imprint
Privacy/privacy, signed outPassPassNot testedNot testedSurface test Privacy
Sign in/login, signed outPassPassNot testedNot testedSurface test Sign in
Workspace sign in/w/e2e/login, signed out, workspace sign-inPassPassNot testedNot testedSurface test Workspace sign in
Sign-in problem/auth/error, signed out, no error codePassPassNot testedNot testedSurface test Sign-in problem
Unsubscribe/unsubscribe, signed out, no tokenPassPassNot testedNot testedSurface test Unsubscribe
Not found/no-such-page, signed out. Any unmatched addressPassPassNot testedNot testedSurface test Not found
New workspace/workspaces/new, signed inPassPassNot testedNot testedSurface test New workspace
New GitHub workspace/workspaces/new/github, signed inPassPassNot testedNot testedSurface test New GitHub workspace
New GitLab workspace/workspaces/new/gitlab, signed inPassPassNot testedNot testedSurface test New GitLab workspace
Integration callback/integrations, signed inPassPassNot testedNot testedSurface test Integration callback
Settings/settings, signed inPassPassNot testedNot testedSurface test Settings
Practice profile/w/e2e/practice-profile, signed inPassPassNot testedNot testedSurface test Practice profile
Activity/w/e2e/activity, signed inPassPassNot testedNot testedSurface test Activity
Workspace activity/w/e2e/workspace-activity, signed inPassPassNot testedNot testedSurface test Workspace activity
Teams/w/e2e/teams, signed inPassPassNot testedNot testedSurface test Teams
Mentor/w/e2e/mentor, signed inPassPassNot testedNot testedSurface test Mentor
Onboarding/w/e2e/onboarding, signed inPassPassNot testedNot testedSurface test Onboarding
Members/w/e2e/admin/members, workspace adminPassPassNot testedNot testedSurface test Members
Teams admin/w/e2e/admin/teams, workspace adminPassPassNot testedNot testedSurface test Teams admin
Practices/w/e2e/admin/practices, workspace adminPassPassNot testedNot testedSurface test Practices
Practice updates/w/e2e/admin/practices/releases, workspace adminPassPassNot testedNot testedSurface test Practice updates
Review settings/w/e2e/admin/practices/review, workspace adminPassPassNot testedNot testedSurface test Review settings
Practice reviews/w/e2e/admin/practices/reviews, workspace adminPassPassNot testedNot testedSurface test Practice reviews
Review runs/w/e2e/admin/practices/reviews/runs, workspace adminPassPassNot testedNot testedSurface test Review runs
Review observations/w/e2e/admin/practices/reviews/observations, workspace adminPassPassNot testedNot testedSurface test Review observations
Review feedback/w/e2e/admin/practices/reviews/feedback, workspace adminPassPassNot testedNot testedSurface test Review feedback
Reviewed work/w/e2e/admin/practices/reviews/work, workspace adminPassPassNot testedNot testedSurface test Reviewed work
AI models/w/e2e/admin/models, workspace adminPassPassNot testedNot testedSurface test AI models
AI usage/w/e2e/admin/usage, workspace adminPassPassNot testedNot testedSurface test AI usage
Integrations/w/e2e/admin/integrations, workspace adminPassPassNot testedNot testedSurface test Integrations
Source control/w/e2e/admin/integrations/scm, workspace adminPassPassNot testedNot testedSurface test Source control
Outline/w/e2e/admin/integrations/outline, workspace adminPassPassNot testedNot testedSurface test Outline
Slack/w/e2e/admin/integrations/slack, workspace adminPassPassNot testedNot testedSurface test Slack
Workspace settings/w/e2e/admin/settings, workspace adminPassPassNot testedNot testedSurface test Workspace settings
Audit log/w/e2e/admin/audit, workspace adminPassPassNot testedNot testedSurface test Audit log
Instance overview/admin, instance adminPassPassNot testedNot testedSurface test Instance overview
Instance users/admin/users, instance adminPassPassNot testedNot testedSurface test Instance users
Instance workspaces/admin/workspaces, instance adminPassPassNot testedNot testedSurface test Instance workspaces
View as user/admin/workspaces/e2e/users, instance adminPassPassNot testedNot testedSurface test View as user
Instance audit log/admin/audit, instance adminPassPassNot testedNot testedSurface test Instance audit log
Practice catalog/admin/catalog, instance adminPassPassNot testedNot testedSurface test Practice catalog
Instance AI models/admin/models, instance adminPassPassNot testedNot testedSurface test Instance AI models
Instance AI usage/admin/usage, instance adminPassPassNot testedNot testedSurface test Instance AI usage
Instance settings/admin/settings, instance adminPassPassNot testedNot testedSurface test Instance settings
Login providers/admin/login-providers, instance adminPassPassNot testedNot testedSurface test Login providers
Feedback inbox/admin/feedback, instance adminPassPassNot testedNot testedSurface test Feedback inbox
Surveys/admin/surveys, instance adminPassPassNot testedNot testedSurface test Surveys
Person data/admin/person-data, instance adminPassPassNot testedNot testedSurface test Person data
Member roleThe workspace member's pages (practice profile, activity, workspace activity, teams, mentor, settings), as e2e-plainPass (E1)Not testedNot testedNot testedA one-off scan in E1 in both schemes found nothing. We did not run the Tab walk as a member.
Permission deniedA member opens an administrator route (/w/e2e/admin/members, /admin)Pass (E1)Not testedNot testedNot testedThe application redirects to the practice profile, which has a title and passes. It shows no message that access was refused.
conversationGreeting, transcript, composer, copilot panelNot testedNot testedNot testedNot testedComponent stories only. The evaluation deployment has no language-model provider, so the live page shows "Hephaestus isn't set up in this workspace yet". The conversation rebuilt in #2389 is not evaluated against a server.
Populated pagesPractice profile with feedback, reviewed-work feedback page, review runs, observations, traces, activity, teams, usage charts, audit entries, connected integrationsNot testedNot testedNot testedNot testedComponent stories only. The deployment held no synced work or review runs.

Complete processes​

A process is complete when a person finishes it from the first step to the last, including the error they make on the way. These ran by keyboard alone.

ProcessRoles and statesEnvironmentSuccess pathError recoveryEvidence or issue
A new person signs in and accepts the termsSigned out, then a new accountE1PassNot tested: the dev sign-in and the terms step were not made to failSpec test "a new person signs in and accepts the terms". Sign-in through GitHub or GitLab leaves the application. That handoff and return are not tested.
A new person signs in and accepts the termsSigned out, then a new accountE2PassNot testedThe same test
A new person signs in and accepts the termsSigned out, then a new accountE3PassNot testedThe same test
Create and delete a practice groupWorkspace administratorE1PassNot tested: a failed request was not inducedSpec test "a practice group is created, shown and deleted"
Create and delete a practice groupWorkspace administratorE2PassNot testedThe same test
Create and delete a practice groupWorkspace administratorE3PassNot testedThe same test
Share an idea with the instance administratorsWorkspace administratorE1PassNot tested: a failed request was not inducedSpec test "an idea is sent to the instance administrators"
Share an idea with the instance administratorsWorkspace administratorE2PassNot testedThe same test
Share an idea with the instance administratorsWorkspace administratorE3PassNot testedThe same test
Create a workspace from a providerSigned inE1, E2, E3Not tested: the evaluation deployment has no GitHub App or GitLab loginNot testedOnly the pages before the provider are in the surface table
Respond to feedback on workSigned in, with feedbackE1, E2, E3Not tested: no feedback exists in the deploymentNot tested
Ask HephaestusWorkspace memberE1, E2, E3Not tested: no language-model providerNot tested
Change settings and delete an accountSigned inE1, E2, E3Not testedNot testedThe settings page passes axe and the Tab walk. The destructive steps were not run
Every process aboveAny roleE4, E5Not testedNot testedNo screen reader has run any process

Remaining work​

An AA conformance claim requires completion of every item here.

NVDA with Firefox on Windows (E4) and VoiceOver with Safari on macOS (E5). On both, for the surfaces in the table above and every process in the second table:

  1. Open the landmark list and the heading list on each page. Expect one navigation named "Primary", a main region and a banner, and one level-1 heading.
  2. Follow a sidebar link and listen. Expect the new page's title, once. Confirm the announcement from RouteAnnouncer is neither missing nor doubled with the screen reader's own.
  3. Press Tab from the top of a page. Expect "Skip to main content" first, and the same order as the keyboard walk. Activate it and confirm the reader lands in the page.
  4. Open the account menu, a dialog and the workspace switcher, and close each with Escape. Expect the name of the widget on opening and focus back on the control on closing.
  5. On Practice setup, expand a group and move a practice with the Move up and Move down menu items. Expect each group announced as a level-2 heading and the move announced.
  6. Open a tab panel with nothing in it, a table (Members, Audit log) and a toast.
  7. Complete each process in the second table, including the steps it lists as not tested.

Surfaces the deployment could not show. Run the same checks and Tab walk on a deployment with synced work, review runs, and a language-model provider. Check these surfaces:

  • Populated rows of the surface table.
  • A conversation: greeting, streaming reply with its status line, retry after failure, and the composer.
  • The reviewed-work feedback page.
  • Connected integration screens.

Roles. Run the keyboard walk and axe scan as a workspace member in all three browsers. Complete the keyboard processes in each role the application supports.

Error recovery. For each process, make a request fail with a stopped server or rejected request. Check that the person receives an explanation, can retry, and keeps their input.

The record itself. Choose a random sample once the above exist and produce the WCAG-EM report.

Observations that fail no criterion​

  • Several forms keep their submit button disabled until a required field is filled, and nothing says which field. A disabled native button leaves the tab order and says nothing. This does not fail an AA criterion, and a visible message on submit would be easier to recover from.
  • The header environment badge (Local, Staging) is a tooltip trigger that a keyboard cannot reach. Its text is on the page, so the tooltip adds nothing that is not already there.
  • The imprint and privacy pages render two level-1 headings when the operator has not configured the text. Headings are in order and no criterion limits their number.

Repeating this evaluation​

# Server (profile e2e), a database seeded with webapp/e2e/seed.sql, and a production build of the SPA
vp run dev:server:e2e
vp -C webapp build && vp -C webapp preview --port 4200

vp run --filter webapp test:e2e accessibility.spec.ts # axe, keyboard and processes, Chromium
vp run --filter webapp test:storybook # axe on every story

The spec runs in the existing end-to-end job in CI, on Chromium. The Firefox and WebKit runs used a throwaway Playwright configuration with a firefox or webkit project and the same file.