npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-native-headless-calendar

v0.2.0

Published

A fast, headless, calendar-system-agnostic (Gregorian, Hijri, Jalali, or your own) React Native calendar for building fully custom date pickers, range pickers, and multi-date calendars.

Readme

react-native-headless-calendar

Build your own React Native calendar without rebuilding calendar logic.

react-native-headless-calendar gives you typed providers, tiny selector hooks, fast date-grid state, and calendar-system adapters for Gregorian, Hijri, Jalali, or your own system. You bring the UI. The library handles selection, navigation, bounds, disabled days, modifiers, payloads, and re-render control.

Documentation · Example app · Contributing

[!WARNING] This library is currently in beta. APIs are still subject to breaking changes before a stable release.

Single, range, and multiple date calendar previews

Why developers reach for it

  • Headless by design: no shipped chrome, no theme object to fight, no fixed layout.
  • Three explicit modes: single date, date range, and multiple dates each have their own provider and typed hooks.
  • Fast cell updates: day cells subscribe to exactly the state they read through useSyncExternalStore.
  • Calendar-system ready: Gregorian is built in; Hijri and Jalali are opt-in sub-paths.
  • Friendly escape hatches: disabled rules, custom modifiers, bounds, grid utilities, month/year views, and custom calendar systems.

Works with iOS, Android, and React Native Web.

Install

yarn add react-native-headless-calendar
npm install react-native-headless-calendar

Peer requirements:

| Peer | Minimum | | -------------- | ---------- | | react | >=18.0.0 | | react-native | >=0.70.0 |

Optional calendar-system peers:

| System | Install | Import | | ------ | ------------------------------------ | ----------------------------------------------- | | Hijri | yarn add @tabby_ai/hijri-converter | react-native-headless-calendar/systems/hijri | | Jalali | yarn add moment-jalaali | react-native-headless-calendar/systems/jalali |

60-second calendar

The library exposes state. Your components decide how it looks.

Quick start calendar example preview

import { memo, useCallback } from 'react';
import { Pressable, Text, View } from 'react-native';
import {
  SingleDateProvider,
  selectSingleDays,
  useSingleCalendarActions,
  useSingleCalendarSelector,
  type SingleDayCellInfo,
} from 'react-native-headless-calendar';

export function Calendar() {
  return (
    <SingleDateProvider onChange={(selection) => console.log(selection.date)}>
      <View style={{ gap: 12 }}>
        <CalendarHeader />
        <Weekdays />
      </View>
      <Days />
    </SingleDateProvider>
  );
}

function CalendarHeader() {
  const title = useSingleCalendarSelector(
    (s) => s.days.displayedMonthLabel + ' ' + s.days.displayedYearLabel
  );
  const { goPrevMonth, goNextMonth } = useSingleCalendarActions();

  return (
    <View
      style={{
        flexDirection: 'row',
        justifyContent: 'space-between',
        width: 280,
      }}
    >
      <Pressable onPress={goPrevMonth}>
        <Text>Prev</Text>
      </Pressable>
      <Text>{title}</Text>
      <Pressable onPress={goNextMonth}>
        <Text>Next</Text>
      </Pressable>
    </View>
  );
}

function Weekdays() {
  const labels = useSingleCalendarSelector((s) => s.days.weekdayLabels);

  return (
    <View style={{ flexDirection: 'row' }}>
      {labels.map((label) => (
        <Text key={label} style={{ width: 40, textAlign: 'center' }}>
          {label.slice(0, 2)}
        </Text>
      ))}
    </View>
  );
}

function Days() {
  const days = useSingleCalendarSelector(selectSingleDays);
  const { selectDate } = useSingleCalendarActions();

  const onPress = useCallback(
    (cell: SingleDayCellInfo) => selectDate(cell.date),
    [selectDate]
  );

  return (
    <View style={{ flexDirection: 'row', flexWrap: 'wrap', width: 280 }}>
      {days.cells.map((cell) => (
        <DayCell
          key={cell.nativeDate.toISOString()}
          cell={cell}
          onPress={onPress}
        />
      ))}
    </View>
  );
}

const DayCell = memo(function DayCell({
  cell,
  onPress,
}: {
  cell: SingleDayCellInfo;
  onPress: (cell: SingleDayCellInfo) => void;
}) {
  return (
    <Pressable
      disabled={cell.isDisabled}
      onPress={() => onPress(cell)}
      style={{
        width: 40,
        height: 40,
        alignItems: 'center',
        justifyContent: 'center',
        opacity: cell.isCurrentMonth ? 1 : 0.35,
        backgroundColor: cell.isSelected ? '#111827' : 'transparent',
        borderRadius: 20,
      }}
    >
      <Text style={{ color: cell.isSelected ? 'white' : '#111827' }}>
        {cell.label}
      </Text>
    </Pressable>
  );
});

That is the core mental model:

  1. Wrap your UI in a mode-specific provider.
  2. Read small pieces of state with a selector hook.
  3. Trigger changes with an actions hook.
  4. Render every pixel yourself.

Selection modes

Choose the provider that matches the product behavior. No mode prop, no runtime guessing.

| Use case | Provider | Read hook | Action hook | | ------------------ | ---------------------- | ----------------------------- | ---------------------------- | | One date | SingleDateProvider | useSingleCalendarSelector | useSingleCalendarActions | | Start and end date | RangeDateProvider | useRangeCalendarSelector | useRangeCalendarActions | | Many dates | MultipleDateProvider | useMultipleCalendarSelector | useMultipleCalendarActions |

Range mode adds initialStart, initialEnd, allowSameDay, minRangeDays, maxRangeDays, and disabledInRangeBehavior.

Multiple mode adds initialDates and maxSelected.

Range picker example

import {
  RangeDateProvider,
  selectRangeDays,
  useRangeCalendarActions,
  useRangeCalendarSelector,
} from 'react-native-headless-calendar';

function BookingCalendar() {
  return (
    <RangeDateProvider
      minDate={new Date()}
      allowSameDay
      maxRangeDays={14}
      disabledInRangeBehavior="reject"
      onConfirm={({ gregorianStartDate, gregorianEndDate }) => {
        console.log({ gregorianStartDate, gregorianEndDate });
      }}
    >
      <RangeGrid />
    </RangeDateProvider>
  );
}

function RangeGrid() {
  const days = useRangeCalendarSelector(selectRangeDays);
  const canConfirm = useRangeCalendarSelector(
    (s) => !!s.rangeStart && !!s.rangeEnd
  );
  const { selectDate, confirm } = useRangeCalendarActions();

  return (
    <>
      {days.cells.map((cell) => (
        <Pressable
          key={cell.nativeDate.toISOString()}
          disabled={cell.isDisabled}
          onPress={() => selectDate(cell.date)}
        >
          <Text>
            {cell.label}
            {cell.isRangeStart ? ' start' : ''}
            {cell.isRangeEnd ? ' end' : ''}
            {cell.isInRange ? ' in range' : ''}
          </Text>
        </Pressable>
      ))}

      <Pressable disabled={!canConfirm} onPress={confirm}>
        <Text>Confirm</Text>
      </Pressable>
    </>
  );
}

Built for quiet performance

Selecting a day re-renders only the cells whose state changed. The rest of the grid keeps its identity.

React DevTools flamegraph showing zero unnecessary re-renders

What makes that work:

  • use*CalendarSelector subscribes through useSyncExternalStore.
  • use*CalendarActions returns stable functions and does not subscribe.
  • days.cells reuse object identity for unchanged cells.
  • React.memo day cells can skip untouched dates naturally.

Rule of thumb: components that only call actions should use use*CalendarActions; components that render state should use the selector hook.

Calendar systems

Gregorian is available from the root export. Hijri and Jalali are optional sub-paths so apps only install what they use.

import {
  SingleDateProvider,
  gregorianSystem,
} from 'react-native-headless-calendar';
import { hijriSystem } from 'react-native-headless-calendar/systems/hijri';
import { jalaliSystem } from 'react-native-headless-calendar/systems/jalali';

export function MultiSystemCalendar() {
  return (
    <SingleDateProvider
      systems={[gregorianSystem, hijriSystem, jalaliSystem]}
      activeSystemId="hijri"
    >
      <MyCalendar />
    </SingleDateProvider>
  );
}

Switch at runtime with setActiveSystem(id):

function SystemSwitch() {
  const { setActiveSystem } = useSingleCalendarActions();

  return (
    <View>
      <Pressable onPress={() => setActiveSystem('gregorian')}>
        <Text>Gregorian</Text>
      </Pressable>
      <Pressable onPress={() => setActiveSystem('hijri')}>
        <Text>Hijri</Text>
      </Pressable>
      <Pressable onPress={() => setActiveSystem('jalali')}>
        <Text>Jalali</Text>
      </Pressable>
    </View>
  );
}

Selections and bounds are carried across systems by absolute native Date, while labels and date parts come from the active system.

Disabled dates and modifiers

Use bounds, explicit dates, ranges, predicates, and named modifiers to keep product rules close to the provider.

<SingleDateProvider
  minDate="2026-01-01"
  maxDate="2026-12-31"
  disabledDates={['2026-02-14']}
  disabledRanges={[{ start: '2026-04-01', end: '2026-04-05' }]}
  disabled={(date) => date.getDay() === 0}
  modifiers={{
    weekend: (date) => date.getDay() === 5 || date.getDay() === 6,
    payday: ['2026-01-31', '2026-02-28'],
  }}
>
  <MyCalendar />
</SingleDateProvider>

Each day cell receives:

cell.isDisabled;
cell.modifiers.weekend;
cell.modifiers.payday;

For ranges that cross disabled interior days, disabledInRangeBehavior controls the result:

| Value | Behavior | | --------- | --------------------------------------------------------------------- | | reject | Ignore the second tap and keep the current rangeStart. | | include | Store the full range; disabled interior cells remain marked disabled. | | exclude | Clamp rangeEnd to the day before the first disabled interior day. |

API map

Providers

| Provider | Initial selection props | Extra props | | ---------------------- | ---------------------------- | ------------------------------------------------------------------------- | | SingleDateProvider | initialDate | - | | RangeDateProvider | initialStart, initialEnd | allowSameDay, minRangeDays, maxRangeDays, disabledInRangeBehavior | | MultipleDateProvider | initialDates | maxSelected |

Shared provider props:

systems, activeSystemId, minDate, maxDate, disabledDates, disabledRanges, disabled, modifiers, firstDayOfWeek, onChange, onConfirm, onClear.

firstDayOfWeek uses 0 for Sunday through 6 for Saturday and defaults to Monday (1).

Selectors

| Single | Range | Multiple | | ------------------------ | ----------------------- | -------------------------- | | selectSingleDays | selectRangeDays | selectMultipleDays | | selectSingleMonths | selectRangeMonths | selectMultipleMonths | | selectSingleYears | selectRangeYears | selectMultipleYears | | selectSingleCanConfirm | selectRangeCanConfirm | selectMultipleCanConfirm |

Every mode also supports inline selectors:

const monthLabel = useSingleCalendarSelector((s) => s.days.displayedMonthLabel);

Actions

Each mode exposes the same action names:

selectDate(input);
clear();
confirm();
goPrevMonth();
goNextMonth();
setDisplayedDate(input);
selectMonth(index);
selectYear(year);
prevYearPage();
nextYearPage();
setActiveSystem(id);
isConfirmable();

Selection payloads

Callbacks receive native Date values plus active-system date parts and systemId.

type SingleSelectionPayload = {
  gregorianDate: Date | undefined;
  systemId: string;
  system: DateParts | undefined;
};

Range and multiple payloads follow the same idea with start/end or date arrays.

Advanced building blocks

The package exports grid utilities for custom layouts and tests:

import {
  buildMonthGrid,
  getYearPage,
  isBetween,
  isoWeekNumber,
  matchDate,
  rotateWeekdayLabels,
} from 'react-native-headless-calendar';

You can also implement CalendarSystem<T> to support another calendar. A system controls parsing, month math, labels, comparison, formatting, and conversion to/from native Date.

Example app

The example app includes single, range, and multiple calendars with day, month, year, and system-switching views.

yarn
yarn example start

Documentation

License

MIT