πŸ“„ a11y-review-sprint1.md 14,008 bytes Apr 20, 2026 πŸ“‹ Raw

HoffDesk Dashboard β€” Accessibility Compliance Review

Reviewer: Wadsworth πŸ“‹ (routed on behalf of Daedalus's domain)
Date: 2026-04-20
Scope: Sprint 1 dashboard UI (dashboard/templates/index.html, dashboard/static/style.css, prototype/index.html)
Standard: WCAG 2.1 AA


Executive Summary

The dashboard is early-stage but has a solid semantic foundation. There are 3 critical issues, 5 serious issues, and 8 moderate issues. Several quick wins can be knocked out in a single pass. The biggest structural gap is no focus indicators anywhere and color contrast failures on secondary/tertiary text.


1. WCAG 2.1 AA β€” Color Contrast

❌ CRITICAL: Tertiary text fails 4.5:1 ratio

Token Hex Use Contrast on #0f1117 Pass?
text-primary #e8e9ed Headlines 13.3:1 βœ…
text-secondary #9ca0b0 Timestamps, labels 5.3:1 βœ…
text-tertiary #6b7084 Muted info 3.5:1 ❌ FAIL
text-inverse #0f1117 On accent bg 1.1:1 ⚠️ Only works on light bg
  • text-tertiary (#6b7084) on bg-primary (#0f1117) = 3.5:1 β€” fails AA for normal text (requires 4.5:1). Used for: skeleton placeholders, location text, forecast labels, footer text, health latency labels, disk usage labels.
  • Fix: Lighten text-tertiary to at least #7f8396 (4.5:1) or #828698 (4.54:1). Recommended: #8b8fa3 (~5:1 for comfortable reading).

⚠️ Warning: Prototype uses different palette

The prototype (prototype/index.html) uses a different color system (Midnight Teal, Forest Emerald, Morning Peach, etc.) with color: #ECB392 on background-color: #043C5C. This passes at 4.78:1 β€” barely. But sage-mist (#CAD0AD) on #043C5C = 5.6:1 βœ… and text-[10px] size classes throughout the prototype are particularly risky (see Touch Targets below).

⚠️ Warning: Status colors on dark bg

Status pills use colored text on semi-transparent tinted backgrounds:
- status-healthy (#22c55e) on status-healthy-bg (rgba(34,197,94,0.12) β‰ˆ #161f1c): β‰ˆ 7.3:1 βœ…
- status-degraded (#f59e0b) on status-degraded-bg: β‰ˆ 6.1:1 βœ…
- status-critical (#ef4444) on status-critical-bg: β‰ˆ 5.2:1 βœ…

These are fine. βœ…


2. WCAG 2.1 AA β€” Focus Indicators

❌ CRITICAL: No visible focus indicators

The CSS has zero :focus-visible or :focus styles. There is no custom focus ring on any interactive element. The default browser focus ring on dark backgrounds is often invisible.

Affected elements:
- HTMX-powered <section> cards (they have hx-get attributes β€” are they focusable?)
- The "Add Event" button in the prototype
- Category toggle buttons in the prototype
- Any links or buttons injected by JS renderers

Fix: Add a visible :focus-visible outline:

:focus-visible {
  outline: 2px solid var(--accent-primary);
  outline-offset: 2px;
}

⚠️ SERIOUS: HTMX sections may not be keyboard-focusable

The <section> elements with hx-get attributes are not inherently focusable. HTMX will swap their innerHTML on load and poll, but if these are meant to be interactive (e.g., clicking into a calendar event), they need tabindex="0" and keyboard event handlers. If they're purely passive displays, they're fine as-is.


3. Screen Reader Compatibility

❌ CRITICAL: Dynamic content has no ARIA live regions

HTMX swaps content via hx-swap="innerHTML" into #calendar-content, #weather-content, #health-content. When these update every 30 seconds, screen readers will not announce the changes.

Fix: Add aria-live="polite" to each content container:

<div id="calendar-content" class="card-body" aria-live="polite" aria-atomic="true">

⚠️ SERIOUS: Prototype "Add Event" form has no accessible name

The collapsible form section has no aria-expanded or aria-controls:

<button onclick="..." aria-expanded="false" aria-controls="quickAddForm">Add Event</button>
<div id="quickAddForm" ...>

⚠️ SERIOUS: Loading indicators need ARIA

The .htmx-indicator elements (loading dots) should have aria-hidden="true" since they're purely decorative, and the content area should have aria-busy="true" during loading.

⚠️ MODERATE: Landmark regions are good

The dashboard correctly uses <header>, <section>, and <footer> landmarks. βœ… Good start. However:
- Sections lack aria-label β€” add e.g., aria-label="Calendar", aria-label="Weather", aria-label="System health"
- The main content area should be wrapped in <main> with the header/footer outside it

⚠️ MODERATE: Skeleton loaders need ARIA

The skeleton loading lines (<div class="skeleton-line">) are read by screen readers as empty divs. Add:

<div class="skeleton-line" aria-hidden="true"></div>

And add a visually hidden label:

<span class="sr-only" aria-live="polite">Loading calendar data...</span>

βœ… Good: <html lang="en"> is set correctly on both files.


4. Responsive Accessibility & Touch Targets

⚠️ SERIOUS: Touch targets too small in prototype

The prototype has several interactive elements with text-[10px] and px-2.5 py-1 on category buttons. These render at roughly 24Γ—22px β€” below the 44Γ—44px CSS minimum for WCAG 2.1 SC 1.3.1 / 2.5.5 (Target Size). The "Add Event" button is py-3 (~42px height) β€” close but still under 44px.

Fix: Minimum min-height: 44px; min-width: 44px on all interactive elements, or padding: 12px 16px minimum.

⚠️ MODERATE: Dashboard max-width 480px is good for mobile

The dashboard container is max-width: 480px with centered layout β€” appropriate for the phone-primary target. βœ…

⚠️ MODERATE: Horizontal scroll in calendar ribbon (prototype)

The calendar ribbon uses overflow-x-auto which requires horizontal scrolling. This needs:
- Scroll container: aria-label="Today's events" on the scroll parent
- Consider adding scroll arrows or left/right swipe hints for mobile
- Ensure the container is keyboard-scrollable (it should be by default with overflow)

⚠️ MODERATE: Viewport meta tag is correct

Both files have <meta name="viewport" content="width=device-width, initial-scale=1.0">. βœ…


5. Form Accessibility (Prototype)

⚠️ SERIOUS: Form inputs lack explicit association

The "Add Event" form uses <label> elements, but they don't use for/id to explicitly associate with their inputs:

<label class="text-sage-mist ...">Event Name</label>
<input type="text" placeholder="What's happening?" ...>

Fix:

<label for="event-name" class="text-sage-mist ...">Event Name</label>
<input id="event-name" type="text" ...>

Same for date, time, and category inputs.

⚠️ MODERATE: No form validation or error states

The form has no:
- required attributes on required fields
- Error messages for invalid input
- aria-invalid or aria-describedby for error feedback
- Submit feedback (loading state, success/error toast)

⚠️ MODERATE: Category buttons have no ARIA role

The category toggle buttons ("Family", "Work", "Personal") should use:

<button role="radio" aria-checked="false" ...>Family</button>

Or simpler: wrap in <fieldset> with <legend> and use aria-pressed:

<button aria-pressed="false" ...>Family</button>

⚠️ MODERATE: Collapsible form lacks ARIA

The "Add Event" toggle button and collapsible form need proper ARIA:

<button aria-expanded="false" aria-controls="quickAddForm" ...>Add Event</button>
<div id="quickAddForm" role="region" aria-label="Add event form" ...>

6. Quick Wins (Low Effort, High Impact)

These can be done in a single pass, estimated 1-2 hours:

# Change Impact Effort
1 Add :focus-visible outline style (2px accent ring) πŸ”΄ Critical β€” keyboard users can't navigate 5 min
2 Lighten --text-tertiary from #6b7084 to #8b8fa3 πŸ”΄ Critical β€” contrast compliance 5 min
3 Add aria-live="polite" to #calendar-content, #weather-content, #health-content πŸ”΄ Critical β€” screen reader users get no updates 5 min
4 Add aria-label to each <section> 🟑 Moderate β€” landmark navigation 5 min
5 Add aria-hidden="true" to skeleton loaders 🟑 Moderate β€” screen reader noise 5 min
6 Add <main> wrapper around content 🟑 Moderate β€” landmark structure 2 min
7 Add for/id pairs on all form labels/inputs 🟠 Serious β€” form accessibility 10 min
8 Add aria-expanded/aria-controls on "Add Event" toggle 🟠 Serious β€” collapsible disclosure 5 min
9 Add min-height: 44px to interactive buttons 🟠 Serious β€” touch target size 10 min
10 Add .sr-only utility class + visually hidden loading text 🟑 Moderate β€” status announcements 5 min

Total estimated time: ~1 hour for all 10 quick wins.


βœ… Quick Wins β€” COMPLETED (2026-04-20)

All 10 quick wins implemented by Daedalus:

# Change Status Details
1 :focus-visible outline style βœ… Done 2px accent ring on dashboard, golden-amber ring on prototype
2 --text-tertiary lightened to #8b8fa3 βœ… Done Now ~5:1 contrast ratio, passes AA
3 aria-live="polite" on 3 content containers βœ… Done Calendar, Weather, Health all have aria-live="polite" aria-atomic="true"
4 aria-label on each <section> βœ… Done "Calendar", "Weather", "System health"
5 aria-hidden="true" on skeleton loaders βœ… Done All .skeleton-line elements hidden from AT
6 <main> wrapper around content βœ… Done Cards wrapped in <main> element
7 for/id pairs on form labels/inputs βœ… Done event-name, event-date, event-time all linked
8 aria-expanded/aria-controls on "Add Event" toggle βœ… Done Button has aria-expanded/aria-controls, JS toggles state
9 min-height: 44px on interactive elements βœ… Done CSS rule for button, a, [role="button"], [tabindex]
10 .sr-only utility + visually hidden loading text βœ… Done .sr-only class added; "Loading calendar data…" etc. in sr-only spans

Additional fixes beyond the quick wins:
- Clock aria-live="off" β€” prevents screen readers from announcing time every 30s
- .htmx-indicator elements have aria-hidden="true"
- Prototype: <fieldset> + <legend> for category buttons with aria-pressed
- Prototype: scroll container has role="list" aria-label="Today's events"
- Prototype: form role="region" aria-label="Add event form"
- prefers-reduced-motion media query added to both files
- Focus ring color matches each file's accent (--accent-primary / #D4A843)


Add to style.css:

/* Screen reader only */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Focus ring */
:focus-visible {
  outline: 2px solid var(--accent-primary);
  outline-offset: 2px;
}

/* Reduce motion for users who prefer it */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* Minimum touch target */
.interactive {
  min-height: 44px;
  min-width: 44px;
}

8. Missing: prefers-reduced-motion

Both the dashboard CSS and prototype use animations (pulse, shimmer, fadeIn, gentlePulse, card-lift hover transforms) with no prefers-reduced-motion override. This violates WCAG 2.1 SC 2.3.3 (Animation from Interactions, AAA level) and is a best practice for AA.

Fix: Add the prefers-reduced-motion media query shown above.


9. JS-Specific Concerns

Dynamic content injection via innerHTML

The renderCalendar(), renderWeather(), and renderHealth() functions inject HTML via innerHTML. This means:
- No XSS risk if data is trusted (CalDAV events), but sanitize if user-generated
- Screen readers may not track DOM changes without aria-live
- Keyboard focus can be lost if content replaces focused elements

Clock update interval

The setInterval(updateClock, 30000) updates page content. Wrap the time display in a container with aria-live="off" (it's not important enough to announce every 30 seconds, and aria-live="polite" would be annoying).


10. Summary Scorecard

Category Score Notes
Color Contrast ⚠️ 6/10 Tertiary text fails; rest passes
Focus Indicators ❌ 0/10 None exist
Keyboard Navigation ⚠️ 3/10 Landmarks good, but no focus styles or keyboard handlers
Screen Reader ⚠️ 4/10 Semantic HTML is decent; missing ARIA lives, labels, expanded states
Touch Targets ⚠️ 4/10 Main layout fine; prototype buttons too small
Forms ⚠️ 3/10 Labels exist visually but not programmatically; no error states
Motion/Animation ⚠️ 5/10 No reduced-motion support
Overall ⚠️ ~4/10 Solid foundation, quick wins will bring to ~7/10

Recommendations Priority

  1. ~~Do now (blocker): Focus indicators, contrast fix, aria-live regions~~ βœ… DONE
  2. ~~Do this sprint: Form labels, aria-expanded, touch targets, reduced-motion~~ βœ… DONE
  3. Sprint 2+: Full keyboard navigation for interactive cards, skip-to-content link, automated a11y testing (axe-core), color-blind simulation review

Review delivered by Wadsworth πŸ“‹ on behalf of the accessibility audit request. Daedalus should integrate these findings into the next sprint iteration.