@mdwebb/react-calendar
v1.0.0
Published
A composable, headless event calendar for React, styled with Tailwind CSS.
Downloads
46
Maintainers
Readme
React Calendar renders a month-windowed list of events as either a grid or a list, with a built-in view toggle and event-type filter. It ships in three layers so you can take exactly as much structure as you need:
- Drop-in — render one
<Calendar>and get the toolbar, filter, search, navigation, and both views with sensible defaults. - Composable — assemble your own layout from compound components
(
Calendar.Root,Calendar.Toolbar,Calendar.GridView, and so on), and swap the card markup with arenderCardslot. - Headless — drive everything yourself with the
useCalendarhook, which owns the state and derivations and renders nothing.
Every state slice (view, selected month, filters, search) is uncontrolled by
default and controllable on demand: pass value + onChange to drive it from
your own state (URL sync, persistence), or omit them to let the calendar manage
it. Events are sorted by date, filtered by type/search/predicate, and the month
window is year-aware and navigable.
Everything is written in TypeScript and tested with Vitest, React Testing Library, and Playwright.
Requirements
- React 18 or 19
- Tailwind CSS v3 — the components are styled with Tailwind utility classes
Installation
npm install react-calendar
# or: yarn add react-calendar
# or: pnpm add react-calendarPeer dependencies you need installed: react and react-dom. The package also
depends on @headlessui/react (filter menu), lucide-react (icons), and
date-fns (date formatting).
Because the components use Tailwind utility classes, add the package to your
Tailwind content globs so those classes are generated. The components ship
class-based dark: variants, so enable darkMode: "class" to use them (toggle
by adding the dark class to a parent, e.g. <html>):
// tailwind.config.js
export default {
darkMode: "class",
content: [
"./src/**/*.{ts,tsx}",
"./node_modules/react-calendar/dist/**/*.{js,cjs}",
],
// ...
};The calendar is responsive (cards reflow to a single column on mobile, controls stack, the month navigator scrolls horizontally) and the default cards adapt to both light and dark surfaces automatically.
Quick start
The drop-in component renders the default layout: month tabs/rail, a grid/list view toggle, an event-type filter, and the events themselves.
import { Calendar, type CalendarEvent } from "react-calendar";
export default function App() {
const { events, loading, error } = useEvents(); // bring your own data hook
return (
<Calendar
events={events}
isLoading={loading}
isError={error}
onEventClick={(event) => navigate(event.url)}
/>
);
}Usage
Compose your own layout
Each part of the calendar is exported as a compound component under Calendar.
Place them inside Calendar.Root in any arrangement, and pass renderCard to
either view to replace the default card:
<Calendar.Root events={events} defaultView="grid">
<Calendar.Toolbar>
<Calendar.MonthTabs />
<Calendar.ViewToggle />
<Calendar.Filter />
</Calendar.Toolbar>
<Calendar.GridView renderCard={(event) => <MyCard event={event} />} />
<Calendar.ListView />
</Calendar.Root>Calendar.Root is the provider; every other compound component reads from it
and must be rendered inside it.
Theming
Pick an overarching palette. Each palette carries a primary accent (for the
toolbar/active chrome) plus a list of colours that are auto-assigned to event
types, so different eventTypes read as different colours within a view.
Switching palette recolours everything at once.
// Built-in palettes: "vivid" | "pastel" | "neon" | "earth" | "mono"
<Calendar events={events} theme="neon" />
// Custom palette (any field optional)
<Calendar
events={events}
theme={{
accent: "#0ea5e9",
accentText: "#0369a1",
typeColors: ["#0ea5e9", "#22c55e", "#f97316"],
}}
/>Override a single event's colour with event.color (a Tailwind class). The
primary accent is published as the CSS variables --rc-accent /
--rc-accent-text; for a headless setup spread themeStyle(theme) onto your
own container, and use buildTypeColorMap(types, theme) to mirror the per-type
colours. Set showImages to render a uniform image area on grid cards
(the event's image, or a themed placeholder when absent).
Go fully headless
useCalendar owns all the state and derivations and renders no markup. Use it
when you want a completely custom interface:
import { useCalendar, type CalendarEvent } from "react-calendar";
function CustomCalendar({ events }: { events: CalendarEvent[] }) {
const cal = useCalendar({ events, defaultView: "list" });
return (
<div>
<div>
{cal.availableTypes.map((type) => (
<button key={type} onClick={() => cal.toggleType(type)}>
{type}: {cal.filters[type] ? "shown" : "hidden"}
</button>
))}
</div>
{cal.visibleEvents.map((event) => (
<article key={event.id}>{event.name}</article>
))}
</div>
);
}API reference
<Calendar> / Calendar.Root props
Both accept the same options (Calendar is the batteries-included layout;
Calendar.Root is the bare provider you compose inside).
| Prop | Type | Default | Description |
| -------------- | ----------------------------------- | ---------- | ------------------------------------------------------- |
| events | CalendarEvent[] | required | The events to display. |
| isLoading | boolean | false | Show skeleton placeholders instead of events. |
| isError | boolean | false | Show the error state. |
| defaultView | "grid" \| "list" | "grid" | Which view is shown first. |
| monthCount | number | 3 | How many months the rolling window spans (1–12). |
| now | Date | new Date() | "Today" — inject for SSR/testing determinism. |
| anchorDate | Date | now | Month the window starts at. |
| view / defaultView / onViewChange | controlled trio | grid | Current view (controlled or uncontrolled). |
| selectedMonth / defaultSelectedMonth / onSelectedMonthChange | controlled trio | anchor month | Selected month index (0–11). |
| filters / defaultFilters / onFiltersChange | controlled trio | all on | Per-type visibility map. |
| search / defaultSearch / onSearchChange | controlled trio | "" | Free-text search over name + description. |
| filter | (event) => boolean | — | Extra predicate (e.g. hide cancelled events). |
| compareEvents| (a, b) => number | by date | Sort comparator for events within a month. |
| showStatus | boolean | true | Render status badges (Live/Cancelled/Sold out/…). |
| showImages | boolean | false | Render an image (or placeholder) on grid cards. |
| theme | CalendarThemeName \| CalendarPalette | "vivid" | Palette: a preset name or a custom palette. |
| onEventClick | (event: CalendarEvent) => void | — | Called when a card's "More Info" link is activated. |
| className | string | — | Added to the root container. |
Compound components
| Component | Description |
| --------------------- | ------------------------------------------------------------------------ |
| Calendar.Root | Provider that owns state; wrap the others in it. |
| Calendar.Toolbar | Layout slot for the controls (stacks on mobile). |
| Calendar.MonthNav | Unified month navigator: prev/next arrows + scrollable month pills. |
| Calendar.MonthTabs | Horizontal month switcher (pills only). |
| Calendar.Nav | Previous/next controls that page the month window (arrows only). |
| Calendar.Search | Text input bound to the calendar's search state. |
| Calendar.ViewToggle | Grid/list switch. |
| Calendar.Filter | Dropdown to toggle which event types are shown. |
| Calendar.GridView | Grid view with a month rail. Accepts renderCard. |
| Calendar.ListView | List view. Accepts renderCard. |
useCalendar(options)
Options match the props above (events, isLoading, isError, defaultView,
monthCount). It returns:
| Field | Type | Description |
| -------------------------- | ------------------------------------- | ------------------------------------------------------ |
| view | "grid" \| "list" | Current view. |
| setView | (view) => void | Switch the view. |
| months | number[] | The rolling window of month indexes (0–11). |
| selectedMonth | number | Currently selected month index. |
| setSelectedMonth | (month) => void | Select a month. |
| getMonthYear | (month) => number | The calendar year a windowed month belongs to. |
| goToNextWindow / goToPreviousWindow | () => void | Page the window forward/back. |
| search / setSearch | string / (s) => void | Current search string and its setter. |
| filters | Record<string, boolean> | Map of event type to whether it is shown. |
| availableTypes | string[] | Distinct event types found in events. |
| toggleType | (type) => void | Toggle one type on/off. |
| setAllTypes | (enabled) => void | Show or hide every type. |
| allSelected | boolean | Whether every type is shown. |
| noneSelected | boolean | Whether every type is hidden. |
| visibleEvents | CalendarEvent[] | Events for selectedMonth after filtering. |
| eventsInMonth(month) | (month) => CalendarEvent[] | Filtered events for a specific month. |
| monthHasAnyEvents(month) | (month) => boolean | Whether a month has any events (ignoring filters). |
The pure helpers filterEvents, monthWindow, deriveEventTypes, the date
formatters (prettyDate, prettyLongDate, ...), and cn are also exported.
CalendarEvent (schema.org-aligned)
Field names follow schema.org/Event so standards-
compliant data is close to drop-in. Only id, name, eventType and
startDate are required.
| Field | Type | Description |
| --------------------------- | -------------------------- | ---------------------------------------------------- |
| id | string | Unique identifier (required). |
| name | string | Event title (required). |
| eventType | string | Filter category, e.g. "football" (required). |
| startDate | string | ISO date or datetime (required). |
| endDate | string | ISO end, for multi-day ranges. |
| description | string | Short details. |
| url | string | Link used by the card action. |
| image | string | Thumbnail/banner URL (schema.org image). |
| eventStatus | EventStatus | scheduled | cancelled | postponed | rescheduled. |
| location | EventLocation \| string | Venue (schema.org location). |
| performers | string[] | Teams / competitors (schema.org performer). |
| offers | EventOffer | { price, priceCurrency, availability } (schema.org).|
| maximumAttendeeCapacity | number | Total capacity. |
| remainingAttendeeCapacity | number | Seats left; drives the "Sold out" badge. |
| color / textColor | string | Tailwind class overrides for the accent. |
Live and Completed are derived from the dates, and Full/Sold out from
capacity — exported as isLive, isCompleted, isFull (plus formatPrice,
locationName).
Drop in schema.org JSON-LD
fromSchemaOrg() maps one or many schema.org Events (incl. SportsEvent,
MusicEvent, …), tolerating nested Place/Offer/Person objects, arrays,
and URL-form enums:
import { Calendar, fromSchemaOrg } from "react-calendar";
const events = fromSchemaOrg(jsonLd); // straight from your CMS / page markup
<Calendar events={events} />;Development
yarn dev # run the example site
yarn test # unit and component tests (Vitest + Testing Library)
yarn test:e2e # end-to-end tests (Playwright)
yarn build # build the example site
yarn build:lib # build the publishable library (ESM + CJS + type declarations)The example site under src/demo is a full documentation site (interactive
playground, tabbed live usage examples with Shiki-highlighted code, theming,
dark mode, API reference, and a live in-frame mobile preview). The library
itself lives in src/lib.
