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

CriterionRequirementImplementation
1.1.1 Non-text ContentAlt text for imagesAll <img> elements have alt attribute
1.3.1 Info and RelationshipsSemantic HTMLUse proper heading hierarchy, lists, tables
1.3.2 Meaningful SequenceLogical reading orderDOM order matches visual order
1.4.1 Use of ColorColor not sole indicatorUse icons + color, patterns + color
1.4.3 Contrast (Minimum)4.5:1 for textAll text meets contrast ratio
1.4.4 Resize Text200% zoom worksNo horizontal scroll at 200% zoom
1.4.11 Non-text Contrast3:1 for UI componentsButtons, inputs, focus indicators

Operable

CriterionRequirementImplementation
2.1.1 KeyboardAll functionality keyboard accessibleTab navigation, keyboard shortcuts
2.1.2 No Keyboard TrapUsers can navigate awayFocus trap only in modals
2.4.1 Bypass BlocksSkip navigation linkSkip link at top of page
2.4.3 Focus OrderLogical focus orderTab order matches visual layout
2.4.6 Headings and LabelsDescriptive headingsClear, unique page titles
2.4.7 Focus VisibleFocus indicator visibleCustom focus styles

Understandable

CriterionRequirementImplementation
3.1.1 Language of Pagelang attribute set<html lang="en">
3.2.1 On FocusNo unexpected changesNo auto-submit on focus
3.3.1 Error IdentificationErrors identifiedError messages with field reference
3.3.2 Labels or InstructionsLabels providedAll inputs have labels

Robust

CriterionRequirementImplementation
4.1.1 ParsingValid HTMLNo duplicate IDs, proper nesting
4.1.2 Name, Role, ValueARIA attributesProper ARIA for custom widgets

Color Contrast Requirements

Severity Colors (with sufficient contrast)

/* 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

/* 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

ShortcutActionScope
Cmd/Ctrl+KOpen command paletteGlobal
?Show keyboard helpGlobal
EscClose modal/Clear selectionGlobal
g hGo to HomeNavigation
g fGo to FindingsNavigation
g pGo to PolicyNavigation
g oGo to OpsNavigation

List Navigation

ShortcutAction
j / Move down
k / Move up
EnterSelect/Open
SpaceToggle checkbox
xSelect current
/Focus search

Implementation Example

@Injectable({ providedIn: 'root' })
export class KeyboardShortcutService {
  private shortcuts = new Map<string, ShortcutHandler>();

  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

@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

/* 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

<!-- Live regions for dynamic updates -->
<div aria-live="polite" aria-atomic="true" class="sr-only">
  {{ statusMessage }}
</div>

<!-- Loading states -->
<div aria-busy="true" aria-describedby="loading-message">
  <span id="loading-message" class="sr-only">Loading findings...</span>
</div>

<!-- Error announcements -->
<div role="alert" aria-live="assertive">
  {{ errorMessage }}
</div>

<!-- Navigation landmarks -->
<nav aria-label="Main navigation">...</nav>
<nav aria-label="Breadcrumb">...</nav>
<main aria-labelledby="page-title">...</main>

Screen Reader Announcements

@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

<!-- Visible label -->
<label for="cve-search">Search CVEs</label>
<input id="cve-search" type="text" aria-describedby="cve-search-hint">
<span id="cve-search-hint" class="hint">Enter CVE ID like CVE-2024-1234</span>

<!-- Hidden label for icon buttons -->
<button aria-label="Close dialog">
  <svg aria-hidden="true">...</svg>
</button>

<!-- Group labels -->
<fieldset>
  <legend>Severity Filter</legend>
  <label><input type="checkbox" name="severity" value="critical"> Critical</label>
  <label><input type="checkbox" name="severity" value="high"> High</label>
</fieldset>

Error Messages

<div class="form-group" [class.has-error]="control.invalid && control.touched">
  <label for="policy-name">Policy Name</label>
  <input
    id="policy-name"
    [attr.aria-invalid]="control.invalid && control.touched"
    [attr.aria-describedby]="control.invalid ? 'policy-name-error' : null">
  <span id="policy-name-error" role="alert" *ngIf="control.invalid && control.touched">
    {{ getErrorMessage(control) }}
  </span>
</div>

Motion and Animation

Reduced Motion Support

/* 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

# 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

Screen Reader Testing

PlatformScreen ReaderBrowser
WindowsNVDAFirefox
WindowsJAWSChrome
macOSVoiceOverSafari
iOSVoiceOverSafari
AndroidTalkBackChrome