Next.js FullCalendar
Next.js FullCalendar for the App Router: FullCalendar 7 loaded on the client with next/dynamic, with month, week and day views and drag and drop.
This component is only available in AdminKit PRO. Learn more.
When your app plans work in time, such as sprints, deadlines or days off, a calendar shows it at a glance. FullCalendar gives your Next.js pages a month, week and day view, with events people can drag and resize. The toolbar uses Bootstrap's btn btn-primary buttons.
The component brings FullCalendar 7 (@fullcalendar/react 7.1.3) to AdminKit for Next.js. It includes the day grid, time grid and interaction plugins of FullCalendar. It uses FullCalendar's classic theme.
Why it renders on the client only
FullCalendar needs the browser, and what it draws depends on today's date. If Next.js rendered it during next build, the HTML would show the month of the build. The browser would then take over that HTML with another date, a step called hydration, and the two would not match. So src/components/plugins/fullcalendar.tsx loads FullCalendar with next/dynamic and ssr: false, which means "only load this in the browser". Only a client component, a file that starts with "use client", may do that:
"use client";
import dynamic from "next/dynamic";
import { Skeleton } from "@/components/ui/skeleton";
export const FullCalendar = dynamic(
async () => {
const [{ default: Calendar }, { default: dayGridPlugin } /* , ... */] =
await Promise.all([
import("@fullcalendar/react"),
import("@fullcalendar/react/daygrid"),
// timegrid, interaction and the classic theme
]);
const builtIn = [dayGridPlugin /* , ... */];
return function FullCalendarClient({ plugins, ...props }: FullCalendarProps) {
return (
<Calendar
plugins={plugins?.length ? [...builtIn, ...plugins] : builtIn}
{...props}
/>
);
};
},
{
ssr: false,
// About the size of the month view, so the card keeps its height
loading: () => (
<div className="position-relative" style={{ aspectRatio: "1.25" }}>
<Skeleton />
</div>
),
}
);The HTML of the page holds the skeleton. In the browser, Next.js fetches FullCalendar and its plugins in parallel, as a separate JavaScript file (a chunk). Then it swaps the skeleton for the calendar. FullCalendar's stylesheets are part of app.scss.
Example
Some of FullCalendar's options are functions, such as dayCellClass and eventClick. Events built from new Date() belong in the browser too, so they show the month the user sees. So put the calendar in a client component:
"use client";
import { useState } from "react";
import { type EventInput, FullCalendar } from "@/components/plugins/fullcalendar";
// day(first, 2, 1, "10:00:00"): the Monday of the third row of the month view at 10 am
const day = (first: Date, week: number, weekday: number, time?: string) => {
const value = new Date(
first.getFullYear(),
first.getMonth(),
1 - first.getDay() + week * 7 + weekday
);
const text = [
value.getFullYear(),
String(value.getMonth() + 1).padStart(2, "0"),
String(value.getDate()).padStart(2, "0"),
].join("-");
return time ? `${text}T${time}` : text;
};
export function TeamCalendar() {
// Runs in the browser, so the events are in the month the user sees
const [events] = useState<EventInput[]>(() => {
const today = new Date();
const first = new Date(today.getFullYear(), today.getMonth(), 1);
return [
{ title: "Sprint planning", start: day(first, 0, 1, "10:00:00"), end: day(first, 0, 1, "11:30:00"), className: "calendar-event-primary" },
{ title: "Q4 roadmap due", start: day(first, 1, 4), className: "calendar-event-danger" },
{ title: "Web Summit, Lisbon", start: day(first, 2, 2), end: day(first, 2, 5), className: "calendar-event-success" },
{ title: "Fiona Green out", start: day(first, 3, 4), className: "calendar-event-warning" },
];
});
return (
<FullCalendar
initialView="dayGridMonth"
todayHint="Today"
headerToolbar={{
left: "prev,next today",
center: "title",
right: "dayGridMonth,timeGridWeek,timeGridDay",
}}
editable
eventDisplay="block"
eventClass="calendar-event"
events={events}
/>
);
}Your page renders that component and stays a server component with its metadata, as src/app/(dashboard)/calendar/page.tsx does:
import { TeamCalendar } from "@/components/team-calendar";
import { PageTitle } from "@/components/ui/page-title";
import { pageMetadata } from "@/lib/site";
export const metadata = pageMetadata("/calendar");
export default function Page() {
return (
<>
<PageTitle title="Calendar" pro />
<div className="card">
<div className="card-body">
<TeamCalendar />
</div>
</div>
</>
);
}Props
FullCalendarProps is FullCalendar's CalendarOptions plus a ref. The FullCalendar docs list every option.
| Prop | Type | Default | Description |
|---|---|---|---|
initialView | string | "dayGridMonth", "timeGridWeek", "timeGridDay", ... | |
events | EventInput[] | The events: title, start, end, className, ... | |
editable | boolean | Events can be dragged and resized | |
headerToolbar | object | The buttons and title of the toolbar | |
plugins | PluginInput[] | Plugins added to the built-in ones; keep it a stable array | |
ref | Ref<CalendarRef> | ref.current.getApi() returns FullCalendar's Calendar API |
Four things are built in, so you don't pass them: the day grid, the time grid, the interaction plugin and a Bootstrap theme based on the classic theme. The interaction plugin handles dragging, resizing and clicking dates. The CalendarOptions, CalendarRef and EventInput types are exported from the same file.
Event colors
Give each kind of event its own color, so meetings and deadlines stand apart. Set eventClass="calendar-event" and put a category class in the event's className: calendar-event-primary, -success, -warning, -danger or -info. The event turns into a tinted chip with a colored left edge, in both color modes. calendar-swatch with the same class draws the key of a legend. The legend is static markup, so it can stay in the server component page:
<ul className="list-inline small text-body-secondary mb-0" aria-label="Event categories">
<li className="list-inline-item me-3">
<span className="calendar-swatch calendar-event-primary me-1" />
Meetings
</li>
<li className="list-inline-item me-0">
<span className="calendar-swatch calendar-event-danger me-1" />
Deadlines
</li>
</ul>The calendar demo adds two touches. calendar-weekend shades the weekends, returned from dayCellClass and dayLaneClass. calendar-today-number marks today's date, returned from dayCellTopInnerClass. On phones, the demo sets height="auto" with useMediaQuery("(max-width: 575.98px)"), so every week of the month shows. The theme colors are the --fc-classic-* custom properties in src/scss/5-vendor/_fullcalendar.scss.
Accessibility
todayHint="Today"gives the today button an accessible name that matches its text.- Don't make color an event's only label. Keep the category in the title or in a legend with an
aria-label. - The toolbar buttons are Bootstrap
<button>elements, reachable with Tab.
Related
- React FullCalendar: the same component in AdminKit for React (Vite)
- Bootstrap 5 FullCalendar: the same plugin in the HTML version
- Date picker: Vanilla Calendar Pro, for picking a date in a form
- Dark mode: the color modes the calendar follows
- Calendar demo
Last updated on
Data tablePro
Next.js DataTables for the App Router: a client-only DataTable loaded with next/dynamic, rendered from server component pages, with Bootstrap 5 styling.
Rich text editorPro
A Next.js Quill editor for the App Router and React 19: plain Quill 2 in a client component that server pages can render, with snow and bubble themes.