Features

Markdown

Accessibility

Externa ships user-first accessibility in the authenticated shell: high contrast and reduce motion in the user menu, OS media queries honored without a toggle, and a skip link into main#main-content. Target is WCAG 2.2 AA across the admin. From externa-core 1.4.0, CI enforces it with an axe browser smoke suite, eslint-plugin-jsx-a11y, and a theme-token contrast check (#252–#255).

Appearance settings
Appearance settings (theme and branding)

Related

Personal theme (light/dark): Account settings. Shell layout: Architecture.

User preferences

PreferenceEffectStorage
High contrastStronger semantic shadcn tokens (--foreground, --muted-foreground, --border, sidebar, …)localStorage + cookie accessibility_high_contrast=1
Reduce motionForces reduced animations/transitions when the OS does not already asklocalStorage + cookie accessibility_reduce_motion=1

UI: user menu → Accessibility submenu (summary Off / N on), same checkmark pattern as Appearance/Language.

Hook: resources/js/hooks/use-accessibility-preferences.tsx (sibling of useAppearance). Blade boot via HandleAppearance + app.blade.php stamps high-contrast / reduce-motion on <html> before paint (cookies are not encrypted — see bootstrap/app.php).

OS media queries (always on)

QueryBehavior
prefers-reduced-motion: reduceMotion kill in CSS — no user toggle required
prefers-contrast: moreSame token bumps as .high-contrast without forcing the menu toggle

The user Reduce motion class only adds force-on when the OS preference is off. High contrast from the OS does not require the menu class.

Authenticated layouts (app-sidebar-layout, app-header-layout) and the auth simple layout:

  1. SkipToContent — first focusable control, visible on :focus
  2. Target main#main-content (tabIndex={-1} for programmatic focus)

The sidebar (desktop and mobile sheet) and the header variant render a real <nav aria-label="Main navigation">; file-detail and chat asides are labelled landmarks.

Focus, overlays, and announcements (1.4.0)

  • Focus ring: every control has a visible, offset, full-opacity focus ring and 3:1 input borders (WCAG 1.4.11 / 2.4.7).
  • Overlays: Sheet, Drawer, Dialog, and the file detail panel return focus to the control that opened them.
  • Navigation: after an Inertia page visit, focus moves to #main-content.
  • Live regions: toasts, notifications, the AI final answer, empty-table status, and the settings save bar ("Saving" / "Saved") are announced through polite or assertive live regions.
  • Reduced motion: targeted rules keep Radix / vaul overlays unmounting correctly while motion is reduced.
  • Language: <html lang> follows the UI locale; content-locale switches carry lang and text alternatives for flags.
  • Icon-only buttons have translated a11y.* labels (en / it / de); decorative icons are hidden from assistive tech.

Brand colours and contrast

Theme tokens (default qiick teal, light, dark, and high-contrast) pass WCAG 2.2 AA: 4.5:1 for text, 3:1 for UI components, borders, and focus indicators. Primary hover states keep AA contrast in both themes.

A custom brand colour from Project settings → Appearance is used for the focus ring only when it reaches 3:1 against the page background; otherwise the teal ring stays (#453). Status is never shown by colour alone. A low-contrast brand can still weaken other brand-coloured surfaces, so check your colour in light and dark, or use high-contrast mode.

Automated checks (externa-core CI)

GateWhat it checks
Axe browser smoketests/Browser/A11ySmokeBrowserTest.php (Pest Browser) fails on critical/serious axe issues on login and key authenticated routes (dashboard, collections, items, item edit, files, users, profile, appearance, project settings, /ai) in light, dark, and high-contrast. Required browser check on code PRs.
eslint-plugin-jsx-a11yErrors on shared React components, warnings on pages
npm run a11y:contrastChecks the documented theme-token pairs (docs/a11y-contrast-pairs.md) for light, dark, high-contrast, and high-contrast dark. Default brand only
Other browser testsLandmarks, focus and announcements, accessibility preferences

Manual coverage lives in core docs/a11y-screen-reader-checklist.md, and known limits of third-party widgets (TipTap, CodeMirror / markdown editor, Leaflet, react-day-picker, recharts, SortableJS) with their mitigations are listed in docs/a11y-third-party-limits.md. A passing axe smoke does not cover those widgets.

Still open: font size / dyslexia fonts.

Source map

ConcernLocation
Hookresources/js/hooks/use-accessibility-preferences.tsx
Menuresources/js/components/user-menu-content.tsx
Skip linkresources/js/components/skip-to-content.tsx
CSSresources/css/app.css (.high-contrast, @media)
Blade / cookiesresources/views/app.blade.php, HandleAppearance
Teststests/Feature/AccessibilityPreferencesTest.php, tests/Browser/AccessibilityPreferencesBrowserTest.php, tests/Browser/A11y*BrowserTest.php
Contrast scriptscripts/a11y-contrast.mjs (npm run a11y:contrast)
Previous
SSO / OIDC