Month, week, and day views accessible via keyboard navigation
Why it matters
Calendar slots that are not reachable via Tab or Arrow keys exclude keyboard-only users and screen reader users entirely — the latter navigate by virtual cursor through focusable elements. This violates WCAG 2.2 SC 2.1.1 (Keyboard) and SC 2.4.7 (Focus Visible), and Section 508 2018 Refresh 502.3. Div-based slot grids without tabIndex or role attributes are the most common cause: they look interactive but are invisible to keyboard navigation. A user with motor impairment who cannot use a mouse literally cannot book an appointment.
Severity rationale
Low because the failure locks out keyboard-only users but does not compromise data integrity — however it violates WCAG 2.2 SC 2.1.1 and Section 508 and constitutes an ADA compliance gap.
Remediation
Use native <button> elements for all calendar slots. Add onKeyDown handlers for Arrow key navigation and ensure visible focus styles are applied via focus-visible utilities.
// src/components/CalendarSlot.tsx
export function CalendarSlot({ status, time, onSelect }) {
function handleKeyDown(e: React.KeyboardEvent) {
if ((e.key === 'Enter' || e.key === ' ') && status === 'available') {
e.preventDefault()
onSelect(time)
}
}
return (
<button
type="button"
tabIndex={0}
onKeyDown={handleKeyDown}
onClick={() => status === 'available' && onSelect(time)}
aria-label={`${status} slot at ${time}`}
className="focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
>
{time}
</button>
)
}
Verify by tabbing through the calendar without a mouse: every slot must receive focus, show a visible focus ring, and respond to Enter/Space.
Detection
-
ID:
keyboard-navigation -
Severity:
low -
What to look for: Count all interactive calendar elements (slot buttons, navigation arrows, view switchers). For each, check for
onKeyDownhandlers, ARIA attributes (role="button",tabIndex,aria-label), and semantic HTML (<button>vs<div>). Verify:- Tab and Shift+Tab move focus between slots in logical order.
- Arrow keys move between dates.
- Enter or Space select or book a slot.
- Visible focus indicators (outline, highlight) exist on interactive elements — look for
focus:outline-*,focus:ring-*, orfocus-visible:*classes. Examine files matchingsrc/components/*Slot*,src/components/*Calendar*,src/components/*calendar*.
-
Pass criteria: Count all interactive calendar elements. At least 90% of interactive elements must use semantic HTML (
<button>or<a>) or have explicittabIndexandroleattributes. At least 1onKeyDownhandler must exist for calendar slot interaction. All interactive elements must have visible focus indicators (CSSoutline,ring, orbox-shadowon:focusor:focus-visible). Report: "X of Y interactive calendar elements are keyboard-accessible." -
Fail criteria: Keyboard navigation skips slots, or no visual focus indicator is present, or slots use
<div>withouttabIndexorrole. -
Skip (N/A) when: Calendar is not interactive (display-only, no clickable slots).
-
Detail on fail: Example:
"Slots have no tabIndex. Pressing Tab skips calendar elements. Only the month selector button is reachable via keyboard." -
Cross-reference: Check
unavailable-feedback— keyboard-activated slots (Enter/Space) must also produce unavailable feedback for non-bookable states. -
Cross-reference: Check
visual-distinction— focus indicators must be visually distinct from state color indicators. -
Cross-reference: Check
responsive-rendering— logical tab order must be maintained at all viewport widths. -
Remediation: Add keyboard support to calendar slots:
export function CalendarSlot({ status, time, onSelect }) { const handleKeyDown = (e: React.KeyboardEvent) => { if ((e.key === 'Enter' || e.key === ' ') && status === 'available') { e.preventDefault() onSelect(time) } if (e.key === 'ArrowRight') { // Move focus to next slot (implementation depends on layout) e.preventDefault() // focusNextSlot() } } return ( <button tabIndex={status === 'available' ? 0 : -1} onKeyDown={handleKeyDown} onClick={() => status === 'available' && onSelect(time)} className="focus:outline-2 focus:outline-offset-2 focus:outline-blue-500" > {time} </button> ) }
External references
- wcag:2.2 · 2.1.1 — Keyboard — all calendar functionality operable via keyboard
- wcag:2.2 · 2.4.7 — Focus Visible — keyboard focus indicator must be visible on slots
- section-508:2018-refresh · 502.3 — Accessibility Services — interactive elements must expose keyboard operability
Taxons
History
- 2026-04-18·v1.0.0·Initial import from booking-calendar-availability·automated