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) onbg-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-tertiaryto 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)
7. Recommended A11y Utility CSS
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
- ~~Do now (blocker): Focus indicators, contrast fix, aria-live regions~~ β DONE
- ~~Do this sprint: Form labels, aria-expanded, touch targets, reduced-motion~~ β DONE
- 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.