Back to Blog
Close-up of programming code on a computer screen in a dark environment
Framework
July 4, 2026
9 min read

How to Add a Calendar to a Next.js App (Without SSR Headaches)

By SimpleCalendarJS Team

SimpleCalendarJS~14 KB gzipped · Zero dependencies · Any framework

You need to add a calendar to your Next.js app. You search "Next.js calendar," and within minutes you're deep in a thread about hydration errors, 'use client' directives, and next/dynamic with ssr: false. The calendar itself is a solved problem — the hard part in Next.js is getting a client-side library to cooperate with Server Components. Here's a clear breakdown of your options and a working implementation that avoids the most common pitfalls.

The Next.js calendar problem

Next.js 13+ introduced the App Router, where every component is a Server Component by default. Server Components render on the server, ship zero JavaScript to the browser, and cannot use hooks, event handlers, or browser APIs. This is great for performance — Vercel reports that properly structured App Router pages ship as little as 50–80 KB of total JavaScript.

The problem: every calendar library is a client-side library. They use useEffect, useState, DOM measurements, and date/time calculations that behave differently in Node.js than in the browser. Drop one into a Server Component and Next.js throws an error. Mark it with 'use client' and you often get hydration mismatches — the server-rendered HTML doesn't match what React produces on the client.

This is not a theoretical issue. react-big-calendar has a documented GitHub issue about Next.js compatibility. FullCalendar has a known CSS issue in Next.js 14 where styles don't apply at all. The common workaround — wrapping everything in next/dynamic with { ssr: false } — disables server rendering entirely for that component, which works but means you're shipping a loading spinner where your calendar should be on first paint.

What each library actually requires in Next.js

Here's what it takes to get a working calendar in the App Router with each major library:

LibraryGzipped Size'use client'next/dynamic + ssr: falseExtra Packages
react-big-calendar~50 KB + localizerRequiredUsually required3+ (calendar + localizer + CSS)
FullCalendar~43 KB minimumRequiredOften required4+ (core + adapter + plugins)
DayPilot~70 KB+RequiredRecommended1
SimpleCalendarJS~14 KBRequiredNot needed1

The distinction matters. Libraries that produce different output on server vs. client need next/dynamic with ssr: false to avoid hydration errors. A vanilla JS library that initialises inside useEffect never runs on the server at all — useEffect is a client-only hook — so the server render produces an empty <div> on both passes. No mismatch, no error, no dynamic import workaround.

Option 1: The react-big-calendar path

react-big-calendar is the most popular React calendar at roughly 1 million weekly npm downloads. In Next.js, the setup requires two files and a dynamic import:

// components/BigCalendar.jsx 'use client'; import { Calendar, dayjsLocalizer } from 'react-big-calendar'; import dayjs from 'dayjs'; import 'react-big-calendar/lib/css/react-big-calendar.css'; const localizer = dayjsLocalizer(dayjs); export default function BigCalendar({ events }) { return ( <div style={{ height: 600 }}> <Calendar localizer={localizer} events={events} startAccessor="start" endAccessor="end" /> </div> ); }
// app/calendar/page.jsx 'use client'; import dynamic from 'next/dynamic'; const BigCalendar = dynamic( () => import('@/components/BigCalendar'), { ssr: false, loading: () => <p>Loading calendar...</p> } ); export default function CalendarPage() { return <BigCalendar events={[]} />; }

Note the trade-offs:

  • Two files just to render a calendar — one for the component, one for the dynamic import wrapper
  • The page itself must be 'use client' because next/dynamic with ssr: false cannot be used in Server Components
  • You lose server-side data fetching on that page — you can't use async function Page() with await fetch() because it's now a Client Component
  • The user sees a "Loading calendar..." placeholder until the JavaScript downloads and executes
  • Total bundle cost: ~50 KB (react-big-calendar) + ~5–10 KB (Day.js) = ~60 KB gzipped minimum

Option 2: The vanilla JS path (recommended)

A vanilla JavaScript calendar initialises inside useEffect, which only runs on the client. The server render produces a single empty <div>. The client render produces the same empty <div>, then useEffect fires and the calendar mounts. No mismatch. No dynamic import needed.

Here's how to add a calendar to a Next.js app using SimpleCalendarJS~14 KB gzipped, zero dependencies:

npm install simple-calendar-js
// components/Calendar.jsx 'use client'; import { useEffect, useRef } from 'react'; import SimpleCalendarJs from 'simple-calendar-js'; import 'simple-calendar-js/dist/simple-calendar-js.min.css'; export default function Calendar({ onEventClick }) { const containerRef = useRef(null); const calendarRef = useRef(null); useEffect(() => { if (!containerRef.current) return; calendarRef.current = new SimpleCalendarJs(containerRef.current, { defaultView: 'month', locale: 'en-US', enabledViews: ['month', 'week', 'day'], fetchEvents: async (start, end) => { const res = await fetch( `/api/events?from=${start.toISOString()}&to=${end.toISOString()}` ); return res.json(); }, onEventClick: (event) => onEventClick?.(event), onSlotClick: (date) => console.log('Slot clicked:', date), }); return () => calendarRef.current?.destroy(); }, []); return <div ref={containerRef} />; }
// app/calendar/page.jsx (Server Component — no 'use client' needed) import Calendar from '@/components/Calendar'; export default async function CalendarPage() { // Server-side data fetching still works on this page return ( <main> <h1>Team Calendar</h1> <Calendar onEventClick={(e) => console.log(e)} /> </main> ); }

That's it. One 'use client' component, one Server Component page. No dynamic import, no ssr: false, no loading placeholder. The page keeps its Server Component benefits — you can await fetch() for initial data, use generateMetadata() for SEO, and the surrounding layout ships zero JavaScript.

What this gives you

  • Month, week, and day views — toggle with the built-in toolbar or call calendarRef.current.setView('week') programmatically
  • Async event fetching — the fetchEvents callback fires with the visible date range on every navigation, so you only load what's on screen
  • Click handlersonEventClick for existing events, onSlotClick for empty time slots
  • 34+ locales built in — pass locale: 'pt-BR' or locale: 'ja-JP' and the calendar renders in that language
  • Automatic cleanupdestroy() in the useEffect return prevents memory leaks on unmount

Server Components + calendar: the "donut" pattern

The recommended Next.js architecture for interactive components is the "donut" pattern: a Server Component wraps a Client Component and passes server-fetched data as props. This keeps data fetching on the server while isolating JavaScript to the smallest possible client boundary.

With SimpleCalendarJS, this pattern works naturally:

// app/dashboard/page.jsx (Server Component) import Calendar from '@/components/Calendar'; import { getUser } from '@/lib/auth'; export async function generateMetadata() { return { title: 'Dashboard — Team Calendar' }; } export default async function DashboardPage() { const user = await getUser(); return ( <main> <h1>Welcome, {user.name}</h1> <p>Your upcoming events:</p> <Calendar /> </main> ); }

The <h1>, <p>, and the getUser() fetch all happen on the server. Only the <Calendar /> ships JavaScript to the browser. Vercel's own documentation calls this pattern the key to 30–50% smaller client bundles compared to marking entire pages as Client Components.

With react-big-calendar's dynamic import approach, the entire page must be 'use client' — so getUser() would need to be moved to an API route or fetched client-side, and generateMetadata() wouldn't work at all.

Theming in Next.js

SimpleCalendarJS uses CSS custom properties, which work identically in Next.js whether you're using CSS Modules, Tailwind, or global styles:

.uc-calendar { --cal-primary: #000000; --cal-primary-dark: #171717; --cal-today-bg: #f5f5f5; --cal-font-size: 14px; }

For dark mode with Next.js and next-themes:

[data-theme='dark'] .uc-calendar { --cal-bg: #0a0a0a; --cal-text: #ededed; --cal-border: #262626; --cal-today-bg: #1a1a1a; }

Four variables and your calendar matches your design system. No eventPropGetter callbacks, no deeply nested selector overrides, no !important flags.

Bundle size: what you're shipping to the browser

In Next.js, bundle size matters even more than in a plain React app. The App Router ships only Client Component JavaScript to the browser — every kilobyte in a 'use client' subtree directly impacts Largest Contentful Paint (LCP) and Total Blocking Time (TBT), both Google ranking signals.

SetupGzipped Sizenpm PackagesDynamic Import Required
react-big-calendar + Day.js localizer~60 KB3Usually yes
FullCalendar + React adapter + daygrid~50 KB4+Often yes
DayPilot Lite~70 KB1Recommended
SimpleCalendarJS (event calendar)~14 KB1No

SimpleCalendarJS delivers month, week, and day event views at 4x lighter than react-big-calendar — and without the SSR workarounds that add complexity and a loading state to your page.

When to use a React-specific calendar in Next.js

There are legitimate reasons to choose react-big-calendar or FullCalendar:

  • Agenda/list view: react-big-calendar includes a built-in agenda view for displaying events in a scrollable list. SimpleCalendarJS focuses on grid-based month, week, and day views.
  • Drag-and-drop rescheduling: react-big-calendar has mature drag-and-drop support for moving and resizing events on the grid.
  • Controlled component pattern: If your calendar's view, date, and events must be driven entirely by React state and props (e.g. for time-travel debugging), a React component handles this natively.
  • Existing investment: If your codebase already uses FullCalendar elsewhere, adding the Next.js adapter keeps the API consistent.

For the majority of Next.js apps that need to display events on a calendar and let users interact with them, a vanilla JS approach is simpler, lighter, and avoids the SSR compatibility issues that plague React-specific calendar libraries.

Summary

  • Next.js Server Components break most calendar libraries — every React calendar requires 'use client', and many need next/dynamic with ssr: false to avoid hydration errors
  • react-big-calendar (~60 KB with localizer) and FullCalendar (~50 KB+ with plugins) both require dynamic imports in the App Router, which means losing server-side data fetching on the page and showing a loading placeholder
  • A vanilla JS calendar initialised in useEffect never runs on the server, so the server and client render both produce an empty <div> — no mismatch, no dynamic import needed
  • SimpleCalendarJS ships a full event calendar (month, week, day views, async event fetching, click handlers, 34+ locales) in ~14 KB gzipped with zero dependencies and full App Router compatibility
  • Use the "donut" pattern — Server Component page wrapping a small Client Component calendar — to keep data fetching on the server and minimise your client JavaScript

Sources & Further Reading

Research & References

Image Credits

All images free to use under the Pexels License.

Frequently Asked Questions

What is the best calendar library for Next.js?

It depends on your needs. FullCalendar (~43 KB+ with plugins) and react-big-calendar (~50 KB + localizer) are the most popular event calendars, but both require 'use client' wrappers and often need dynamic imports with ssr: false to avoid hydration errors in the App Router. SimpleCalendarJS (~14 KB, zero dependencies) works in a single 'use client' component with no dynamic import required — it initialises in useEffect, so it never touches the server render.

How do I add a calendar to a Next.js App Router project?

Create a Client Component (add 'use client' at the top), install your calendar library, and initialise it inside a useEffect hook. For vanilla JS libraries like SimpleCalendarJS, use a useRef for the container and useEffect for setup and cleanup. For React-specific libraries like react-big-calendar, you may also need next/dynamic with ssr: false to prevent hydration mismatches.

Why do calendar libraries cause hydration errors in Next.js?

Hydration errors happen when the HTML rendered on the server differs from what React produces on the client. Calendar libraries use date/time calculations, locale formatting, and DOM measurements that produce different output in Node.js vs. the browser. Even with 'use client', Next.js still pre-renders Client Components on the server, which can trigger mismatches.

Do I need 'use client' for a calendar in Next.js?

Yes. Any calendar component that uses browser APIs, event handlers, or useEffect must be a Client Component. The key is to isolate it — create a small calendar wrapper with 'use client' and import it into your Server Component page. This keeps the rest of your page server-rendered while only shipping the calendar's JavaScript to the client.

Can I use a vanilla JavaScript calendar in Next.js?

Yes. The pattern is identical to integrating any imperative DOM library in React: useRef for the container, useEffect for initialisation and cleanup. Because the calendar only initialises inside useEffect (which never runs on the server), you avoid hydration errors entirely — no dynamic import with ssr: false needed.

How much JavaScript does a calendar add to my Next.js bundle?

It varies widely. react-big-calendar adds ~50 KB gzipped plus a mandatory localizer (5–64 KB more). FullCalendar starts at ~43 KB for a minimal setup and grows with each plugin. SimpleCalendarJS adds ~14 KB gzipped with zero additional dependencies. In Next.js, every kilobyte in a Client Component is JavaScript that ships to the browser and impacts your Core Web Vitals.

📅

Add a calendar to your app today

Free for personal projects. $49/year or $199 lifetime per commercial project.