|
| 1 | +# Desktop App — Accessibility Audit |
| 2 | + |
| 3 | +WCAG 2.1 Level AA and general legibility review. Last updated: Jan 2026. |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## Strengths |
| 8 | + |
| 9 | +### Semantic structure |
| 10 | +- **Modals**: `role="dialog"`, `aria-modal="true"`, `aria-labelledby`, `aria-describedby` where helpful (CreateUserModal, PasswordPrompt, LogOutConfirmModal, DeleteChatHistoryConfirmModal, SetupModal, UserSettingsModal) |
| 11 | +- **Forms**: Labels with `htmlFor` on CreateUserModal, LanguageSelector, SetupModal, SetupSection |
| 12 | +- **Navigation**: Sidebar uses `<nav aria-label={t('ui.navAccountAndSettings')}>` |
| 13 | +- **Decorative icons**: Lucide icons in sidebar use `aria-hidden` |
| 14 | + |
| 15 | +### Keyboard and focus |
| 16 | +- **Modal focus trap**: `useModalFocusTrap` in CreateUserModal, PasswordPrompt, LogOutConfirmModal, DeleteChatHistoryConfirmModal, UserSettingsModal; SetupModal uses it |
| 17 | +- **Escape to close**: Modals respond to Escape |
| 18 | +- **Tab trapping**: Focus stays inside open modal |
| 19 | +- **Focus restore**: Focus returns to trigger element when modal closes |
| 20 | + |
| 21 | +### Touch targets |
| 22 | +- `--touch-target-min: 2.75rem` (44px) for primary controls (WCAG 2.5.5) |
| 23 | +- Send button, modal actions, sidebar buttons meet minimum size |
| 24 | + |
| 25 | +### Screen reader support |
| 26 | +- Chat send: `aria-label={t('ui.send')}` |
| 27 | +- Copy message: `aria-label` + `role="status"` on tooltip |
| 28 | +- Modal close (SetupModal): `aria-label={t('ui.close')}` |
| 29 | +- Form errors: `role="alert"` on CreateUserModal, PasswordPrompt |
| 30 | + |
| 31 | +--- |
| 32 | + |
| 33 | +## Issues |
| 34 | + |
| 35 | +### Critical |
| 36 | + |
| 37 | +#### 1. Focus indicator removed (WCAG 2.4.7 — Focus Visible) |
| 38 | +Several inputs and controls use `outline: none` on `:focus` with no visible replacement: |
| 39 | + |
| 40 | +| File | Selector | Issue | |
| 41 | +|------|----------|-------| |
| 42 | +| ChatInterface.css | `.chat-input:focus` | `outline: none` | |
| 43 | +| CreateUserModal.css | `.form-input:focus` | `outline: none` | |
| 44 | +| PasswordPrompt.css | `.form-input:focus` | `outline: none` | |
| 45 | +| SetupScreen.css | `.selector-dropdown:focus` | `outline: none` | |
| 46 | +| SetupSection.css | `.selector-dropdown:focus`, `.existing-model-selector:focus` | `outline: none` | |
| 47 | +| LanguageSelector.css | `.language-select:focus` | `outline: none` (warm.css adds box-shadow) | |
| 48 | + |
| 49 | +**Recommendation:** Add `:focus-visible` styles: `outline: 2px solid var(--color-primary); outline-offset: 2px` or equivalent. Reserve `outline: none` only for cases where another visible focus style exists. |
| 50 | + |
| 51 | +--- |
| 52 | + |
| 53 | +#### 2. Chat input has no accessible name (WCAG 1.3.1, 4.1.2) |
| 54 | +The chat textarea relies solely on a placeholder. Placeholders are not sufficient as labels. |
| 55 | + |
| 56 | +``` |
| 57 | +<textarea placeholder={t('ui.askHealthQuestion')} ... /> |
| 58 | +``` |
| 59 | + |
| 60 | +**Recommendation:** Add `aria-label={t('ui.askHealthQuestion')}` or a visually hidden `<label>` with `htmlFor`. |
| 61 | + |
| 62 | +--- |
| 63 | + |
| 64 | +#### 3. Password input has no label (WCAG 1.3.1, 4.1.2) |
| 65 | +PasswordPrompt uses a placeholder instead of an associated label. The subtitle explains context but does not provide an accessible name for the input. |
| 66 | + |
| 67 | +**Recommendation:** Add `aria-label={t('ui.password')}` or a visually hidden label. |
| 68 | + |
| 69 | +--- |
| 70 | + |
| 71 | +### High |
| 72 | + |
| 73 | +#### 4. Password visibility toggle not keyboard accessible |
| 74 | +The show/hide password buttons use `tabIndex={-1}`, so they are skipped in the tab order. |
| 75 | + |
| 76 | +**Location:** CreateUserModal (2 instances), PasswordPrompt |
| 77 | + |
| 78 | +**Recommendation:** Remove `tabIndex={-1}` so the toggle is focusable. Add `aria-label`: |
| 79 | +- `aria-label={showPassword ? t('ui.hidePassword') : t('ui.showPassword')}` |
| 80 | + |
| 81 | +--- |
| 82 | + |
| 83 | +#### 5. Chat error not announced to screen readers |
| 84 | +When an error appears in the chat, it is not announced. |
| 85 | + |
| 86 | +``` |
| 87 | +{error && <div className="chat-error">{error}</div>} |
| 88 | +``` |
| 89 | + |
| 90 | +**Recommendation:** Add `role="alert"` so it is announced when it appears. |
| 91 | + |
| 92 | +--- |
| 93 | + |
| 94 | +#### 6. Loading spinner not announced |
| 95 | +The loading spinner is decorative and has no accessible alternative. |
| 96 | + |
| 97 | +**Recommendation:** Add `aria-live="polite"` region with `aria-busy="true"` on the loading container, or a `sr-only` message such as "Loading…" that updates when loading completes. |
| 98 | + |
| 99 | +--- |
| 100 | + |
| 101 | +### Medium |
| 102 | + |
| 103 | +#### 7. Potential color contrast in warm theme |
| 104 | +Warm palette uses oklch. Verify contrast ratios: |
| 105 | + |
| 106 | +- **--color-text-muted** (oklch 0.48 0.035 100) on **--color-bg** (0.985 0.008 100) — may be close to 4.5:1 |
| 107 | +- **--color-text-faint** (0.55) — may fall below 4.5:1 for normal text |
| 108 | +- **Assistant bubble** — Very low contrast by design; may not meet 1.4.3 for users with low vision |
| 109 | + |
| 110 | +**Recommendation:** Run a contrast checker on warm theme combinations. Consider bumping `--color-text-muted` and `--color-text-faint` chroma/lightness if needed. |
| 111 | + |
| 112 | +--- |
| 113 | + |
| 114 | +#### 8. Document language |
| 115 | +`index.html` has `lang="en"` and the app supports multiple languages. When the user switches language, the document `lang` is not updated. |
| 116 | + |
| 117 | +**Recommendation:** Set `document.documentElement.lang` when language changes, or ensure initial load matches user preference. |
| 118 | + |
| 119 | +--- |
| 120 | + |
| 121 | +### Low |
| 122 | + |
| 123 | +#### 9. Skip link |
| 124 | +No skip-to-content link. For a single-main-view desktop app this may be acceptable, but a skip link can still help keyboard users when multiple regions exist. |
| 125 | + |
| 126 | +--- |
| 127 | + |
| 128 | +#### 10. Heading hierarchy |
| 129 | +Multiple screens use `h1` or `h2`. Ensure a single `h1` per view and a logical order (h1 → h2 → h3). Error screen uses `h1`; Loading and UserProfileSelector use `h1`-style titles. Verify order when views switch. |
| 130 | + |
| 131 | +--- |
| 132 | + |
| 133 | +## Summary checklist |
| 134 | + |
| 135 | +| Criterion | Status | |
| 136 | +|----------|--------| |
| 137 | +| 1.1.1 Non-text content | Partial — icons have aria-hidden where decorative | |
| 138 | +| 1.3.1 Info and relationships | Issues — chat input, password input need labels | |
| 139 | +| 1.4.3 Contrast (minimum) | Verify — warm theme, assistant bubble | |
| 140 | +| 2.1.1 Keyboard | Pass — modals, forms keyboard accessible | |
| 141 | +| 2.4.7 Focus visible | Fail — many focus outlines removed | |
| 142 | +| 4.1.2 Name, role, value | Partial — several inputs need accessible names | |
| 143 | +| 4.1.3 Status messages | Partial — chat error needs role="alert" | |
| 144 | + |
| 145 | +--- |
| 146 | + |
| 147 | +## Recommended fixes (priority order) |
| 148 | + |
| 149 | +1. ~~Add visible focus indicators for all interactive elements (replace `outline: none` with `:focus-visible` styles).~~ **Done** |
| 150 | +2. ~~Add `aria-label` or label to chat textarea and PasswordPrompt input.~~ **Done** |
| 151 | +3. ~~Make password visibility toggles keyboard accessible and add `aria-label`.~~ **Done** |
| 152 | +4. ~~Add `role="alert"` to the chat error container.~~ **Done** |
| 153 | +5. Run contrast checks on the warm theme and tweak if needed. |
0 commit comments