FullCalendar
freeFullCalendar renders day, week, month, year, and agenda views over one event list, with built-in drag-to-reschedule and resize-to-adjust-duration.
dayjsmotion
Preview
Dark
August 2026
Sun
Mon
Tue
Wed
Thu
Fri
Sat
26
27
28
29
30
31
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
2
3
4
5
Investor Update00:00
Sprint Planning09:00
Design Review14:00
Product Launch09:00
import { useState } from 'react';
import FullCalendar from '@/components/composites/FullCalendar';
import type { FullCalendarEvent } from '@/components/composites/FullCalendar';
const now = new Date();
const day = (n: number, hour = 0, minute = 0) =>
new Date(now.getFullYear(), now.getMonth(), n, hour, minute).toISOString();
const initialEvents: FullCalendarEvent[] = [
{
id: 1,
title: 'Sprint Planning',
startDate: day(3, 9, 0),
endDate: day(3, 10, 0),
color: 'blue',
description: 'Plan the next two-week sprint.',
type: 'meeting',
},
{
id: 2,
title: 'Design Review',
startDate: day(8, 14, 0),
endDate: day(8, 15, 0),
color: 'purple',
description: 'Review the new onboarding flow.',
type: 'review',
},
{
id: 3,
title: 'Investor Update',
startDate: day(12),
endDate: day(14),
color: 'orange',
description: 'Quarterly investor progress update.',
type: 'deadline',
},
{
id: 4,
title: 'Product Launch',
startDate: day(20, 9, 0),
endDate: day(20, 9, 30),
color: 'green',
description: 'Ship the v2 release.',
type: 'launch',
},
];
export default function UsageDemo() {
const [events, setEvents] = useState(initialEvents);
return (
<div className="h-145 w-full">
<FullCalendar
fillHeight
events={events}
onChange={(updatedEvents) => setEvents(updatedEvents)}
/>
</div>
);
}Installation
Add this component with the NateUI CLI.
npx nateui@latest add FullCalendarExamples
Custom Header
Preview
Dark
August 2026
Sun
Mon
Tue
Wed
Thu
Fri
Sat
26
27
28
29
30
31
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
2
3
4
5
Customer Call11:00
Board Meeting13:00
import { useState } from 'react';
import dayjs from 'dayjs';
import Button from '@/components/ui/Button';
import Select from '@/components/ui/Select';
import FullCalendar from '@/components/composites/FullCalendar';
import { PiArrowLeft, PiArrowRight } from 'react-icons/pi'
import type {
CalendarView,
FullCalendarEvent,
} from '@/components/composites/FullCalendar';
const now = new Date();
const day = (n: number, hour = 0, minute = 0) =>
new Date(now.getFullYear(), now.getMonth(), n, hour, minute).toISOString();
const initialEvents: FullCalendarEvent[] = [
{
id: 1,
title: 'Customer Call',
startDate: day(6, 11, 0),
endDate: day(6, 11, 30),
color: 'blue',
description: 'Renewal discussion.',
type: 'meeting',
},
{
id: 2,
title: 'Board Meeting',
startDate: day(16, 13, 0),
endDate: day(16, 15, 0),
color: 'purple',
description: 'Monthly board update.',
type: 'meeting',
},
];
const viewOptions: { label: string; value: CalendarView }[] = [
{ label: 'Day', value: 'day' },
{ label: 'Week', value: 'week' },
{ label: 'Month', value: 'month' },
{ label: 'Year', value: 'year' },
{ label: 'Agenda', value: 'agenda' },
];
export default function CustomHeaderDemo() {
const [events, setEvents] = useState(initialEvents);
return (
<div className="h-145 w-full">
<FullCalendar
fillHeight
events={events}
onChange={(updatedEvents) => setEvents(updatedEvents)}
renderHeaderStart={({ selectedDate }) => (
<h5>{dayjs(selectedDate).format('MMMM YYYY')}</h5>
)}
renderHeaderEnd={({
handlePrevious,
handleNext,
setSelectedDate,
view,
setView,
}) => (
<div className="flex items-center gap-2">
<Button
icon={<PiArrowLeft />}
onClick={handlePrevious}
className="text-sm"
/>
<Button onClick={() => setSelectedDate(new Date())}>Today</Button>
<Button
icon={<PiArrowRight />}
onClick={handleNext}
className="text-sm"
/>
<Select
className="w-28"
options={viewOptions}
value={viewOptions.find((option) => option.value === view)}
onChange={(selected) => setView(selected.value)}
/>
</div>
)}
/>
</div>
);
}Resizable Duration
Preview
Dark
August 2026
30 Sun
| Mo | Tu | We | Th | Fr | Sa | Su |
|---|---|---|---|---|---|---|
import { useState } from 'react';
import FullCalendar from '@/components/composites/FullCalendar';
import type { FullCalendarEvent } from '@/components/composites/FullCalendar';
const now = new Date();
const day = (n: number, hour = 0, minute = 0) =>
new Date(now.getFullYear(), now.getMonth(), n, hour, minute).toISOString();
const initialEvents: FullCalendarEvent[] = [
{
id: 1,
title: 'Focus Block',
startDate: day(now.getDate(), 9, 0),
endDate: day(now.getDate(), 11, 0),
color: 'green',
description: 'Deep work, no meetings.',
type: 'focus',
},
{
id: 2,
title: 'Client Workshop',
startDate: day(now.getDate(), 14, 0),
endDate: day(now.getDate(), 15, 30),
color: 'purple',
description: 'Requirements workshop.',
type: 'meeting',
},
];
export default function ResizableDurationDemo() {
const [events, setEvents] = useState(initialEvents);
return (
<div className="h-145 w-full">
<FullCalendar
fillHeight
view="day"
events={events}
onChange={(updatedEvents) => setEvents(updatedEvents)}
/>
</div>
);
}Custom Day Sidebar
Preview
Dark
August 2026
30 Sun
import { useState } from 'react';
import dayjs from 'dayjs';
import Calendar from '@/components/ui/Calendar';
import FullCalendar from '@/components/composites/FullCalendar';
import type { FullCalendarEvent } from '@/components/composites/FullCalendar';
const now = new Date();
const initialEvents: FullCalendarEvent[] = [
{
id: 1,
title: 'Project Check-in',
startDate: new Date(now.getTime() - 30 * 60 * 1000).toISOString(),
endDate: new Date(now.getTime() + 30 * 60 * 1000).toISOString(),
color: 'blue',
description: 'Review current milestones and blockers.',
type: 'meeting',
},
];
export default function CustomDaySidebarDemo() {
const [events, setEvents] = useState(initialEvents);
return (
<div className="h-145 w-full">
<FullCalendar
fillHeight
view="day"
events={events}
onChange={(updatedEvents) => setEvents(updatedEvents)}
renderDayViewSidebar={({
selectedDate,
setSelectedDate,
currentEvents,
}) => (
<aside className="hidden w-80 shrink-0 flex-col border-l lg:flex">
<div className="border-b px-5 py-4">
<p className="text-sm text-muted-foreground">Selected date</p>
<p className="font-semibold">
{dayjs(selectedDate).format('dddd, MMMM D')}
</p>
</div>
<Calendar
className="mx-auto w-fit py-4"
value={selectedDate}
onChange={(date) => date && setSelectedDate(date)}
/>
<div className="border-t px-5 py-4">
<p className="mb-3 font-semibold">Happening now</p>
{currentEvents.length > 0 ? (
<div className="space-y-3">
{currentEvents.map((event) => (
<div key={event.id} className="rounded-md bg-muted p-3">
<p className="font-medium">{event.title}</p>
<p className="text-sm text-muted-foreground">
{dayjs(event.startDate).format('h:mm A')} -{' '}
{dayjs(event.endDate).format('h:mm A')}
</p>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground">
No events are in progress.
</p>
)}
</div>
</aside>
)}
/>
</div>
);
}Custom Event Render
Preview
Dark
August 2026
Sun
Mon
Tue
Wed
Thu
Fri
Sat
26
27
28
29
30
31
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
2
3
4
5
Team Sync
Launch Day
Renewal Due
import { useState } from 'react';
import type { ReactNode } from 'react';
import { PiFlag, PiRocketLaunch, PiUsers } from 'react-icons/pi'
import FullCalendar from '@/components/composites/FullCalendar';
import type { FullCalendarEvent } from '@/components/composites/FullCalendar';
const now = new Date();
const day = (n: number, hour = 0, minute = 0) =>
new Date(now.getFullYear(), now.getMonth(), n, hour, minute).toISOString();
const initialEvents: FullCalendarEvent[] = [
{
id: 1,
title: 'Team Sync',
startDate: day(4, 10, 0),
endDate: day(4, 10, 30),
color: 'blue',
description: 'Weekly team sync.',
type: 'meeting',
},
{
id: 2,
title: 'Launch Day',
startDate: day(22),
endDate: day(22),
color: 'green',
description: 'Ship to production.',
type: 'launch',
},
{
id: 3,
title: 'Renewal Due',
startDate: day(27),
endDate: day(27),
color: 'orange',
description: 'Contract renewal deadline.',
type: 'deadline',
},
];
const typeIcon: Record<string, ReactNode> = {
meeting: <PiUsers />,
launch: <PiRocketLaunch />,
deadline: <PiFlag />,
};
const typeBorder: Record<string, string> = {
meeting: 'border-l-palette-blue',
launch: 'border-l-palette-emerald',
deadline: 'border-l-palette-orange',
};
export default function CustomEventRenderDemo() {
const [events, setEvents] = useState(initialEvents);
return (
<div className="h-145 w-full">
<FullCalendar
fillHeight
events={events}
onChange={(updatedEvents) => setEvents(updatedEvents)}
renderEvent={({ event }) => ({
className: `bg-card border rounded-l-none border-l-4 ${
typeBorder[event.type] ?? 'border-l-muted-foreground'
}`,
content: (
<div className="flex items-center gap-2 truncate">
<span className="text-muted-foreground">
{typeIcon[event.type]}
</span>
<span className="truncate font-medium">{event.title}</span>
</div>
),
})}
/>
</div>
);
}More Overflow
Preview
Dark
August 2026
Sun
Mon
Tue
Wed
Thu
Fri
Sat
26
27
28
29
30
31
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
2
3
4
5
Interview 109:00
Interview 210:00
Interview 311:00
Interview 412:00
Interview 513:00
import { useState } from 'react';
import dayjs from 'dayjs';
import FullCalendar from '@/components/composites/FullCalendar';
import type { FullCalendarEvent } from '@/components/composites/FullCalendar';
const now = new Date();
const day = (n: number, hour = 0, minute = 0) =>
new Date(now.getFullYear(), now.getMonth(), n, hour, minute).toISOString();
const busyDay = 10;
const initialEvents: FullCalendarEvent[] = Array.from(
{ length: 7 },
(_, i) => ({
id: i + 1,
title: `Interview ${i + 1}`,
startDate: day(busyDay, 9 + i, 0),
endDate: day(busyDay, 9 + i, 45),
color: i % 2 === 0 ? 'blue' : 'purple',
description: 'Candidate interview slot.',
type: 'meeting',
}),
);
const dotColor: Record<string, string> = {
blue: 'bg-palette-blue',
purple: 'bg-palette-purple',
};
export default function MoreOverflowDemo() {
const [events, setEvents] = useState(initialEvents);
return (
<div className="w-full h-145">
<FullCalendar
fillHeight
events={events}
onChange={(updatedEvents) => setEvents(updatedEvents)}
renderMoreContent={({ overflowedEvents }) => (
<div className="flex flex-col gap-1">
<p className="px-2 text-xs font-medium text-muted-foreground">
{overflowedEvents.length} more
</p>
{overflowedEvents.map((event) => (
<div
key={event.id}
className="flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-accent"
>
<span
className={`h-2 w-2 shrink-0 rounded-full ${
dotColor[event.color] ?? 'bg-palette-gray'
}`}
/>
<span className="flex-1 truncate text-sm font-medium">
{event.title}
</span>
<span className="text-xs text-muted-foreground">
{dayjs(event.startDate).format('h:mm A')}
</span>
</div>
))}
</div>
)}
/>
</div>
);
}Fill Height
Preview
Dark
August 2026
Sun
Mon
Tue
Wed
Thu
Fri
Sat
26
27
28
29
30
31
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
2
3
4
5
Standup09:00
Retro16:00
import { useState } from 'react';
import FullCalendar from '@/components/composites/FullCalendar';
import type { FullCalendarEvent } from '@/components/composites/FullCalendar';
const now = new Date();
const day = (n: number, hour = 0, minute = 0) =>
new Date(now.getFullYear(), now.getMonth(), n, hour, minute).toISOString();
const initialEvents: FullCalendarEvent[] = [
{
id: 1,
title: 'Standup',
startDate: day(6, 9, 0),
endDate: day(6, 9, 15),
color: 'blue',
description: 'Daily standup.',
type: 'meeting',
},
{
id: 2,
title: 'Retro',
startDate: day(14, 16, 0),
endDate: day(14, 17, 0),
color: 'purple',
description: 'Sprint retrospective.',
type: 'meeting',
},
];
export default function FillHeightDemo() {
const [events, setEvents] = useState(initialEvents);
return (
<div className="h-145 w-full rounded-card border">
<FullCalendar
fillHeight
events={events}
onChange={(updatedEvents) => setEvents(updatedEvents)}
/>
</div>
);
}API
FullCalendar
| Prop | Description | Type | Default |
|---|---|---|---|
events | Events to render. color accepts 'blue' | 'green' | 'red' | 'yellow' | 'purple' | 'orange' | 'gray'; any other value falls back to gray. | FullCalendarEvent[] | — |
view | Active view. The built-in header only switches between Month, Week, and Agenda; reach Day and Year through view directly or a custom renderHeaderEnd. | CalendarView | 'month' |
fillHeight | Fills the parent's height instead of the view's own default height. The parent chain must be height-bounded (an explicit height somewhere above), or the calendar collapses. | boolean | false |
onCellClick | Fires when an empty cell or time slot is clicked. | (date: Date) => void | — |
onEventClick | Fires when an event is clicked. Not called by the year view (its event bullets aren't clickable). | (event: FullCalendarEvent) => void | — |
onChange | Fires after an event is moved (drag) or resized, with the full updated list and the one event that changed. | (events: FullCalendarEvent[], event: FullCalendarEvent) => void | — |
onMoreClick | Observes activation of a month-view cell's "+N more" control alongside the built-in or customized overflow Popover. Only used by the month view. | (overflowedEvents: FullCalendarEvent[], allEvents: FullCalendarEvent[]) => void | — |
renderMoreContent | Customizes or replaces the built-in overflow Popover body in the month view; it is not required to enable the "+N more" Popover. | (props: MoreContentRenderProps) => ReactNode | — |
renderEvent | Replaces an event's rendered content and class name. Not called by the year view. | (props: EventRenderProps) => EventRenderResult | — |
renderDayViewSidebar | Replaces the day view's complete sidebar wrapper, including its width and responsive behavior. Receives the selected date, its setter, and currently active single-day events. Return null to hide the sidebar. | (props: DayViewSidebarRenderProps) => ReactNode | — |
renderHeaderStart | Replaces the header's left/start section. | (payload: FullCalendarHeaderPayload) => ReactNode | string | — |
renderHeaderEnd | Replaces the header's right/end section. | (payload: FullCalendarHeaderPayload) => ReactNode | string | — |
Drag-to-move and resize-to-adjust-duration are always active on single-day events in the month, week, and day views — there is no prop to disable them. Both report through onChange, so a parent that doesn't feed updated events back in will see the drag or resize visually revert.
Types
type CalendarView = 'day' | 'week' | 'month' | 'year' | 'agenda';
type FullCalendarEvent = {
id: number;
startDate: string;
endDate: string;
title: string;
color: string;
description: string;
type: string;
disabled?: boolean;
};
type EventRenderProps = {
event: FullCalendarEvent;
view: CalendarView;
};
type EventRenderResult = {
className?: string;
content: ReactNode;
};
type MoreContentRenderProps = {
overflowedEvents: FullCalendarEvent[];
allEvents: FullCalendarEvent[];
};
type DayViewSidebarRenderProps = {
selectedDate: Date;
setSelectedDate: (date: Date) => void;
currentEvents: FullCalendarEvent[];
};
type FullCalendarHeaderPayload = {
handlePrevious: () => void;
handleNext: () => void;
selectedDate: Date;
view: CalendarView;
setView: (view: CalendarView) => void;
setSelectedDate: (date: Date) => void;
};