Files
driftwood/docs/plans/2026-02-27-wcag-aaa-compliance-design.md
lashman dc97a6cbbc Add WCAG 2.2 AAA compliance design document
Comprehensive accessibility design covering all four WCAG principles
(Perceivable, Operable, Understandable, Robust) for the Driftwood
GTK4/libadwaita AppImage manager.
2026-02-27 09:35:35 +02:00

12 KiB

WCAG 2.2 AAA Compliance Design for Driftwood

Context

Driftwood is a GTK4/libadwaita AppImage manager. The app already uses semantic libadwaita widgets (ActionRow, PreferencesGroup, SwitchRow, NavigationView) and has partial accessibility support: ~11 accessible labels, status badges with text+color, keyboard shortcuts, and reduced-motion CSS. However, it falls short of full WCAG 2.2 AAA compliance.

This design covers every change needed to achieve AAA compliance across all four WCAG principles: Perceivable, Operable, Understandable, and Robust.

Approach: Hybrid Helpers + Direct Properties

Create centralized accessibility helper functions in widgets.rs for patterns that repeat (labeled buttons, described badges, live announcements), then add direct accessible properties for unique cases in each UI file. This keeps the code DRY while ensuring nothing is missed.

Scope

In scope: All UI code in src/ui/, src/window.rs, and data/resources/style.css. Out of scope: CLI (src/cli.rs), core backend modules, build system.


1. Perceivable (WCAG 1.x)

1.1 Non-text Content (1.1.1 - Level A)

Current state: 8 icon-only buttons, some with accessible labels, some without.

Changes needed:

  • Add update_property(&[AccessibleProperty::Label(...)]) to every icon-only button:
    • library_view.rs: menu button, search button, grid button, list button
    • duplicate_dialog.rs: delete button per row
    • widgets.rs: copy button
    • preferences.rs: remove directory button
  • Add accessible descriptions to integration emblem overlay in app_card.rs
  • Add accessible labels to all gtk::Image icons used as prefixes in rows (check icons in integration_dialog, category icons in cleanup_wizard)

1.2 Time-based Media - N/A (no audio/video)

1.3 Adaptable

1.3.1 Info and Relationships (A):

  • Add AccessibleRole::Group to badge boxes in detail_view, library_view list rows, and app_card
  • Add AccessibleProperty::Label to ListBox containers in library_view (list view), preferences (directory list, cleanup wizard lists)
  • Add AccessibleRelation::LabelledBy connecting PreferencesGroup titles to their child row containers where applicable

1.3.6 Identify Purpose (AAA):

  • Set AccessibleRole::Banner on the detail view banner
  • Set AccessibleRole::Navigation on the NavigationView wrapper
  • Set AccessibleRole::Search on the search bar
  • Set AccessibleRole::Status on status badges
  • Set AccessibleRole::Main on the main content area

1.4 Distinguishable

1.4.6 Enhanced Contrast (AAA - 7:1 ratio):

  • Add a prefers-contrast: more media query in style.css for high-contrast mode
  • In high-contrast mode: solid opaque borders on app cards (no alpha), bolder status badge colors, thicker focus rings (3px)
  • Verify that libadwaita theme variables meet 7:1 in both light and dark mode (they do - libadwaita's named colors are designed for WCAG AA, and the prefers-contrast: more variant handles AAA)

1.4.8 Visual Presentation (AAA):

  • Text is already relative-sized (em units)
  • libadwaita handles line height and spacing according to system preferences
  • No changes needed beyond ensuring we do not override user-configured text spacing

1.4.11 Non-text Contrast (AA):

  • Increase focus ring width to 3px (currently 2px) in high-contrast mode
  • Ensure status badge borders are visible at 3:1 ratio against their background

2. Operable (WCAG 2.x)

2.1 Keyboard Accessible

2.1.3 Keyboard No Exception (AAA):

  • Verify all dashboard actionable rows are keyboard-activatable (they use activatable: true + action_name)
  • Verify cleanup wizard checkboxes are keyboard-toggleable via activatable_widget
  • Add keyboard shortcut Ctrl+Q for quit (already exists in GNOME via app.quit)
  • Ensure delete button in duplicate_dialog can be reached via Tab

2.2 Enough Time

2.2.4 Interruptions (AAA):

  • Toast notifications already have timeouts and can be dismissed
  • No other auto-updating content exists

2.3 Seizures and Physical Reactions

2.3.3 Animation from Interactions (AAA):

  • Expand prefers-reduced-motion CSS to cover ALL transitions:
    • navigation view slide transitions (currently only covers stack)
    • adw::Dialog presentation animations
    • flowboxchild hover/active transitions
    • Search bar reveal animation
    • Spinner animations (already respected by adw::Spinner)

2.4 Navigable

2.4.7 Focus Visible (AA) + 2.4.13 Focus Appearance (AAA):

  • Add focus-visible styles for ALL focusable elements, not just app cards:
    • button:focus-visible - 3px solid outline with accent color
    • row:focus-visible - highlight with outline
    • switch:focus-visible
    • checkbutton:focus-visible
    • searchentry:focus-visible
    • comborow:focus-visible
    • spinrow:focus-visible
    • expander:focus-visible
  • Focus indicator must be at least 2px thick and have 3:1 contrast (AAA requires this)

2.4.8 Location (AAA):

  • Update the window title dynamically to reflect current page: "Driftwood - Dashboard", "Driftwood - Security Report", "Driftwood - {App Name}"
  • This is already partially done via NavigationPage titles; ensure the window's actual title property updates

2.5.8 Target Size (AA):

  • Audit all clickable targets for minimum 24x24px
  • The copy button, delete button (duplicate dialog), and remove directory button need min-width: 24px; min-height: 24px in CSS
  • Status badges in actionable rows are not click targets (the row is), so this is fine

3. Understandable (WCAG 3.x)

3.1 Readable

3.1.1 Language of Page (A):

  • The GTK accessible layer reads the locale from the system. No explicit action needed for a native desktop app.

3.1.3 Unusual Words (AAA):

  • Add tooltip_text explanations for technical terms displayed in the UI:
    • "FUSE" -> tooltip: "Filesystem in Userspace - required for mounting AppImages"
    • "XWayland" -> tooltip: "X11 compatibility layer for Wayland desktops"
    • "AppImage Type 1/2" -> tooltip: "Type 1 uses ISO9660, Type 2 uses SquashFS"
    • "CVE" -> tooltip: "Common Vulnerabilities and Exposures - security vulnerability identifier"
    • "SHA256" -> tooltip: "Cryptographic hash for verifying file integrity"
    • "Firejail" -> tooltip: "Linux application sandboxing tool"
    • "zsync" -> tooltip: "Efficient delta-update download protocol"
    • "fusermount" -> tooltip: "User-space filesystem mount utility"

3.1.4 Abbreviations (AAA):

  • Expand "CVE" to "CVE (vulnerability)" on first use in security report
  • Expand "SHA256" to "SHA256 checksum" in detail view

3.1.5 Reading Level (AAA):

  • Review all user-facing strings for plain language (most are already simple)
  • Replace "No update information embedded" with "This app cannot check for updates automatically"
  • Replace "AppImage does not contain update information" with "No automatic update support"

3.2 Predictable - Already compliant (no unexpected changes)

3.3 Input Assistance

3.3.5 Help (AAA):

  • Add contextual descriptions to all PreferencesGroup widgets (already mostly done)
  • Add description text to any PreferencesGroup missing it

3.3.6 Error Prevention - All (AAA):

  • Destructive actions already have confirmation (confirm-before-delete setting, alert dialogs)
  • Add confirmation to bulk "Remove All Suggested" in duplicate dialog (currently executes immediately)
  • Add confirmation to "Clean Selected" in cleanup wizard (currently executes immediately)

4. Robust (WCAG 4.x)

4.1.2 Name, Role, Value (A)

Accessible Names (all interactive elements): Every button, toggle, switch, row, and input must have an accessible name. The full list of items needing labels:

Widget File Label to add
Menu button library_view.rs "Main menu"
Search toggle library_view.rs "Toggle search"
Grid view toggle library_view.rs "Switch to grid view"
List view toggle library_view.rs "Switch to list view"
Copy button widgets.rs "Copy to clipboard"
Delete button duplicate_dialog.rs "Delete this AppImage"
Remove directory preferences.rs "Remove scan directory"
Close button cleanup_wizard.rs "Close dialog"
Clean Selected cleanup_wizard.rs "Clean selected items"
Remove All Suggested duplicate_dialog.rs "Remove all suggested duplicates"
Add Location preferences.rs "Add scan directory"
Scan Now library_view.rs "Scan for AppImages"
Preferences library_view.rs "Open preferences"

Accessible Roles:

  • FlowBox -> already has label
  • ListBox (list view) -> add AccessibleProperty::Label("AppImage library list")
  • ListBox (preferences directory list) -> add label "Scan directories"
  • ListBox (cleanup items) -> add label per category
  • ListBox (integration dialog identity) -> add label "Application details"
  • ListBox (integration dialog actions) -> add label "Integration actions"
  • Status badges -> add AccessibleRole::Status (or use update_property with AccessibleProperty::Label)

Accessible States:

  • Switch rows -> GTK handles this automatically via SwitchRow
  • Scan button -> add AccessibleState::Busy while scanning is in progress
  • Security scan row -> add AccessibleState::Busy during scan
  • Analyze toolkit row -> add AccessibleState::Busy during analysis

4.1.3 Status Messages (AA)

Live region announcements for async operations:

Create a helper function announce(text: &str) in widgets.rs that uses a hidden GTK Label with AccessibleRole::Alert to broadcast status changes to screen readers.

Operations needing announcements:

  • Scan start ("Scanning for AppImages...")
  • Scan complete ("{n} AppImages found, {m} new")
  • Update check start/complete
  • Security scan start/complete
  • Cleanup analysis start/complete
  • Search results count change ("{n} results" or "No results")

Files Modified

File Changes
data/resources/style.css Focus indicators for all widgets, high-contrast media query, reduced-motion expansion, target size minimums
src/ui/widgets.rs New announce() live region helper, accessible label on copy_button, accessible role on status badges
src/ui/library_view.rs Accessible labels on all header buttons, list box label, search results announcement, accessible roles
src/ui/app_card.rs Accessible description for emblem overlay
src/ui/detail_view.rs Accessible roles on banner, busy states on async rows, tooltips for technical terms, plain-language rewrites
src/ui/dashboard.rs Tooltips for technical terms, accessible labels on any unlabeled elements
src/ui/duplicate_dialog.rs Accessible label on delete buttons, confirmation before bulk remove, list box labels
src/ui/cleanup_wizard.rs Accessible labels on buttons, confirmation before cleanup, list box labels, busy announcement
src/ui/preferences.rs Accessible label on remove button and add button, list box labels
src/ui/security_report.rs Accessible labels, tooltips for CVE terms
src/ui/integration_dialog.rs List box labels, accessible descriptions
src/ui/update_dialog.rs Plain-language rewrites
src/window.rs Window title updates per page, live announcements for scan/update/clean operations

Verification

After implementation:

  1. cargo build - zero errors, zero warnings
  2. cargo test - all tests pass
  3. Run with Orca screen reader - verify every element is announced correctly
  4. Tab through entire app - verify all elements have visible focus indicators
  5. Set GTK_THEME=Adwaita:dark - verify dark mode focus/contrast
  6. Set high-contrast theme - verify enhanced contrast mode
  7. Set GTK_DEBUG=interactive - inspect accessible tree
  8. Keyboard-only navigation test through every screen
  9. Verify all targets meet 24x24px minimum
  10. Verify reduced-motion disables all animations