Moltenship Time
Views

Week and day

Time-grid views with an all-day row, a current-time line, and DST-aware hours.

Live, in your time zone. Drag to move, resize or create events.

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.

  1. Time gridTimeGridView
  2. Day headerTimeGridHeader
  3. All-day rowTimeGridAllDay
  4. Title
  5. Time
  6. Timed eventTimeGridEvent
  7. Resize handledata-resize="end"
  8. Time gutter--calendar-gutter-width
  9. Outside working hoursTimeGridAvailability
  10. Now lineuseNow
  11. Overlap lanesTimeGridEventLayout
  12. Day columnTimeGridColumn
Rendered by TimeGridView
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-width wide: 3rem, or 4rem from the sm breakpoint.
  • An event's TimeGridEventLayout sets its top and height from its times, and its left and width from 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-height tall (3rem by default).
  • Events: overlapping events sit side by side, or as overlapLayout sets. 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 onEventChange on Root, 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-pending and render at 60% opacity.

The day view works on narrow screens:

Live, in your time zone. Drag to move, resize or create events.

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:

Live. Switch the layout, or drag events 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. stackIndent sets 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.

FocusKeyEffect
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 slotHome, EndFirst or last day shown
A day or slotPage Up, Page DownBack or forward a whole week (or day)
A slotEnterThe event covering it. Escape goes back to the slot it starts in
An all-day cellEnterGo 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 slotShift+EnterOpen that date in the day view, when the week has dayView
A slotSpaceStart an hour-long event there. Arrow keys extend it, and Enter saves it
An all-day cellSpaceStart an all-day event there
An eventSpacePick it up. Arrow keys move it by slots and days, Enter saves, and Escape cancels
An eventShift+SpacePick 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.

Edit on GitHub

Last updated on

On this page