# StellaOps UI Accessibility Guide ## Overview StellaOps web interface aims for WCAG 2.1 AA compliance. This document outlines accessibility requirements, testing procedures, and implementation patterns. ## WCAG 2.1 AA Compliance Checklist ### Perceivable | Criterion | Requirement | Implementation | |-----------|-------------|----------------| | **1.1.1 Non-text Content** | Alt text for images | All `` elements have `alt` attribute | | **1.3.1 Info and Relationships** | Semantic HTML | Use proper heading hierarchy, lists, tables | | **1.3.2 Meaningful Sequence** | Logical reading order | DOM order matches visual order | | **1.4.1 Use of Color** | Color not sole indicator | Use icons + color, patterns + color | | **1.4.3 Contrast (Minimum)** | 4.5:1 for text | All text meets contrast ratio | | **1.4.4 Resize Text** | 200% zoom works | No horizontal scroll at 200% zoom | | **1.4.11 Non-text Contrast** | 3:1 for UI components | Buttons, inputs, focus indicators | ### Operable | Criterion | Requirement | Implementation | |-----------|-------------|----------------| | **2.1.1 Keyboard** | All functionality keyboard accessible | Tab navigation, keyboard shortcuts | | **2.1.2 No Keyboard Trap** | Users can navigate away | Focus trap only in modals | | **2.4.1 Bypass Blocks** | Skip navigation link | Skip link at top of page | | **2.4.3 Focus Order** | Logical focus order | Tab order matches visual layout | | **2.4.6 Headings and Labels** | Descriptive headings | Clear, unique page titles | | **2.4.7 Focus Visible** | Focus indicator visible | Custom focus styles | ### Understandable | Criterion | Requirement | Implementation | |-----------|-------------|----------------| | **3.1.1 Language of Page** | `lang` attribute set | `` | | **3.2.1 On Focus** | No unexpected changes | No auto-submit on focus | | **3.3.1 Error Identification** | Errors identified | Error messages with field reference | | **3.3.2 Labels or Instructions** | Labels provided | All inputs have labels | ### Robust | Criterion | Requirement | Implementation | |-----------|-------------|----------------| | **4.1.1 Parsing** | Valid HTML | No duplicate IDs, proper nesting | | **4.1.2 Name, Role, Value** | ARIA attributes | Proper ARIA for custom widgets | ## Color Contrast Requirements ### Severity Colors (with sufficient contrast) ```css /* All severity colors meet 4.5:1 contrast on white background */ :root { --color-critical: #991b1b; /* 7.8:1 on white */ --color-critical-bg: #fef2f2; --color-high: #9a3412; /* 7.5:1 on white */ --color-high-bg: #fff7ed; --color-medium: #854d0e; /* 7.2:1 on white */ --color-medium-bg: #fefce8; --color-low: #166534; /* 6.8:1 on white */ --color-low-bg: #f0fdf4; --color-info: #1e40af; /* 8.2:1 on white */ --color-info-bg: #eff6ff; } ``` ### Dark Mode Contrast ```css /* Dark mode maintains contrast ratios */ [data-theme="dark"] { --color-critical: #fca5a5; /* 4.5:1 on dark background */ --color-high: #fdba74; --color-medium: #fde047; --color-low: #86efac; --color-info: #93c5fd; } ``` ## Keyboard Navigation ### Global Shortcuts | Shortcut | Action | Scope | |----------|--------|-------| | `Cmd/Ctrl+K` | Open command palette | Global | | `?` | Show keyboard help | Global | | `Esc` | Close modal/Clear selection | Global | | `g h` | Go to Home | Navigation | | `g f` | Go to Findings | Navigation | | `g p` | Go to Policy | Navigation | | `g o` | Go to Ops | Navigation | ### List Navigation | Shortcut | Action | |----------|--------| | `j` / `↓` | Move down | | `k` / `↑` | Move up | | `Enter` | Select/Open | | `Space` | Toggle checkbox | | `x` | Select current | | `/` | Focus search | ### Implementation Example ```typescript @Injectable({ providedIn: 'root' }) export class KeyboardShortcutService { private shortcuts = new Map(); register(key: string, handler: ShortcutHandler): void { this.shortcuts.set(key.toLowerCase(), handler); } @HostListener('window:keydown', ['$event']) handleKeydown(event: KeyboardEvent): void { // Ignore when typing in inputs if (this.isTyping(event)) return; const key = this.normalizeKey(event); const handler = this.shortcuts.get(key); if (handler) { event.preventDefault(); handler.execute(); } } } ``` ## Focus Management ### Focus Trap for Modals ```typescript @Directive({ selector: '[appFocusTrap]' }) export class FocusTrapDirective implements AfterViewInit, OnDestroy { private focusableElements: HTMLElement[] = []; private previousActiveElement: HTMLElement | null = null; ngAfterViewInit(): void { this.previousActiveElement = document.activeElement as HTMLElement; this.focusableElements = this.getFocusableElements(); this.focusFirst(); } ngOnDestroy(): void { this.previousActiveElement?.focus(); } @HostListener('keydown.tab', ['$event']) handleTab(event: KeyboardEvent): void { const first = this.focusableElements[0]; const last = this.focusableElements[this.focusableElements.length - 1]; if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } } } ``` ### Focus Visible Styles ```css /* Custom focus indicator */ *:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; border-radius: var(--radius-sm); } /* Remove default outline for mouse users */ *:focus:not(:focus-visible) { outline: none; } /* High contrast mode focus */ @media (forced-colors: active) { *:focus-visible { outline: 3px solid CanvasText; } } ``` ## Screen Reader Support ### ARIA Attributes ```html
{{ statusMessage }}
Loading findings...
{{ errorMessage }}
...
``` ### Screen Reader Announcements ```typescript @Injectable({ providedIn: 'root' }) export class AnnouncerService { private liveRegion: HTMLElement; constructor() { this.createLiveRegion(); } announce(message: string, politeness: 'polite' | 'assertive' = 'polite'): void { this.liveRegion.setAttribute('aria-live', politeness); this.liveRegion.textContent = ''; // Force reflow void this.liveRegion.offsetWidth; this.liveRegion.textContent = message; } private createLiveRegion(): void { this.liveRegion = document.createElement('div'); this.liveRegion.setAttribute('aria-live', 'polite'); this.liveRegion.setAttribute('aria-atomic', 'true'); this.liveRegion.className = 'sr-only'; document.body.appendChild(this.liveRegion); } } ``` ## Form Accessibility ### Input Labels ```html Enter CVE ID like CVE-2024-1234
Severity Filter
``` ### Error Messages ```html
{{ getErrorMessage(control) }}
``` ## Motion and Animation ### Reduced Motion Support ```css /* Respect user preference */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } } /* Safe animations (opacity, transform only) */ .fade-in { animation: fadeIn 200ms ease-out; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } /* Alternative for reduced motion */ @media (prefers-reduced-motion: reduce) { .fade-in { animation: none; opacity: 1; } } ``` ## Testing Procedures ### Automated Testing ```bash # axe-core CLI npm run test:a11y # Lighthouse accessibility audit lighthouse --only-categories=accessibility https://localhost:4200 # Pa11y for CI pa11y https://localhost:4200 --standard WCAG2AA ``` ### Manual Testing Checklist - [ ] Tab through entire page - all interactive elements focusable - [ ] Keyboard shortcuts work without mouse - [ ] Focus indicator visible on all focusable elements - [ ] Screen reader announces page changes - [ ] Zoom to 200% - no horizontal scroll - [ ] High contrast mode renders correctly - [ ] Reduced motion preference respected ### Screen Reader Testing | Platform | Screen Reader | Browser | |----------|---------------|---------| | Windows | NVDA | Firefox | | Windows | JAWS | Chrome | | macOS | VoiceOver | Safari | | iOS | VoiceOver | Safari | | Android | TalkBack | Chrome | ## Related Documentation - [Information Architecture](./information-architecture.md) - [Component Library](./components.md) - [SPRINT_040 - Keyboard Accessibility](../../implplan/SPRINT_20251229_040_FE_keyboard_accessibility.md)