V1 Web PWA · Surface walkthrough

Shell: Keyboard and accessibility

A per-surface walkthrough of the V1 Web PWA: layout, states, interactions, data, and cross-references.

unspecified
13sections4 minread1table

On this page

Source: apps/oshun/web/src/components/AccessibilityShell.tsx, apps/oshun/web/src/components/KeyboardShortcutHelp.tsx, apps/oshun/web/src/components/CommandPalette.tsx, apps/oshun/web/src/components/FocusModeToggle.tsx, apps/oshun/web/src/navigation/routes.ts (WEB_SHELL_ROUTE_PATHS), apps/oshun/web/src/navigation/assistant-entry.ts (dispatchOshunAssistantOpen)

Global keyboard, focus, and screen-reader behavior. If something here is broken, the whole product is broken for the people who depend on it.

AccessibilityShell renders skip links as the first focusable elements.

  • Tab from page load — focus lands on first visible skip link
  • Skip targets
    • #main-content — "Skip to main content"
    • #shell-nav — "Skip to navigation"
    • #domains — "Skip to domains"
  • Activation — Enter focuses target via requestAnimationFrame
  • Visible on focus — high-contrast pill at top-left; never invisible
  • Fallback — when targets absent, only #main-content is shown
  • Screen reader — links announced as "Skip to "

Global keyboard shortcuts#

Implemented in AccessibilityShell + KeyboardShortcutHelp. Press Shift+? to open the help dialog (KeyboardShortcutHelp.tsx:74).

Keys Action
Alt+/ Focus shell search input (AccessibilityShell.tsx:203)
⌘K / Ctrl+K Open command palette (command-palette/CommandPalette.tsx:794)
Alt+1Alt+5 Go to Home / Explore / Activity / Library / Profile by
index (AccessibilityShell.tsx:167-187)
Alt+A Open assistant overlay (dispatchOshunAssistantOpen,
AccessibilityShell.tsx:189-200)
Esc Close modals / overlays / menus
Shift+? Open keyboard shortcut help (KeyboardShortcutHelp.tsx:74)
Alt+f Toggle focus mode (FocusModeToggle, FocusModeToggle.tsx:185)

There is no g <letter> chord handler; route navigation is Alt+1..5, not a two-key g-prefixed chord.

For each binding, verify:

  • Fires in the browser when not focused in a text input
  • Suppressed when typing in <input>, <textarea>, contenteditable
  • No conflict with browser/OS shortcuts on macOS, Windows, Linux
  • Help dialog lists the binding with the correct keys
  • DiscoverableKeyboardShortcutHelp reachable from menu or ?

Command palette#

CommandPalette.tsx — opens via ⌘K / Ctrl+K.

  • Open — keystroke opens; focus traps inside; Esc closes
  • Search — fuzzy match across commands; recents pinned at top
  • Categories — navigate, create, search, assistant, settings
  • Keyboard nav — Up/Down moves selection; Enter activates; Tab/Shift+Tab equivalent
  • Screen reader — combobox role; selection announced
  • Mobile — full-screen overlay; native keyboard pushes content up

Focus management#

  • Focus visible — every interactive element has a visible focus ring (no outline: none without a replacement)
  • Focus trap in modals — Tab cycles inside; Esc closes; focus returns to opener
  • Focus on route change — moves to #main-content heading on navigation
  • No focus loss — when an element is removed, focus moves to the next sensible target
  • autofocus discipline — only on intentional entry points (sign-in email, search)

ARIA & landmarks#

  • <main id="main-content"> present on every routable page
  • <nav id="shell-nav"> for primary shell nav
  • <nav> for domain switcher — labeled with aria-label="Domains"
  • Headings — one <h1> per page; subsequent levels nested correctly
  • Buttons vs links — button for actions, link for navigation; never <div onClick>
  • aria-live — toasts (polite), errors (assertive), no other live regions
  • aria-expanded — on toggles for menus, accordions
  • aria-current="page" — on active shell-nav item
  • aria-busy — on regions while loading

Screen reader walk#

Test on at least one of: VoiceOver (macOS/iOS), NVDA (Windows), TalkBack (Android).

  • First nav — page title, then main landmark, then content
  • Form fields — label, input, error, hint all spoken
  • Live regions — toast, offline banner, sync complete spoken at correct urgency
  • Dynamic content — loading → loaded transitions announced
  • Tables/lists — semantic roles, row/column count when applicable

Color and contrast#

  • WCAG 2.2 AA — body text ≥ 4.5:1, large text ≥ 3:1, UI ≥ 3:1
  • Forced-colors / Windows high-contrast — UI still usable; colors come from system
  • Color not the only signal — error states use icon + text, not red alone
  • Dark mode — same ratios met
  • Color-blind palettes — green/red distinctions also use shape/text

Motion and animation#

  • prefers-reduced-motion: reduce — page transitions, toast slides, carousel autoplay, parallax, decorative motion all disabled or reduced
  • No animation > 5 s without an off control
  • No flashing > 3× / s (seizure safety)

Pointer / touch#

  • Touch targets ≥ 44×44 px on coarse pointers
  • Spacing — adjacent targets separated by ≥ 8 px to prevent mis-taps
  • Hover-only — no information conveyed by hover alone; keyboard equivalent exists
  • Long-press — has a non-long-press alternative

Focus mode#

FocusModeToggle.tsx — minimize chrome for reading.

  • ToggleAlt+f or button in header
  • Active — chrome reduced; nav hidden; theme adjusts
  • Exit — same toggle, Esc, or navigation
  • Persists per route — not globally sticky
  • Screen reader — announces "Focus mode on/off"

Language#

  • <html lang> matches active locale
  • Language switcher (LanguageSwitcher) updates lang and reloads strings
  • RTL supportdir reverses; layout mirrors where designed
  • Hreflang alternates — set per route for SEO

Cross-references#

Open questions / known gaps#

  • Confirm aria-keyshortcuts annotations present on shortcut-bound buttons
  • Stand up a regular axe-core / pa11y CI sweep so per-view files can reference a baseline rather than re-running the same checks