How to Add a Calendar to a Gatsby Site (Without Breaking the Build)
By SimpleCalendarJS Team
You need to add a calendar to your Gatsby site. You install a calendar library, run gatsby build, and immediately hit ReferenceError: window is not defined. The calendar works fine in gatsby develop — but the production build crashes every time. The problem isn't the calendar library. It's how Gatsby builds your site, and most calendar libraries aren't designed for it.
Why calendars break in Gatsby
Gatsby is a static site generator. When you run gatsby build, it renders every React component to HTML inside Node.js — not a browser. This is what makes Gatsby fast: visitors get pre-built HTML before any JavaScript loads. But calendar libraries need the DOM. They measure container widths, attach scroll listeners, and reference window, document, and Element to position events on a time grid. None of that exists in Node.js.
The result is one of three build failures:
ReferenceError: window is not defined— the library accesseswindowat import timeReferenceError: Element is not defined— the library touchesElement.prototypeduring module initialisation (this is the exact error FullCalendar throws)- Hydration mismatch — the static HTML doesn't match what React produces in the browser
Unlike Next.js or Remix, which render on every request, Gatsby renders once at build time. If a single import path touches a browser API, gatsby build fails and your site doesn't deploy. The error doesn't surface in gatsby develop because development mode skips the static HTML generation step.
Gatsby's client-only escape hatches
Gatsby documents four approaches for client-side only packages. Each has different trade-offs for calendar integration:
1. useEffect + dynamic import() (simplest)
import { useEffect, useRef } from "react"; export default function CalendarWrapper() { const containerRef = useRef(null); useEffect(() => { import("some-calendar-lib").then(({ default: Calendar }) => { new Calendar(containerRef.current, { /* options */ }); }); }, []); return <div ref={containerRef} />; }
useEffect never runs during Gatsby's build. The dynamic import() ensures the library isn't even bundled into the server-side code path. This is the recommended approach for calendar libraries.
2. @loadable/component
import loadable from "@loadable/component"; const Calendar = loadable(() => import("./CalendarComponent"));
Gatsby's docs recommend loadable-components as the standard way to handle client-only packages. It works for both SSR and client rendering. But for a calendar — which has no meaningful server-rendered output — it adds a dependency and a plugin (gatsby-plugin-loadable-components-ssr) for no benefit over useEffect.
3. typeof window guard
const isBrowser = typeof window !== "undefined"; export default function CalendarPage() { if (!isBrowser) return <div style={{ height: 600 }} />; return <CalendarComponent />; }
Simple but fragile. If CalendarComponent imports a library that references window at the module level, the import itself crashes the build — the guard never runs. This only works when the offending code is in your component, not in a third-party library's entry point.
4. Webpack null-loader in gatsby-node.js
// gatsby-node.js exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => { if (stage === "build-html" || stage === "develop-html") { actions.setWebpackConfig({ module: { rules: [ { test: /some-calendar-lib/, use: loaders.null(), }, ], }, }); } };
This replaces the calendar module with an empty module during the HTML build stage. It works, but it's a build configuration hack — you're teaching webpack to ignore a package, which can break if the package structure changes. Reserve this for libraries that crash on import and can't be dynamically imported.
Option 1: react-calendar (date picker only)
If you only need date selection — a form input, not an event grid — react-calendar is the simplest path:
npm install react-calendar
import { useState } from "react"; import Calendar from "react-calendar"; import "react-calendar/dist/Calendar.css"; export default function DatePicker() { const [date, setDate] = useState(new Date()); return <Calendar onChange={setDate} value={date} />; }
This renders a month grid where users pick a date. It's mostly build-safe in Gatsby because it doesn't access window at the module level. But it cannot display events on a week grid, show time slots, or handle event click interactions. If you need any of those, you need an event calendar.
Option 2: FullCalendar in Gatsby
FullCalendar is the most popular JavaScript event calendar with roughly 680,000 weekly npm downloads across its packages. In Gatsby, it requires both a webpack workaround and a runtime guard:
npm install @fullcalendar/core @fullcalendar/react @fullcalendar/daygrid
// gatsby-node.js exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => { if (stage === "build-html" || stage === "develop-html") { actions.setWebpackConfig({ module: { rules: [ { test: /@fullcalendar/, use: loaders.null(), }, ], }, }); } };
// components/EventCalendar.jsx import React, { useState, useEffect } from "react"; export default function EventCalendar() { const [calendarComponent, setCalendarComponent] = useState(null); useEffect(() => { Promise.all([ import("@fullcalendar/react"), import("@fullcalendar/daygrid"), ]).then(([{ default: FullCalendar }, { default: dayGridPlugin }]) => { setCalendarComponent( <FullCalendar plugins={[dayGridPlugin]} initialView="dayGridMonth" events={[{ title: "Team Standup", date: "2026-09-15" }]} /> ); }); }, []); return calendarComponent || <div style={{ height: 600 }}>Loading...</div>; }
Note the trade-offs:
- Three npm packages before a single event renders
- A gatsby-node.js webpack rule to prevent the build from crashing — Gatsby-specific configuration that doesn't transfer to other frameworks
- Dynamic imports with state management to render the calendar only after hydration
- Total bundle cost: ~43 KB gzipped, growing with each additional view plugin (timegrid, list, interaction)
- Advanced features like resource scheduling require a premium license starting at $480 per developer
Option 3: Vanilla JS with SimpleCalendarJS (recommended)
A vanilla JavaScript calendar sidesteps Gatsby's build entirely. It initialises inside useEffect with a dynamic import(), which means the library is never loaded during the Node.js build step. The static HTML contains an empty <div>. The browser hydrates, useEffect fires, and the calendar mounts. No webpack workaround, no loadable-components plugin, no build configuration.
Here's how to add a calendar to a Gatsby site using SimpleCalendarJS — ~14 KB gzipped, zero dependencies:
npm install simple-calendar-js
// src/components/EventCalendar.jsx import React, { useEffect, useRef } from "react"; export default function EventCalendar() { const containerRef = useRef(null); const calendarRef = useRef(null); useEffect(() => { let destroyed = false; import("simple-calendar-js").then(({ default: SimpleCalendarJs }) => { if (destroyed || !containerRef.current) return; import("simple-calendar-js/dist/simple-calendar-js.min.css"); 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) => console.log("Event clicked:", event), onSlotClick: (date) => console.log("Slot clicked:", date), }); }); return () => { destroyed = true; calendarRef.current?.destroy(); }; }, []); return <div ref={containerRef} />; }
// src/pages/calendar.jsx import React from "react"; import EventCalendar from "../components/EventCalendar"; export default function CalendarPage() { return ( <main> <h1>Team Calendar</h1> <EventCalendar /> </main> ); } export const Head = () => <title>Calendar</title>;
That's it. One component file, one page. No gatsby-node.js webpack rules, no loadable-components plugin, no extra packages. The destroyed flag prevents a race condition if the component unmounts before the dynamic import resolves.
What this gives you
- Month, week, and day views with a built-in toolbar
- Async event fetching —
fetchEventsfires with the visible date range on every navigation, loading only what's on screen - Click handlers —
onEventClickfor existing events,onSlotClickfor empty time slots - 34+ locales built in — pass
locale: 'pt-BR'orlocale: 'ja-JP' - Automatic cleanup —
destroy()in the useEffect return prevents memory leaks
Sourcing events with Gatsby's data layer
Gatsby's GraphQL data layer lets you pull data from any source at build time and pass it to pages as props. You can source calendar events from a CMS, Google Sheets, or a JSON file and inject them as initial data:
// gatsby-node.js exports.createPages = async ({ actions }) => { actions.createPage({ path: "/calendar", component: require.resolve("./src/templates/calendar.jsx"), context: { // Pass static events from your data source }, }); };
// src/templates/calendar.jsx import React from "react"; import EventCalendar from "../components/EventCalendar"; export default function CalendarTemplate({ pageContext }) { return ( <main> <h1>Events</h1> <EventCalendar initialEvents={pageContext.events} /> </main> ); }
The build fetches events once and bakes them into the page's JSON data. The calendar component receives initialEvents as a prop and displays them immediately — no loading spinner for the first view. Subsequent navigations (switching months, changing views) trigger fetchEvents for fresh data.
Theming the calendar
SimpleCalendarJS uses CSS custom properties. Override them in your Gatsby site's global stylesheet or a component-scoped CSS file:
.uc-calendar { --cal-primary: #663399; --cal-primary-dark: #442266; --cal-today-bg: #f3eefa; --cal-font-size: 14px; }
Four lines and your calendar matches Gatsby's signature purple. For dark mode:
@media (prefers-color-scheme: dark) { .uc-calendar { --cal-bg: #1a1a2e; --cal-text: #e4e4e7; --cal-border: #2d2d44; --cal-today-bg: #2d2d44; } }
Bundle size comparison
Gatsby sites are static — every kilobyte of client-side JavaScript is code the browser must download, parse, and execute before the page becomes interactive. This directly impacts Time to Interactive (TTI) and Total Blocking Time (TBT), both Core Web Vitals that affect search rankings.
| Setup | Gzipped Size | npm Packages | Build Workaround |
|---|---|---|---|
| FullCalendar + React adapter | ~43 KB | 3+ | gatsby-node.js null-loader + dynamic imports |
| react-big-calendar | ~30 KB | 2 (+ moment/date-fns) | typeof window guard or dynamic import |
| react-calendar (date picker only) | ~7 KB | 1 | Minimal — mostly build-safe |
| SimpleCalendarJS | ~14 KB | 1 | None — useEffect only |
SimpleCalendarJS delivers full event calendar features (month, week, day views, async fetching, click handlers) at a fraction of FullCalendar's bundle cost, with the simplest Gatsby integration of any event calendar — zero build configuration required.
A note on Gatsby in 2026
Gatsby still has roughly 290,000 weekly npm downloads and powers over 35,000 live websites. It's not dead — but it's not where new projects start. After Netlify acquired Gatsby Inc. in February 2023, much of the core team left and development slowed. The plugin ecosystem, once Gatsby's biggest strength, is largely unmaintained. Gatsby Cloud shut down.
If you're maintaining an existing Gatsby site, the patterns in this post work. If you're starting a new project, consider Next.js or Astro — both support static generation with better SSR escape hatches and active ecosystem development.
When to use a React-specific calendar instead
There are valid reasons to choose a React-native calendar in Gatsby:
- Date selection only: If you need a date input for a form, react-calendar is lightweight, mostly build-safe, and purpose-built for that use case.
- Drag-and-drop rescheduling: FullCalendar's interaction plugin 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, a React-native library handles this natively.
- Gatsby source plugins: If your events live in Google Calendar and you want them sourced at build time via
gatsby-source-google-calendar, a tightly integrated React calendar pairs well with Gatsby's GraphQL layer.
For the majority of Gatsby sites that need to display events on a calendar and let users interact with them, a vanilla JS approach initialised in useEffect is simpler, lighter, and avoids the build configuration workarounds that every React-based calendar library requires in Gatsby.
Summary
- Gatsby renders every page at build time in Node.js — calendar libraries that access
window,document, orElementcrashgatsby buildeven when they work fine ingatsby develop - Four client-only patterns exist:
useEffect+ dynamic import (simplest),@loadable/component(Gatsby-recommended but adds a plugin),typeof windowguard (fragile), and webpack null-loader ingatsby-node.js(build-level hack) - FullCalendar requires 3+ packages, a webpack null-loader rule in
gatsby-node.js, and dynamic imports with state management — three layers of workarounds - A vanilla JS calendar in
useEffectwith dynamicimport()never loads during the build — zero configuration, zero build errors - SimpleCalendarJS ships month, week, and day event views with async fetching, click handlers, and 34+ locales in ~14 KB gzipped — the simplest build-safe integration of any event calendar in Gatsby
- Pair it with Gatsby's GraphQL data layer to source initial events at build time for instant first render
Sources & Further Reading
Research & References
- Gatsby build fails with "Element is not defined" when using FullCalendar — gatsbyjs/gatsby GitHub #15161
- Using Client-Side Only Packages — Gatsby Documentation
- Debugging HTML Builds — Gatsby Documentation
- Gatsby, React & Hydration — gatsbyjs/gatsby GitHub Discussion #17914
- Fixing Gatsby's Rehydration Issue — LogRocket Blog
- The Perils of Hydration — Josh W. Comeau
- FullCalendar with Gatsby — Stephen Gream
- Gatsby — the "window is not defined" error — GreenRoots Blog
- Is Gatsby.js Still Worth Using in 2026? — Robin Wieruch
- Static Site Generators in 2026: Astro vs Hugo vs Next.js vs Eleventy vs Gatsby — AI Leapers
Image Credits
- Cover: Laptop with Code Editor Displaying Programming Code — Pexels
All images free to use under the Pexels License.
Frequently Asked Questions
How do I add a calendar to a Gatsby site?
Install a calendar library via npm, create a React component, and initialise the calendar inside a useEffect hook — which only runs in the browser. For vanilla JS libraries like SimpleCalendarJS, use a dynamic import() inside useEffect so the library never loads during Gatsby's Node.js build. No loadable-components plugin or webpack workaround needed.
Why does my calendar crash with 'window is not defined' in Gatsby?
Gatsby renders every page to static HTML at build time using Node.js, where browser APIs like window, document, and Element don't exist. Calendar libraries depend on these APIs for DOM measurements and event positioning. When Gatsby's build process imports or renders a calendar component, it throws a ReferenceError. The fix is to ensure the calendar code only runs in the browser — using useEffect, loadable-components, or a typeof window guard.
What is the best calendar library for Gatsby?
For date selection, react-calendar is lightweight and mostly build-safe. For event scheduling with month, week, and day views, FullCalendar works but requires 3+ packages and webpack workarounds to survive Gatsby's build. SimpleCalendarJS (~14 KB, zero dependencies) provides the same core views with a simple useEffect pattern — no build configuration changes needed.
Is Gatsby still maintained in 2026?
Gatsby is still available on npm with roughly 290,000 weekly downloads and powers over 35,000 live websites. However, development slowed significantly after Netlify acquired Gatsby Inc. in 2023 and much of the core team left. The plugin ecosystem is largely unmaintained. Gatsby works fine for existing sites, but most new React projects in 2026 choose Next.js or Astro instead.
Do I need loadable-components to use a calendar in Gatsby?
No. While Gatsby's documentation recommends @loadable/component for client-side only packages, it's not necessary for a calendar. A dynamic import() inside useEffect achieves the same result — the library loads only in the browser — without adding a dependency or configuring a Gatsby plugin. loadable-components is most useful when you need server-side rendering of the component itself, which calendars don't.
Can I use a vanilla JavaScript calendar in Gatsby?
Yes. Create a React component with a useRef for the container and useEffect for initialisation. Use a dynamic import() to load the calendar library inside useEffect, which only runs in the browser. The build produces an empty div, useEffect fires after hydration and mounts the calendar, and the useEffect cleanup function calls destroy() to prevent memory leaks.
Add a calendar to your app today
Free for personal projects. $49/year or $199 lifetime per commercial project.
