# Month, week, and day views accessible via keyboard navigation

- **Pattern:** `ab-000523` (`booking-calendar-availability.calendar-display.keyboard-navigation`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000523
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000523)

## 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.

```tsx
// 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 `onKeyDown` handlers, ARIA attributes (`role="button"`, `tabIndex`, `aria-label`), and semantic HTML (`<button>` vs `<div>`). Verify:
  1. Tab and Shift+Tab move focus between slots in logical order.
  2. Arrow keys move between dates.
  3. Enter or Space select or book a slot.
  4. Visible focus indicators (outline, highlight) exist on interactive elements — look for `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:

  ```tsx
  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.1.1
- wcag 2.4.7
- section-508 502.3

Taxons: accessibility

HTML version: https://auditbuffet.com/patterns/ab-000523
