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.
Skip links#
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-contentis 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+1–Alt+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
- Discoverable —
KeyboardShortcutHelpreachable 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: nonewithout a replacement) - Focus trap in modals — Tab cycles inside; Esc closes; focus returns to opener
- Focus on route change — moves to
#main-contentheading on navigation - No focus loss — when an element is removed, focus moves to the next sensible target
-
autofocusdiscipline — 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 witharia-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.
- Toggle —
Alt+for 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) updateslangand reloads strings - RTL support —
dirreverses; layout mirrors where designed - Hreflang alternates — set per route for SEO
Cross-references#
- 01-app-shell.md
- 05-notifications.md — toast live-region policies
- Feature spec:
V1/features.md
Open questions / known gaps#
- Confirm
aria-keyshortcutsannotations 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