react-native-super-calendar
react-native-super-calendar
A generic, themeable month / week / day calendar for React Native.
- 📆 Month grid plus day / 3-day / week / custom-N time-grids
- 🤏 Zoomable week/day grid: pinch on iOS & Android, Ctrl/Cmd + scroll on web (UI thread, no re-renders)
- ♾️ Virtualized, snap-paging months/weeks/days via
@legendapp/list - 🧩 Bring-your-own event type (
CalendarEvent<T>) and arenderEventescape hatch - 🗓️ Date selection (single / multiple / range via
useDateRange), disabled days, and a scrollingMonthList - 🪝 Headless
useMonthGridhook to build a fully custom calendar - 🎨 Fully themeable, with sensible defaults (no styling library required)
- 🌐 Runs on iOS, Android and web (web via react-native-web; see Web)
This is a ground-up reimagining inspired by the excellent
react-native-big-calendar.
It keeps the familiar month/week/day model but is built around Reanimated and
modern list virtualization — trading framework-agnosticism for a richer,
gesture-driven experience. It's not a fork; the API differs, and the name is
an homage. 🙇
Already using react-native-big-calendar? The migration guide has a copy-paste prompt for your coding agent plus a manual prop mapping.
At a glance
| Capability | react-native-super-calendar | react-native-big-calendar |
|---|---|---|
| Month / week / day / 3-day / custom / agenda | ✅ | ✅ |
Generic event typing (CalendarEvent<T>) |
✅ | ✅ |
| Virtualized, snap-paged views | ✅ | ❌ renders all dates |
| Pinch-to-zoom (native) / Ctrl-scroll (web) | ✅ | ❌ |
| Drag to move & resize events | ✅ | ❌ (declined upstream) |
| Date selection (single / range / multiple) | ✅ useDateRange + disabled days |
❌ |
Headless grid hook (useMonthGrid) |
✅ | ❌ |
| Overlapping events | ✅ side-by-side columns | ⚠️ stacked / indented |
Month paging fires onChangeDate |
✅ | ⚠️ known gaps |
| Recurring events | ✅ expandRecurringEvents |
❌ expand them yourself |
| Time-zone display | ✅ eventsInTimeZone |
❌ |
| Dark mode | ✅ darkTheme preset |
❌ bring your own palette |
renderEvent across every mode & event type |
✅ | ⚠️ breaks for all-day/multi-day |
| Web | ✅ arrow-key paging, Ctrl-scroll zoom | ⚠️ partial |
| Runtime dependencies | Reanimated + Gesture Handler + Legend List | dayjs + calendarize (lighter) |
Legend: ✅ supported · ⚠️ partial or with known issues · ❌ not available. The last row is the honest trade-off: big-calendar has a smaller footprint and fewer native peers, so it can be the simpler choice when you don't need the gestures, virtualization, or the helpers above.
What it adds over react-native-big-calendar
- 🤏 Pinch-to-zoom time grid — row height is a Reanimated shared value, so zooming runs on the UI thread with zero React re-renders.
- ♾️ Virtualized, snap-paged views (via
@legendapp/list) — swipe across years of dates, with native one-page paging (or opt intofreeSwipe). - 🧩 Generic events + render-prop component —
CalendarEvent<T>carries your own fields, andrenderEventis a component (so it may use hooks) that receives the event box's live pixel height for progressive disclosure as the grid zooms.
Feature parity. It also covers the rest of react-native-big-calendar's
surface: month / day / 3-day / week / custom N-day (and weekEndsOn
partial-weeks) / agenda (schedule) modes, all-day events (lane +
allDay flag, toggle the lane with showAllDayEventCell), multi-day clipping,
minHour/maxHour, ampm (hour axis and event times), showTime, timeslots,
hideHours, showWeekNumber, weekNumberPrefix, hourComponent,
sortedMonthView, moreLabel, showAdjacentMonths, showSixWeeks,
disableMonthEventCellPress, a default month weekday header
(renderHeaderForMonthView), a custom month date badge
(renderCustomDateForMonth), activeDate, per-event
disabled, onPress/onLongPress for events, cells and date headers,
onChangeDateRange, resetPageOnPressCell, swipeEnabled,
verticalScrollEnabled, showVerticalScrollIndicator, an agenda
itemSeparatorComponent, eventCellStyle, calendarCellStyle, a
headerComponent slot, date-fns locale, right-to-left column order (isRTL),
and theming. Text styling that big-calendar exposes via calendarCellTextStyle
is covered by CalendarTheme.text; overlapping events are laid out in
side-by-side columns automatically.
Trade-offs (where react-native-big-calendar may suit you better)
- It's opinionated about peers: Reanimated, Gesture Handler and
@legendapp/listare required.react-native-big-calendaris more self-contained (no Reanimated/Gesture Handler). - RTL is cosmetic (
isRTLreverses the day-column order, like big-calendar's): the hour gutter stays on the left and paging follows the system scroll direction. Enable React Native'sI18nManagerfor full RTL.
Relationship to flash-calendar
The date-picker surface (MonthList, useDateRange, and the headless
useMonthGrid) is inspired by
flash-calendar, an excellent
headless date picker for React Native. If you only need date selection,
flash-calendar is the lighter, more focused choice: a dedicated, FlashList-based
picker with no event model. react-native-super-calendar folds picking into a
full gesture calendar, so one library covers events and date selection, at the
cost of the Reanimated, Gesture Handler, and Legend List peers. Pick
flash-calendar for a standalone picker; pick this when you also need the event
views.
Install
npm install react-native-super-calendar
Peer dependencies
The full calendar relies on the following being installed in your app:
npm install react-native-reanimated react-native-worklets react-native-gesture-handler @legendapp/list date-fns
Make sure Reanimated and Gesture Handler are set up per their own docs (Babel
plugin, GestureHandlerRootView at the root of your app).
These are declared as optional peers so web-only installs (the /dom and
/picker entry points) aren't asked to install React Native packages they don't
use. The full calendar still needs them: because its components import Reanimated
and Gesture Handler directly, a missing one surfaces as a clear Metro
Unable to resolve "react-native-reanimated" build error rather than a silent
failure, so install the line above when you use Calendar or the time grid.
Picker only? Skip Reanimated
If you only need date selection, import it from the
react-native-super-calendar/picker entry point. It contains the month grid,
selection, and the headless useMonthGrid, with none of the timetable code and
no Reanimated dependency, so it works on every bundler (Metro included) without
shipping the week/day grid. A picker-only app installs just:
npm install react-native-gesture-handler @legendapp/list date-fns
import { MonthList, useDateRange } from "react-native-super-calendar/picker";
react-native-reanimated and react-native-worklets are declared as optional
peers, so this entry point won't pull them in. (Metro doesn't tree-shake the main
barrel, so the subpath is what guarantees the timetable code is left out.)
React DOM (web without React Native)
For a plain react-dom app (no React Native, no react-native-web), import the
react-native-super-calendar/dom entry point. It ships real DOM components,
MonthView, MonthList (the date picker), and TimeGrid (day/week/N-day, with
Ctrl/⌘-scroll and pinch zoom plus drag to move and resize), built on the same
pure core and Legend List's DOM renderer. A web app installs just:
npm install react react-dom @legendapp/list date-fns
import { MonthList, TimeGrid, useDateRange } from "react-native-super-calendar/dom";
The React Native peers (react-native, react-native-gesture-handler,
react-native-reanimated, react-native-worklets) are all optional, so a web
install pulls none of them. Styling is plain inline styles driven by a theme
prop (defaultDomTheme / darkDomTheme), no stylesheet import required.
A selected range renders as a centered rounded "pill" band by default (its
height and colour are the rangeBandHeight / rangeBackground theme tokens).
Pass fillCellOnSelection to MonthView / MonthList to fill the whole cell
edge to edge instead.
Headless core (any renderer)
Want the date math and selection model without any of the built-in UI? The
react-native-super-calendar/headless entry point exports just the pure pieces,
buildMonthGrid / useMonthGrid, useDateRange and the selection helpers,
layoutDayEvents, and the date utilities, with zero React Native, Reanimated, or
Legend List imports. It's what the DOM components are built on, and it works in
any React renderer (react-dom, Solid via its React compat, your own).
import { buildMonthGrid, nextDateRange } from "react-native-super-calendar/headless";
Usage
import { useState } from "react";
import { Calendar, type CalendarEvent } from "react-native-super-calendar";
type MyEvent = { id: string; color: string };
const events: CalendarEvent<MyEvent>[] = [
{
id: "1",
color: "#1F6FEB",
title: "Lecture",
start: new Date(2026, 5, 19, 10, 0),
end: new Date(2026, 5, 19, 11, 30),
},
];
export function MyCalendar() {
const [mode, setMode] = useState<"month" | "week" | "day">("week");
const [date, setDate] = useState(new Date());
return (
<Calendar
mode={mode}
date={date}
events={events}
weekStartsOn={1}
onChangeDate={setDate}
onPressEvent={(event) => console.log(event.id)}
onPressDay={(day) => {
setDate(day);
setMode("day");
}}
/>
);
}
Custom events
The built-in renderer draws a simple titled box. Pass renderEvent — a React
component, not a callback — to take full control. Because it's rendered as a
component, it may use hooks. The same renderer is used in every mode — month
chips, the all-day lane, the timed grid and the schedule list — and always
receives isAllDay (plus continuesBefore/continuesAfter for clipped
multi-day segments on the grid), so one component covers them all. On the
week/day grid you also receive boxHeight, a Reanimated shared value tracking
the live pixel height of the box (driven by pinch-zoom), so you can reveal
detail progressively without re-rendering:
import Animated, { useAnimatedStyle } from "react-native-reanimated";
import { Pressable, Text } from "react-native";
import type { RenderEventArgs } from "react-native-super-calendar";
// Define the component once (don't inline it, or it remounts every render).
function MyEvent({ event, boxHeight, onPress }: RenderEventArgs<MyEvent>) {
const detailStyle = useAnimatedStyle(() => ({
display: (boxHeight?.value ?? Infinity) >= 84 ? "flex" : "none",
}));
return (
<Pressable style={{ flex: 1, backgroundColor: event.color }} onPress={onPress}>
<Text>{event.title}</Text>
<Animated.View style={detailStyle}>
<Text>{event.start.toLocaleTimeString()}</Text>
</Animated.View>
</Pressable>
);
}
<Calendar /* ... */ renderEvent={MyEvent} />;
The built-in renderer hard-clips a title that overflows its box. Pass
ellipsizeTitle to <Calendar> for a trailing ellipsis (…) instead.
Drag to move and resize
Pass onDragEvent to make events draggable on the week/day grid. Move an event
(long-press it on native, click-drag it on web) — drag vertically to
change the time, horizontally to move it to another day (within the visible
range) — or drag the grip at its bottom edge to resize. The handler receives
the new start/end, snapped to dragStepMinutes (default 15) — update your own
event state in response. On web a plain click still selects and right-click still
fires, so drag coexists with both:
<Calendar
/* ... */
onDragEvent={(event, start, end) =>
setEvents((prev) => prev.map((e) => (e.id === event.id ? { ...e, start, end } : e)))
}
/>
Reject a drop. Return false from onDragEvent to refuse the new placement
— the event snaps back to where it started. Use it to forbid overlaps,
out-of-bounds slots, or locked events:
onDragEvent={(event, start, end) => {
if (event.locked || overlapsAnother(event, start, end)) return false;
setEvents((prev) => prev.map((e) => (e.id === event.id ? { ...e, start, end } : e)));
}}
Haptics on grab. onDragStart fires the instant an event is picked up for a
move or resize, before anything is committed. The library stays expo-free, so
bring your own haptics, e.g. expo-haptics:
import * as Haptics from "expo-haptics";
<Calendar
/* ... */
onDragStart={() => {
void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
}}
/>;
Drag to create. Pass onCreateEvent to sweep out a new event on empty grid
space: long-press and drag on native, click-drag on web. The handler
receives the snapped start/end on release (a stationary press yields a
one-step range) — create your own event in response. On native it supersedes
onLongPressCell on empty space; on web, dragging empty space creates instead of
scrolling (use the wheel to scroll), matching desktop calendars, and Escape
cancels an in-progress sweep before it commits.
<Calendar
/* ... */
onCreateEvent={(start, end) =>
setEvents((prev) => [...prev, { id: makeId(), title: "New event", start, end }])
}
/>
Recurring events
Give an event a recurrence rule and expand it into concrete occurrences for the
range you're showing with expandRecurringEvents. The calendar doesn't expand
recurrences itself, so you control the window (and can memoize it):
import { Calendar, expandRecurringEvents } from "react-native-super-calendar";
const events = [
// Every weekday standup, 20 occurrences:
{
title: "Standup",
start,
end,
recurrence: { freq: "weekly", weekdays: [1, 2, 3, 4, 5], count: 20 },
},
];
const visible = expandRecurringEvents(events, rangeStart, rangeEnd);
<Calendar /* ... */ events={visible} />;
Rules support freq (daily/weekly/monthly/yearly), interval, count,
until, and weekdays (for weekly). Each occurrence keeps the original
duration and fields; non-recurring events pass through unchanged.
Date selection
Date picking lives on MonthList, the vertically-scrolling month list (the
horizontally-paged month view is for browsing events, not picking). The
selection renders as a background band across the span, with no per-day circle,
so it stays distinct from the "today" badge. For ranges, the useDateRange hook
owns the state machine: the first press sets the start, the second sets the end
(auto-swapping if earlier), a third press starts over. Tap two days, or
long-press and drag to sweep a range (the list auto-scrolls at the edges, so a
range can span months):
import { MonthList, useDateRange } from "react-native-super-calendar";
function RangePicker() {
const [date, setDate] = useState(new Date());
const { range, onPressDate, selectRange } = useDateRange();
return (
<MonthList
date={date}
weekStartsOn={1}
selectedRange={range ?? undefined}
onPressDay={onPressDate}
onSelectDrag={selectRange}
onChangeVisibleMonth={setDate}
/>
);
}
Use selectedDates to mark discrete days instead of a range. The band colour is
the rangeBackground theme token.
Disabled days. minDate, maxDate and isDateDisabled render days dimmed,
ignore taps, and keep them out of any selection (drag included). Hand the same
constraints to useDateRange so a blocked day never opens a range:
const minDate = useMemo(() => new Date(), []); // no past dates
const { range, onPressDate, selectRange } = useDateRange({ minDate });
<MonthList
date={date}
weekStartsOn={1}
selectedRange={range ?? undefined}
minDate={minDate}
isDateDisabled={(d) => d.getDay() === 0} // also block Sundays
onPressDay={onPressDate}
onSelectDrag={selectRange}
/>;
Month list
MonthList is the continuous, virtualized vertical scroll of months behind the
picker above (a month title then its grid, under a fixed weekday header), sized
per month with no adjacent-month fill. It also renders events: pass events,
and optionally a renderEvent. Both renderEvent and keyExtractor default,
so an events-free picker needs neither:
import { MonthList } from "react-native-super-calendar";
<MonthList
date={new Date()}
events={events}
weekStartsOn={1}
renderEvent={MyEvent}
keyExtractor={(event) => event.id}
onChangeVisibleMonth={setDate}
/>;
Headless month grid
Want your own day-cell markup but not the date maths? useMonthGrid(month, options) returns the weeks, the weekday headers, and per-day state
(isToday/isSelected/isInRange/isDisabled/isCurrentMonth/…) for you to
render however you like:
import { useMonthGrid } from "react-native-super-calendar";
const { weeks, weekdays } = useMonthGrid(month, { weekStartsOn: 1, selectedRange: range });
// weekdays -> header cells; weeks[].days -> your own <DayCell />
Need it outside React (tests, exports)? Call the pure buildMonthGrid(month, options); the hook is just a memoized wrapper. buildMonthWeeks(month, weekStartsOn) returns the raw Date[][].
Time zones
Events lay out from their local wall-clock time. To display them in a specific
IANA zone regardless of the device, run them through eventsInTimeZone (or a
single date through toZonedTime). It's DST-correct via Intl:
import { Calendar, eventsInTimeZone } from "react-native-super-calendar";
// Render every event at its New York wall-clock time.
<Calendar /* ... */ events={eventsInTimeZone(events, "America/New_York")} />;
The returned dates are for display only — they carry the zone's wall clock, not the original instant, so keep your source events around for editing/saving.
Theming
<Calendar
// ...
theme={{
colors: { todayBackground: "#E5484D", nowIndicator: "#E5484D" },
text: { dayNumber: { fontSize: 24, fontWeight: "800" } },
}}
/>
See CalendarTheme for the full set of tokens. Anything you omit falls back to
defaultTheme.
For dark mode, pass the built-in darkTheme (switch on the system scheme with
useColorScheme()):
import { Calendar, darkTheme, defaultTheme } from "react-native-super-calendar";
import { useColorScheme } from "react-native";
const scheme = useColorScheme();
<Calendar /* ... */ theme={scheme === "dark" ? darkTheme : defaultTheme} />;
Modes
mode is one of month, week, day, 3days, custom, or schedule. For
custom, set numberOfDays (e.g. mode="custom" numberOfDays={5} for a
work-week). Day/3-day/custom views page by their column count; week pages by
the calendar week. schedule is a vertical, day-grouped agenda list of the
events you pass (no time grid).
Month view
Each day cell shows as many event chips as its height allows and collapses the
rest into a +N more label (tap it via onPressMore). The fit is measured at
runtime, so taller grids (fewer week rows, larger screens) show more.
<Calendar mode="month" /* ... */ /> // auto-fit (default)
<Calendar mode="month" maxVisibleEventCount={3} /* ... */ /> // fixed cap
Pass maxVisibleEventCount for a fixed cap instead — recommended when you pass a
custom renderEvent, since auto-fit assumes the built-in chip height. Customize
the overflow text with moreLabel (e.g. "+{moreCount}").
Localization
Pass a date-fns Locale to localize weekday and
date labels:
import { fr } from "date-fns/locale";
<Calendar /* ... */ locale={fr} weekStartsOn={1} />;
Pass isRTL to reverse the day-column order in every view (month grid, week/day
grid and the all-day lane). It's cosmetic — the hour gutter stays on the left and
paging follows the system scroll direction — so enable React Native's
I18nManager alongside it for full right-to-left behaviour.
<Calendar /* ... */ isRTL locale={ar} weekStartsOn={6} />
Week/day grid options
<Calendar
mode="week"
// ...
minHour={7} // window the grid to 07:00–21:00
maxHour={21}
ampm // 12-hour hour labels ("7 AM")
onPressCell={(date) => createEventAt(date)} // tap empty space -> date+time
/>
minHour/maxHourclamp the visible hours (defaults0/24); events and the now-line outside the window are hidden, and the initial scroll is adjusted.ampmswitches hour labels to 12-hour AM/PM (default 24h).onPressCell(date)fires when empty grid space is tapped, with the date+time under the touch — handy for "create event". (Event taps still go toonPressEvent.)- Long-press mirrors every tap:
onLongPressEvent(event),onLongPressCell(date)(week/day), andonLongPressDay(date)(month). All optional. - All-day events render in a lane above the time grid (and as chips in month
cells), excluded from the timed columns. Mark an event
allDay: true, or it's inferred when it spans whole days (midnight-to-midnight).renderEventreceivesisAllDayso you can style the chip. The lane is hidden when there are none. freeSwipe(defaultfalse) controls paging: by default one day/week/month moves per swipe; set it to allow a fling to carry across several pages (still snapping to a page boundary). Applies to all modes.
Business hours
Pass businessHours to tint the closed hours on the week/day grid. It's a
function of the day, so open hours can vary (and weekends can read as closed) —
return { start, end } (hours, fractions allowed) to shade outside that range,
or null to shade the whole day. The tint colour is the theme's
outsideHoursBackground.
<Calendar
/* ... */
businessHours={(date) => {
const weekday = date.getDay();
if (weekday === 0 || weekday === 6) return null; // weekends closed
return { start: 9, end: 17 };
}}
/>
Web
The calendar runs on react-native-web;
its dependencies (@legendapp/list v3, Reanimated and Gesture Handler) all
support web. Add the web peers to your app:
npx expo install react-dom react-native-web @expo/metro-runtime
All modes render and navigate. Two touch gestures are remapped for web:
horizontal swipe paging becomes ← / → arrow-key paging (previous / next
page), and pinch-to-zoom on the week/day grid becomes Ctrl/Cmd + scroll. The
runnable example/ builds with expo start --web.
If a renderEvent wraps events in a portaling overlay (a context menu, popover,
etc.) from a UI library, portal it into your app's React root, not
document.body. react-native-web registers React's event delegation on the root
element (#root under Expo), so an overlay mounted outside it renders correctly
but its click handlers never fire. Most libraries take a container prop for
this; the example's context menu portals into #root for exactly this reason.
Components
<Calendar> is the batteries-included entry point. The building blocks it wraps
are also exported for advanced layouts:
| Export | Description |
|---|---|
Calendar |
Top-level component; switches between month/week/day. |
MonthView |
A single month grid. |
MonthPager |
Horizontally-paged, virtualized months. |
MonthList |
Vertically-scrolling, continuous list of months. |
TimeGrid |
Paged, pinch-zoomable week/day time-grid. |
DefaultEvent |
The built-in event renderer. |
useDateRange |
Range-selection state machine for the month view. |
useMonthGrid |
Headless grid data (weeks + per-day state) for custom UIs. |
useCalendarTheme |
Read the active theme inside a custom renderer. |
Notes & limitations
- Multi-day events are supported: pass one event and it appears on every day
it spans. On the week/day grid each day shows the clipped segment (so a
23:00→01:00 event renders 23:00–24:00, then 00:00–01:00), and
renderEventreceivescontinuesBefore/continuesAfterso you can draw continuation hints. All-day events (an explicitallDayflag or a midnight-to-midnight span) render in a dedicated lane above the time grid. weekStartsOndefaults to0(Sunday). Pass1for Monday-first.- Controlled
date. The calendar is controlled: echoonChangeDateback into thedateprop, or paging and the "today" realign won't track. - External
cellHeight. If you owncellHeight, drive zoom through the pinch gesture. Programmatic writes outside a pinch won't propagate to off-screen pages until the next gesture settles. - Stable props. Pass stable
renderEvent/keyExtractor/on*references (module scope oruseCallback) so the memoized inner views can skip renders.
Example app
A runnable Expo demo lives in example/ — month/week/day modes, a
multi-day event, drill-into-day on tap, and one-page paging.
cd example
pnpm install
pnpm expo run:ios # or: pnpm expo run:android
It consumes the library straight from ../src (via the example's
metro.config.js), so edits to the package hot-reload into the demo. A custom
dev build is required (Reanimated worklets aren't available in Expo Go).
License
MIT
