Skip to main content

Introduction

react-native-headless-calendar is a headless, hooks-only calendar primitive for React Native. Pick a provider for your selection mode, read state with one hook, mutate with another — you bring every pixel of UI.

The API at a Glance

The library exports three providers (one per selection mode), two hooks per mode, and built-in selectors for the shapes you'll render most often.

ModeProviderRead hookWrite hook
Single dateSingleDateProvideruseSingleCalendarSelectoruseSingleCalendarActions
Date rangeRangeDateProvideruseRangeCalendarSelectoruseRangeCalendarActions
Multiple datesMultipleDateProvideruseMultipleCalendarSelectoruseMultipleCalendarActions
import {
SingleDateProvider,
selectSingleDays,
selectSingleCanConfirm,
useSingleCalendarActions,
useSingleCalendarSelector,
} from 'react-native-headless-calendar';

There is no bundled <Calendar>, no theme object, and no label dictionary. You render your own components with the data the hooks expose.

Quickest Possible Example

import { Pressable, Text, View } from 'react-native';
import {
SingleDateProvider,
selectSingleCanConfirm,
selectSingleDays,
useSingleCalendarActions,
useSingleCalendarSelector,
} from 'react-native-headless-calendar';

function Calendar() {
const days = useSingleCalendarSelector(selectSingleDays);
const { selectDate, goPrevMonth, goNextMonth, confirm, clear } =
useSingleCalendarActions();
const canConfirm = useSingleCalendarSelector(selectSingleCanConfirm);

return (
<View>
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<Pressable onPress={goPrevMonth}>
<Text></Text>
</Pressable>
<Text>
{days.displayedMonthLabel} {days.displayedYearLabel}
</Text>
<Pressable onPress={goNextMonth}>
<Text></Text>
</Pressable>
</View>

<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
{days.cells.map((cell) => (
<Pressable
key={cell.nativeDate.toISOString()}
onPress={() => selectDate(cell.date)}
disabled={cell.isDisabled}
style={{
width: 40,
height: 40,
opacity: cell.isCurrentMonth ? 1 : 0.4,
backgroundColor: cell.isSelected ? '#0f172a' : 'transparent',
}}
>
<Text style={{ color: cell.isSelected ? '#fff' : '#0f172a' }}>
{cell.label}
</Text>
</Pressable>
))}
</View>

<Pressable onPress={confirm} disabled={!canConfirm}>
<Text>Done</Text>
</Pressable>
<Pressable onPress={clear}>
<Text>Clear</Text>
</Pressable>
</View>
);
}

export default function Screen() {
return (
<SingleDateProvider onConfirm={({ date }) => console.log(date)}>
<Calendar />
</SingleDateProvider>
);
}

Why Three Providers?

Each mode has its own store and snapshot type. That keeps TypeScript precise (selectedDate on single, rangeStart / rangeEnd on range, selectedDates on multiple) and avoids a single mega-snapshot full of optional fields you'll never use.

Use RangeDateProvider for check-in / check-out flows and MultipleDateProvider when users pick several unrelated days.

For ranges, disabledInRangeBehavior ('reject' | 'include' | 'exclude') controls what happens when the span between start and end includes disabled days — see Providers.

Key Properties

  • Headless. Zero opinions about layout, colour, or copy.
  • Hooks-only. Two primitives per mode plus named selectors for common views.
  • Granular re-renders. useSyncExternalStore under the hood; day cells keep stable references when their state is unchanged.
  • Multi-system. Built-in Gregorian; optional Hijri and Jalali sub-paths; bring your own CalendarSystem.
  • TypeScript-first. Every hook, cell type, and payload is exported.

Where Next