Annotations
Time-based information beside events, such as listening history, sleep, workouts and focus sessions.
Annotations show what happened alongside the events: what you listened to, when you slept, a workout, a focus session. Unlike events they are read-only, and they never take part in overlap layout, so they never move, narrow or reorder an event. Try them in the live demo; the sidebar's Activity list shows and hides each source.
The calendar doesn't fetch anything. Your app loads the data from its integrations and passes it to
Root, the same way it passes events.
Add them
Describe each source once, then pass the sources and the annotations to Root:
import { MoonIcon, MusicIcon } from "lucide-react";
import type { TimeGridAnnotationSource } from "@/components/calendar/time-grid-view/time-grid-annotations";
const sources: TimeGridAnnotationSource[] = [
{ id: "sleep", label: "Sleep", color: "purple", Icon: MoonIcon },
{
id: "music",
label: "Music",
color: "green",
Icon: MusicIcon,
// Tracks less than five minutes apart draw as one listening session.
mergeGap: Temporal.Duration.from({ minutes: 5 }),
},
];
const annotations = recentlyPlayed.map((play) => ({
id: play.id,
sourceId: "music",
start: Temporal.Instant.from(play.startedAt),
end: Temporal.Instant.from(play.endedAt),
title: `${play.track} · ${play.artist}`,
}));
<Calendar.Root annotationSources={sources} annotations={annotations} {...props} />;The library knows no kinds of source: "sleep" and "music" are your names. Sources show in the order
you list them, and leaving one out of annotationSources hides its annotations. Like events,
annotations are half-open, [start, end). Invalid ones are skipped and reported through
onDiagnosticsChange.
The week and day views draw annotations as soon as Root has sources. Without any, they reserve no
space for them.
How they draw
- A rail beside the events. Each day column keeps a narrow strip at its start, 18px in the week
and 30px in the day, whenever the root has annotation sources. Events move over by that much once,
the same in every column whatever the data, and keep their layout. With
placement: "overlay"the parts draw behind events instead and reserve nothing. - Capsules with the source's icon. Each annotation draws as a capsule in its source's color, with the icon inside its top. A mark is never shorter than it is wide, so a short session is a circle with the icon.
- Merging. A source's annotations closer than its
mergeGapdraw as one mark, such as tracks into a listening session. Marks whose circles would touch merge too, so song-level history stays readable when zoomed out. - Lanes. When different sources overlap, such as music during a focus session, they share the rail in up to three narrower lanes. Too narrow for an icon, a mark drops it.
- Days. An annotation across midnight shows on both days. A part that starts the day before fades in at the top and has no icon.
- Details. Hovering a mark shows a tooltip, and clicking it opens a popover listing what it merges. Both are Base UI components, one of each for the whole grid. The popover keeps focus until Escape, outside click or its close button, then returns it to where it was. Pressing or dragging a mark never starts a new event.
Customize
The rail is registry source like the rest of the view, built from small parts you can restyle, recompose or edit. From the lightest change to the heaviest:
CSS. Each mark has data-slot="annotation-mark", data-source, data-color, data-kind
(bar or dot), and data-starts-before or data-ends-after when it's cut at midnight. The demo
gives sleep a night sky this way:
[data-slot="annotation-mark"][data-source="sleep"] {
background: linear-gradient(to bottom, oklch(0.5 0.19 282), oklch(0.7 0.14 275));
}Your own composition. Pass a component as the view's annotations.Annotations. It gets one
column's marks, already merged and placed in lanes, and composes the parts:
import {
AnnotationMark,
AnnotationMarkIcon,
AnnotationRail,
type TimeGridAnnotationsProps,
} from "@/components/calendar/time-grid-view/time-grid-annotations";
function MyAnnotations({ marks }: TimeGridAnnotationsProps) {
return (
<AnnotationRail>
{marks.map((mark) => (
<AnnotationMark key={mark.key} mark={mark} className="data-[source=music]:bg-[#1DB954]">
<AnnotationMarkIcon>
{mark.source.id === "music" ? <SpotifyLogo /> : null}
</AnnotationMarkIcon>
</AnnotationMark>
))}
</AnnotationRail>
);
}
createWeekView<Meeting>({ annotations: { Annotations: MyAnnotations } });AnnotationMarkhas the variantscapsule(default),bar(a thin bar that keeps its true length) andband(a tint across the column behind events, withplacement: "overlay"). Base UI'srenderprop swaps its element, for example for a link to the track.AnnotationMarkIcondraws the source'sIcon. Children replace it.useAnnotationMark()gives any part inside a mark its mark and width.
Rail size and lanes. These are numbers rather than CSS, because merging and lane packing use them:
createWeekView<Meeting>({
annotations: { rail: { width: 10, markWidth: 4 } }, // thin bars, as in Amie's desktop app
});The parts themselves. time-grid-annotations.tsx is yours: change the capsule, the tooltip or
the popover there.
Your own view. useTimeGridAnnotations(columns, geometry) returns the marks for any time-grid
columns, and summarizeAnnotationMarks returns a day's totals per source.
Anatomy
- Annotation rail
AnnotationRail - Cut at midnight
data-starts-before - Mark
AnnotationMark - Icon
AnnotationMarkIcon - Short mark
data-kind="dot" - Day summary
aria-describedby - Shared lanes
lane, lanes - Events, never moved
TimeGridEvent
TimeGridColumn time-grid-column.tsx
└── TimeGridAnnotations time-grid-annotations.tsx, or yours
└── AnnotationRail data-slot="annotation-rail", aria-hidden
└── AnnotationMark data-slot="annotation-mark", one per mark
└── AnnotationMarkIcon data-slot="annotation-mark-icon"Options
Prop
Type
Set them as the week or day view's annotations option.
Keyboard and screen readers
Annotations add no keys and no tab stops: the grid keeps its single tab stop and its
key map. The rail is aria-hidden, and screen readers get the
same information in words instead:
| Where | What a screen reader hears |
|---|---|
| A day's header | A summary of its annotations: "Annotations: Sleep until 7:05 AM. Music, 3 times, 2 hours, 10 minutes." |
| A slot, with ↑ ↓ | The items drawn across it, after its time: "Music: Nightcall, 9:00 AM to 9:04 AM; Genesis, 9:05 AM to 9:09 AM" |
Hover effects respect prefers-reduced-motion.
Data model
Prop
Type
A source has an id, a label that names it in summaries and tooltips, and an optional
mergeGap. The registry's TimeGridAnnotationSource adds color and Icon.
Last updated on
Interactions
Save moved, resized, created, and deleted events, show them while they save, and decide which changes to allow.
Availability
Working hours, closed days and holidays, people and rooms in their own time zones, shading you can restyle, a busy or free status, and a policy that keeps bookings inside working hours.