Repository navigation
Description
Several buttons in packages/react-core and apps/web contain only an icon and have no aria-label or visually hidden text, so they have no accessible name. A screen reader announces each of them as just "button", and voice-control users have no name to say. The worst cases are on the interactive-instrument toolbar, which participants use in the gateway as well as clinicians in the web app, and the copy, download and print buttons on the results summary. Newer code in the same workspaces already does this correctly (the email-template row actions, the permission editor, TimepointTable), so these are the outliers.
Where
packages/react-core:
src/components/CopyButton/CopyButton.tsx:17-47: the shared copy button, used for the assignment link indatahub/subjects/$subjectId/assignments.tsx:51andsession/remote-assignment.tsx:61, and on the results summary. It also changes only its icon on success, so nothing announces that the copy worked.src/components/InstrumentSummary/InstrumentSummary.tsx:116-121: download and print.src/components/InteractiveContent/InteractiveContent.tsx:129-153: zoom out, zoom in, full screen and the language menu trigger.src/components/FormContent/FormContent.tsx:27-29: the instructions (info) button next to a form's title.
// InteractiveContent.tsx:141-147
<Button size="icon" type="button" variant="outline" onClick={() => setScale(scale + 25)}>
<ZoomInIcon />
</Button>
<Separator className="mx-1 h-6" orientation="vertical" />
<Button size="icon" type="button" variant="outline" onClick={() => void handleToggleFullScreen()}>
<FullscreenIcon />
</Button>apps/web:
src/components/Navbar/Navbar.tsx:47-54: the mobile menu (hamburger) trigger.src/providers/WalkthroughProvider.tsx:473-475: the tutorial's close (X) button.src/components/AssignmentEmailForm/AssignmentEmailForm.tsx:152-154: the "email language" help tooltip trigger.src/components/InstrumentCard/InstrumentCard.tsx:86-92: the open-source licence badge tooltip trigger.src/components/DemoBanner/DemoBanner.tsx:93-95: the "use these credentials" button in each demo-user row.
Reproduce
- Turn on VoiceOver (macOS) or NVDA and start any interactive instrument, e.g. from Administer Instrument in a session.
- Tab through the toolbar above the instrument.
Actual: four consecutive controls are announced as "button" with no name. The same happens for the copy, download and print buttons on the summary at /datahub/subjects/<id>/table/<recordId>, the hamburger menu on a phone-width window, and the tutorial's close button.
Expected: each control has a name in the interface language, e.g. "Zoom in", "Full screen", "Copy to clipboard", "Download results", "Print", "Open menu", "Close tutorial".
Tests
packages/react-core/src/components/InteractiveContent/__tests__/InteractiveContent.test.tsx:it('should give every toolbar button an accessible name, so screen readers can tell zoom, full screen and language apart'), asserting withscreen.getByRole('button', { name: ... }).packages/react-core/src/components/CopyButton/__tests__/CopyButton.test.tsx:it('should expose an accessible name, so the copy action is announced').apps/web/src/components/Navbar/__tests__/Navbar.test.tsx:it('should name the menu trigger, so it is announced as more than "button"').- e2e:
testing/src/specs/instrument-completion.spec.tscan locate the summary's download and print buttons withgetByRole('button', { name })instead of by position.
Suggested fix
Add a translated aria-label (inline t({ en, es, fr })) to each listed button. For CopyButton, also accept a label prop and swap it for a "Copied" label, or add a role="status" live region, when the copy succeeds.
Activity
joshunrau commentedon Oct 26, 2026
Still reproduces on
main(d86740e). Every listed site is unchanged.Sites the list misses
apps/web/src/routes/_app/admin/settings.tsx:179-183,:203-207,:240-247: the three help buttons on the settings page. A name alone does not fix these. They areHoverCardtriggers, which Radix documents as "intended for sighted users only" and "ignored by screen readers" (https://www.radix-ui.com/primitives/docs/components/hover-card). The 1.1.15 trigger sets noaria-describedby, so the help text is never tied to the button. Switch them to theTooltip+aria-labelpattern inMailServerForm.tsx:90-102. RadixTooltippointsaria-describedbyat its content while it is open.BulkRemoteAssignmentWizard.tsx:179andadmin/users/create.tsx:362(the welcome-email fallback dialog, whose whole job is copying the message) both import libui'sCopyButton. Fixing react-core's leaves them unnamed.apps/playground/src/components/Header/ShareButton/ShareButton.tsx:96uses react-core'sCopyButton. Playground icon-only buttons have no accessible names, and editor tabs cannot be closed from the keyboard #1763 lists the Share trigger (:49-51) but not this one, so this issue fixes it.CopyButtonreact-core's copy is libui's with only the import paths changed, and libui 6.18.0 (the latest release) ships the same file, so there is no upstream fix to wait for. It accepts only
size,textandvariantand spreads nothing, so a caller cannot passaria-label.label?: LocalizedText(the prop shapepackages/react-core/AGENTS.mdprescribes for copy the host supplies), defaulting to "Copy to clipboard". Pass "Copy link" fromassignments.tsx:51andremote-assignment.tsx:61.@opendatacapture/react-core.role="status"sr-onlyspan next to the button. Render it empty from the start and fill it on success. Screen readers do not reliably re-announce a focused button whose name changes, so swapping the label is the weaker option.SUCCESSresets only ononMouseLeave(CopyButton.tsx:29-31), and clicks are ignored in that state, so a keyboard user can copy only once. Also reset ononBlur. That keeps the ignore-while-copied test atCopyButton.test.tsx:57passing.CopyButton.test.tsxnever initialises i18n. Once the button callst(), addi18n.init({ translations: {} })inbeforeAll, as the AGENTS.md says.Other notes
InstrumentCard.tsx:86-92renders one of two badges. Give each its own name ("Open-source license" / "Not an open-source license"), not one generic "License".InteractiveContentlanguage trigger and Gateway header's theme and language buttons have no accessible name, so screen readers announce both as just "button" #1722'sLanguageToggleboth need "Change language". Put oneLocalizedTextconstant next toLANGUAGE_LABELSinpackages/react-core/src/utils/language.ts. Gateway header's theme and language buttons have no accessible name, so screen readers announce both as just "button" #1722 still needs libui'sLanguageToggleto forward the name, and 6.18.0 does not, so do not block this issue on it.Tests
InstrumentCard.test.tsx:49finds the badge withgetByRole('button', { name: '' }), so naming the badge breaks it.testing/src/pages/_app/session/remote-assignment.page.ts:20-22finds the copy button withinput[readonly] + buttonbecause it has no name. Switch it togetByRole('button', { name: 'Copy link' }), andremote-assignment.spec.ts:85-109then covers the name end to end.instrument-completion.spec.ts:104is the subject table's export menu. Assert the three names after thesummaryHeadingcheck at:42.playground.spec.ts:218-227does, and its preview renders the sameInteractiveContent, so assert the toolbar names there.admin-settings.spec.tsnorsettings.test.tsxcovers them yet.auth.spec.ts:51: find the demo row's button by name. For the hamburger, add aPHONE_VIEWPORTtest likedatahub.spec.ts:7-8. No spec opens the mobile nav today.InteractiveContent.test.tsx:161,227,234,InstrumentSummary.test.tsx:68,103,192,FormContent.test.tsx:35,WalkthroughProvider.test.tsx:98.Landing
#1825 edits
Navbar.tsx,Navbar.test.tsxandInstrumentSummary.test.tsx. #1730 replaces a test inNavbar.test.tsx. Branch frommainafter #1825 merges.Triage note written with Claude Code. Code references point at
main@d86740e.