Accessibility pattern · Data display

Calendar

The month is a grid with one tab stop: the arrow keys move by day and week, each day says its date and its events, and Enter opens that day's list below. Changing month is announced politely, and a list view sets the same events out as an agenda, which reads better than a crowded grid.

WCAG criteria
7
Keyboard rules
7
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

October 2026

Monday Tuesday Wednesday Thursday Friday Saturday Sunday

Arrow keys move by day and week, Page Up and Page Down by month. Enter shows the day's events.

Monday, 5 October

    The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

    Keyboard

    Keys it answers to

    Every action works without a pointer.

    KeyWhat it does
    TabMoves into the grid on the focused day, today to begin with; the next Tab goes to the day's events.
    Arrow Left or Arrow RightMoves to the previous or next day, turning the month when it reaches the edge.
    Arrow Up or Arrow DownMoves to the same day in the previous or next week.
    Home or EndMoves to Monday or Sunday of the same week.
    Page Up or Page DownMoves to the same date in the previous or next month.
    Enter or SpaceShows the focused day's events below the grid and moves focus to their heading; Shift+Tab goes back to the day.
    Enter or SpaceOn Previous month, Next month, Today or List view, does what the button says; focus stays on the button.

    Screen readers

    What it announces

    Written from the roles, names and states in the markup.

    WhenExpected announcement
    Tab reaches the gridOctober 2026, grid. Monday, 5 October, 2 events: Sprint planning, Design review, current date, selected
    Arrow Right moves onTuesday, 6 October
    Enter on 14 OctoberWednesday, 14 October, heading level 4
    Next month is pressedNovember 2026
    List view is pressedList view, toggle button, pressed

    These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

    Code

    Copy the code

    The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

    The markup

    <div class="ap-calendar" data-ap-calendar data-today="2026-10-05" data-region="IN">
      <div class="ap-calendar__bar">
        <div class="ap-calendar__nav">
          <button type="button" class="ap-calendar__step" data-ap-step="-1" aria-label="Previous month">
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m15 18-6-6 6-6"/></svg>
          </button>
          <h3 class="ap-calendar__month" id="calendar-month" aria-live="polite" data-ap-month>October 2026</h3>
          <button type="button" class="ap-calendar__step" data-ap-step="1" aria-label="Next month">
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m9 18 6-6-6-6"/></svg>
          </button>
        </div>
        <div class="ap-calendar__tools">
          <button type="button" class="ap-btn" data-ap-today>Today</button>
          <button type="button" class="ap-btn ap-calendar__view" aria-pressed="false" data-ap-list-view>
            <svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 6h11M9 12h11M9 18h11"/><path d="M4.5 6h.01M4.5 12h.01M4.5 18h.01"/></svg>
            List view
          </button>
        </div>
      </div>
    
      <div class="ap-calendar__month-view" data-ap-month-view>
        <table class="ap-calendar__grid" role="grid" aria-labelledby="calendar-month" aria-describedby="calendar-keys">
          <thead>
            <tr data-ap-weekdays>
              <th scope="col"><abbr title="Monday" aria-hidden="true">Mon</abbr><span class="ap-calendar__sr">Monday</span></th>
              <th scope="col"><abbr title="Tuesday" aria-hidden="true">Tue</abbr><span class="ap-calendar__sr">Tuesday</span></th>
              <th scope="col"><abbr title="Wednesday" aria-hidden="true">Wed</abbr><span class="ap-calendar__sr">Wednesday</span></th>
              <th scope="col"><abbr title="Thursday" aria-hidden="true">Thu</abbr><span class="ap-calendar__sr">Thursday</span></th>
              <th scope="col"><abbr title="Friday" aria-hidden="true">Fri</abbr><span class="ap-calendar__sr">Friday</span></th>
              <th scope="col"><abbr title="Saturday" aria-hidden="true">Sat</abbr><span class="ap-calendar__sr">Saturday</span></th>
              <th scope="col"><abbr title="Sunday" aria-hidden="true">Sun</abbr><span class="ap-calendar__sr">Sunday</span></th>
            </tr>
          </thead>
          <tbody data-ap-days></tbody>
        </table>
        <p class="ap-calendar__keys" id="calendar-keys">Arrow keys move by day and week, Page Up and Page Down by month. Enter shows the day's events.</p>
    
        <section class="ap-calendar__day" aria-labelledby="calendar-day-name">
          <h4 class="ap-calendar__day-name" id="calendar-day-name" tabindex="-1" data-ap-day-name>Monday, 5 October</h4>
          <ul class="ap-calendar__events" data-ap-day-events></ul>
          <p class="ap-calendar__none" data-ap-day-none hidden>Nothing planned for this day.</p>
        </section>
      </div>
    
      <div class="ap-calendar__agenda" data-ap-agenda hidden>
        <ol class="ap-calendar__list" aria-labelledby="calendar-month">
          <li class="ap-calendar__group" data-date="2026-09-24">
            <h4 class="ap-calendar__date">Thursday, 24 September</h4>
            <ul class="ap-calendar__events">
              <li class="ap-calendar__event"><span class="ap-calendar__time">11:00</span> <span class="ap-calendar__what">Quarterly planning</span></li>
            </ul>
          </li>
          <li class="ap-calendar__group" data-date="2026-09-29">
            <h4 class="ap-calendar__date">Tuesday, 29 September</h4>
            <ul class="ap-calendar__events">
              <li class="ap-calendar__event"><span class="ap-calendar__time">15:00</span> <span class="ap-calendar__what">Design review</span></li>
            </ul>
          </li>
          <li class="ap-calendar__group" data-date="2026-10-02">
            <h4 class="ap-calendar__date">Friday, 2 October</h4>
            <ul class="ap-calendar__events">
              <li class="ap-calendar__event" data-kind="holiday"><span class="ap-calendar__time">All day</span> <span class="ap-calendar__what">Gandhi Jayanti, office closed</span></li>
            </ul>
          </li>
          <li class="ap-calendar__group" data-date="2026-10-05">
            <h4 class="ap-calendar__date">Monday, 5 October</h4>
            <ul class="ap-calendar__events">
              <li class="ap-calendar__event"><span class="ap-calendar__time">10:00</span> <span class="ap-calendar__what">Sprint planning</span></li>
              <li class="ap-calendar__event"><span class="ap-calendar__time">16:00</span> <span class="ap-calendar__what">Design review</span></li>
            </ul>
          </li>
          <li class="ap-calendar__group" data-date="2026-10-08">
            <h4 class="ap-calendar__date">Thursday, 8 October</h4>
            <ul class="ap-calendar__events">
              <li class="ap-calendar__event"><span class="ap-calendar__time">11:30</span> <span class="ap-calendar__what">Client call: Rao &amp; Sons</span></li>
            </ul>
          </li>
          <li class="ap-calendar__group" data-date="2026-10-12">
            <h4 class="ap-calendar__date">Monday, 12 October</h4>
            <ul class="ap-calendar__events">
              <li class="ap-calendar__event"><span class="ap-calendar__time">13:00</span> <span class="ap-calendar__what">Team lunch</span></li>
            </ul>
          </li>
          <li class="ap-calendar__group" data-date="2026-10-14">
            <h4 class="ap-calendar__date">Wednesday, 14 October</h4>
            <ul class="ap-calendar__events">
              <li class="ap-calendar__event"><span class="ap-calendar__time">10:00</span> <span class="ap-calendar__what">Accessibility audit</span></li>
              <li class="ap-calendar__event"><span class="ap-calendar__time">14:00</span> <span class="ap-calendar__what">Hiring panel</span></li>
              <li class="ap-calendar__event"><span class="ap-calendar__time">16:30</span> <span class="ap-calendar__what">Design critique</span></li>
            </ul>
          </li>
          <li class="ap-calendar__group" data-date="2026-10-16">
            <h4 class="ap-calendar__date">Friday, 16 October</h4>
            <ul class="ap-calendar__events">
              <li class="ap-calendar__event" data-kind="release"><span class="ap-calendar__time">17:00</span> <span class="ap-calendar__what">Release 2.4</span></li>
            </ul>
          </li>
          <li class="ap-calendar__group" data-date="2026-10-19">
            <h4 class="ap-calendar__date">Monday, 19 October</h4>
            <ul class="ap-calendar__events">
              <li class="ap-calendar__event"><span class="ap-calendar__time">10:00</span> <span class="ap-calendar__what">Sprint planning</span></li>
            </ul>
          </li>
          <li class="ap-calendar__group" data-date="2026-10-21">
            <h4 class="ap-calendar__date">Wednesday, 21 October</h4>
            <ul class="ap-calendar__events">
              <li class="ap-calendar__event"><span class="ap-calendar__time">11:00</span> <span class="ap-calendar__what">Workshop: writing alt text</span></li>
            </ul>
          </li>
          <li class="ap-calendar__group" data-date="2026-10-23">
            <h4 class="ap-calendar__date">Friday, 23 October</h4>
            <ul class="ap-calendar__events">
              <li class="ap-calendar__event"><span class="ap-calendar__time">15:00</span> <span class="ap-calendar__what">Quarterly review</span></li>
            </ul>
          </li>
          <li class="ap-calendar__group" data-date="2026-10-27">
            <h4 class="ap-calendar__date">Tuesday, 27 October</h4>
            <ul class="ap-calendar__events">
              <li class="ap-calendar__event"><span class="ap-calendar__time">15:00</span> <span class="ap-calendar__what">Design review</span></li>
            </ul>
          </li>
          <li class="ap-calendar__group" data-date="2026-10-30">
            <h4 class="ap-calendar__date">Friday, 30 October</h4>
            <ul class="ap-calendar__events">
              <li class="ap-calendar__event"><span class="ap-calendar__time">12:00</span> <span class="ap-calendar__what">Hindi content review</span></li>
              <li class="ap-calendar__event"><span class="ap-calendar__time">16:00</span> <span class="ap-calendar__what">Retrospective</span></li>
            </ul>
          </li>
          <li class="ap-calendar__group" data-date="2026-11-02">
            <h4 class="ap-calendar__date">Monday, 2 November</h4>
            <ul class="ap-calendar__events">
              <li class="ap-calendar__event"><span class="ap-calendar__time">10:00</span> <span class="ap-calendar__what">Sprint planning</span></li>
            </ul>
          </li>
          <li class="ap-calendar__group" data-date="2026-11-06">
            <h4 class="ap-calendar__date">Friday, 6 November</h4>
            <ul class="ap-calendar__events">
              <li class="ap-calendar__event" data-kind="release"><span class="ap-calendar__time">17:00</span> <span class="ap-calendar__what">Release 2.5</span></li>
            </ul>
          </li>
        </ol>
        <p class="ap-calendar__none" data-ap-agenda-none hidden>Nothing planned this month.</p>
      </div>
    
      <span class="ap-calendar__sr" data-ap-say="one" hidden>1 event</span>
      <span class="ap-calendar__sr" data-ap-say="many" hidden>{n} events</span>
      <span class="ap-calendar__sr" data-ap-say="more" hidden>+{n} more</span>
    </div>
    

    WCAG 2.2

    What it meets

    The success criteria this pattern takes care of, and how.

    • 1.3.1 Info and Relationships Level A

      Days sit in a table with weekday column headers, and each day's events are a real list, in the grid and in the agenda.

    • 1.4.1 Use of Color Level A

      Today has a ring and the word Today, and the chosen day is filled and bold, so neither depends on color.

    • 2.1.1 Keyboard Level A

      Every day, month and event can be reached from the keyboard, with the keys the grid pattern defines.

    • 2.4.3 Focus Order Level A

      Enter moves focus to the day's events, and Shift+Tab returns to the same day in the grid.

    • 2.4.7 Focus Visible Level AA

      The focused day has a two-pixel ring inside its cell, separate from the today ring and the selected fill.

    • 4.1.2 Name, Role, Value Level A

      Today carries aria-current=date, the chosen day aria-selected, and List view aria-pressed.

    • 4.1.3 Status Messages Level AA

      The month name is a polite live region, so changing month is announced without moving focus.

    Usage

    When to use it

    Use it

    • Events people place in time and scan by week, like a team calendar, class timetable or bookings.
    • When the shape of the month matters: busy weeks, gaps and deadlines at a glance.

    Use something else

    • Picking one date for a form field: use a date picker, which is a different pattern.
    • Long lists of events read in order: the agenda list on its own is simpler.
    • A single day's schedule with times: use a day view or a list of times.

    Common failures

    How it usually goes wrong

    • A grid of divs

      Days drawn as a flex of boxes have no rows, columns or headers, so a screen reader cannot say which weekday a date falls on. A table with column headers keeps the structure.

    • Every day a tab stop

      Thirty-one stops before the next control is exhausting. One day takes focus and the arrow keys move between days.

    • Today as a colored circle

      A tinted number means nothing to a screen reader or to someone who cannot see the tint. Today has aria-current, a ring and the word itself.

    • Events hidden behind +2 more

      Truncated chips are fine for the eye, but every event must still be reachable. The day's name lists all its events, and Enter shows them in full.

    • Silent month changes

      Pressing Next month and hearing nothing leaves people unsure it worked. The month name is a polite live region.

    • No other way in

      A packed month grid is slow to read cell by cell. The list view gives the same events as an agenda grouped by day.

    Notes

    Building it

    • This is a grid of events, not a date picker; it is not in a dialog and choosing a day does not fill a field, but the keys follow the same grid pattern.
    • data-today pins today to 5 October 2026 so the demo stays put; leave it off and the calendar uses the real date.
    • The agenda list is the data: the script reads its data-date groups to fill the grid and the day panel, and the list view simply shows it.
    • Dates and weekday names come from Intl.DateTimeFormat in the page's language, and are redrawn if the language changes.
    • On narrow screens the event chips become dots; the day's name still lists every event, and the list view is the better choice there.

    Sources: WAI-ARIA Authoring Practices: Grid · WAI-ARIA Authoring Practices: Date picker dialog example

    Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction