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
No focus-visible styling on hand-rolled controls. Grep for focus-visible|focus: outside components/ui/ → nothing. Raw <button> (lang-switch.tsx:10, search-panel.tsx:69 facet chips, field-list.tsx delete), the four raw <select>, and the sidebar NavLinks (app-shell.tsx:23-62) have no keyboard focus ring (only hover/active). The ui/* kit has rings; screens lose them the moment they hand-roll a control.
No focus management on route change (<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.
Authority tabs claim tab semantics they don't fulfill.authorities-page.tsx:48-62 sets role="tablist"/role="tab"/aria-selected on router NavLinks, but there's no roving tabindex/arrow-key handling and no aria-controls/tabpanel — announcing "tab" semantics without the keyboard contract is worse than plain links. (Search's visibility filter uses honest <button aria-pressed>.)
Lang switch is two bare aria-pressed buttons with no group label (role="group"/aria-label) and low-contrast inactive state (text-neutral-400).
<html lang> never updated.index.html hardcodes lang="en"; switching to Swedish doesn't update document.documentElement.lang (wrong for SR pronunciation / browser translation).
Suggested fixes
Route custom controls through ui/Button/ui/select, or add focus-visible:ring-3 focus-visible:ring-ring/50 outline-none to them and the NavLinks.
On route change, focus the <main> heading (tabIndex={-1}); add a "skip to content" link as the first focusable element.
Either drop the tab ARIA roles in favor of an honest <button aria-pressed> filter group, or implement the full tab pattern; wrap lang switch in role="group" aria-label; ensure inactive contrast ≥ AA.
Sync document.documentElement.lang on language change.
Source: frontend UX/design audit, 2026-06-06.
**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
- **No focus-visible styling on hand-rolled controls.** Grep for `focus-visible|focus:` outside `components/ui/` → nothing. Raw `<button>` (`lang-switch.tsx:10`, `search-panel.tsx:69` facet chips, `field-list.tsx` delete), the four raw `<select>`, and the sidebar `NavLink`s (`app-shell.tsx:23-62`) have no keyboard focus ring (only hover/active). The `ui/*` kit has rings; screens lose them the moment they hand-roll a control.
- **No focus management on route change** (`<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.
- **Authority tabs claim tab semantics they don't fulfill.** `authorities-page.tsx:48-62` sets `role="tablist"`/`role="tab"`/`aria-selected` on router `NavLink`s, but there's no roving tabindex/arrow-key handling and no `aria-controls`/`tabpanel` — announcing "tab" semantics without the keyboard contract is worse than plain links. (Search's visibility filter uses honest `<button aria-pressed>`.)
- **Lang switch** is two bare `aria-pressed` buttons with no group label (`role="group"`/`aria-label`) and low-contrast inactive state (`text-neutral-400`).
- **`<html lang>` never updated.** `index.html` hardcodes `lang="en"`; switching to Swedish doesn't update `document.documentElement.lang` (wrong for SR pronunciation / browser translation).
## Suggested fixes
- Route custom controls through `ui/Button`/`ui/select`, or add `focus-visible:ring-3 focus-visible:ring-ring/50 outline-none` to them and the NavLinks.
- On route change, focus the `<main>` heading (`tabIndex={-1}`); add a "skip to content" link as the first focusable element.
- Either drop the tab ARIA roles in favor of an honest `<button aria-pressed>` filter group, or implement the full tab pattern; wrap lang switch in `role="group" aria-label`; ensure inactive contrast ≥ AA.
- Sync `document.documentElement.lang` on language change.
_Source: frontend UX/design audit, 2026-06-06._
Focus-visible rings — a shared 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.)
Skip link + route focus — a "Skip to content" link is now the first focusable element (sr-only until focused, jumps to #main-content); <main> is id="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.
Honest semantics — the authority "tabs" were router links faking role="tab"/aria-selected with no keyboard tab-contract; they're now an honest <nav aria-label> of NavLinks using native aria-current="page". The lang switch is wrapped in a labelled role="group".
<html lang> sync — a single i18n.on("languageChanged") listener sets document.documentElement.lang to the active sv/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-live route announcer (focusing <main>/the per-route <h1> covers the core need).
Done — merged to `main` (`d082836`).
- **Focus-visible rings** — a shared `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.)
- **Skip link + route focus** — a "Skip to content" link is now the first focusable element (sr-only until focused, jumps to `#main-content`); `<main>` is `id="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.
- **Honest semantics** — the authority "tabs" were router links faking `role="tab"`/`aria-selected` with no keyboard tab-contract; they're now an honest `<nav aria-label>` of `NavLink`s using native `aria-current="page"`. The lang switch is wrapped in a labelled `role="group"`.
- **`<html lang>` sync** — a single `i18n.on("languageChanged")` listener sets `document.documentElement.lang` to the active `sv`/`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-live` route announcer (focusing `<main>`/the per-route `<h1>` covers the core need).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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).