Event blocks
StableHow an event looks in each calendar view: a block in the week grid, a bar for all-day, a chip in a month cell.
All three are filled with their calendar’s pattern (Team solid, Focus hatched, Personal outlined, Releases dashed) and named in full for screen readers. With onOpen they are buttons that open the event; without it they are still pictures with the same name, for previews and printouts. A block shows more as it grows: one line under 30 pixels, then title over time, then the linked task from 84 pixels. PatternSwatch is the small square key used in legends and lists.
Updated View source
In each view
import { useRef, useState } from 'react'import { AllDayBar, EventBlock, MonthChip } 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 at = (day: number, h: number, m = 0) => new Date(2026, 9, day, h, m)
const sprint: CalendarEvent = { id: 'sprint', title: 'Skill cleanup sprint', start: new Date(2026, 9, 12), end: new Date(2026, 9, 15), allDay: true, calendar: 'team' }const standup: CalendarEvent = { id: 'standup', title: 'Standup', start: at(14, 9, 30), end: at(14, 9, 45), calendar: 'team' }const review: CalendarEvent = { id: 'review', title: 'pr-review prompt review', start: at(14, 11), end: at(14, 12), calendar: 'team' }const lunch: CalendarEvent = { id: 'lunch', title: 'Lunch with Sam', start: at(14, 12, 30), end: at(14, 13, 30), calendar: 'personal' }const focus: CalendarEvent = { id: 'focus', title: 'Focus: skill naming guide', start: at(14, 13, 30), end: at(14, 15, 30), calendar: 'focus', taskId: 'RC-123' }const release: CalendarEvent = { id: 'release', title: 'Release window', start: at(14, 15, 30), end: at(14, 17), calendar: 'release' }
const label = 'font-mono text-[11px] tracking-[0.12em] text-fg-3 uppercase'
/** * One event in each view. Height decides how much a block shows: under 30px one line, then titleUsage
import { AllDayBar, EventBlock, MonthChip, PatternSwatch } from '@rchursin/product/calendar'<EventBlock event={event} height={44} positioned={false} onOpen={openDetails} />Examples
Pattern swatches
The key for each calendar at xs, sm and md, always with its name.
import { useId } from 'react'import { PatternSwatch } from '@rchursin/product/calendar'import { calendars } from '@rchursin/product/data/sample'
/** PatternSwatch is the key for a calendar, in three sizes. It is decorative: the name is always written beside it. */export default function EventBlocksLegend() { const id = useId() return ( <div className="flex flex-col gap-3"> <p id={id} className="font-mono text-[11px] tracking-[0.12em] text-fg-3 uppercase"> Calendars </p> <ul aria-labelledby={id} className="flex flex-col gap-3 text-[13px]"> {calendars.map((c) => ( <li key={c.id} className="grid grid-cols-[4rem_5rem_auto] items-center gap-3"> <span className="flex items-center gap-2"> <PatternSwatch pattern={c.pattern} size="xs" /> <PatternSwatch pattern={c.pattern} size="sm" /> <PatternSwatch pattern={c.pattern} size="md" /> </span> <span className="font-medium text-fg">{c.name}</span> <span className="text-fg-3">{c.pattern}</span> </li> ))} </ul> </div> )}API
EventBlock props
| Prop | Type | Default | Description |
|---|---|---|---|
eventrequired | CalendarEvent | – | The event; its calendar picks the pattern. |
heightrequired | number | – | Drawn height in pixels (the time grid works it out from the duration). Under 30 it folds to one line. |
style | CSSProperties | – | Placement from the time grid (top, left, width). |
open | boolean | – | Its details are open: lifted with a shadow and aria-expanded. |
onOpen | (event: CalendarEvent, el: HTMLButtonElement) => void | – | Makes the block a button: called on press with the event and the block, to anchor a popover. Without it the block is a picture (role="img"), not a Tab stop. |
positioned | boolean | true | Absolutely placed in a day column; false puts it in normal flow, full width (previews, specimens). |
className | string | – | Extra classes on the block. |
AllDayBar props
| Prop | Type | Default | Description |
|---|---|---|---|
eventrequired | CalendarEvent | – | An all-day event. |
open | boolean | – | Its details are open. |
onOpen | (event: CalendarEvent, el: HTMLButtonElement) => void | – | Makes the bar a button, called on press. Without it the bar is a picture (role="img"). |
tabIndex | number | – | For grids that move focus with the arrow keys (roving tabindex). Only used with onOpen. |
continues | { before?: boolean; after?: boolean } | – | It runs past the visible days: that end is squared off, and the left bar is dropped when it continues from before. |
className | string | – | Extra classes. |
style | CSSProperties | – | Placement (e.g. grid column span). |
MonthChip props
| Prop | Type | Default | Description |
|---|---|---|---|
eventrequired | CalendarEvent | – | Any event. All-day events are drawn as a full-width AllDayBar. |
open | boolean | – | Its details are open: the selected background. |
onOpen | (event: CalendarEvent, el: HTMLButtonElement) => void | – | Makes the chip a button, called on press. Without it the chip is a picture (role="img"). |
tabIndex | number | – | For roving focus inside a month grid. Only used with onOpen. |
continues | { before?: boolean; after?: boolean } | – | Passed on to AllDayBar for all-day events. |
className | string | – | Extra classes. |
PatternSwatch props
| Prop | Type | Default | Description |
|---|---|---|---|
patternrequired | 'solid' | 'hatched' | 'outline' | 'dashed' | – | The calendar’s pattern (calendarById(id).pattern). |
size | 'xs' | 'sm' | 'md' | 'md' | 6, 8 or 12 pixels. |
className | string | – | Extra classes. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| Tab | Moves to the next event (events with onOpen only). |
| EnterSpace | Opens the event (calls onOpen). |
- Each has an aria-label that says the whole event: “Weekly skill review, Friday 10 to 11 AM, Team calendar”, or “Skill cleanup sprint, Monday to Wednesday, all day, Team calendar”.
- With onOpen it is a <button> with aria-haspopup="dialog" and aria-expanded. Without it, it is a non-interactive element with role="img" and the same name, so it is read but never a dead Tab stop.
- Text on every fill meets 4.5:1 (patternText picks the colours); the fills and swatches are aria-hidden, and the calendar is named in the label.
- Focus shows as the shared focus ring. The open event is marked by aria-expanded and on screen by a shadow (blocks and bars) or the selected background (month chips).
Guidelines
Do
- Give every block an onOpen that shows its details wherever people can act on events; leave it off for read-only previews.
- Name the calendar in text wherever a swatch appears.
- Let the time grid size blocks; keep the 20 pixel minimum it uses.
Don’t
- Don’t colour calendars; use the four patterns.
- Don’t cram a block shorter than 30 pixels with two lines; it folds to one on purpose.
In use
Related
- Week and day viewDays across, hours down: a time grid with an all-day row, overlaps side by side, and a line at the current time.
- Month viewThe month as a keyboard grid: up to three events a day, “+N more” for the rest, and pattern squares on phones.
- Fills & patternsSolid, hatched, outline and dashed: the second “colour” of a black-and-white interface, plus the dot-grid canvas.
- Agenda listEvents grouped by day under headings that can stick, earliest first, each with its time, calendar pattern and people.
- Event layoutThe calendar’s layout maths: overlapping events side by side, all-day bars stacked into lanes, and the sentence a screen reader hears.
Source
The component’s own files, exactly as they run on this site.
/** * How an event looks in each view. All three are named in full for screen readers ("Weekly skill * review, Friday 10 to 11 AM, Team calendar"). With onOpen they are buttons that open the event's * details; without it they are still pictures (role="img", same name), so there is no dead Tab stop. * * EventBlock time grid (week, day): 2px left bar, title over time; one line under 30px * AllDayBar the all-day row, and all-day events in the month grid * MonthChip a timed event in a month cell: pattern square, start time, title */import type { CSSProperties, ReactNode } from "react"import { cx } from "../../components/cx"import type { CalendarEvent } from "../data/sample"import { formatTimeShort } from "../lib/dates"import { calendarById, eventLabel, eventTime } from "./events"import { PatternFill, PatternSwatch, patternText } from "./patterns"
type Open = (event: CalendarEvent, el: HTMLButtonElement) => void
/** Below this height a block shows one line: "Standup, 9:30 AM" */const SHORT_BLOCK = 30