# WCAG 2.2 AAA Accessibility Remediation Design **Date:** 2026-02-18 **Status:** Approved **Scope:** 147 accessibility issues across 17 existing files, 3 new files ## Decisions | Decision | Choice | |----------|--------| | Visual approach | Balance accessibility with dark aesthetic | | Modals | Native `` element | | Style cards | Listbox pattern with existing `useFocusableList` hook | | Template colors | Runtime contrast validation only, no JSON template edits | | Justified text | Override to `left` at render time | | Contrast enforcement | Auto-correct via `ensureContrast()` utility at render time | ## Section 1: CSS Foundation (~25 issues) ### Root font size Change `:root { font-size: 16px }` to `font-size: 100%` in `index.css` so user browser preferences are respected. ### Overflow Remove `overflow: hidden` from `body` and `#root`. Replace with `overflow-x: hidden` to prevent horizontal scroll but allow vertical content access when zoomed to 200%. ### Focus indicators Replace `rgba(99, 102, 241, 0.3)` focus ring with `rgba(99, 102, 241, 0.8)` at 2px. Meets 3:1 contrast on dark backgrounds while fitting indigo theme. ### Reduced motion Add global CSS rule: ```css @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } ``` ### Forced colors Add `@media (forced-colors: active)` rules for Windows High Contrast Mode compatibility. ### Color replacements Across all components: `text-zinc-600` -> `text-zinc-400`, `text-zinc-500` -> `text-zinc-400`. All text hits 7:1+ contrast on zinc-950. Keeps dark aesthetic. ## Section 2: Modal System (~20 issues) ### New `src/hooks/useDialog.ts` Reusable hook wrapping native ``: - Manages `dialogRef`, `showModal()`/`close()` - Restores focus to trigger element on close - Wires `aria-labelledby` - Framer Motion animations on inner content div ### KeyboardShortcutsHelp (App.tsx) Convert from `motion.div` overlay to ``. Add `aria-labelledby` to heading. Close button gets `aria-label="Close shortcuts"`. Escape/focus trap handled natively. ### ExportOptionsModal Same `` conversion. Radio buttons wrapped in `
`. Close button labeled. ### StylePreviewModal Same `` conversion. Close button labeled. Iframe HTML gets `lang="en"`. ### Animation approach `dialog[open]` CSS selector for entrance. `::backdrop` gets fade-in. Framer Motion on inner content for exit. ## Section 3: Keyboard Access & Interactive Elements (~20 issues) ### Logo button Replace `` with ``. ### Single-character shortcut Scope `?`/`/` listener to only fire when `document.activeElement` is body or non-input element. Prevents intercepting typing in search. ### Style cards as listbox Container: `role="listbox"` + `aria-label="Typography styles"`. Each card: `role="option"` + `tabIndex` + `aria-selected` + `onKeyDown`. Wire `useFocusableList` for arrow keys. Add `:focus-visible` indigo ring. ### Favorite button Add `aria-label` (dynamic: "Add to favorites"/"Remove from favorites") + `aria-pressed`. ### Category filters Wrap in `
`. Active button: `aria-pressed="true"`. ### Paper size buttons Wrap in `
`. Active button: `aria-pressed="true"`. ### Search input Add `aria-label="Search templates"` + `aria-describedby` pointing to live result count region. ### Icon-only buttons Add `aria-label` to all: zoom buttons, external link (Preview), close buttons. Decorative icons next to text: `aria-hidden="true"`. ### Target sizes Increase padding on small buttons to hit 44x44px minimum. Use `min-w-[44px] min-h-[44px]` with centered content where inline growth not possible. ## Section 4: ARIA Live Regions & Status Announcements (~15 issues) ### Root status region Persistent `
` in App.tsx root. Updated on `appState` changes. ### Generating state Add `aria-busy="true"` + `role="status"` to container. ### Preview loading Wrap "Loading..." in `role="status"`. ### Success message `aria-live="polite"` on button text area for "Saved!" announcement. ### Search results Visually-hidden `role="status"` announcing "{N} templates found". Debounced 300ms. ### Error fixes - Remove `aria-live="polite"` where `role="alert"` already set (implies assertive) - Remove 5-second auto-dismiss from FileUpload error - Add `aria-describedby` linking FileUpload error to dropzone - Template loading: `role="status"`. Template error: `role="alert"` ### Loading state Replace `return null` with minimal `
Loading TypoGenie
`. ## Section 5: Semantic Structure & Landmarks (~15 issues) ### Skip navigation `Skip to main content` in index.html. ### Page title `TypoGenie - Markdown to Word Converter` ### Noscript `` ### Landmarks - `id="main-content"` on `
` in both app states - `