Week and day
Time-grid views with an all-day row, a current-time line, and DST-aware hours.
Try both views in the live demo.
createWeekView and createDayView show the hours of the day down the side and one column per day.
The week view has seven columns, starting on weekRules.firstDay. The day view has one column for
the anchor date.
Add them
Copy registry/time-grid-view into your app (it uses shadcn's button), along with the theme and
registry/calendar, as described in UI source. Then
register the views:
import { createDayView, createWeekView } from "@/components/calendar/time-grid-view/time-grid-view";
const getEventColor = (meeting: Meeting) => meeting.color;
export const Calendar = createCalendar<Meeting>()({
eventAdapter,
views: [
createWeekView<Meeting>({ dayView: "day", getEventColor }),
createDayView<Meeting>({ getEventColor }),
],
});The views are named week and day. Pass your event type, as in createWeekView<Meeting>, so
getEventColor receives your records.
Anatomy
Both views render TimeGridView from registry/time-grid-view, with seven day columns for the week
and one for the day. The view builds these parts itself, so they're not parts you compose: to change
one, edit its file. The tree shows where each part lives.
- Time grid
TimeGridView - Day header
TimeGridHeader - All-day row
TimeGridAllDay - Title
- Time
- Timed event
TimeGridEvent - Resize handle
data-resize="end" - Time gutter
--calendar-gutter-width - Outside working hours
TimeGridAvailability - Now line
useNow - Overlap lanes
TimeGridEventLayout - Day column
TimeGridColumn
TimeGridView time-grid-view.tsx
├── TimeGridHeader time-grid-header.tsx
├── TimeGridAllDay time-grid-all-day.tsx
└── TimeGridBody time-grid-body.tsx, scrolls
├── Time gutter --calendar-gutter-width
└── TimeGridColumn time-grid-column.tsx, one per day, data-date
├── TimeGridEvent time-grid-event.tsx
│ ├── Title and time
│ └── Resize handles data-resize="start" and "end"
└── Now line data-slot="time-grid-now"- The gutter is
--calendar-gutter-widthwide: 3rem, or 4rem from thesmbreakpoint. - An event's
TimeGridEventLayoutsets itstopandheightfrom its times, and itsleftandwidthfrom the overlap layout. A column renders its events in layout order, so a stacked event draws over the ones it overlaps. useNow()moves the now line.
Options
Prop
Type
Set options when you create the view, or per calendar with
viewOptions={{ week: { dayView: "day" } }} on Root.
What it shows
- Header: the weekday and date of each column, with today highlighted and marked
aria-current="date". The corner shows the time zone's offset, such as "GMT+2". - All-day row: all-day events, plus timed and floating events that last 24 hours or more. Busy days collapse to three rows with "+N more", and a chevron in the corner expands the row.
- Hours: labels come from
Intl, so they read "1 PM" or "13:00" depending on the locale. Each hour is--calendar-hour-heighttall (3rem by default). - Events: overlapping events sit side by side, or as
overlapLayoutsets. Events shorter than 45 minutes show on one line, as "Standup, 9:30 AM". Events that run past midnight show their real times on both days, as "10:00 PM – 2:00 AM". A title that doesn't fit its event shows in full in a tooltip while you hover or focus the event. - Daylight saving time: hours skipped when the clocks go forward are hatched, and hours repeated when they go back are tinted, with both occurrences side by side.
- Current time: a line across today's column, from
useNow(). The grid opens scrolled to the current time, or to 08:00 when today isn't visible, and keeps its scroll position when you move between weeks. - Editing: with
onEventChangeonRoot, drag an event to move it (onto the all-day row to make it all-day), drag its top or bottom edge to resize it, and drag across empty time or all-day cells to create one. See Interactions. - Changes being saved: events with a pending change get
data-pendingand render at 60% opacity.
The day view works on narrow screens:
Overlapping events
overlapLayout sets how events that overlap share a day column. Switch between the layouts here,
and drag an event to see the others make room:
"columns"(the default) puts overlapping events side by side, in as few lanes as they need. Each event widens into lanes that stay free beside it, as Google Calendar draws them."equal"uses the same lanes, but every event in a busy stretch is one lane wide, so the lanes line up."stack"cascades them. Each event is indented past the earlier events it overlaps and drawn on top of them, reaching to the column's end.stackIndentsets the indent as a fraction of the column width (0.15 by default). A deep stack indents less, so the top event keeps at least half the column. Hover or focus a covered event to bring it to the top and read its title.
createWeekView<Meeting>({ dayView: "day", overlapLayout: "stack" });
// Or switch it at runtime, per view:
<Calendar.Root viewOptions={{ week: { overlapLayout }, day: { overlapLayout } }} />;Every layout keeps the same rules:
- Events that touch, one ending as the next starts, don't overlap. An event shorter than 15 minutes still takes up 15 so it stays readable, so it can overlap the next one.
- Overlapping events form a group, laid out on its own, so a change to one group never moves events in another.
- The order is fixed: by start, then longer first, then event ID. It's the order events are drawn in, and the order ↑ and ↓ walk them. ← and → step to the overlapping event drawn beside, and mirror right to left.
- An event picked up with the keyboard, and changes still being saved, are laid out the same way, so an event lands where it will stay.
The resource columns view takes the same options, for each resource's column. The scrolling week and day views do too.
Keyboard
Tab into the preview above, or click a slot, and try these keys. The Accessibility page has the full map and what a screen reader hears.
| Focus | Key | Effect |
|---|---|---|
| An all-day cell | ↓ | First slot of the day |
| A slot | ↑ ↓ | Previous or next 15-minute slot. Up from the first slot reaches the all-day cell |
| A day or slot | ← → | Same slot on the previous or next day |
| A day or slot | Home, End | First or last day shown |
| A day or slot | Page Up, Page Down | Back or forward a whole week (or day) |
| A slot | Enter | The event covering it. Escape goes back to the slot it starts in |
| An all-day cell | Enter | Go into the day's all-day events |
| A timed event | ↑ ↓, ← → | Previous or next event in the column, or the event beside it that overlaps it |
| A day or slot | Shift+Enter | Open that date in the day view, when the week has dayView |
| A slot | Space | Start an hour-long event there. Arrow keys extend it, and Enter saves it |
| An all-day cell | Space | Start an all-day event there |
| An event | Space | Pick it up. Arrow keys move it by slots and days, Enter saves, and Escape cancels |
| An event | Shift+Space | Pick up its end to resize it. Shift+Space again switches to its start |
Moving a picked-up timed event above the first slot makes it all-day, and moving an all-day event below the all-day row makes it timed. Only the focused slot is an element, so a week's 672 slots cost nothing until you visit them.
Markup for interactions
The view connects the pointer layer for you. If you change its markup, keep the attributes the
pointer layer reads: day columns have data-date and cover the full 24 hours, all-day cells have
data-date and data-all-day, each event is a <button> with data-event-id, and events you can
edit have data-resize="start" and data-resize="end" handles.
Last updated on