Back to Blog
Modern laptop on a desk displaying programming code on screen
Framework
July 5, 2026
9 min read

How to Add a Calendar to an Astro Site (Zero-Framework Overhead)

By SimpleCalendarJS Team

SimpleCalendarJS~14 KB gzipped · Zero dependencies · Any framework

You chose Astro because it ships zero JavaScript by default. Your pages are fast, your Lighthouse scores are green, and your content renders at the edge in milliseconds. Then you need to add a calendar — and suddenly you're importing React, configuring hydration directives, and watching your bundle balloon past 100 KB for a single interactive component. There's a better path.

The Astro calendar problem

Astro's architecture is built on islands — isolated interactive components floating in a sea of static HTML. Everything is server-rendered by default. JavaScript only ships to the browser for components you explicitly mark with a client:* directive. This is what makes Astro sites so fast: a typical content page ships 0 KB of client-side JavaScript.

The problem starts when you search for "Astro calendar." Most results point to React-based libraries — FullCalendar, react-big-calendar, Syncfusion — that require a React island. A React island doesn't just ship the calendar code. It ships the React runtime itself: ~40 KB gzipped of framework JavaScript before a single calendar event renders.

Astro's official documentation on framework components makes this clear: every framework component needs a client:* directive to become interactive, and each directive comes with trade-offs. client:load hydrates immediately but risks hydration mismatches. client:only avoids mismatches but skips server rendering entirely — your calendar is invisible until JavaScript loads.

For a framework built on shipping less JavaScript, pulling in an entire UI runtime for one component is a steep price.

What each approach actually costs

Here's what it takes to get a working interactive calendar in Astro with each major library:

LibraryGzipped SizeRequires Framework RuntimeAstro DirectiveExtra Packages
react-big-calendar~50 KB + ~40 KB React + localizerYes (React)client:only="react"4+
FullCalendar (React)~43 KB + ~40 KB ReactYes (React)client:only="react"5+
Syncfusion Calendar~60 KB+ + ~40 KB ReactYes (React)client:only="react"3+
SimpleCalendarJS~14 KBNoNone (<script> tag)1

The "Requires Framework Runtime" column is the key. React-based calendars don't just add calendar code — they add the entire React reconciler, virtual DOM, and event system. In a regular React app that cost is shared across every component. In Astro, your calendar island might be the only reason React exists on the page, making that ~40 KB pure overhead.

Option 1: The React island path

If you follow the most common advice online, you'll create a React component and wrap it in a client:only island:

// src/components/BigCalendar.jsx 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> ); }
--- // src/pages/calendar.astro import BigCalendar from '../components/BigCalendar.jsx'; --- <main> <h1>Team Calendar</h1> <BigCalendar client:only="react" events={[]} /> </main>

This works, but the trade-offs are significant:

  • ~95–100 KB gzipped ships to the browser (React runtime + calendar + localizer)
  • client:only means no server-rendered HTML for the calendar — users see nothing until JS loads
  • You need @astrojs/react installed and configured in astro.config.mjs
  • You can't pass functions as props across the island boundary (Astro cannot serialize functions from server to client)
  • Event data must be serialized — no passing complex objects or callbacks from the .astro page

You've effectively turned Astro's biggest advantage — zero client JS — into a liability for that section of the page.

Option 2: The vanilla JS path (recommended)

Astro has first-class support for vanilla JavaScript through standard <script> tags. Scripts in .astro components are automatically bundled as ES modules, processed by Vite, and injected into the page. No client:* directive required — they just run on the client.

Here's how to add a calendar to an Astro site using SimpleCalendarJS~14 KB gzipped, zero dependencies, no framework runtime:

npm install simple-calendar-js
--- // src/pages/calendar.astro import Layout from '../layouts/Layout.astro'; --- <Layout title="Team Calendar"> <main> <h1>Team Calendar</h1> <div id="calendar"></div> </main> </Layout> <script> import SimpleCalendarJs from 'simple-calendar-js'; import 'simple-calendar-js/dist/simple-calendar-js.min.css'; const container = document.getElementById('calendar'); const calendar = new SimpleCalendarJs(container, { 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); }, }); </script>

That's the entire implementation. No React. No @astrojs/react. No client:only. No hydration directive. Astro bundles the <script> as an ES module, and the calendar initialises when the page loads. The rest of your page — the layout, the <h1>, the navigation — remains static HTML with zero JavaScript.

What this gives you

  • Month, week, and day views with a built-in navigation toolbar
  • Async event fetchingfetchEvents 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
  • ~14 KB total JavaScript added to the page — down from ~95–100 KB with a React island

Making it a reusable Astro component

If you need the calendar on multiple pages, extract it into a reusable .astro component:

--- // src/components/Calendar.astro const { locale = 'en-US', apiEndpoint = '/api/events' } = Astro.props; --- <div id="calendar" data-locale={locale} data-api={apiEndpoint} ></div> <script> import SimpleCalendarJs from 'simple-calendar-js'; import 'simple-calendar-js/dist/simple-calendar-js.min.css'; const container = document.getElementById('calendar'); const locale = container.dataset.locale; const apiEndpoint = container.dataset.api; const calendar = new SimpleCalendarJs(container, { defaultView: 'month', locale, enabledViews: ['month', 'week', 'day'], fetchEvents: async (start, end) => { const res = await fetch( `${apiEndpoint}?from=${start.toISOString()}&to=${end.toISOString()}` ); return res.json(); }, onEventClick: (event) => { console.log('Event clicked:', event); }, }); </script>
--- // src/pages/calendar.astro import Layout from '../layouts/Layout.astro'; import Calendar from '../components/Calendar.astro'; --- <Layout title="Team Calendar"> <main> <h1>Team Calendar</h1> <Calendar locale="en-US" apiEndpoint="/api/events" /> </main> </Layout>

Notice the pattern: server-side props (locale, apiEndpoint) are passed to the client via data-* attributes on the container element. This is Astro's recommended approach for sending data from the server to client-side scripts — no serialization issues, no framework boundary limitations.

Theming with CSS custom properties

SimpleCalendarJS uses CSS custom properties, which work identically whether you're using Astro's scoped styles, Tailwind, or global CSS:

.uc-calendar { --cal-primary: #7c3aed; --cal-primary-dark: #6d28d9; --cal-today-bg: #f5f3ff; --cal-font-size: 14px; }

For dark mode with Astro's built-in class-based toggling:

.dark .uc-calendar { --cal-bg: #0f172a; --cal-text: #e2e8f0; --cal-border: #1e293b; --cal-today-bg: #1e1b4b; }

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

Bundle impact: preserving Astro's zero-JS advantage

Astro's core promise is performance. A typical Astro content site ships 0 KB of client-side JavaScript. The moment you add an interactive component, that number goes up — the question is by how much.

SetupJS Added to Page (gzipped)Framework RuntimeServer-Rendered
No calendar0 KBNoneYes
SimpleCalendarJS (<script>)~14 KBNoneStatic HTML + script
FullCalendar + React island~83 KB+React (~40 KB)client:only = No
react-big-calendar + React island~95 KB+React (~40 KB)client:only = No
Syncfusion + React island~100 KB+React (~40 KB)client:only = No

With SimpleCalendarJS, your Astro site goes from 0 KB to 14 KB — a full interactive event calendar for less JavaScript than most hero image lazy-loaders. With a React island calendar, you're shipping 6–7x more JavaScript, and the calendar section isn't even server-rendered.

When to use a React island for your calendar

There are valid reasons to bring React into an Astro project for a calendar:

  • Existing React codebase: If your Astro site wraps a React app or shares components with a React project, using the same calendar keeps the API consistent
  • Drag-and-drop rescheduling: react-big-calendar has mature drag-and-drop support for moving and resizing events directly on the grid
  • Shared island state: If your calendar needs to share reactive state with other React components on the same page (e.g. a sidebar filter updating the calendar), a React island keeps that state management simple
  • Agenda/list view: react-big-calendar includes a built-in agenda view for displaying events in a scrollable list

For the majority of Astro sites — blogs, marketing pages, documentation, dashboards — that need to display events and let users navigate between views, a vanilla JS calendar keeps your bundle lean and your architecture simple.

Summary

  • Astro ships 0 KB of JavaScript by default — adding a React-based calendar breaks that promise with ~80–100 KB of framework runtime plus calendar code
  • React islands in Astro require client:only, which skips server rendering and shows nothing until JavaScript loads — the opposite of Astro's content-first philosophy
  • Vanilla JS calendars work natively in Astro through standard <script> tags — no client:* directive, no framework adapter, no hydration workarounds
  • SimpleCalendarJS adds a full event calendar (month, week, day views, async fetching, click handlers, 34+ locales) in ~14 KB gzipped — preserving most of Astro's zero-JS advantage
  • Use data-* attributes to pass server-side props to your calendar's <script> tag, following Astro's recommended pattern for server-to-client data transfer

Sources & Further Reading

Research & References

Image Credits

All images free to use under the Pexels License.

Frequently Asked Questions

Can I add an interactive calendar to an Astro site without React?

Yes. Astro supports vanilla JavaScript through standard <script> tags in .astro components. A vanilla JS calendar library like SimpleCalendarJS initialises via a <script> tag with no framework runtime required — you get full month, week, and day views with event handling while shipping only the calendar's ~14 KB instead of ~40+ KB of React overhead.

What is the best calendar library for Astro?

It depends on your needs. FullCalendar (~43 KB+) and react-big-calendar (~50 KB + localizer + React runtime) are popular but require framework islands with client:only or client:load directives. SimpleCalendarJS (~14 KB, zero dependencies) works directly in an Astro component's <script> tag — no framework adapter, no hydration directive, no island overhead.

Do I need client:load to add a calendar in Astro?

Only if you're using a framework component (React, Vue, Svelte). Astro's client:* directives are designed for framework islands. A vanilla JS library initialised in a <script> tag runs automatically on the client — Astro bundles it as an ES module and injects it into the page. No directive needed.

Why does FullCalendar need client:only in Astro?

FullCalendar's React adapter relies on browser APIs and React's runtime, which produce different output on the server than in the browser. Using client:only='react' tells Astro to skip server rendering entirely for that component, avoiding hydration mismatches. The trade-off is that users see an empty placeholder until the JavaScript downloads and executes.

How much JavaScript does a calendar add to my Astro site?

It varies dramatically. An Astro site ships 0 KB of JS by default. Adding react-big-calendar via a React island adds ~50 KB (calendar) + ~40 KB (React runtime) + ~5-10 KB (localizer) = ~95-100 KB gzipped. FullCalendar with the React adapter is similar. SimpleCalendarJS adds ~14 KB gzipped with no additional runtime — preserving most of Astro's zero-JS advantage.

Can I use client:visible to lazy-load a calendar in Astro?

Yes, if you're using a framework island. The client:visible directive delays hydration until the component enters the viewport, which is great for calendars below the fold. With vanilla JS, you can achieve the same effect using an IntersectionObserver in your <script> tag, though for most pages the calendar is above the fold and should load immediately.

📅

Add a calendar to your app today

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