Calendar Month Cell
- Stable
- WCAG 2.2 evidence
- RTL
One day of a month view with its events: the date, the day's CalendarEvents and an optional
"+2 more" line. Its State (Default, Today, Selected, Weekend, Outside) comes from isToday, isSelected,
isOutsideMonth and the date itself, never from colour alone: Today is a filled circle plus aria-current, Selected is
a fill plus aria-selected, Outside is dimmed and inert (WCAG 1.4.1, 4.1.2). The cell has role="gridcell" and is named by its full date, formatted with Intl.DateTimeFormat in the provider's
locale (1.3.1). Render it inside an element with role="row" inside role="grid" (or pass another role). The events
and the date button are real focusable controls in the tab order; the arrow-key movement between the cells of a grid
belongs to the container that lays the grid out, as CalendarMonth does for date pickers. Consumer duties: the grid, its row and column order (the first day of the week follows the locale), time zones, and the
keyboard movement across cells if the grid should have it.
import { CalendarMonthCell } from "@nexera-ui/react";- 12
- examples
- 9
- props
- 5
- live controls
- 1
- platform
- 7
- WCAG criteria
- 1
- block uses it
Try every prop. Copy the code.
Change the props and the code updates. Check light and dark, LTR and RTL, and phone width.
import { CalendarMonthCell } from "@nexera-ui/react";
<CalendarMonthCell />Examples 11
The same examples as Storybook, rendered live. Open Code to copy one.
Today
Figma State=Today: the date in a brand circle, announced as "today" and exposed with aria-current.
import { CalendarDate } from "@internationalized/date";
import { CalendarEvent, CalendarMonthCell } from "@nexera-ui/react";
const thursday = new CalendarDate(2026, 10, 15);
const at = (iso: string) => new Date(iso);
const zone = "Asia/Karachi";
const defaultEvents = (
<>
<CalendarEvent
title="1:1 Sara and Ayesha"
start={at("2026-10-15T06:00:00Z")}
end={at("2026-10-15T06:30:00Z")}
timeZone={zone}
/>
<CalendarEvent
title="Annual leave"
type="out-of-office"
start={at("2026-10-15T00:00:00Z")}
timeZone={zone}
/>
</>
);
<CalendarMonthCell date={thursday} more="+2 more">
{defaultEvents}
</CalendarMonthCell>;
Selected
Figma State=Selected: the nav/active-bg fill and aria-selected; the "+2 more" line is off in Figma.
import { CalendarDate } from "@internationalized/date";
import { CalendarEvent, CalendarMonthCell } from "@nexera-ui/react";
const thursday = new CalendarDate(2026, 10, 15);
const at = (iso: string) => new Date(iso);
const zone = "Asia/Karachi";
const defaultEvents = (
<>
<CalendarEvent
title="1:1 Sara and Ayesha"
start={at("2026-10-15T06:00:00Z")}
end={at("2026-10-15T06:30:00Z")}
timeZone={zone}
/>
<CalendarEvent
title="Annual leave"
type="out-of-office"
start={at("2026-10-15T00:00:00Z")}
timeZone={zone}
/>
</>
);
<CalendarMonthCell date={thursday} more="+2 more" isSelected>
{defaultEvents}
</CalendarMonthCell>;
Weekend
Figma State=Weekend: the page fill, derived from the date (a Saturday).
import { CalendarDate } from "@internationalized/date";
import { CalendarEvent, CalendarMonthCell } from "@nexera-ui/react";
const at = (iso: string) => new Date(iso);
const zone = "Asia/Karachi";
<CalendarMonthCell date={new CalendarDate(2026, 10, 17)} more="+2 more">
{
<CalendarEvent
title="Focus time"
type="focus-time"
start={at("2026-10-17T04:00:00Z")}
timeZone={zone}
/>
}
</CalendarMonthCell>;
Outside
Figma State=Outside: a day of the next or previous month, dimmed and inert (aria-disabled).
import { CalendarDate } from "@internationalized/date";
import { CalendarEvent, CalendarMonthCell } from "@nexera-ui/react";
const at = (iso: string) => new Date(iso);
const zone = "Asia/Karachi";
const defaultEvents = (
<>
<CalendarEvent
title="1:1 Sara and Ayesha"
start={at("2026-10-15T06:00:00Z")}
end={at("2026-10-15T06:30:00Z")}
timeZone={zone}
/>
<CalendarEvent
title="Annual leave"
type="out-of-office"
start={at("2026-10-15T00:00:00Z")}
timeZone={zone}
/>
</>
);
<CalendarMonthCell date={new CalendarDate(2026, 9, 30)} more="+2 more" isOutsideMonth>
{defaultEvents}
</CalendarMonthCell>;
Pressable
onDatePress makes the number a button named by the full date, and onMorePress makes the overflow line a button.
import { CalendarDate } from "@internationalized/date";
import { CalendarEvent, CalendarMonthCell } from "@nexera-ui/react";
const thursday = new CalendarDate(2026, 10, 15);
const at = (iso: string) => new Date(iso);
const zone = "Asia/Karachi";
const defaultEvents = (
<>
<CalendarEvent
title="1:1 Sara and Ayesha"
start={at("2026-10-15T06:00:00Z")}
end={at("2026-10-15T06:30:00Z")}
timeZone={zone}
/>
<CalendarEvent
title="Annual leave"
type="out-of-office"
start={at("2026-10-15T00:00:00Z")}
timeZone={zone}
/>
</>
);
<CalendarMonthCell date={thursday} more="+2 more">
{defaultEvents}
</CalendarMonthCell>;
Month view
A month grid assembled from the cells (October 2026, Monday first, six weeks): today on the 14th, a selected day you can change with the date button, outside days, and events. The arrow-key movement between cells belongs to the grid container; here Tab walks the buttons.
import { CalendarDate } from "@internationalized/date";
import { useState } from "react";
import { CalendarEvent, CalendarMonthCell } from "@nexera-ui/react";
export function MonthView() {
const [selected, setSelected] = useState(15);
const first = new CalendarDate(2026, 9, 28);
const days = Array.from({ length: 42 }, (_, index) => first.add({ days: index }));
return (
<div role="grid" aria-label="October 2026" className="w-[73.5rem]">
{Array.from({ length: 6 }, (_, week) => (
<div key={week} role="row" className="grid grid-cols-7">
{days.slice(week * 7, week * 7 + 7).map((date) => {
const outside = date.month !== 10;
const isSelected = !outside && date.day === selected;
return (
<CalendarMonthCell
key={date.toString()}
date={date}
isToday={date.month === 10 && date.day === 14}
isSelected={isSelected}
isOutsideMonth={outside}
more={!outside && date.day === 15 ? "+2 more" : undefined}
onDatePress={
outside
? undefined
: () => {
setSelected(date.day);
}
}
>
{!outside && date.day === 15 ? defaultEvents : null}
{!outside && date.day === 20 ? (
<CalendarEvent title="Kashmir Day" type="all-day" />
) : null}
</CalendarMonthCell>
);
})}
</div>
))}
</div>
);
}
Many events
A busy day: the cell grows with its events instead of clipping them, and each long title truncates with the whole name kept.
import { CalendarDate } from "@internationalized/date";
import { CalendarEvent, CalendarMonthCell } from "@nexera-ui/react";
const thursday = new CalendarDate(2026, 10, 15);
const at = (iso: string) => new Date(iso);
const zone = "Asia/Karachi";
<CalendarMonthCell date={thursday} more="+2 more">
{Array.from({ length: 6 }, (_, index) => (
<CalendarEvent
key={index}
title={`Quarterly planning session ${String(index + 1)} with the whole payroll and benefits team`}
start={at(`2026-10-15T0${String(index + 3)}:00:00Z`)}
end={at(`2026-10-15T0${String(index + 3)}:30:00Z`)}
timeZone={zone}
/>
))}
</CalendarMonthCell>;
Right to left
Right-to-left (Arabic): the date circle and the events start at the right edge, and the digits are Arabic.
import { CalendarDate } from "@internationalized/date";
import { CalendarEvent, CalendarMonthCell } from "@nexera-ui/react";
const thursday = new CalendarDate(2026, 10, 15);
<CalendarMonthCell date={thursday} more="+٢ أخرى" isToday>
{
<>
<CalendarEvent title="اجتماع فريق الرواتب" time="٩:٠٠ ص" labels={{ to: "إلى" }} />
<CalendarEvent title="إجازة" type="out-of-office" labels={{ outOfOffice: "خارج المكتب" }} />
</>
}
</CalendarMonthCell>;
Narrow container
The cell never overflows a narrow column: the events truncate and a cell with many events grows instead of clipping them.
import { CalendarDate } from "@internationalized/date";
import { CalendarEvent, CalendarMonthCell } from "@nexera-ui/react";
const thursday = new CalendarDate(2026, 10, 15);
const at = (iso: string) => new Date(iso);
const zone = "Asia/Karachi";
const defaultEvents = (
<>
<CalendarEvent
title="1:1 Sara and Ayesha"
start={at("2026-10-15T06:00:00Z")}
end={at("2026-10-15T06:30:00Z")}
timeZone={zone}
/>
<CalendarEvent
title="Annual leave"
type="out-of-office"
start={at("2026-10-15T00:00:00Z")}
timeZone={zone}
/>
</>
);
<CalendarMonthCell date={thursday} more="+2 more" isToday>
{defaultEvents}
</CalendarMonthCell>;
Full calendar week
import { CalendarDate } from "@internationalized/date";
import { useState } from "react";
import {
Button,
CalendarDayHeader,
CalendarEvent,
CalendarMonth,
CalendarMonthCell,
Checkbox,
Input,
Modal,
Segment,
SegmentedControl,
} from "@nexera-ui/react";
const at = (iso: string) => new Date(iso);
const monthExampleEvents: Record<number, string[]> = {
"1": ["Payroll cut-off"],
"5": ["Kashmir Day", "Ops review"],
"9": ["Board meeting"],
"13": ["Interview: Mehak", "Design review"],
"14": ["Hina Raza: first day", "Town hall"],
"15": ["1:1 Sara and Ayesha", "Interview: Zara", "+3 more"],
"16": ["Ayesha: leave"],
"17": ["Ayesha: leave"],
"20": ["Focus time"],
"22": ["Security training", "Team lunch"],
"27": ["Performance reviews"],
"30": ["Payroll run", "Payslips sent", "+1 more"],
"31": ["Quarter close"],
};
const weekExampleEvents = [
...[0, 1, 2, 3, 4].map((day) => ({
title: "Daily standup",
day,
x: 59 + 133 * day,
y: 73,
w: 127,
h: 22,
time: "09:30 to 09:45",
type: "timed" as const,
})),
{
title: "Payroll review",
day: 0,
x: 59,
y: 145,
w: 127,
h: 46,
time: "11:00 to 12:00",
type: "timed" as const,
},
{
title: "Focus time",
day: 0,
x: 59,
y: 289,
w: 127,
h: 94,
time: "14:00 to 16:00",
type: "focus-time" as const,
},
{
title: "Interview: Mehak Ali",
day: 1,
x: 192,
y: 121,
w: 127,
h: 34,
time: "10:30 to 11:15",
type: "tentative" as const,
},
{
title: "Design review",
day: 1,
x: 192,
y: 289,
w: 127,
h: 46,
time: "14:00 to 15:00",
type: "tentative" as const,
},
{
title: "Quarterly town hall",
day: 2,
x: 325,
y: 337,
w: 127,
h: 46,
time: "15:00 to 16:00",
type: "timed" as const,
},
{
title: "Lunch with new joiners",
day: 2,
x: 325,
y: 241,
w: 127,
h: 46,
time: "13:00 to 14:00",
type: "timed" as const,
},
{
title: "Interview: Zara Siddiqui",
day: 3,
x: 458,
y: 121,
w: 63.5,
h: 34,
time: "10:30 to 11:15",
type: "timed" as const,
},
{
title: "1:1 Sara and Ayesha",
day: 3,
x: 521.5,
y: 145,
w: 63.5,
h: 22,
time: "11:00 to 11:30",
type: "timed" as const,
},
{
title: "Vendor demo",
day: 3,
x: 458,
y: 385,
w: 127,
h: 22,
time: "16:00 to 16:30",
type: "declined" as const,
},
{
title: "Write Q4 plan",
day: 3,
x: 458,
y: 265,
w: 127,
h: 70,
time: "13:30 to 15:00",
type: "focus-time" as const,
},
{
title: "Leadership sync",
day: 4,
x: 591,
y: 193,
w: 127,
h: 46,
time: "12:00 to 13:00",
type: "timed" as const,
},
];
function FullCalendarExample({ initialView }: { initialView: "week" | "month" }) {
const [view, setView] = useState<string>(initialView);
const [date, setDate] = useState(new CalendarDate(2026, 10, initialView === "week" ? 15 : 22));
const [query, setQuery] = useState("");
const [detail, setDetail] = useState<string | null>(null);
const [newTitle, setNewTitle] = useState("");
const [createdEvents, setCreatedEvents] = useState<{ title: string; date: string }[]>([]);
const month = new Intl.DateTimeFormat("en-US", {
month: "long",
year: "numeric",
timeZone: "UTC",
}).format(date.toDate("UTC"));
const start = new CalendarDate(date.year, date.month, 1);
const offset = (start.toDate("UTC").getUTCDay() + 6) % 7;
const first = start.subtract({ days: offset });
const rowCount = Math.ceil((offset + start.calendar.getDaysInMonth(start)) / 7);
const dates = Array.from({ length: rowCount * 7 }, (_, i) => first.add({ days: i }));
const weekStart = date.subtract({ days: (date.toDate("UTC").getUTCDay() + 6) % 7 });
const shownWeek = weekStart;
const event = (title: string, day: CalendarDate) => (
<CalendarEvent
key={title}
title={title}
start={day.toDate("Asia/Karachi")}
timeZone="Asia/Karachi"
style={{
borderWidth: /Interview: Mehak|Design review/.test(title) ? 1 : 0,
borderInlineStartWidth: /Interview: Mehak|Design review/.test(title) ? 1 : 3,
}}
type={
/leave/i.test(title)
? "out-of-office"
: /Focus|Quarter close/.test(title)
? "focus-time"
: /first day|Kashmir/.test(title)
? "all-day"
: /Interview: Mehak|Design review/.test(title)
? "tentative"
: "timed"
}
onPress={() => {
setDetail(title);
}}
/>
);
const toolbar = (
<div
className={`box-border flex items-center gap-2 ${view === "month" ? "h-9" : "h-[61px] border-b border-default px-4"}`}
>
<Button
variant="secondary"
onPress={() => {
setDate(new CalendarDate(2026, 10, 15));
}}
>
Today
</Button>
<Button
variant="ghost"
aria-label="Previous period"
onPress={() => {
setDate(view === "month" ? date.subtract({ months: 1 }) : date.subtract({ days: 7 }));
}}
>
‹
</Button>
<Button
variant="ghost"
aria-label="Next period"
onPress={() => {
setDate(view === "month" ? date.add({ months: 1 }) : date.add({ days: 7 }));
}}
>
›
</Button>
<h4 className="m-0 flex-1 text-base font-semibold">
{view === "week" ? `${shownWeek.day} to ${shownWeek.add({ days: 6 }).day} ${month}` : month}
</h4>
<SegmentedControl
aria-label="Calendar view"
selectedKeys={[view]}
onSelectionChange={(keys) => {
setView(String([...keys][0]));
}}
>
<Segment id="day" label="Day" />
<Segment id="week" label="Week" />
<Segment id="month" label="Month" />
<Segment id="schedule" label="Schedule" />
</SegmentedControl>
</div>
);
return (
<section
aria-label="Full calendar"
className="box-border flex w-[1280px] flex-col gap-[14px] rounded-xl border border-default bg-surface p-6 text-primary"
>
<div className="flex flex-col gap-1">
<h3 className="m-0 text-base font-semibold leading-6">
{view === "month"
? "Month view"
: view === "week"
? "Week view"
: view === "day"
? "Day view"
: "Schedule view"}
</h3>
<p className="m-0 text-[13px] leading-[18px] text-muted">
{view === "month"
? 'Holidays, leave and meetings at a glance; "+N more" opens the day. Weekends are tinted.'
: "The main scheduling screen: toolbar, mini month and calendars on the left, all-day row, time grid with overlapping events and a current-time line."}
</p>
</div>
{view === "month" && toolbar}
<div
className={`flex overflow-hidden ${view === "month" ? "" : "rounded-xl border border-default"}`}
>
{view === "week" && (
<aside
className="box-border flex w-[232px] shrink-0 flex-col gap-4 border-e border-default p-4"
aria-label="Calendar sidebar"
>
<Button
variant="ghost"
className="self-start rounded-[16px] bg-nav-active-bg px-[18px] text-brand-text shadow-sm"
style={{ minHeight: 44 }}
onPress={() => {
setDetail("New event");
}}
>
+ Create
</Button>
<CalendarMonth
selectionMode="single"
aria-label="Choose date"
value={date}
onChange={(value) => {
setDate(new CalendarDate(value.year, value.month, value.day));
}}
className="w-full [&_td_[role=button]]:h-[26px] [&_td_[role=button]]:rounded-full [&_td_[role=button]]:text-xs [&_th]:h-4 [&_th]:text-xs [&_h2]:text-xs [&_button]:size-6"
style={{ padding: 0 }}
/>
<h4 className="m-0 text-sm font-semibold">My calendars</h4>
{[
["Sara Malik", "#2563eb"],
["Interviews", "#db2777"],
["Team leave", "#7c3aed"],
["Public holidays (Pakistan)", "#16a34a"],
["Focus time", "#047857"],
].map(([label, color]) => (
<Checkbox key={label} defaultSelected={label !== "Focus time"}>
<span className="flex items-center gap-2">
<span className="size-2 rounded-full" style={{ background: color }} />
{label}
</span>
</Checkbox>
))}
</aside>
)}
<div className="min-w-0 flex-1">
{view !== "month" && toolbar}
{view === "month" ? (
<div
role="grid"
tabIndex={0}
onKeyDown={(event) => {
const buttons = [
...event.currentTarget.querySelectorAll<HTMLButtonElement>(
'[role="gridcell"] > button',
),
];
const index = buttons.findIndex((button) => button === event.target);
const movement: Record<string, number> = {
ArrowLeft: -1,
ArrowRight: 1,
ArrowUp: -7,
ArrowDown: 7,
Home: -(index % 7),
End: 6 - (index % 7),
};
const offset = movement[event.key];
if (index >= 0 && offset !== undefined) {
event.preventDefault();
buttons[Math.max(0, Math.min(buttons.length - 1, index + offset))]?.focus();
}
}}
aria-label={month}
className="flex flex-col overflow-hidden rounded-[10px] border border-default"
>
<div role="row" className="grid grid-cols-7 bg-sunken">
{["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"].map((d) => (
<div role="columnheader" key={d} className="p-2 text-xs text-muted">
{d}
</div>
))}
</div>
{Array.from({ length: rowCount }, (_, row) => (
<div role="row" key={row} className="grid grid-cols-7">
{dates.slice(row * 7, row * 7 + 7).map((day) => (
<CalendarMonthCell
key={day.toString()}
date={day}
isToday={day.toString() === "2026-10-15"}
isSelected={day.compare(date) === 0}
isOutsideMonth={day.month !== date.month}
onDatePress={() => {
setDate(day);
}}
more={
day.month === 10 && day.year === 2026
? monthExampleEvents[day.day]?.find((t) => t.startsWith("+"))
: undefined
}
onMorePress={() => {
setDate(day);
setView("day");
}}
>
{day.month === 10 &&
day.year === 2026 &&
(monthExampleEvents[day.day] ?? [])
.filter(
(t) =>
!t.startsWith("+") && t.toLowerCase().includes(query.toLowerCase()),
)
.map((t) => event(t, day))}
{createdEvents
.filter((item) => item.date === day.toString())
.map((item) => event(item.title, day))}
</CalendarMonthCell>
))}
</div>
))}
</div>
) : view === "week" ? (
<>
<div className="flex h-[70px] ps-14">
{Array.from({ length: 7 }, (_, i) => (
<CalendarDayHeader
key={i}
date={shownWeek.add({ days: i })}
isToday={shownWeek.add({ days: i }).toString() === "2026-10-15"}
className="flex-1"
/>
))}
</div>
<div className="box-border relative h-8 border-y border-default">
<span className="absolute start-1 top-2 text-[11px] text-muted">All day</span>
<CalendarEvent
title="Hina Raza: first day"
type="all-day"
className="absolute left-[325px] top-[5px] w-[127px]"
start={shownWeek.add({ days: 2 }).toDate("UTC")}
onPress={() => {
setDetail("Hina Raza: first day");
}}
/>
<CalendarEvent
title="Ayesha Khan: annual leave"
type="out-of-office"
className="absolute left-[591px] top-[5px] w-[260px]"
start={shownWeek.add({ days: 4 }).toDate("UTC")}
onPress={() => {
setDetail("Ayesha Khan: annual leave");
}}
/>
</div>
<div className="relative h-[528px] w-[987px]" aria-label="Week events">
{Array.from({ length: 11 }, (_, i) => (
<div
key={i}
className="absolute start-0 flex h-px w-full items-start"
style={{ top: i * 48 }}
>
<span className="relative -top-2 w-14 text-center text-xs text-muted">
{i > 0 ? `${String(8 + i).padStart(2, "0")}:00` : ""}
</span>
<span className="h-px flex-1 bg-border-subtle" />
</div>
))}
{Array.from({ length: 8 }, (_, i) => (
<div
key={i}
className="absolute top-0 h-full w-px bg-border-subtle"
style={{ left: 56 + i * 133 }}
/>
))}
{weekExampleEvents
.filter((e) => e.title.toLowerCase().includes(query.toLowerCase()))
.map((e, i) => (
<CalendarEvent
key={i}
title={e.title}
type={e.type}
layout="block"
time={e.h > 40 ? e.time : ""}
start={shownWeek.add({ days: e.day }).toDate("Asia/Karachi")}
timeZone="Asia/Karachi"
onPress={() => {
setDetail(`${e.title} · ${e.time}`);
}}
className="absolute"
style={{
left: e.x,
top: e.y,
width: e.w,
height: e.h,
minHeight: e.h,
borderWidth: e.type === "tentative" ? 1.2 : 0,
borderInlineStartWidth: e.type === "tentative" ? 1.2 : 3,
paddingTop: 3,
paddingBottom: 3,
}}
/>
))}
{createdEvents
.filter((item) => item.date === date.toString())
.map((item, index) => (
<CalendarEvent
key={`${item.date}-${index}`}
title={item.title}
className="absolute left-[458px] top-[433px] w-[127px]"
onPress={() => {
setDetail(item.title);
}}
/>
))}
<div
className="absolute left-[455px] top-[115px] h-[2px] w-[133px] bg-status-danger-solid"
role="img"
aria-label="Current time"
/>
</div>
</>
) : (
<div className="flex min-h-[528px] flex-col gap-3 p-4">
<Input
aria-label="Filter events"
placeholder="Search events"
value={query}
onChange={setQuery}
/>
{weekExampleEvents
.filter(
(e) =>
e.title.toLowerCase().includes(query.toLowerCase()) &&
(view !== "day" || e.day === 3),
)
.map((e, i) => (
<CalendarEvent
key={i}
title={e.title}
type={e.type}
layout="block"
time={e.time}
start={shownWeek.add({ days: e.day }).toDate("Asia/Karachi")}
onPress={() => {
setDetail(`${e.title} · ${e.time}`);
}}
/>
))}
</div>
)}
</div>
</div>
<Modal
title={detail ?? "Event details"}
isOpen={detail !== null}
onOpenChange={(open) => {
if (!open) setDetail(null);
}}
>
<p className="text-sm text-muted">
{detail === "New event"
? "Create an event for your selected date."
: `Event on ${date.toString()}. Times use Asia/Karachi.`}
</p>
{detail === "New event" && (
<>
<Input
label="Event title"
placeholder="Add a title"
value={newTitle}
onChange={setNewTitle}
/>
<Button
isDisabled={!newTitle.trim()}
onPress={() => {
setCreatedEvents((previous) => [
...previous,
{ title: newTitle.trim(), date: date.toString() },
]);
setNewTitle("");
setDetail(null);
}}
>
Create event
</Button>
</>
)}
<Button
variant="secondary"
onPress={() => {
setDetail(null);
}}
>
Close
</Button>
</Modal>
</section>
);
}
export function FullCalendarWeek() {
return <FullCalendarExample initialView="week" />;
}
Full calendar month
import { CalendarDate } from "@internationalized/date";
import { useState } from "react";
import {
Button,
CalendarDayHeader,
CalendarEvent,
CalendarMonth,
CalendarMonthCell,
Checkbox,
Input,
Modal,
Segment,
SegmentedControl,
} from "@nexera-ui/react";
const at = (iso: string) => new Date(iso);
const monthExampleEvents: Record<number, string[]> = {
"1": ["Payroll cut-off"],
"5": ["Kashmir Day", "Ops review"],
"9": ["Board meeting"],
"13": ["Interview: Mehak", "Design review"],
"14": ["Hina Raza: first day", "Town hall"],
"15": ["1:1 Sara and Ayesha", "Interview: Zara", "+3 more"],
"16": ["Ayesha: leave"],
"17": ["Ayesha: leave"],
"20": ["Focus time"],
"22": ["Security training", "Team lunch"],
"27": ["Performance reviews"],
"30": ["Payroll run", "Payslips sent", "+1 more"],
"31": ["Quarter close"],
};
const weekExampleEvents = [
...[0, 1, 2, 3, 4].map((day) => ({
title: "Daily standup",
day,
x: 59 + 133 * day,
y: 73,
w: 127,
h: 22,
time: "09:30 to 09:45",
type: "timed" as const,
})),
{
title: "Payroll review",
day: 0,
x: 59,
y: 145,
w: 127,
h: 46,
time: "11:00 to 12:00",
type: "timed" as const,
},
{
title: "Focus time",
day: 0,
x: 59,
y: 289,
w: 127,
h: 94,
time: "14:00 to 16:00",
type: "focus-time" as const,
},
{
title: "Interview: Mehak Ali",
day: 1,
x: 192,
y: 121,
w: 127,
h: 34,
time: "10:30 to 11:15",
type: "tentative" as const,
},
{
title: "Design review",
day: 1,
x: 192,
y: 289,
w: 127,
h: 46,
time: "14:00 to 15:00",
type: "tentative" as const,
},
{
title: "Quarterly town hall",
day: 2,
x: 325,
y: 337,
w: 127,
h: 46,
time: "15:00 to 16:00",
type: "timed" as const,
},
{
title: "Lunch with new joiners",
day: 2,
x: 325,
y: 241,
w: 127,
h: 46,
time: "13:00 to 14:00",
type: "timed" as const,
},
{
title: "Interview: Zara Siddiqui",
day: 3,
x: 458,
y: 121,
w: 63.5,
h: 34,
time: "10:30 to 11:15",
type: "timed" as const,
},
{
title: "1:1 Sara and Ayesha",
day: 3,
x: 521.5,
y: 145,
w: 63.5,
h: 22,
time: "11:00 to 11:30",
type: "timed" as const,
},
{
title: "Vendor demo",
day: 3,
x: 458,
y: 385,
w: 127,
h: 22,
time: "16:00 to 16:30",
type: "declined" as const,
},
{
title: "Write Q4 plan",
day: 3,
x: 458,
y: 265,
w: 127,
h: 70,
time: "13:30 to 15:00",
type: "focus-time" as const,
},
{
title: "Leadership sync",
day: 4,
x: 591,
y: 193,
w: 127,
h: 46,
time: "12:00 to 13:00",
type: "timed" as const,
},
];
function FullCalendarExample({ initialView }: { initialView: "week" | "month" }) {
const [view, setView] = useState<string>(initialView);
const [date, setDate] = useState(new CalendarDate(2026, 10, initialView === "week" ? 15 : 22));
const [query, setQuery] = useState("");
const [detail, setDetail] = useState<string | null>(null);
const [newTitle, setNewTitle] = useState("");
const [createdEvents, setCreatedEvents] = useState<{ title: string; date: string }[]>([]);
const month = new Intl.DateTimeFormat("en-US", {
month: "long",
year: "numeric",
timeZone: "UTC",
}).format(date.toDate("UTC"));
const start = new CalendarDate(date.year, date.month, 1);
const offset = (start.toDate("UTC").getUTCDay() + 6) % 7;
const first = start.subtract({ days: offset });
const rowCount = Math.ceil((offset + start.calendar.getDaysInMonth(start)) / 7);
const dates = Array.from({ length: rowCount * 7 }, (_, i) => first.add({ days: i }));
const weekStart = date.subtract({ days: (date.toDate("UTC").getUTCDay() + 6) % 7 });
const shownWeek = weekStart;
const event = (title: string, day: CalendarDate) => (
<CalendarEvent
key={title}
title={title}
start={day.toDate("Asia/Karachi")}
timeZone="Asia/Karachi"
style={{
borderWidth: /Interview: Mehak|Design review/.test(title) ? 1 : 0,
borderInlineStartWidth: /Interview: Mehak|Design review/.test(title) ? 1 : 3,
}}
type={
/leave/i.test(title)
? "out-of-office"
: /Focus|Quarter close/.test(title)
? "focus-time"
: /first day|Kashmir/.test(title)
? "all-day"
: /Interview: Mehak|Design review/.test(title)
? "tentative"
: "timed"
}
onPress={() => {
setDetail(title);
}}
/>
);
const toolbar = (
<div
className={`box-border flex items-center gap-2 ${view === "month" ? "h-9" : "h-[61px] border-b border-default px-4"}`}
>
<Button
variant="secondary"
onPress={() => {
setDate(new CalendarDate(2026, 10, 15));
}}
>
Today
</Button>
<Button
variant="ghost"
aria-label="Previous period"
onPress={() => {
setDate(view === "month" ? date.subtract({ months: 1 }) : date.subtract({ days: 7 }));
}}
>
‹
</Button>
<Button
variant="ghost"
aria-label="Next period"
onPress={() => {
setDate(view === "month" ? date.add({ months: 1 }) : date.add({ days: 7 }));
}}
>
›
</Button>
<h4 className="m-0 flex-1 text-base font-semibold">
{view === "week" ? `${shownWeek.day} to ${shownWeek.add({ days: 6 }).day} ${month}` : month}
</h4>
<SegmentedControl
aria-label="Calendar view"
selectedKeys={[view]}
onSelectionChange={(keys) => {
setView(String([...keys][0]));
}}
>
<Segment id="day" label="Day" />
<Segment id="week" label="Week" />
<Segment id="month" label="Month" />
<Segment id="schedule" label="Schedule" />
</SegmentedControl>
</div>
);
return (
<section
aria-label="Full calendar"
className="box-border flex w-[1280px] flex-col gap-[14px] rounded-xl border border-default bg-surface p-6 text-primary"
>
<div className="flex flex-col gap-1">
<h3 className="m-0 text-base font-semibold leading-6">
{view === "month"
? "Month view"
: view === "week"
? "Week view"
: view === "day"
? "Day view"
: "Schedule view"}
</h3>
<p className="m-0 text-[13px] leading-[18px] text-muted">
{view === "month"
? 'Holidays, leave and meetings at a glance; "+N more" opens the day. Weekends are tinted.'
: "The main scheduling screen: toolbar, mini month and calendars on the left, all-day row, time grid with overlapping events and a current-time line."}
</p>
</div>
{view === "month" && toolbar}
<div
className={`flex overflow-hidden ${view === "month" ? "" : "rounded-xl border border-default"}`}
>
{view === "week" && (
<aside
className="box-border flex w-[232px] shrink-0 flex-col gap-4 border-e border-default p-4"
aria-label="Calendar sidebar"
>
<Button
variant="ghost"
className="self-start rounded-[16px] bg-nav-active-bg px-[18px] text-brand-text shadow-sm"
style={{ minHeight: 44 }}
onPress={() => {
setDetail("New event");
}}
>
+ Create
</Button>
<CalendarMonth
selectionMode="single"
aria-label="Choose date"
value={date}
onChange={(value) => {
setDate(new CalendarDate(value.year, value.month, value.day));
}}
className="w-full [&_td_[role=button]]:h-[26px] [&_td_[role=button]]:rounded-full [&_td_[role=button]]:text-xs [&_th]:h-4 [&_th]:text-xs [&_h2]:text-xs [&_button]:size-6"
style={{ padding: 0 }}
/>
<h4 className="m-0 text-sm font-semibold">My calendars</h4>
{[
["Sara Malik", "#2563eb"],
["Interviews", "#db2777"],
["Team leave", "#7c3aed"],
["Public holidays (Pakistan)", "#16a34a"],
["Focus time", "#047857"],
].map(([label, color]) => (
<Checkbox key={label} defaultSelected={label !== "Focus time"}>
<span className="flex items-center gap-2">
<span className="size-2 rounded-full" style={{ background: color }} />
{label}
</span>
</Checkbox>
))}
</aside>
)}
<div className="min-w-0 flex-1">
{view !== "month" && toolbar}
{view === "month" ? (
<div
role="grid"
tabIndex={0}
onKeyDown={(event) => {
const buttons = [
...event.currentTarget.querySelectorAll<HTMLButtonElement>(
'[role="gridcell"] > button',
),
];
const index = buttons.findIndex((button) => button === event.target);
const movement: Record<string, number> = {
ArrowLeft: -1,
ArrowRight: 1,
ArrowUp: -7,
ArrowDown: 7,
Home: -(index % 7),
End: 6 - (index % 7),
};
const offset = movement[event.key];
if (index >= 0 && offset !== undefined) {
event.preventDefault();
buttons[Math.max(0, Math.min(buttons.length - 1, index + offset))]?.focus();
}
}}
aria-label={month}
className="flex flex-col overflow-hidden rounded-[10px] border border-default"
>
<div role="row" className="grid grid-cols-7 bg-sunken">
{["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"].map((d) => (
<div role="columnheader" key={d} className="p-2 text-xs text-muted">
{d}
</div>
))}
</div>
{Array.from({ length: rowCount }, (_, row) => (
<div role="row" key={row} className="grid grid-cols-7">
{dates.slice(row * 7, row * 7 + 7).map((day) => (
<CalendarMonthCell
key={day.toString()}
date={day}
isToday={day.toString() === "2026-10-15"}
isSelected={day.compare(date) === 0}
isOutsideMonth={day.month !== date.month}
onDatePress={() => {
setDate(day);
}}
more={
day.month === 10 && day.year === 2026
? monthExampleEvents[day.day]?.find((t) => t.startsWith("+"))
: undefined
}
onMorePress={() => {
setDate(day);
setView("day");
}}
>
{day.month === 10 &&
day.year === 2026 &&
(monthExampleEvents[day.day] ?? [])
.filter(
(t) =>
!t.startsWith("+") && t.toLowerCase().includes(query.toLowerCase()),
)
.map((t) => event(t, day))}
{createdEvents
.filter((item) => item.date === day.toString())
.map((item) => event(item.title, day))}
</CalendarMonthCell>
))}
</div>
))}
</div>
) : view === "week" ? (
<>
<div className="flex h-[70px] ps-14">
{Array.from({ length: 7 }, (_, i) => (
<CalendarDayHeader
key={i}
date={shownWeek.add({ days: i })}
isToday={shownWeek.add({ days: i }).toString() === "2026-10-15"}
className="flex-1"
/>
))}
</div>
<div className="box-border relative h-8 border-y border-default">
<span className="absolute start-1 top-2 text-[11px] text-muted">All day</span>
<CalendarEvent
title="Hina Raza: first day"
type="all-day"
className="absolute left-[325px] top-[5px] w-[127px]"
start={shownWeek.add({ days: 2 }).toDate("UTC")}
onPress={() => {
setDetail("Hina Raza: first day");
}}
/>
<CalendarEvent
title="Ayesha Khan: annual leave"
type="out-of-office"
className="absolute left-[591px] top-[5px] w-[260px]"
start={shownWeek.add({ days: 4 }).toDate("UTC")}
onPress={() => {
setDetail("Ayesha Khan: annual leave");
}}
/>
</div>
<div className="relative h-[528px] w-[987px]" aria-label="Week events">
{Array.from({ length: 11 }, (_, i) => (
<div
key={i}
className="absolute start-0 flex h-px w-full items-start"
style={{ top: i * 48 }}
>
<span className="relative -top-2 w-14 text-center text-xs text-muted">
{i > 0 ? `${String(8 + i).padStart(2, "0")}:00` : ""}
</span>
<span className="h-px flex-1 bg-border-subtle" />
</div>
))}
{Array.from({ length: 8 }, (_, i) => (
<div
key={i}
className="absolute top-0 h-full w-px bg-border-subtle"
style={{ left: 56 + i * 133 }}
/>
))}
{weekExampleEvents
.filter((e) => e.title.toLowerCase().includes(query.toLowerCase()))
.map((e, i) => (
<CalendarEvent
key={i}
title={e.title}
type={e.type}
layout="block"
time={e.h > 40 ? e.time : ""}
start={shownWeek.add({ days: e.day }).toDate("Asia/Karachi")}
timeZone="Asia/Karachi"
onPress={() => {
setDetail(`${e.title} · ${e.time}`);
}}
className="absolute"
style={{
left: e.x,
top: e.y,
width: e.w,
height: e.h,
minHeight: e.h,
borderWidth: e.type === "tentative" ? 1.2 : 0,
borderInlineStartWidth: e.type === "tentative" ? 1.2 : 3,
paddingTop: 3,
paddingBottom: 3,
}}
/>
))}
{createdEvents
.filter((item) => item.date === date.toString())
.map((item, index) => (
<CalendarEvent
key={`${item.date}-${index}`}
title={item.title}
className="absolute left-[458px] top-[433px] w-[127px]"
onPress={() => {
setDetail(item.title);
}}
/>
))}
<div
className="absolute left-[455px] top-[115px] h-[2px] w-[133px] bg-status-danger-solid"
role="img"
aria-label="Current time"
/>
</div>
</>
) : (
<div className="flex min-h-[528px] flex-col gap-3 p-4">
<Input
aria-label="Filter events"
placeholder="Search events"
value={query}
onChange={setQuery}
/>
{weekExampleEvents
.filter(
(e) =>
e.title.toLowerCase().includes(query.toLowerCase()) &&
(view !== "day" || e.day === 3),
)
.map((e, i) => (
<CalendarEvent
key={i}
title={e.title}
type={e.type}
layout="block"
time={e.time}
start={shownWeek.add({ days: e.day }).toDate("Asia/Karachi")}
onPress={() => {
setDetail(`${e.title} · ${e.time}`);
}}
/>
))}
</div>
)}
</div>
</div>
<Modal
title={detail ?? "Event details"}
isOpen={detail !== null}
onOpenChange={(open) => {
if (!open) setDetail(null);
}}
>
<p className="text-sm text-muted">
{detail === "New event"
? "Create an event for your selected date."
: `Event on ${date.toString()}. Times use Asia/Karachi.`}
</p>
{detail === "New event" && (
<>
<Input
label="Event title"
placeholder="Add a title"
value={newTitle}
onChange={setNewTitle}
/>
<Button
isDisabled={!newTitle.trim()}
onPress={() => {
setCreatedEvents((previous) => [
...previous,
{ title: newTitle.trim(), date: date.toString() },
]);
setNewTitle("");
setDetail(null);
}}
>
Create event
</Button>
</>
)}
<Button
variant="secondary"
onPress={() => {
setDetail(null);
}}
>
Close
</Button>
</Modal>
</section>
);
}
export function FullCalendarMonth() {
return <FullCalendarExample initialView="month" />;
}
Props 9
Press "Try it" on a card to load that prop into the playground.
9 props shown
date*NexeraDateValueThe day of the cell (an @internationalized/date CalendarDate, for example parseDate("2026-10-15")). The visible
number is this date formatted for the locale, with its digits, and the cell is named
by the full date ("Thursday, October 15, 2026").
childrenNexeraReactNodeThe events of the day: CalendarEvents in the compact layout. Each is its own
focusable button or link.
moreNexeraReactNodeText of the overflow line. Shown when provided. Plain text
unless onMorePress is set. Write what is hidden ("+2 more events"): it is read with the full date after it.
onMorePressNexera((event: PressEvent) => void)Makes the more line a button and is called when it is pressed (for example to open the day).
onDatePressNexera((event: PressEvent) => void)Makes the day number a button and is called when it is pressed (for example to open the day view). It is named by the full date.
isTodayNexerabooleanThe cell holds today's date: a filled brand circle around the number, and aria-current="date".
Not derived from the clock on purpose (it depends on the viewer's time zone and a server render would freeze it):
pass isToday(date, getLocalTimeZone()) from @internationalized/date.
isSelectedNexerabooleanThe cell is the selected day: nav/active-bg fill and aria-selected="true". When set (to
true or false) every cell of the grid reports its selection; leave it out in a grid that has none.
isOutsideMonthNexerabooleanThe day belongs to the previous or next month: the cell takes the page fill and a dimmed number.
An outside cell without events and without a pressable date is inert and gets aria-disabled="true".
isWeekendNexerabooleanThe day is a weekend day: bg/page fill. Derived from date and the locale (Saturday and
Sunday in en-US, Friday and Saturday in ar-EG); pass it to follow your own work week.
* Required. React Aria props shown are the ones most apps use; the component accepts the rest of its React Aria props too.
Accessible by default.
Built on React Aria, and covered by the WCAG 2.2 evidence generated on every build.
WCAG 2.2 evidence
3 direct · 4 supporting- 1.3.1Info and RelationshipsLevel A · tested directly
- 1.4.1Use of ColorLevel A · supporting test
- 1.4.4Resize TextLevel AA · supporting test
- 1.4.10ReflowLevel AA · supporting test
- 1.4.12Text SpacingLevel AA · supporting test
- 2.1.1KeyboardLevel A · tested directly
- 4.1.2Name, Role, ValueLevel A · tested directly
Fits any width.
Nexera components respond to the space they are given. Drag the corner of the frame, or pick a width.
Styling hooks
Pass className to add Tailwind classes (merged last). State is exposed as data attributes, so you can style it with variants like data-pressed:.
data-hovereddata-pressed
<CalendarMonthCell className="data-hovered:opacity-90 shadow-sm" />Used in blocks
Related components
- AccordionItemOne collapsible section of an FAQ or settings page.
- AreaChartA stacked area chart: layers piled up over time with the title, range switch, actions and legend of the card.
- AvailabilityRowOne person's availability across a window of the day: an avatar with initials and the name, and a timeline with the busy periods drawn as bars.
- AvatarA person or organisation shown as a photo, initials or an icon.
- AvatarGroupA row of overlapping avatars with a "+N" chip for the rest.
- AvatarLabelAn avatar with a name and an optional subtitle beside it, for a person in a list, a header or a mention.
- BadgeShort status or category label: `Tone` x `Variant` x `Size`, with an optional dot, leading icon and remove button.
- BadgeGroupA pill that pairs a small badge with a short message, for an announcement or a status line.