Covers 147 issues across 8 sections: CSS foundation, modal system, keyboard access, ARIA live regions, semantic structure, DOCX output, template contrast validation, and miscellaneous fixes.
8.2 KiB
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 <dialog> 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:
@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 <dialog>:
- 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 <dialog>. Add aria-labelledby to heading. Close button gets aria-label="Close shortcuts". Escape/focus trap handled natively.
ExportOptionsModal
Same <dialog> conversion. Radio buttons wrapped in <fieldset><legend>. Close button labeled.
StylePreviewModal
Same <dialog> 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 <motion.div onClick> with <motion.button aria-label="TypoGenie - Reset to home">.
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 <div role="group" aria-label="Filter by category">. Active button: aria-pressed="true".
Paper size buttons
Wrap in <div role="group" aria-label="Paper size">. 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 <div role="status" aria-live="polite" className="sr-only"> 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"whererole="alert"already set (implies assertive) - Remove 5-second auto-dismiss from FileUpload error
- Add
aria-describedbylinking FileUpload error to dropzone - Template loading:
role="status". Template error:role="alert"
Loading state
Replace return null with minimal <div role="status">Loading TypoGenie</div>.
Section 5: Semantic Structure & Landmarks (~15 issues)
Skip navigation
<a href="#main-content" class="sr-only focus:not-sr-only">Skip to main content</a> in index.html.
Page title
<title>TypoGenie - Markdown to Word Converter</title>
Noscript
<noscript><p>TypoGenie requires JavaScript to run.</p></noscript>
Landmarks
id="main-content"on<main>in both app states<nav aria-label="Style filters">around category buttons<section aria-label="Style list">and<section aria-label="Style preview">in StyleSelector
Progress stepper
Wrap in <nav aria-label="Progress"><ol>. Active step: aria-current="step".
Shortcuts list
Convert to <dl> with <dt>/<dd>.
Decorative elements
aria-hidden="true" on blob backgrounds, window-chrome dots, visual dividers.
Iframe lang
Both Preview.tsx and StylePreviewModal.tsx: inject <html lang="en">.
Section 6: DOCX Output Accessibility (~8 issues)
Document metadata
Add title (from filename/first h1), description, language to Document constructor.
Image handling
New <img> handler producing [Image: {alt text}] placeholder paragraph in italic.
Table headers
Set tableHeader: true on TableRow containing <th> elements.
Heading preservation in table mode
Add HeadingLevel to paragraph inside processHeaderAsTable table cells.
Code block style
Add custom "Code" paragraph style to DOCX styles. Apply to code blocks.
Section 7: Template Contrast Validation (~20 issues)
New src/utils/contrastUtils.ts
hexToRgb(hex)- parse hex to RGBrelativeLuminance(rgb)- WCAG luminance formulacontrastRatio(color1, color2)- ratio calculationensureContrast(fg, bg, minRatio)- auto-correct foreground color until ratio met
Integration in templateRenderer.ts
Run ensureContrast() on every text/background pair at CSS generation time. Body text: 7:1. Large text (18pt+ or 14pt+ bold): 4.5:1.
del element fix
When resolving del color, if border palette color fails contrast, fall back to textSecondary.
Justified text
Override justify to left at render time (1.4.8 AAA prohibits justified text).
Line-height floor
Enforce minimum 1.5 for body text, 1.0 for headings. Clamp upward if template specifies lower.
Section 8: Remaining Miscellaneous (~10 issues)
Iframe keyboard
tabIndex={-1} on preview iframe in StyleSelector (passive preview, not interactive).
Error boundary
New src/components/ErrorBoundary.tsx wrapping <App />. Renders accessible error with role="alert" on crash.
Framer Motion reduced motion
Import useReducedMotion from motion/react in App.tsx. When true, disable infinite blob/gradient animations via conditional animate props.
Hidden buttons on small screens
Add always-visible <kbd>?</kbd> hint regardless of breakpoint.
Truncation
Add title attribute with full description on line-clamp-2 elements.
Files Modified (17)
index.html, src/index.css, src/App.tsx, src/main.tsx, src/components/StyleSelector.tsx, src/components/Preview.tsx, src/components/ExportOptionsModal.tsx, src/components/StylePreviewModal.tsx, src/components/FileUpload.tsx, src/hooks/useSettings.ts, src/hooks/useTemplates.ts, src/services/templateRenderer.ts, src/utils/docxConverter.ts, src/utils/fontUtils.ts
Files Created (3)
src/utils/contrastUtils.ts, src/hooks/useDialog.ts, src/components/ErrorBoundary.tsx