CODEBASE_ANALYSIS.md: - Comprehensive architecture review - State management analysis (Jotai + EventStore + Dexie) - Component patterns assessment - Security audit findings (zero vulnerabilities) - Performance analysis - 12-week S-tier improvement roadmap ACCESSIBILITY_PLAN.md: - Current state assessment (16% ARIA coverage) - WCAG 2.1 AA compliance roadmap - Phased implementation plan: - Phase 1: Foundation (keyboard nav, focus management) - Phase 2: Screen reader support - Phase 3: Visual accessibility - Phase 4: Advanced features - Component-specific accessibility requirements
11 KiB
Accessibility Improvement Plan
This document outlines the accessibility improvements planned for Grimoire to achieve WCAG 2.1 AA compliance.
Current State Assessment
Current Coverage: ~16% of components have ARIA attributes
| Category | Status | Details |
|---|---|---|
| Keyboard Navigation | ⚠️ Partial | Cmd+K works, limited elsewhere |
| Screen Reader Support | ⚠️ Partial | Basic labels, missing live regions |
| Focus Management | ✅ Good | Visible focus rings |
| Color Contrast | ⚠️ Unchecked | No WCAG verification |
| Loading States | ✅ Good | Skeletons with aria-busy |
| Error Handling | ⚠️ Partial | Errors not announced |
Phase 1: Foundation (Priority: High)
1.1 Keyboard Navigation Improvements
Files to update: CommandLauncher.tsx, Home.tsx, TabBar.tsx
// Add keyboard shortcuts help modal (Cmd+?)
const KEYBOARD_SHORTCUTS = [
{ keys: ['⌘', 'K'], description: 'Open command palette' },
{ keys: ['⌘', '1-9'], description: 'Switch workspace' },
{ keys: ['Escape'], description: 'Close dialog/modal' },
{ keys: ['↑', '↓'], description: 'Navigate list items' },
{ keys: ['Enter'], description: 'Select/confirm' },
];
Tasks:
- Create
KeyboardShortcutsDialogcomponent - Add
Cmd+?shortcut to show help - Add keyboard navigation to window tiles (focus, close, resize)
- Implement roving tabindex for command list
- Add skip links for main content areas
1.2 Focus Management
Files to update: components/ui/dialog.tsx, GlobalAuthPrompt.tsx
// Focus trap for modals
import { FocusTrap } from '@radix-ui/react-focus-trap';
// Return focus after dialog close
const previousFocusRef = useRef<HTMLElement | null>(null);
useEffect(() => {
if (open) {
previousFocusRef.current = document.activeElement as HTMLElement;
} else {
previousFocusRef.current?.focus();
}
}, [open]);
Tasks:
- Verify all dialogs trap focus properly
- Return focus to trigger element on close
- Add
autoFocusto first interactive element in dialogs - Prevent focus from leaving modal while open
1.3 Screen Reader Announcements (Live Regions)
Create new file: src/components/ui/Announcer.tsx
import { createContext, useContext, useState, useCallback } from 'react';
interface AnnouncerContextValue {
announce: (message: string, politeness?: 'polite' | 'assertive') => void;
}
const AnnouncerContext = createContext<AnnouncerContextValue | null>(null);
export function AnnouncerProvider({ children }: { children: React.ReactNode }) {
const [politeMessage, setPoliteMessage] = useState('');
const [assertiveMessage, setAssertiveMessage] = useState('');
const announce = useCallback((message: string, politeness: 'polite' | 'assertive' = 'polite') => {
if (politeness === 'assertive') {
setAssertiveMessage(message);
setTimeout(() => setAssertiveMessage(''), 1000);
} else {
setPoliteMessage(message);
setTimeout(() => setPoliteMessage(''), 1000);
}
}, []);
return (
<AnnouncerContext.Provider value={{ announce }}>
{children}
<div
aria-live="polite"
aria-atomic="true"
className="sr-only"
>
{politeMessage}
</div>
<div
aria-live="assertive"
aria-atomic="true"
className="sr-only"
>
{assertiveMessage}
</div>
</AnnouncerContext.Provider>
);
}
export function useAnnounce() {
const context = useContext(AnnouncerContext);
if (!context) throw new Error('useAnnounce must be used within AnnouncerProvider');
return context.announce;
}
Integration points:
- Wrap app in
AnnouncerProvider - Announce when command executes: "Opening profile viewer"
- Announce when window closes: "Window closed"
- Announce loading complete: "Timeline loaded, 50 events"
- Announce errors: "Error: Failed to load profile"
Phase 2: Form Accessibility (Priority: High)
2.1 Form Error Association
Pattern to implement across all forms:
interface FormFieldProps {
id: string;
label: string;
error?: string;
description?: string;
}
function FormField({ id, label, error, description, children }: FormFieldProps) {
const errorId = `${id}-error`;
const descriptionId = `${id}-description`;
return (
<div>
<Label htmlFor={id}>{label}</Label>
{description && (
<span id={descriptionId} className="text-sm text-muted-foreground">
{description}
</span>
)}
{React.cloneElement(children as React.ReactElement, {
id,
'aria-describedby': [
description ? descriptionId : null,
error ? errorId : null,
].filter(Boolean).join(' ') || undefined,
'aria-invalid': !!error,
})}
{error && (
<span id={errorId} role="alert" className="text-sm text-destructive">
{error}
</span>
)}
</div>
);
}
Files to update:
SpellDialog.tsx- Spell creation formSettingsDialog.tsx- Settings inputsWorkspaceSettings.tsx- Workspace name inputCommandLauncher.tsx- Command input
2.2 Required Field Indicators
// Add to Label component
function Label({ required, children, ...props }) {
return (
<label {...props}>
{children}
{required && <span aria-hidden="true" className="text-destructive ml-1">*</span>}
{required && <span className="sr-only"> (required)</span>}
</label>
);
}
Phase 3: Component ARIA Improvements (Priority: Medium)
3.1 Event Renderers
Base pattern for all renderers:
// BaseEventRenderer.tsx additions
<article
aria-label={`${kindName} by ${authorName}`}
aria-describedby={`event-${event.id}-content`}
>
<header>
<UserName pubkey={pubkey} aria-label={`Author: ${displayName}`} />
<time dateTime={isoDate} aria-label={`Posted ${relativeTime}`}>
{relativeTime}
</time>
</header>
<div id={`event-${event.id}-content`}>
{children}
</div>
</article>
Tasks:
- Add
articlelandmark to event containers - Add proper
timeelements with dateTime - Add aria-labels to interactive elements
- Ensure all buttons have labels (close, menu, copy, etc.)
3.2 Feed/Timeline Components
Files to update: Feed.tsx, ReqViewer.tsx
// Add feed landmarks
<section aria-label="Event timeline" role="feed" aria-busy={loading}>
<h2 className="sr-only">Timeline</h2>
{events.map((event, index) => (
<article
key={event.id}
aria-posinset={index + 1}
aria-setsize={events.length}
>
<KindRenderer event={event} />
</article>
))}
</section>
Tasks:
- Add
role="feed"to timeline containers - Add
aria-posinsetandaria-setsizefor virtual lists - Add
aria-busyduring loading - Announce when new events arrive
3.3 Collapsible/Accordion
Files to update: ui/accordion.tsx, ui/collapsible.tsx
// Ensure proper ARIA states
<button
aria-expanded={isOpen}
aria-controls={contentId}
>
Toggle
</button>
<div
id={contentId}
aria-hidden={!isOpen}
role="region"
>
{children}
</div>
Phase 4: Color & Visual Accessibility (Priority: Medium)
4.1 Color Contrast Audit
Tool: Use axe-core for automated checking
npm install -D @axe-core/react
// Development-only accessibility audit
if (process.env.NODE_ENV === 'development') {
import('@axe-core/react').then(axe => {
axe.default(React, ReactDOM, 1000);
});
}
Known issues to check:
- Muted foreground text (
hsl(215 20.2% 70%)) - Gradient text (
.text-grimoire-gradient) - Disabled state opacity (50%)
- Placeholder text color
4.2 High Contrast Mode
Create theme option:
/* Add to index.css */
@media (prefers-contrast: more) {
:root {
--foreground: 0 0% 0%;
--background: 0 0% 100%;
--muted-foreground: 0 0% 30%;
--border: 0 0% 0%;
}
.dark {
--foreground: 0 0% 100%;
--background: 0 0% 0%;
--muted-foreground: 0 0% 80%;
--border: 0 0% 100%;
}
}
Tasks:
- Add system preference detection
- Create high-contrast theme variables
- Test with Windows High Contrast Mode
- Add manual toggle in settings
4.3 Reduced Motion
/* Already partially implemented, verify coverage */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Tasks:
- Audit all animations (Framer Motion, CSS transitions)
- Ensure skeleton pulse respects preference
- Verify window transitions can be disabled
Phase 5: Testing & Documentation (Priority: High)
5.1 Automated Testing
Add to CI pipeline:
# .github/workflows/accessibility.yml
name: Accessibility Checks
on: [push, pull_request]
jobs:
a11y:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npm run build
- name: Run axe-core
run: npx @axe-core/cli http://localhost:4173
5.2 Manual Testing Checklist
Keyboard-only testing:
- Can navigate entire app without mouse
- Focus order is logical
- All interactive elements are reachable
- Can dismiss dialogs with Escape
- Can activate buttons with Enter/Space
Screen reader testing (with VoiceOver/NVDA):
- Page structure is announced correctly
- Links and buttons describe their purpose
- Form fields have associated labels
- Errors are announced when they occur
- Loading states are announced
Visual testing:
- Content readable at 200% zoom
- No horizontal scrolling at 320px width (for non-tiling views)
- Focus indicators visible
- Color not sole means of conveying info
5.3 Accessibility Documentation
Create docs/ACCESSIBILITY.md:
- Document keyboard shortcuts
- List known limitations
- Provide screen reader recommendations
- Document testing procedures
Implementation Phases
Phase 1: Foundation (2-3 weeks)
- Live region announcer
- Keyboard shortcuts help
- Focus management fixes
Phase 2: Forms (1-2 weeks)
- Error association pattern
- Required field indicators
- Form validation feedback
Phase 3: Components (2-3 weeks)
- Event renderer improvements
- Feed landmarks
- Dialog ARIA fixes
Phase 4: Visual (1-2 weeks)
- Color contrast audit
- High contrast mode
- Reduced motion support
Phase 5: Testing (Ongoing)
- Automated CI checks
- Manual testing protocol
- Documentation
Success Metrics
| Metric | Current | Target |
|---|---|---|
| axe-core violations | Unknown | 0 critical, <5 minor |
| ARIA coverage | 16% | 90%+ |
| Keyboard accessibility | Partial | Full |
| Color contrast ratio | Unknown | 4.5:1 minimum |
| WCAG 2.1 Level | Unknown | AA |