# SophiaClaw Accessibility Audit Report

**Audit Date:** March 11, 2026  
**Audit Scope:** macOS SwiftUI, TUI (Terminal UI), Cross-Platform Consistency  
**Standard:** WCAG 2.1 AA Compliance  
**Auditor:** AI Governance Assessment

---

## Executive Summary

This audit evaluated SophiaClaw's accessibility implementation across macOS SwiftUI and Terminal UI (TUI) platforms against WCAG 2.1 AA guidelines. The audit identified **18 issues** across keyboard navigation, screen reader support, focus management, and color contrast.

### Overall Assessment

| Platform       | Critical | High  | Medium | Total  |
| -------------- | -------- | ----- | ------ | ------ |
| macOS SwiftUI  | 2        | 5     | 4      | 11     |
| TUI            | 1        | 3     | 3      | 7      |
| Cross-Platform | 0        | 1     | 1      | 2      |
| **Total**      | **3**    | **9** | **8**  | **20** |

---

## 1. Color Contrast Analysis

### Verified Color Pairs (from GUIDELINES.md)

| Foreground         | Background               | Ratio  | Compliance            |
| ------------------ | ------------------------ | ------ | --------------------- |
| `#E8E3D5` (text)   | `#2B2F36` (userBg)       | 6.2:1  | ✅ AA Pass            |
| `#F6C453` (accent) | `#1E232A` (codeBlock)    | 8.1:1  | ✅ AA Pass            |
| `#F97066` (error)  | `#2F1F1F` (toolErrorBg)  | 5.4:1  | ✅ AA Pass            |
| `#7B7F87` (dim)    | `#2B2F36` (userBg)       | 2.8:1  | ❌ AA Fail (AAA only) |
| `.labelColor`      | `.windowBackgroundColor` | System | ✅ AA Pass            |

### Issues Found

#### ISSUE #1: Dim Text Fails WCAG AA ❌

- **Priority:** Medium
- **Standard:** WCAG 2.1 AA requires 4.5:1 for normal text
- **Location:** TUI `src/tui/theme/theme.ts:14` - `dim: "#7B7F87"`
- **Impact:** Secondary text, captions, scroll info, and hints are not readable for users with low vision
- **Fix:** Use `#8D949E` (4.5:1 ratio) or reserve dim for decorative content only

#### ISSUE #2: Status Colors Rely on Color Alone ❌

- **Priority:** High
- **Standard:** WCAG 2.1 Level A - 1.4.1 Use of Color
- **Location:** `ChatInputBar.swift:103-113`, `SidebarView.swift:190-192`
- **Problem:** Status indicators (green=connected, red=error, blue=thinking) convey meaning through color only
- **Fix:** Add text labels or shapes alongside color indicators

---

## 2. Keyboard Navigation Audit

### macOS SwiftUI Keyboard Support

| Component        | Tab Traversal               | Mnemonic Keys   | Escape Closes      | Arrow Navigation |
| ---------------- | --------------------------- | --------------- | ------------------ | ---------------- |
| SidebarView      | ✅ Via `.sophiaSidebarItem` | ❌ None         | ❌ Not implemented | ✅ Internal list |
| ChatInputBar     | ✅ Via TextEditor           | ✅ Enter (Send) | ❌ Not implemented | ❌ N/A           |
| SettingsView     | ✅ Via form controls        | ❌ None         | ❌ Not implemented | ✅ Pickers       |
| OnboardingWizard | ✅ Via form controls        | ❌ None         | ❌ Not implemented | ✅ RadioGroups   |
| SessionSwitcher  | ✅ Via Menu                 | ✅ Cmd+N (New)  | ✅ Menu semantics  | ✅ Menu          |

### Issues Found

#### ISSUE #3: No Escape Key Handler in Chat ❌

- **Priority:** High
- **Location:** `ChatInputBar.swift`, `MessageBubble.swift`
- **Problem:** Escape should cancel operation or clear input
- **Fix:** Add `.onKeyPress(.escape)` handlers

```swift
.onKeyPress(.escape) {
    if status == .thinking || status == .executingTools(0) {
        onCancel()
        return .handled(true)
    }
    return .handled(false)
}
```

#### ISSUE #4: No Keyboard Shortcut for Settings ❌

- **Priority:** Medium
- **Location:** `SettingsView.swift`
- **Problem:** No Cmd+, shortcut to open settings
- **Fix:** Add `.keyboardShortcut(",", modifiers: .command)` to settings entry

#### ISSUE #5: Sidebar Missing Skip Links ❌

- **Priority:** Medium
- **Location:** `SidebarView.swift`
- **Problem:** No skip navigation for users to bypass repetitive sidebar
- **Fix:** Add hidden skip links at top of main content area

#### ISSUE #6: TUI Keyboard Traps Possible ⚠️

- **Priority:** Critical
- **Location:** `src/tui/components/filterable-select-list.ts`, `src/tui/components/chat-log.ts`
- **Problem:** FilterableSelectList handles most keys but may not properly pass Tab through
- **Fix:** Ensure Ctrl+Tab or Ctrl+Shift+Tab always escapes component

---

## 3. Screen Reader Support (VoiceOver)

### macOS Accessibility APIs Usage

| API                         | Usage Found  | Coverage |
| --------------------------- | ------------ | -------- |
| `accessibilityLabel`        | ❌ Not found | 0%       |
| `accessibilityHint`         | ❌ Not found | 0%       |
| `accessibilityValue`        | ❌ Not found | 0%       |
| `accessibilityRemoveTraits` | ❌ Not found | 0%       |
| `.help()`                   | ✅ Partial   | ~30%     |

### Issues Found

#### ISSUE #7: No Accessibility Labels on Buttons ❌

- **Priority:** Critical
- **Location:** All button components (`VoiceButton.swift`, `ChatInputBar.swift`, `SidebarView.swift`, `SettingsView.swift`)
- **Problem:** Buttons have `.help()` tooltips but no `.accessibilityLabel()`
- **Impact:** VoiceOver reads "button" without meaningful description
- **Fix:** Add `.accessibilityLabel("Voice Input")`, `.accessibilityHint("Hold to record voice")`

```swift
// VoiceButton.swift
Button(action: onTap) {
    Image(systemName: isRecording ? "mic.fill" : "mic")
        .font(.system(size: 18))
        .foregroundStyle(isRecording ? .white : .primary)
}
.accessibilityLabel(isRecording ? "Stop Recording" : "Start Voice Recording")
.accessibilityHint("Tap to toggle voice recording")
```

#### ISSUE #8: Status Indicators Not Announced ❌

- **Priority:** High
- **Location:** `ChatInputBar.swift:100-115`, `SidebarView.swift:190`
- **Problem:** Status dot changes not announced to VoiceOver
- **Fix:** Add `.accessibilityValue(status.description)` and post announcements

```swift
// StatusIndicator
.onChange(of: status) { newStatus in
    UIAccessibility.post(
        notification: .announcement,
        argument: newStatus.description
    )
}
.accessibilityValue(status.description)
.accessibilityLabel("Connection Status")
```

#### ISSUE #9: Message Bubbles Lack Roles ❌

- **Priority:** High
- **Location:** `MessageBubble.swift`
- **Problem:** User vs assistant messages not distinguished semantically
- **Fix:** Add `.accessibilityElement(children: .combine)` with role traits

```swift
.accessibilityElement(children: .combine)
.accessibilityLabel(message.role.isUser ? "Your message" : "Assistant message")
.accessibilityTraits(message.role.isUser ? .none : .text)
```

#### ISSUE #10: TUI Screen Reader Considerations ⚠️

- **Priority:** Medium
- **Location:** All TUI components
- **Problem:** TUI relies on terminal reading order but lacks explicit ARIA-like patterns
- **Fix:** Cannot fix in pure terminal; document screen reader limitations

---

## 4. Focus Management

### macOS Focus State Usage

| Pattern           | Found        | Example |
| ----------------- | ------------ | ------- |
| `@FocusState`     | ❌ Not found | -       |
| `.focused()`      | ❌ Not found | -       |
| `.focusable()`    | ❌ Not found | -       |
| `.onAppear` focus | ❌ Not found | -       |

### Issues Found

#### ISSUE #11: No Initial Focus Management ❌

- **Priority:** High
- **Location:** `ChatInputBar.swift`, `ChatComposer.swift` (shared)
- **Problem:** Text input should auto-focus on view appear
- **Fix:** Add focus state and auto-focus logic

```swift
@FocusState private var isInputFocused: Bool

var body: some View {
    TextEditor(text: $text)
        .focused($isInputFocused)
        .onAppear {
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
                isInputFocused = true
            }
        }
}
```

#### ISSUE #12: Focus Restoration After Modal ❌

- **Priority:** Medium
- **Location:** `ApprovalModal.swift`, `OnboardingWizard.swift`
- **Problem:** Focus not tracked when closing modals
- **Fix:** Store focus reference and restore on dismissal

---

## 5. Motion & Vestibular Support

### Reduce Motion Pattern Usage

**Found:** `GUIDELINES.md:507` references implementation at `apps/shared/.../ChatMessageViews.swift:576`

```swift
@Environment(\.accessibilityReduceMotion) private var reduceMotion

animation(
    reduceMotion ? .linear(duration: 0.1) : .spring(response: 0.35),
    value: animatedValue
)
```

### Issues Found

#### ISSUE #13: StatusDot Animation Not Reduced ✅ Partially Fixed

- **Priority:** Medium
- **Location:** `SophiaTheme.swift:200-212`
- **Problem:** Pulse animation always runs regardless of reduceMotion setting
- **Fix:** Check reduceMotion environment

```swift
@Environment(\.accessibilityReduceMotion) private var reduceMotion

if isActive && !reduceMotion {
    Circle()
        .fill(color)
        .frame(width: 16, height: 16)
        .opacity(0.3)
        .scaleEffect(1.2)
        .animation(
            Animation.easeInOut(duration: 1.5)
                .repeatForever(autoreverses: true),
            value: isActive
        )
}
```

#### ISSUE #14: VoiceButton Pulse Animation ❌

- **Priority:** Medium
- **Location:** `VoiceButton.swift:7-39`
- **Problem:** Pulse animation always plays, no reduceMotion check
- **Fix:** Add environment check

```swift
@Environment(\.accessibilityReduceMotion) private var reduceMotion

.onChanged(of: isRecording) { recording in
    if recording {
        withAnimation(
            reduceMotion ? .linear(duration: 0.2) : .easeInOut(duration: 1).repeatForever(autoreverses: true)
        ) {
            pulseScale = 1.3
        }
    }
}
```

---

## 6. Cognitive Load & Usability

### Issues Found

#### ISSUE #15: Generic Error Messages ❌

- **Priority:** Medium
- **Location:** `ChatInputBar.swift`, `SettingsView.swift`
- **Problem:** Error messages lack recovery guidance
- **Example:** "Invalid token" vs "Token expired. Please sign in again at https://..."
- **Fix:** Follow `src/errors/messages.ts` pattern with recovery suggestions

#### ISSUE #16: Expert Mode Disclosure Partial ✅

- **Priority:** Medium (partially addressed)
- **Location:** `SettingsView.swift` throughout
- **Status:** Expert mode toggles hide advanced options (`AppState.shared.gatewayConfig.isExpertModeEnabled`)
- **Issue:** Not all advanced sections consistently wrapped in expert mode check
- **Fix:** Audit all settings sections for consistent progressive disclosure

---

## 7. Cross-Platform Consistency

### Color Inconsistency

| Element        | macOS SwiftUI            | TUI               | Chat UI             |
| -------------- | ------------------------ | ----------------- | ------------------- |
| Primary Accent | `#7B61FF` (purple)       | `#F6C453` (amber) | `#9333EA` (purple)  |
| Error          | `.systemRed`             | `#F97066`         | `.systemRed`        |
| Success        | `.systemGreen`           | `#7DD3A5`         | `.systemGreen`      |
| Background     | `.windowBackgroundColor` | `#2B2F36`         | `.systemBackground` |

### Issue Found

#### ISSUE #17: Accent Color Divergence ⚠️

- **Priority:** Medium
- **Standard:** GUIDELINES.md:16-17 allows platform variation with documented rationale
- **Status:** Documented as intentional (terminal doesn't render purple well)
- **Recommendation:** Add unit tests to prevent accidental color drift

#### ISSUE #18: Missing Cross-Platform Keyboard Parity ⚠️

- **Priority:** Medium
- **Location:** TUI vs macOS navigation patterns
- **Problem:** TUI uses Vim-style j/k, macOS uses arrow keys only
- **Fix:** Document as intentional power-user feature, or add optional Vim mode to macOS

---

## Remediation Plan

### Phase 1: Critical Security & Access (Week 1)

| Issue                  | Priority | Files to Fix                  | Effort |
| ---------------------- | -------- | ----------------------------- | ------ |
| #7: No Aural Labels    | Critical | All button components         | 4h     |
| #3: Escape Key Handler | Critical | Chat views, modals            | 2h     |
| #6: TUI Keyboard Traps | Critical | FilterableSelectList, ChatLog | 3h     |

**Total Phase 1:** 9 hours

### Phase 2: High Priority Navigation (Week 2)

| Issue                    | Priority | Files to Fix               | Effort |
| ------------------------ | -------- | -------------------------- | ------ |
| #2: Color-Only Status    | High     | ChatInputBar, Sidebar      | 2h     |
| #8: Status Announcements | High     | StatusIndicator, ViewModel | 3h     |
| #9: Message Roles        | High     | MessageBubble, ChatView    | 2h     |
| #11: Focus Management    | High     | ChatInputBar, Composer     | 3h     |
| #4: Settings Shortcut    | Medium   | Sidebar, AppMenu           | 1h     |

**Total Phase 2:** 11 hours

### Phase 3: Medium & Polish (Week 3)

| Issue                            | Priority | Files to Fix              | Effort |
| -------------------------------- | -------- | ------------------------- | ------ |
| #1: Dim Text Contrast            | Medium   | theme.ts, palette.ts      | 2h     |
| #5: Skip Links                   | Medium   | Sidebar, Dashboard        | 2h     |
| #12: Focus Restoration           | Medium   | ApprovalModal, Onboarding | 3h     |
| #13: Reduce Motion (StatusDot)   | Medium   | SophiaTheme.swift         | 1h     |
| #14: Reduce Motion (VoiceButton) | Medium   | VoiceButton.swift         | 1h     |
| #15: Error Messages              | Medium   | Error system, UI          | 4h     |
| #16: Expert Mode Audit           | Medium   | Settings views            | 2h     |
| #18: Keyboard Parity Docs        | Medium   | GUIDELINES.md             | 1h     |

**Total Phase 3:** 16 hours

---

## Files Requiring Fixes

### macOS SwiftUI (Priority Order)

1. `apps/macos/Sources/SOPHIAClaw/UI/Chat/ChatInputBar.swift` - Issues #2, #3, #7, #8, #11
2. `apps/macos/Sources/SOPHIAClaw/UI/Chat/MessageBubble.swift` - Issues #7, #9
3. `apps/macos/Sources/SOPHIAClaw/UI/Components/VoiceButton.swift` - Issues #7, #14
4. `apps/macos/Sources/SOPHIAClaw/UI/Sidebar/SidebarView.swift` - Issues #2, #4, #5, #7
5. `apps/macos/Sources/SOPHIAClaw/UI/Settings/SettingsView.swift` - Issue #7
6. `apps/macos/Sources/SOPHIAClaw/Theme/SophiaTheme.swift` - Issue #13
7. `apps/macos/Sources/SOPHIAClaw/UI/Main/OnboardingWizard.swift` - Issue #11
8. `apps/macos/SOURCES/SOPHIAClaw/UI/Approval/ApprovalModal.swift` - Issue #12

### TUI

1. `src/tui/components/filterable-select-list.ts` - Issue #6
2. `src/tui/components/chat-log.ts` - Issue #6
3. `src/tui/theme/theme.ts` - Issue #1
4. `src/tui/components/markdown-message.ts` - Issue #7 (documentation)

### Shared

1. `apps/shared/SOPHIAClawKit/Sources/SOPHIAClawChatUI/ChatMessageViews.swift` - Issue #9, #13
2. `apps/shared/SOPHIAClawKit/Sources/SOPHIAClawChatUI/ChatComposer.swift` - Issue #11
3. `apps/shared/SOPHIAClawKit/Sources/S/OPHIAClawChatUI/ChatViewModel.swift` - Issue #8

---

## Testing Recommendations

### Manual Testing Protocol

1. **VoiceOver Testing** (macOS)
   - Enable VoiceOver: System Settings > Accessibility > VoiceOver
   - Navigate through all views with VoiceOver
   - Verify: All buttons announced with labels, status changes announced, messages distinguished
   - Tool: Built-in macOS VoiceOver

2. **Keyboard-Only Navigation**
   - Disconnect mouse/touchpad
   - Navigate entire app using only: Tab, Shift+Tab, Enter, Space, Arrows, Escape, Cmd+,
   - Verify: All interactive elements reachable, no keyboard traps, Escape closes modals
   - Tool: Physical keyboard

3. **Contrast Testing**
   - Use Xcode > Debug > Color Contrast Analyzer
   - Test all text/background combinations
   - Verify: 4.5:1 minimum for normal text, 3:1 for large text
   - Tools: Xcode, `wcag-contrast` npm package

4. **Reduce Motion Testing**
   - Enable: System Settings > Accessibility > Display > Reduce Motion
   - Navigate app, verify animations minimized
   - Tool: macOS Accessibility settings

### Automated Tests

Add to CI/CD:

```bash
# Install contrast checker
npm install -g wcag-contrast

# Verify TUI color pairs
npx tsx scripts/verify-color-contrast.ts

# Run accessibility lint
pnpm lint:accessibility
```

---

## Compliance Summary

### WCAG 2.1 Level A compliance: Partial

| Criterion                  | Status | Notes                                    |
| -------------------------- | ------ | ---------------------------------------- |
| 1.1.1 Non-text Content     | ❌     | Images lack alt text                     |
| 1.3.1 Info & Relationships | ⚠️     | Partial via grouping, no explicit labels |
| 1.3.2 Meaningful Sequence  | ✅     | Linear layouts                           |
| 1.4.1 Use of Color         | ❌     | Status uses color only                   |
| 1.4.3 Contrast (Minimum)   | ⚠️     | Dim text fails 4.5:1                     |
| 2.1.1 Keyboard             | ⚠️     | Partial, some traps possible             |
| 2.1.2 No Keyboard Trap     | ⚠️     | Needs verification in TUI                |
| 2.4.3 Focus Order          | ✅     | Default SwiftUI order                    |
| 2.4.7 Focus Visible        | ⚠️     | Depends on system theme                  |
| 4.1.2 Name, Role, Value    | ❌     | Missing accessibilityLabel               |

### WCAG 2.1 Level AA Compliance: Not achieved

Level A blockers must be fixed before pursuing AA.

---

## Appendix: Reference Implementations

### SwiftUI Accessibility Pattern

```swift
Button(action: onSend) {
    Image(systemName: "arrow.up")
        .font(.system(size: 20, weight: .semibold))
        .frame(width: 36, height: 36)
        .background(canSend ? Color.accentColor : Color(.controlBackgroundColor))
        .foregroundStyle(canSend ? .white : .secondary)
        .clipShape(Circle())
}
.buttonStyle(.plain)
.accessibilityLabel(canSend ? "Send message" : "Message disabled")
.accessibilityHint("Sends your typed message to the assistant")
.accessibilityState(.isDisabled, !canSend)
```

### TUI Screen Reader Best Practices

```typescript
// Use text markers alongside color
const statusLine = [
  palette.success("[✓]"), // Text checkmark + green
  palette.text(" Ready"),
].join("");

// Avoid box characters as sole structure
// Box lines: ─ ─ ─ ─ ─
// Plain ASCII: ----------- (more predictable)
```

---

**Next Review:** After Phase 1 completion (Week 2)  
**Review Owner:** UI/UX Team  
**Sign-off Required:** Accessibility compliance before public release
