Agenda list
StableEvents grouped by day under headings that can stick, earliest first, each with its time, calendar pattern and people.
The Calendar’s agenda view and the Overview’s day list. Days without events are skipped; an empty range shows an empty state. Past events dim, the one under way says Now, and with onOpenEvent every row becomes a button named in full.
Updated View source
Three days
import { AgendaList } from '@rchursin/product/calendar'import type { CalendarEvent } from '@rchursin/product/data/sample'import { SampleBadge } from '@rchursin/product/shell/layout'
/** A fixed "now" (Tuesday, October 13, 2026, 10:40 AM), so the page prerenders the same list every time */const now = new Date(2026, 9, 13, 10, 40)const at = (day: number, h: number, m = 0) => new Date(2026, 9, day, h, m)
const events: CalendarEvent[] = [ { id: 'sprint', title: 'Skill cleanup sprint', start: new Date(2026, 9, 13), end: new Date(2026, 9, 15), allDay: true, calendar: 'team' }, { id: 'standup', title: 'Standup', start: at(13, 9, 30), end: at(13, 9, 45), calendar: 'team', attendees: ['me', 'mq', 'tp', 'iv'] }, { id: 'focus', title: 'Focus: fix test-fixer', start: at(13, 10), end: at(13, 11, 30), calendar: 'focus', taskId: 'RC-128' }, { id: 'lunch', title: 'Lunch with Sam', start: at(13, 12, 30), end: at(13, 13, 30), calendar: 'personal' }, { id: 'review', title: 'pr-review prompt review', start: at(14, 11), end: at(14, 12), calendar: 'team', taskId: 'RC-125', attendees: ['me', 'tp'] }, { id: 'release', title: 'Release window', start: at(15, 15), end: at(15, 17), calendar: 'release', location: 'Room 3' },]
/** Grouped by day, earliest first. Finished events dim, the one under way says Now, and each calendar shows as a pattern. */export default function AgendaWeek() { return ( <div className="flex w-full max-w-lg flex-col gap-2"> <div className="flex justify-end"> <SampleBadge /> </div> <AgendaList events={events} from={now} days={3} now={now} /> </div> )}Usage
import { AgendaList } from '@rchursin/product/calendar'<AgendaList events={events} days={7} onOpenEvent={(event, row) => openDetails(event, row)} />Examples
Rows that open
onOpenEvent turns rows into buttons. Here each opens a popover; Esc closes it and focus goes back to the row. Headings stick while the box scrolls.
import { useRef, useState } from 'react'import { AgendaList } from '@rchursin/product/calendar'import { calendarById, eventTime } from '@rchursin/product/calendar/events'import type { CalendarEvent } from '@rchursin/product/data/sample'import { SampleBadge } from '@rchursin/product/shell/layout'import { Popover, usePopover } from '@rchursin/product/ui'
const now = new Date(2026, 9, 13, 10, 40)const at = (day: number, h: number, m = 0) => new Date(2026, 9, day, h, m)
const events: CalendarEvent[] = [ { id: 'standup-13', title: 'Standup', start: at(13, 9, 30), end: at(13, 9, 45), calendar: 'team' }, { id: 'focus', title: 'Focus: fix test-fixer', start: at(13, 10), end: at(13, 11, 30), calendar: 'focus', taskId: 'RC-128' }, { id: 'cost', title: 'Agent cost check-in', start: at(13, 14), end: at(13, 14, 45), calendar: 'team', location: 'Room 3' }, { id: 'standup-14', title: 'Standup', start: at(14, 9, 30), end: at(14, 9, 45), calendar: 'team' }, { id: 'review', title: 'pr-review prompt review', start: at(14, 11), end: at(14, 12), calendar: 'team', taskId: 'RC-125' }, { id: 'guide', title: 'Focus: skill naming guide', start: at(14, 13, 30), end: at(14, 15, 30), calendar: 'focus', taskId: 'RC-123' }, { id: 'standup-15', title: 'Standup', start: at(15, 9, 30), end: at(15, 9, 45), calendar: 'team' }, { id: 'release', title: 'Release window', start: at(15, 15), end: at(15, 17), calendar: 'release' }, { id: 'skills', title: 'Weekly skill review', start: at(16, 10), end: at(16, 11), calendar: 'team', taskId: 'RC-118' },API
AgendaList props
| Prop | Type | Default | Description |
|---|---|---|---|
eventsrequired | CalendarEvent[] | – | Any events; only those that fall in the range are shown. All-day events end at midnight after their last day. |
from | Date | today | First day listed. |
days | number | 14 | How many days to cover. |
onOpenEvent | (event: CalendarEvent, el: HTMLButtonElement) => void | – | Makes each row a button. You get the event and the row, to anchor a popover to. |
openEventId | string | null | – | The event whose details are open: its row is marked selected and aria-expanded. |
dayHeadings | boolean | true | Day headings. Turn off for a single day under your own title. |
sticky | boolean | false | Headings stick to the top of a scrolling parent. |
now | Date | new Date() | What counts as now, for Today, past events and the Now tag. Pass a fixed date for prerendered pages. |
empty | ReactNode | – | Replaces the default empty state (“Nothing scheduled”). |
className | string | – | Extra classes on the list. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| Tab | Moves from row to row when rows are buttons. |
| EnterSpace | Opens the event (calls onOpenEvent). |
- Each day is a group (role="group") named by its <h3> heading (“Today”, the date and the number of events), or by the full date when dayHeadings is off. Screen-reader users can jump from day to day by heading; the groups are not landmarks, so several lists on a page never repeat landmark names.
- With onOpenEvent, a row is a <button> named in full: “Focus: fix test-fixer, Tuesday 10:00 to 11:30 AM, Focus calendar, task RC-128, happening now”, with aria-haspopup="dialog" and aria-expanded.
- The calendar shows as a pattern swatch (decorative) and is also written in the meta line, so nothing depends on the fill.
- Attendee faces are aria-hidden and only drawn from 640 pixels up.
Guidelines
Do
- Pass now and from as fixed dates on prerendered pages.
- Give a custom empty for a single day (“Nothing on this day.”).
- Put a sticky list inside a scrolling box with a fixed height.
Don’t
- Don’t pass onOpenEvent unless something opens; the rows announce a popup.
- Don’t nest it under an <h3>; its day headings are <h3>s.
In use
Related
- Agenda widgetA mini month with event markers and the chosen day’s agenda, for a dashboard.
- CalendarThe whole calendar: week, day, month and agenda views, calendars told apart by pattern, and create, edit and delete.
- Event blocksHow an event looks in each calendar view: a block in the week grid, a bar for all-day, a chip in a month cell.
- Mini calendarA small month for sidebars and widgets, with up to three squares under days that have events.
- PopoverA panel beside its trigger, on the native popover API: never clipped, closed by Esc or a click outside, and it hands focus back.
Source
The component’s own files, exactly as they run on this site.
/** * AgendaList: events grouped by day, earliest first, under day headings that can stick while * the list scrolls. Days without events are skipped; an empty range shows an empty state. * * <AgendaList events={events} from={today} days={14} onOpenEvent={(ev, el) => …} sticky /> * * Rows are buttons when `onOpenEvent` is given, named in full for screen readers * ("Standup, Tuesday 9:30 to 9:45 AM, Team calendar, happening now"). Each day is a group named by * its heading (not a region landmark, so lists on one page never repeat landmark names). */import { useId, type ReactNode } from "react"import { cx } from "../../components/cx"import { personById, type CalendarEvent } from "../data/sample"import { addDays, dtf, formatTime, relativeDay, startOfDay } from "../lib/dates"import { AvatarStack, EmptyState } from "../ui/misc"import { calendarById, eventLabel, eventsOnDay } from "./events"import { PatternSwatch } from "./patterns"
export interface AgendaListProps { events: CalendarEvent[]