Moltenship Time
Views

Year

Twelve months at a glance, with a dot for each event color on a day and a popover of its events.

Live, in your time zone. Click a day to see its events, or a month's name to open it.

Try it in the live demo: pick Year in the view switcher, or press Y.

createYearView shows the anchor's calendar year, January to December, as twelve small month grids. Weeks start on weekRules.firstDay. Each date shows once: a week that reaches into the month before or after leaves those cells empty, so every month keeps its own shape.

Add it

Copy registry/year-view into your app (it uses shadcn's popover), along with the theme and registry/calendar, as described in UI source. Then register the view:

import { createYearView } from "@/components/calendar/year-view/year-view";

export const Calendar = createCalendar<Meeting>()({
  eventAdapter,
  views: [
    createMonthView<Meeting>({ getEventColor: (meeting) => meeting.color }),
    createYearView<Meeting>({ getEventColor: (meeting) => meeting.color }),
    createDayView<Meeting>(),
  ],
});

The view is named year. Previous and Next move by one year. Its range is the whole year, so onVisibleRangeChange asks for a year of events at a time: see loading events by range.

Anatomy

YearView renders the parts below. To change one, edit it in the registry/year-view files you copied.

  1. Year viewYearView
  2. MonthYearMonthGrid
  3. Month name, opens the month
  4. Weekday initials
  5. Todayaria-current="date"
  6. Outside the month
  7. Day celldata-date
  8. Event colorsdata-color
  9. Day popoverYearDayPopover
  10. EventYearEvent
Rendered by YearView
YearView                         year-view.tsx
├── YearMonthGrid                year-month.tsx, one per month
│   ├── Month name               opens the month view
│   ├── Weekday initials
│   └── Day cell                 data-date, the popover's trigger
│       ├── Date                 aria-current="date" on today
│       └── Event dots           data-color, one per color
└── YearDayPopover               year-day-popover.tsx, one for the whole year
    ├── Date                     opens the day view
    └── YearEvent                a [data-event-id] button per event

The popover is one Base UI popover with a detached trigger in every day, so the year never mounts hundreds of popovers.

Options

Prop

Type

What it shows

  • Months: two to a row on a phone, up to six on a wide screen. Each keeps room for six weeks, so months side by side line up.
  • Days: today's date sits in a filled circle. Under each date, a dot for every event color on that day, up to three, in the order the day's events start. All-day and multi-day events count on every date they cover.
  • A day's events: clicking a day opens a popover with its events, all-day first, then by start time. Each event is a <button data-event-id>, so the app's own event details open from it as from any other view. The date at the top opens the day in the view named day.
  • Month names open that month in the view named month.
  • Closed days: with availability, see below.
  • Scrolling: where the months don't fit, the view opens with today's month, or the day you last focused, in view. Today, Previous, Next and a picked date scroll their month into view. Only the view scrolls, never the page around it.
  • Editing: none. The year shows events without moving or creating them; open a day or a month to edit.

Closed days

With availability, a day without working hours is named "closed" for screen readers, as in the month view. What's shaded depends on closedDays:

  • "exceptions" (the default) shades the dates an exception closes, such as a public holiday or an offsite. Weekly closed days, such as weekends, stay plain: the weekday columns already show them, and a third of the year in gray would bury the holidays.
  • "all" shades the weekly closed days too.
  • "none" shades nothing.

Shaded days next to each other in a week join into one band with rounded ends, so a weekend or a long holiday reads as one stretch, and the rows stay apart. To draw it differently, such as hatching, pass renderUnavailable; its band carries the reason, the exception's label and whether open time meets each end.

Set closedDays when you create the view, or per calendar with viewOptions={{ year: { closedDays: "all" } }} on Root.

Keyboard

Tab into the preview above, or click a day, and try these keys. The Accessibility page has the full map.

FocusKeyEffect
A day← →Previous or next day, across months
A day↑ ↓Same weekday a week earlier or later, across months
A dayHome, EndFirst or last day of the week's row in its month
A dayPage Up, Page DownSame day in the previous or next month
A dayEnter, SpaceOpen the day's events. Tab moves through them; Escape returns to the day
A dayShift+EnterOpen the day in the day view

Moving past December 31 or before January 1 navigates to the next or previous year.

Build your own

The strategy pieces are exported, for a year that looks different, such as a heat map of busy days: getYearRange, navigateYear, getYearTitle, projectYear and yearSpatialNavigation. projectYear returns twelve months of week rows, each day with its event fragments in display order, or null outside the month. See Custom views.

Edit on GitHub

Last updated on

On this page