# 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](/pricing/#nextjs).

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](https://fullcalendar.io/) 7 (`@fullcalendar/react` 7.1.3) to [AdminKit for Next.js](/nextjs/). 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:

```tsx title="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:

```html
<div id="fullcalendar-example"></div>

<script type="module" src="/preview/pro/js/fullcalendar.js"></script>
<script>
var today = new Date();
var first = new Date(today.getFullYear(), today.getMonth(), 1);
// day(2, 1, "10:00:00"): the Monday of the third row of the month view at 10 am
var day = function (week, weekday, time) {
  var value = new Date(first.getFullYear(), first.getMonth(), 1 - first.getDay() + week * 7 + weekday);
  var text = [
    value.getFullYear(),
    String(value.getMonth() + 1).padStart(2, "0"),
    String(value.getDate()).padStart(2, "0")
  ].join("-");
  return time ? text + "T" + time : text;
};

new FullCalendar.Calendar(document.getElementById("fullcalendar-example"), {
  plugins: FullCalendar.plugins,
  initialView: "dayGridMonth",
  todayHint: "Today",
  headerToolbar: {
    left: "prev,next today",
    center: "title",
    right: "dayGridMonth,timeGridWeek,timeGridDay"
  },
  editable: true,
  eventDisplay: "block",
  eventClass: "calendar-event",
  events: [
    { title: "Sprint planning", start: day(0, 1, "10:00:00"), end: day(0, 1, "11:30:00"), className: "calendar-event-primary" },
    { title: "Q4 roadmap due", start: day(1, 4), className: "calendar-event-danger" },
    { title: "Web Summit, Lisbon", start: day(2, 2), end: day(2, 5), className: "calendar-event-success" },
    { title: "Fiona Green out", start: day(3, 4), className: "calendar-event-warning" }
  ]
}).render();
</script>
```
```tsx code title="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:

```tsx title="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](https://fullcalendar.io/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:

```tsx
<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](https://nextjs-demo.adminkit.io/calendar) 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](/react/docs/plugins/calendar/): the same component in AdminKit for React (Vite)
- [Bootstrap 5 FullCalendar](/bootstrap/docs/plugins/calendar/): the same plugin in the HTML version
- [Date picker](/nextjs/docs/forms/date-picker/): Vanilla Calendar Pro, for picking a date in a form
- [Dark mode](/nextjs/docs/customization/dark-mode/): the color modes the calendar follows
- [Calendar demo](https://nextjs-demo.adminkit.io/calendar)

---

Canonical HTML page: https://adminkit.io/nextjs/docs/plugins/calendar/

Site overview for agents: https://adminkit.io/llms.txt (expanded: https://adminkit.io/llms-full.txt) · Documentation: https://adminkit.io/bootstrap/docs/ · Questions: support@adminkit.io
