A flexible, performant, and themeable React Native calendar for scheduling apps
npm install react-native-resource-calendarA flexible, performant, and themeable React Native calendar for scheduling apps β built with Zustand, Reanimated, and
Expo compatibility.
---
- β
Multi-resource/multi-days timeline layout
- π¨ Customizable event slots (Body, TopRight)
- π± Smooth Reanimated drag-and-drop
- πͺΆ Lightweight and Expo-ready
---
https://github.com/user-attachments/assets/68fe0283-73ce-4689-8241-6587b817ecbd
---
This library supports multiple Expo SDK versions via npm dist-tags.
β
Expo SDK 54 (default / latest)
``bash`
npm install react-native-resource-calendaror
yarn add react-native-resource-calendar
This installs the latest release, compatible with Expo SDK 54.
π§ Expo SDK 53 (legacy)
`bash`
npm install react-native-resource-calendar@legacyor
yarn add react-native-resource-calendar@legacy
Use this if your app is still running Expo SDK 53.
This library relies on several React Native ecosystem packages that must be installed in your app.
If youβre using Expo, run the following to ensure compatible versions:
`bash`
npx expo install \
react-native-gesture-handler \
react-native-reanimated \
react-native-svg \
@shopify/flash-list \
@shopify/react-native-skia
If youβre using bare React Native (not Expo), install them manually:
`bash`
npm install \
react-native-gesture-handler \
react-native-reanimated \
react-native-svg \
@shopify/flash-list \
@shopify/react-native-skia
π¦ Optional: Haptics Support (Expo Only)
Haptic feedback is optional.
If you want to enable vibration feedback when interacting with components, install the Expo Haptics package and set
enableHapticFeedback to true in your component config.
π¦ Install (Expo)
`bash`
npx expo install expo-haptics
---
Follow these steps to get started quickly with React Native Resource Calendar.
`tsx
import React from 'react';
import {StyleSheet, TouchableOpacity, View} from 'react-native';
import {Calendar, DraggedEventDraft, Event, LayoutMode, useCalendarBinding} from "react-native-resource-calendar";
import {SafeAreaView} from "react-native-safe-area-context";
import {ThemedText} from "@/components/ThemedText";
import EventTopRight from "@/components/EventTopRight";
import {FontAwesome} from "@expo/vector-icons";
import {statusColor} from "@/utilities/helpers";
import {resourceData} from "@/assets/fakeData";
export default function App() {
const {
useGetSelectedEvent,
useSetSelectedEvent,
useGetDraggedEventDraft
} = useCalendarBinding();
const selectedEvent = useGetSelectedEvent();
const setSelectedEvent = useSetSelectedEvent();
const draggedEventDraft = useGetDraggedEventDraft();
const [date, setDate] = React.useState(new Date());
const [resources, setResources] = React.useState(resourceData);
const [hourHeight, setHourHeight] = React.useState(120);
const [numberOfColumns, setNumberOfColumns] = React.useState(3);
const [layoutMode, setLayoutMode] = React.useState
const updateResourcesOnDrag = React.useCallback(
(draft: DraggedEventDraft) => {
setResources((prev: any) => {
const {event, from, to, resourceId, date} = draft;
return prev.map((res: any) => {
if (res.id === resourceId) {
// was the event originally in a different resource?
const wasDifferentResource = event.resourceId !== resourceId;
// clone event with new times and resourceId
const updatedEvent = {
...event,
from,
to,
resourceId,
date
};
return {
...res,
events: wasDifferentResource
// if moved from another resource, append it here
? [...res.events, updatedEvent]
// else update it in place
: res.events.map((e: any) => (e.id === event.id ? updatedEvent : e)),
};
}
if (res.id === event.resourceId && event.resourceId !== resourceId) {
return {
...res,
events: res.events.filter((e: any) => e.id !== event.id),
};
}
return res;
});
});
},
[setResources]
);
const eventStyleOverrides = (event: Event) => {
const bg = statusColor(event.meta?.status)
return {container: {backgroundColor: bg}, time: {color: "black"}};
};
const randomPropsGenerator = () => {
const randomHourHeight = Math.floor(Math.random() * (120 - 60 + 1)) + 60;
const randomNumberOfColumns = Math.floor(Math.random() * (5 - 1 + 1)) + 1;
setHourHeight(randomHourHeight);
setNumberOfColumns(randomNumberOfColumns);
setLayoutMode(layoutMode === 'stacked' ? 'columns' : 'stacked');
}
const addDays = (days: number) => {
const newDate = new Date(date);
newDate.setDate(newDate.getDate() + days);
setDate(newDate);
};
return (
theme={{
typography: {
fontFamily: 'NunitoSans',
},
}}
resources={resources}
date={date}
startMinutes={8 * 60}
numberOfColumns={numberOfColumns}
hourHeight={hourHeight}
eventSlots={{
// Body: ({event, ctx}) =>
TopRight: ({event, ctx}) =>
}}
eventStyleOverrides={eventStyleOverrides}
overLappingLayoutMode={layoutMode}
/>
{
selectedEvent &&
onPress={() => {
setSelectedEvent(null);
}}
>
}}>
Cancel
onPress={() => {
if (draggedEventDraft) {
updateResourcesOnDrag(draggedEventDraft!);
}
setSelectedEvent(null);
}}
>
color: "#fff"
}}
>
Save
}
bottom: 40,
position: "absolute",
gap: 12
}}>
onPress={() => {
setDate(new Date());
}}
>
width: 16,
height: 16,
backgroundColor: "#4d959c",
borderRadius: 99
}}
/>
onPress={randomPropsGenerator}
>
);
}
`
---
The Calendar component accepts a flexible set of props for customizing layout, theme, and interactivity.
| Prop | Type | Default | Description |
|-----------------------------|------------------------------------------------------------------------------------------------------------------|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|
| date | Date | new Date() | The anchor day shown in the timeline. In multi-day modes this is the first visible day. |mode
| | CalendarMode ('day' \| '3days' \| 'week') | 'day' | Controls the column semantics. day = many resources for one day. 3days/week = several days for one resource. |activeResourceId
| | number | first resources[0].id | When mode !== 'day', columns represent days for this resource. |resources
| | Array | required | Resource columns. Each resource includes its dayβs events, optional disabledBlocks, and disableIntervals. |timezone
| | string | device time zone | Used for time labels and converting block taps to a Date. |startMinutes
| | number | 0 | Start of visible timeline in minutes after midnight (e.g. 8 * 60 = 08:00). |numberOfColumns
| | number | 3 | Day mode only. How many resource columns to show side-by-side. (In multi-day modes, the column count is fixed by the mode: 3 or 7.) |hourHeight
| | number | 120 | Vertical density, px per hour. Affects drag/resize and scroll snap. |snapIntervalInMinutes
| | number | 5 | Drag/resize snapping granularity (in minutes). |overLappingLayoutMode
| | LayoutMode ('stacked' \| 'columns') | 'stacked' | Strategy to lay out overlapping events inside a column. |theme
| | CalendarTheme | β | Typography & palette overrides. |enableHapticFeedback
| | boolean | false | Enable haptic feedback. |eventSlots
| | EventSlots | β | Slot renderers to customize event content (e.g. { Body, TopRight }). |eventStyleOverrides
| | StyleOverrides \| ((event: Event) => StyleOverrides \| undefined) | β | Per-event style override (object or function). |isEventSelected
| | (event: Event) => boolean | () => false | Marks which events are currently selected. |isEventDisabled
| | (event: Event) => boolean | () => false | Marks events as disabled (non-interactive). |onResourcePress
| | (resource: Resource) => void | β | Invoked when a resource header is pressed. |onBlockLongPress
| | (resource: Resource, date: Date) => void | β | Long-press on an empty block (grid). |onBlockTap
| | (resource: Resource, date: Date) => void | β | Tap on an empty block (grid). |onDisabledBlockPress
| | (block: DisabledBlock) => void | β | Tap on a disabled block (e.g., lunch). |onEventPress
| | (event: Event) => void | β | Tap on an event. |onEventLongPress
| | (event: Event) => void | β | Long-press on an event. The calendar also preps internal drag state here. |
---
`ts
type ResourceId = number;
type Event = {
id: number;
resourceId: ResourceId;
date: string; // 'yyyy-MM-dd' eg. '2025-11-09'
from: number;
to: number;
title?: string;
description?: string;
meta?: {
[key: string]: any;
}
};
type DisabledBlock = {
id: number;
resourceId: ResourceId;
date: string; // 'yyyy-MM-dd' eg. '2025-11-09'
from: number;
to: number;
title?: string;
};
type DisabledInterval = {
resourceId: ResourceId;
date: string; // 'yyyy-MM-dd' eg. '2025-11-09'
from: number;
to: number;
};
type Resource = {
id: ResourceId;
name: string;
avatar?: string;
};
type DraggedEventDraft = {
event: Event,
date: string; // 'yyyy-MM-dd' eg. '2025-11-09'
from: number,
to: number,
resourceId: ResourceId
}
type CalendarTheme = {
typography?: {
fontFamily?: string;
};
};
type CalendarMode = 'day' | '3days' | 'week';
``
---
If you find this project helpful or interesting, please consider giving it a βοΈ on GitHub!