Data
EventCalendar
Event calendar with month, week and day views — multi-day strips with a +N more overflow, overlapping time-grid blocks, a now indicator, slot and event callbacks and a localized toolbar. No date library.
Import
import { EventCalendar } from "@zephora/react";Examples
Month view
`maxEventsPerCell` caps the strips per day cell before the `+N more` button takes over. Pass a fixed `defaultDate` (and fixed event dates) so server-rendered output stays deterministic.
SunMonTueWedThuFriSat
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
const base = new Date(2026, 6, 1); // fixed month — keeps SSR deterministic
const july = (day: number, hour = 0, minute = 0) => new Date(2026, 6, day, hour, minute);
const events: CalendarEvent[] = [
{ id: "e1", title: "Sprint planning", start: july(6, 9), end: july(6, 10, 30) },
{ id: "e2", title: "Design review", start: july(8, 13), end: july(8, 14) },
{ id: "e3", title: "Standup", start: july(10, 9), end: july(10, 9, 15) },
{ id: "e4", title: "Release 2.4", start: july(10, 11), end: july(10, 12), color: "#16a34a" },
{ id: "e5", title: "1:1 with Ada", start: july(10, 14), end: july(10, 14, 30) },
{ id: "e6", title: "Retro", start: july(10, 15), end: july(10, 16) },
{ id: "e7", title: "Conference", start: july(14), end: july(17), allDay: true, color: "#8b5cf6" },
{ id: "e8", title: "On-call week", start: july(20), end: july(25), allDay: true, color: "#f59e0b" },
{ id: "e9", title: "Marketing sync", start: july(21, 10), end: july(21, 11) },
{ id: "e10", title: "Demo day", start: july(24, 16), end: july(24, 17) },
];
<EventCalendar events={events} defaultDate={base} maxEventsPerCell={3} />Week view
Overlapping events share the day column side by side. `onSlotClick` fires with the clicked `slotDuration` slot — here it appends a new event.
5 Sun6 Mon7 Tue8 Wed9 Thu10 Fri11 Sat
All-day
function WeekPlanner() {
const [events, setEvents] = React.useState<CalendarEvent[]>(weekEvents);
return (
<EventCalendar
events={events}
defaultView="week"
defaultDate={new Date(2026, 6, 6)}
minTime={8 * 60}
maxTime={19 * 60}
slotDuration={30}
showNowIndicator={false}
onSlotClick={(start, end) =>
setEvents((prev) => [
...prev,
{ id: `new-${prev.length + 1}`, title: "New event", start, end },
])
}
/>
);
}Custom event render
`renderEvent` replaces the content inside every event chip — here a colored dot driven by the event `color`.
SunMonTueWedThuFriSat
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
<EventCalendar
events={events}
defaultDate={base}
renderEvent={(event) => (
<span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
<span
style={{
inlineSize: 8,
blockSize: 8,
borderRadius: "50%",
background: event.color ?? "#6366f1",
}}
/>
{event.title}
</span>
)}
/>API
EventCalendar props
| Prop | Type | Default | Description |
|---|---|---|---|
events * | CalendarEvent[] | — | Events to display. Order does not matter — layout sorts internally. |
view | "month" | "week" | "day" | — | Controlled active view. |
defaultView | EventCalendarView | "month" | Initial view in uncontrolled mode. |
onViewChange | (view: EventCalendarView) => void | — | Fires when the view switcher changes the view. |
views | EventCalendarView[] | ["month", "week", "day"] | Which views the switcher offers. |
date | Date | — | Controlled anchor date (any day inside the visible month / week / day). |
defaultDate | Date | today | Initial anchor date in uncontrolled mode. Pass a fixed date for deterministic SSR. |
onDateChange | (date: Date) => void | — | Fires when navigation changes the anchor date. |
firstDayOfWeek | number | from locale | First day of the week: 0 = Sunday, 1 = Monday… |
minTime | number | 0 | Time-grid window start, minutes since midnight. |
maxTime | number | 1440 | Time-grid window end, minutes since midnight. |
slotDuration | number | 30 | Time-grid slot step in minutes (gutter lines + `onSlotClick` granularity). |
maxEventsPerCell | number | 3 | Event strips per month cell before the "+N more" button takes over. |
showNowIndicator | boolean | true | Red "now" line in week / day views when today is visible. |
onEventClick | (event: CalendarEvent, e: MouseEvent) => void | — | Fires when an event chip is clicked. |
onSlotClick | (start: Date, end: Date, e: MouseEvent) => void | — | Empty slot click — a full day in month view, a `slotDuration` slot in week / day. |
renderEvent | (event, { view }) => ReactNode | — | Replaces the default content inside every event chip. |
renderDayCell | (date: Date, events: CalendarEvent[]) => ReactNode | — | Replaces the default day-number content of a month cell. |
locale | string | from config | BCP 47 locale for all Intl formatting. |
unstyled | boolean | false | Headless mode — skips Zephora styling. |
classNames | Partial<Record<EventCalendarSlot, string>> | — | Per-slot class overrides. |
CalendarEvent props
| Prop | Type | Default | Description |
|---|---|---|---|
id * | string | — | Unique event id. |
title * | ReactNode | — | Event title. |
start * | Date | — | Event start. |
end * | Date | — | Event end. Treated as exclusive at exact day boundaries (00:00 ends the previous day). |
allDay | boolean | — | Renders in the month strips / all-day row instead of the time grid. |
color | string | — | Accent color for the event chip (any CSS color). |
data | unknown | — | Arbitrary consumer payload, passed back through callbacks untouched. |
Keyboard
| Key | Action |
|---|---|
ArrowLeft / ArrowRight | Move day focus to the previous / next day in the month grid. |
ArrowUp / ArrowDown | Move day focus to the same weekday in the previous / next week. |
Home / End | Move day focus to the first / last day of the week. |
PageUp / PageDown | Go to the previous / next month. |
Enter / Space | Selects the focused day (fires `onSlotClick`). |
Escape | Closes an open "+N more" day overlay. |
Tab | Moves through the toolbar, event chips and more buttons. |