Your first calendar
Events, a time zone and a height are all HapioCalendar needs; the views, the toolbar and the keyboard come with it.
After installing the block, render it with your events:
import type { CalendarEventInput } from '@hapio/calendar-react'
import { HapioCalendar } from '@/components/hapio-calendar/hapio-calendar'
const events: CalendarEventInput[] = [
{
id: 'haircut-anna',
title: 'Haircut · Anna',
start: '2026-10-22T09:00:00+02:00',
end: '2026-10-22T10:00:00+02:00',
},
// All-day: dates, and the end is exclusive (19 and 20 October).
{ id: 'training', title: 'Staff training', start: '2026-10-19', end: '2026-10-21' },
]
export function TeamCalendar() {
return (
<HapioCalendar
events={events}
timeZone="Europe/Stockholm"
nowIndicator
className="h-[max(36rem,calc(100svh-17rem))]"
/>
)
}That is a complete calendar:
- A toolbar with Today, Previous and Next (named "Previous Week", "Next Week" and so on by the view), the range's title, and a view switcher: a radio group named "Calendar view" with the radios "Week view", "Day view", "Month view" and "List view".
- Four views (
defaultViews): a week and a day time grid, a month grid and a week list. It opens on the week of today. Choose others withviewsanddefaultView; see Views. - A height: the root has the height you give it (here with Tailwind's
h-[…];styleworks too), the toolbar included, and the view scrolls inside it. Without one the calendar grows to its content, and a time grid shows the whole day at full size. - A time zone: events are drawn in
Europe/Stockholmwherever the browser is. WithouttimeZonethe calendar uses the browser's zone, which only the browser knows: the server then renders a skeleton of the same size and the calendar appears after hydration. The zone alone does not make the server render complete: it takes an IANAtimeZoneand a date (defaultDate, or a controlleddate) ornow, because which week is "today" is the browser's to say too. Without one, as here, the toolbar renders on the server and the week appears after hydration. See Time zones and SSR and RSC. - Accessibility: Tab reaches the events as one stop (with
navLinks, the day headers first), the arrow keys move between them in time order, and every event has a name (its visible time and title) and a description (the full date, time and resources). Navigation is announced. See Accessibility.
Events
An event needs an id and a start. Its end is exclusive and defaults to one hour later for a
timed event and one day later for an all-day event. How a date is written decides what it means:
start / end | Meaning |
|---|---|
'2026-10-22T09:00:00+02:00', a Date, epoch ms | an exact instant |
'2026-10-22T09:00[Europe/Stockholm]' | an instant in a zone |
'2026-10-22' | an all-day date (never converted through a time zone) |
'2026-10-22T09:00' (no offset) | a wall time in the calendar's zone ("floating") |
Temporal objects work as well (Instant, ZonedDateTime, PlainDate, PlainDateTime). Give
an event href and it renders as a real link; give it color (any CSS color) and it is filled
with it (contrastColor sets its text color; lists show the color as a dot). Events and data lists every field, accessor mode for your own objects, and
event sources that load by range.
Props you will reach for
'use client'
import type { CalendarEvent, CalendarEventInput, EventClickArgs } from '@hapio/calendar-react'
import Link from 'next/link'
import { useState } from 'react'
import { HapioCalendar } from '@/components/hapio-calendar/hapio-calendar'
const H24 = { hour: '2-digit', minute: '2-digit', hour12: false } as const
export function BookingsCalendar({ bookings }: { bookings: readonly CalendarEventInput[] }) {
const [open, setOpen] = useState<CalendarEvent | null>(null)
const onEventClick = (args: EventClickArgs<undefined>) => {
// Ctrl/Cmd-click, Shift-click and the middle button open the link as the browser does.
if (args.isModified) return
args.preventDefault() // stay on the page instead of following `href`
setOpen(args.event)
}
return (
<>
<HapioCalendar
events={bookings}
timeZone="Europe/Stockholm"
locale="en-GB"
weekStartsOn={1}
formats={{ eventTime: H24, slotLabel: H24 }}
navLinks
nowIndicator
scrollTime="07:00"
businessHours
linkComponent={Link}
onEventClick={onEventClick}
className="h-[max(36rem,calc(100svh-17rem))]"
/>
{open === null ? null : <p>Selected: {open.title}</p>}
</>
)
}locale(default'en-US') formats dates and names;weekStartsOn(ISO: Monday is 1) defaults to the locale's week;formatstakesIntl.DateTimeFormatoptions per part (here 24-hour times).navLinksmakes day headers link to the day view;nowIndicatordraws the current time;scrollTimeis where the time grid starts scrolled;businessHoursshades the time outside Monday to Friday 09:00–17:00 (or the hours you give).linkComponentrenders event links with your router's link (Next'sLinkhere).onEventClickhears clicks and Enter on events. For an event withhref,args.preventDefault()keeps the browser on the page; a modified click (isModified) is always left to the browser, so "open in a new tab" keeps working.
Every prop is listed in the HapioCalendar reference.
Next steps
- Make it interactive: selection, editing, drag-in.
- Load events per visible range: Events and data.
- Keep the view and date in the URL: Navigation.
- Change how it looks: Theming and Customization.