Frontend a11y: focus-visible on custom controls, route focus management, skip link, tablist/lang semantics #52
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: High. From a frontend UX audit. Keyboard-driven fast data entry is a stated efficiency goal, and this is a public-institution tool.
Problems
focus-visible|focus:outsidecomponents/ui/→ nothing. Raw<button>(lang-switch.tsx:10,search-panel.tsx:69facet chips,field-list.tsxdelete), the four raw<select>, and the sidebarNavLinks (app-shell.tsx:23-62) have no keyboard focus ring (only hover/active). Theui/*kit has rings; screens lose them the moment they hand-roll a control.<Outlet/>,app-shell.tsx:74) and no skip-to-content link — keyboard/SR users tab through the whole sidebar on every navigation and focus is left wherever it was.authorities-page.tsx:48-62setsrole="tablist"/role="tab"/aria-selectedon routerNavLinks, but there's no roving tabindex/arrow-key handling and noaria-controls/tabpanel— announcing "tab" semantics without the keyboard contract is worse than plain links. (Search's visibility filter uses honest<button aria-pressed>.)aria-pressedbuttons with no group label (role="group"/aria-label) and low-contrast inactive state (text-neutral-400).<html lang>never updated.index.htmlhardcodeslang="en"; switching to Swedish doesn't updatedocument.documentElement.lang(wrong for SR pronunciation / browser translation).Suggested fixes
ui/Button/ui/select, or addfocus-visible:ring-3 focus-visible:ring-ring/50 outline-noneto them and the NavLinks.<main>heading (tabIndex={-1}); add a "skip to content" link as the first focusable element.<button aria-pressed>filter group, or implement the full tab pattern; wrap lang switch inrole="group" aria-label; ensure inactive contrast ≥ AA.document.documentElement.langon language change.Source: frontend UX/design audit, 2026-06-06.
Done — merged to
main(d082836).focusRing(outline-none focus-visible:ring-3 focus-visible:ring-ring/50, matching the kit, keyboard-only) on the five remaining bare controls: lang-switch, theme-switch, search visibility chips, the field-list row button, and the authority kind links. (The four raw<select>s and sidebar links already gained rings in #51/#49.)#main-content);<main>isid="main-content" tabIndex={-1}and receives focus on every route change (skipping the initial mount so deep-links don't yank focus). Keyboard/SR users no longer tab through the sidebar on every navigation.role="tab"/aria-selectedwith no keyboard tab-contract; they're now an honest<nav aria-label>ofNavLinks using nativearia-current="page". The lang switch is wrapped in a labelledrole="group".<html lang>sync — a singlei18n.on("languageChanged")listener setsdocument.documentElement.langto the activesv/en(covers the switcher, the config default, and startup), so SR pronunciation / browser translation are correct.New i18n keys
common.language+common.skipToContent(en/sv parity, guarded by the #60 parity test); 226 tests green; typecheck/lint/build/check:size (215.5 KB gz)/check:colors clean; no codename; no new dependency.Follow-ups (out of scope): a full ARIA tab-widget is intentionally not used (these are navigation, not a tab panel); automated axe/a11y scanning in CI; an
aria-liveroute announcer (focusing<main>/the per-route<h1>covers the core need).