AdminKitDocs
Plugins

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.

Pro

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:

src/components/plugins/fullcalendar.tsx (shortened)
"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:

src/components/team-calendar.tsx
"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:

src/app/(dashboard)/calendar/page.tsx
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.

PropTypeDefaultDescription
initialViewstring"dayGridMonth", "timeGridWeek", "timeGridDay", ...
eventsEventInput[]The events: title, start, end, className, ...
editablebooleanEvents can be dragged and resized
headerToolbarobjectThe buttons and title of the toolbar
pluginsPluginInput[]Plugins added to the built-in ones; keep it a stable array
refRef<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.

Last updated on

On this page