Calendars

Events on a grid of time: one stateful Calendar with month, week, and day views, plus ten base components. Nine event colors and a mobile single-day collapse.

Demo

Every export with real props — the ten base components and the whole Calendar, whose view control remounts it.

Component
color
import { CalendarMonthViewEvent } from "@/components/application/calendar/base-components/calendar-month-view-event"; <CalendarMonthViewEvent label="Design review" supportingText="10:30 AM" color="brand" />

The two chips are not symmetrical

CalendarMonthViewEvent defaults withDot to true and lays the dot, label, and time out on one line; CalendarDwViewEvent has no default for it, stacks the time under the label, and puts the dot at the end. Both take the same nine colors from the same exported style object, and both are real <button type="button"> elements since 2026-08-23. Read from the source.

Contextual demo

Six working schedules at the real viewport: search filters, both add buttons create, the channel chips narrow.

The family

Eleven exports across eleven modules: the stateful Calendar plus ten base components, no barrel. The views are internal.

The components in this family, with their descriptions and import paths
ComponentDescriptionImport from
CalendarThe whole surface, and the only stateful export: it holds the anchor date, the selected day, the view, and a minute-ticking clock, and renders the header plus one of the three views. Its internals — MonthView, WeekView, WeekViewDay, DayView, MobileSingleDayGrid, PositionedEvent — are not exported.@/​components/​application/​calendar/​calendar
CalendarHeaderThe bar above every view: the date icon, the title with a week badge, the period line, a search button, the Prev/Today/Next group, the view menu, and Add event. Fully controlled since 2026-08-26 — every control takes a handler, and search and Add event render only when you supply one. The title is the full date in day view and the month and year otherwise.@/​components/​application/​calendar/​base-components/​calendar-header
CalendarViewDropdownThe view menu: a secondary Button trigger over a 216px single-selection Dropdown with radio indicators and keyboard-shortcut addons. Takes its options as data, so Day, Week, and Month are not baked in.@/​components/​application/​calendar/​base-components/​calendar-view-dropdown
CalendarDateIconThe 64px date tile at the start of the header: a gray month strip over a bold brand day, in a ringed 8px-radius box. Hidden below md by the header, not by itself.@/​components/​application/​calendar/​base-components/​calendar-date-icon
CalendarColumnHeaderOne weekday column head. Renders a real button when you pass onClick and a plain div otherwise, with default, selected, and today states on the 24px date circle.@/​components/​application/​calendar/​base-components/​calendar-column-header
CalendarMonthViewCellOne day cell in the month grid: the date circle, your event children, logical hairlines, a hover tint and a hover add-button from md up, and a disabled fill for days outside the month.@/​components/​application/​calendar/​base-components/​calendar-month-view-cell
CalendarMonthViewEventThe month grid’s event chip: a real button with an optional leading dot, a truncating label, an optional time, and one of the nine color ramps. collapseOnMobile swaps the whole chip for a bare dot below md.@/​components/​application/​calendar/​base-components/​calendar-month-view-event
CalendarDwViewCellOne 48px half-hour slot in a day or week column: a hover tint, logical hairlines, and a 28px add-event button that reveals on hover or focus.@/​components/​application/​calendar/​base-components/​calendar-dw-view-cell
CalendarDwViewEventThe day and week chip: a real button that fills the block its parent positions, with the label over the start time and an optional trailing dot. The time is drawn only above 30 minutes.@/​components/​application/​calendar/​base-components/​calendar-dw-view-event
CalendarRowLabelOne hour row in the time gutter: a 96px block on the secondary fill with its label pulled 8px above the hour line, except the first, which sits below it.@/​components/​application/​calendar/​base-components/​calendar-row-label
CalendarTimeMarkerThe current-time rule across a day or week grid: a dot centered on the gutter border, 1px lines, and a <time> element with an optional machine-readable dateTime and an sr-only “Current time” prefix.@/​components/​application/​calendar/​base-components/​calendar-time-marker

Built from

Eleven layers make up the calendar; seven have their own docs page.

Component
Where it’s used here
ButtonFive call sites: the header’s primary sm Add event and its tertiary sm search button (named “Search events” since 2026-08-23), the mini calendar’s two 32px tertiary month-nav buttons at radius 6, and the two secondary add-event buttons inside the cells, resized to 32 and 28 at the same radius.
ButtonGroupThe header’s Prev / Today / Next control at size sm, with Today stretched to fill the row below lg. The two arrow items are icon-only and carry aria-labels — “Previous period” and “Next period” — since 2026-08-23; a ButtonGroupItem’s id is a collection key, not a name.
DropdownThe view menu: Dropdown.Root, a 216px popover placed bottom end, and a single-selection menu with disallowEmptySelection, radio indicators, and the keyboard-shortcut addons.
BadgeThe “Week N” chip beside the header title, at size sm, color gray, type modern. N is computed as the day of the month over seven, rounded up.
ButtonUtilityThe day sidebar’s three event actions — Copy link, Delete, and Edit — at size xs, color tertiary, each named by its tooltip.
AvatarThe day sidebar’s guest stack: five photo avatars and one initials avatar at size sm, each with a 1.5px bg-primary ring, overlapping by 8px.
@untitledui/iconsPlus on both add-event buttons and Add event, SearchLg, ArrowLeft and ArrowRight in the button group, ChevronDown on the view trigger, ChevronLeft and ChevronRight on the mini calendar, and Calendar, Clock, BellRinging01, Copy01, Trash01, and Edit01 in the sidebar.

The three views

Three views at two breakpoints — the sheet’s six Calendar Types. The view prop seeds each instance, once.

Search, “Add event”, and the per-cell add button are wired — activate one.

view="month" (the default) — the 7-column grid, three chips per day and a “+N” row after that; below md the selected day’s events move to a footer

These three are the only clock-dependent things on the page

Read from the source: Calendar seeds its anchor date from today() and exposes no prop for it, so a fixture pinned to a fixed date would open on an empty month. The schedule below is therefore anchored on the current week — a standup, a design review, a 1:1, a customer call, and a three-day design sprint — and the current-time marker draws itself from a real clock that ticks once a minute. Every chip, cell, header, and gutter specimen further down uses fixed strings instead.

Search, “Add event”, and the per-cell add button are wired — activate one.

view="week" — seven scrolling columns over a 72px gutter at 48px per half hour; below md this same instance renders the single-day grid

Search, “Add event”, and the per-cell add button are wired — activate one.

view="day" — one column plus the 328px detail sidebar, which is lg and up only
Month at 375px — the chips become dots, the selected day’s events move to a footer, and the weekday heads narrow to single letters
Week at 375px — there is no mobile week: below md this same instance renders the single-day grid, over a 56px gutter instead of 72
Day at 375px — the 328px detail sidebar is lg and up, so the phone gets the bare column and the day strip above it

Sized for a page, not a panel

Read from the source: the root is md:min-h-[912px] in month view and a fixed h-[912px] in week and day, and the sheet draws it 1,280px wide. Inside this docs column it is doing its best at roughly half that. The contextual demo above is the honest view of its proportions. Note too that the component returns null until its mount effect runs, so nothing at all arrives from the server.

Event chips

Two chips, nine ramps, two states each, five durations — every event symbol the sheet draws, plus the mobile dot.

color="gray"
color="brand"
color="green"
color="blue"
color="indigo"
color="purple"
color="pink"
color="orange"
color="yellow"
color="gray", hover
color="brand", hover
color="green", hover
color="blue", hover
color="indigo", hover
color="purple", hover
color="pink", hover
color="orange", hover
color="yellow", hover

Why the hover row is drawn, not hovered

The sheet ships a Hover symbol for all nine month chips and all nine day-and-week chips, and a specimen cannot produce one with a pointer. The row above is the real component with the ramp’s 100 fill applied statically — the same value the kit writes as hover:bg-utility-<color>-100. Hover a chip in the row above it to confirm the two agree.
collapseOnMobile — below md every chip is a bare 8px dot in the ramp’s 500, which is what the sheet’s nine Mobile month-event symbols are. Shown in a real 375px viewport, because the rule reads the window and not this column

The ramps flip in dark mode

Every chip color is on the utility-* scale, which inverts in dark mode; bare interface-* does not. Switch the theme at the top of this page and the nine cells above follow. The chips carry no role beyond “button” and no color information in the accessibility tree, which is why the guidelines insist the label says what the event is.
color="gray" — default over hover
color="brand" — default over hover
color="green" — default over hover
color="blue" — default over hover
color="indigo" — default over hover
color="purple" — default over hover
color="pink" — default over hover
color="orange" — default over hover
color="yellow" — default over hover
30 minutes — one 48px slot, and no time: Calendar passes supportingText only above 30 minutes
60 minutes — two stacked slots, with the start time under the label
90 minutes with withDot — three slots; the dot trails the label here rather than leading it
120 minutes — four stacked slots, the tallest Type the sheet draws
Empty — the slot with no event in it, which is the sheet’s fifth Type and the component’s own resting state

Height is arithmetic, not a prop

Read from the source: SLOT_HEIGHT is 48 and one slot is 30 minutes, so PositionedEvent computes a block’s top as the minutes since midnight over 30, times 48, and its height the same way — 60, 90, and 120-minute events are exactly two, three, and four stacked cells. Anything under 30 minutes draws at one full slot — the sheet’s smallest Type — since 2026-09-04; the old half-slot block left 12px for a chip that needs 28. The wrapper insets the chip by 6px on every side; the chip itself is h-full w-full flex-1, so it fills whatever it is given.

Day and column states

Cells and headers in every state the sheet draws, at both breakpoints. Only the header is a real button.

8
Default — the hover tint and the 32px add button are md and up only
9
Today — bg-secondary in the circle, plus a forced-colors outline
10
Selected — bg-brand-solid and white text, and Highlight under forced colors
11
2 more...
Disabled — the bg-secondary_alt fill for a day outside the month, the date at 50%, no add button
12
Hover — md:hover:bg-primary_hover on the cell and the 32px add button revealed, drawn statically because the sheet ships a Hover symbol and a specimen cannot hold a pointer
Mobile — the sheet’s 56×88 cell: no hover tint, no add button, an 88px floor, and a bare dot for each chip. Three states in a real 375px viewport

The “more” row, rebuilt

The last cell above reproduces the overflow row by hand, because it is written inline in MonthView rather than exported: text-utility-interface-500 at 12px semibold, inset 4px below md and 8px from md up, reading +N on mobile and N more... on desktop, and dimmed to 50% inside a disabled cell. All three of those behaviors landed on 2026-08-23. The cap itself, MAX_EVENTS_PER_CELL, is 3 and is not a prop.
Mon8
Default, not clickable — a div, 4px gap from md
Today, clickable — a button with aria-current="date"; 8px gap from md
Selected, clickable — a button with aria-pressed; 8px gap from md
Thu
Month view’s header — no day prop, so the weekday stands alone
Mobile — the sheet’s 64×64 column header stacks the weekday over the date instead of setting them in a row, and the selected and today circles keep their fills. Default, today, and selected at 375px

Tab into the two middle cells

Passing onClick is what makes the header a real <button type="button">, with a focus ring and the right ARIA state; without it the component renders a div, which is exactly what month view wants. Before 2026-08-23 the clickable case was a div too, with no role, no keyboard path, and selected and today distinguished by color alone.

The time gutter

The hour label, the half-hour slot, and the marker at both breakpoints. Two hours rebuilt, the marker pinned to 10:30.

9 AM
10 AM
CalendarRowLabel at 96px per hour, CalendarDwViewCell at 48px per half hour, and CalendarTimeMarker across both — hover a slot for its 28px add button, then Tab to it. The slots here are wired; a Calendar’s are not

Three 1px decisions live here

Read from the source, all landed 2026-08-23: the row label sits pe-[7px] from the gutter edge (it drew 8px) with its baseline pulled -translate-y-2, 8px above the hour line (it drew 9), and the marker’s dot sits at -start-1 so its 8px circle centers on the 1px border rather than a pixel inboard. The first label is the exception on purpose — it pushes down by 4px so it does not clip the top of the grid. None of the three has been measured in a browser yet. The slots above pass onAddEvent, which is what makes their 28px add button render at all — a Calendar never passes it in week or day view, which the Code tab records as an open item.
Mobile — below md the marker sets its time inline inside the gutter at a 6px gap; from md it is pulled outside the gutter border instead. The sheet’s two marker symbols