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.
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.
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.
ID: booking-calendar-availability.calendar-display.keyboard-navigation
Severity: low
What to look for: Count all interactive calendar elements (slot buttons, navigation arrows, view switchers). For each, check for onKeyDown handlers, ARIA attributes (role="button", tabIndex, aria-label), and semantic HTML (<button> vs <div>). Verify:
focus:outline-*, focus:ring-*, or focus-visible:* classes.
Examine files matching src/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 explicit tabIndex and role attributes. At least 1 onKeyDown handler must exist for calendar slot interaction. All interactive elements must have visible focus indicators (CSS outline, ring, or box-shadow on :focus or :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> without tabIndex or role.
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>
)
}