react-native-agenda-kit
v0.1.0
Published
A fast expandable calendar, agenda list and day timeline for React Native — declared row heights, worklet-driven scroll sync, zero measurement.
Maintainers
Readme
react-native-agenda-kit
An expandable month/week calendar, an agenda list and a day timeline for React Native — built to stay smooth under a hard fling on a low-end Android phone.
Install · Quick start · Features · Docs · API · Migration
Three surfaces share one date owner and one integer index space, so
tapping a day, flinging the agenda, swiping the week strip and paging the
timeline all stay in sync without latches, timers or changedItems
bookkeeping.
Why another calendar
It is a rewrite of the parts of react-native-calendars that get slow, with the
same public API so a migration is a copy, not a rewrite.
- Nothing is measured. Row heights are declared per row type. A wrong
number clips inside its slot instead of shifting the list — so a fling never
jumps, and there is no
onLayoutround-trip on the critical path. - Scroll sync runs on the UI thread. Every scroll handler is a worklet; the JS thread is told only at settle. The originating scroller ignores moves it produced itself, which is the whole echo-proofing story.
- A windowed agenda with a moving backdrop. Rows entering the window during a fast fling are deferred and skeleton tiles show through their slots, so a fling that outruns JS never shows bare ground.
- No date library. Civil-date arithmetic is integer maths on epoch days. Worklets never touch strings.
- One accent, eleven colours. The whole visual surface is eleven colour tokens and three font families.
Install
npm install react-native-agenda-kit
npm install react-native-reanimated react-native-worklets react-native-gesture-handlerWith Expo, use npx expo install for the three peers so the versions match
your SDK.
Every scroll handler is a worklet, so the worklets Babel plugin must run over
this package. With Expo (babel-preset-expo) it already does. Bare React
Native, in babel.config.js:
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: ['react-native-worklets/plugin'], // must be last
};Then wrap your app once, as gesture-handler requires, and rebuild:
<GestureHandlerRootView style={{ flex: 1 }}>{/* … */}</GestureHandlerRootView>If the plugin is missing, the kit says so by name in development rather than scrolling wrongly.
📖 Full walkthrough, including Expo vs bare and the rebuild step: docs/installation.md
Compatibility
| Peer | Minimum | Developed against |
| --- | --- | --- |
| react | ≥ 18.0.0 | 19.2.0 |
| react-native | ≥ 0.74.0 | 0.83.6 |
| react-native-reanimated | ≥ 4.0.0 | 4.2.1 |
| react-native-worklets | ≥ 0.3.0 | 0.7.4 |
| react-native-gesture-handler | ≥ 2.16.0 | 2.30.0 |
| | | | --- | --- | | New Architecture | ✅ Required — Reanimated 4 runs only on it | | Old Architecture | ❌ | | iOS / Android | ✅ Supported and tested | | Web | ❌ Not supported | | Expo | ✅ Managed and bare; use a development build rather than Expo Go | | TypeScript | ✅ Types ship for both ESM and CJS |
📖 Module formats, accessibility behaviour, font scaling, time zones: docs/compatibility.md
Quick start
import { useMemo, useState } from 'react';
import { Text, View } from 'react-native';
import {
AgendaList,
CalendarProvider,
ExpandableCalendar,
buildRange,
type AgendaSection,
type DateString,
} from 'react-native-agenda-kit';
type Item = { id: string; time: string; title: string };
const ROW_HEIGHTS = { Regular: 64 }; // declared, never measured
export default function Screen() {
const [date, setDate] = useState<DateString>('2026-09-10');
// The index space: a padded-to-whole-weeks window. Build it once.
const range = useMemo(
() => buildRange(new Date(2026, 6, 1), new Date(2026, 10, 30), 1),
[],
);
const sections: AgendaSection<Item>[] = useMemo(
() => [
{
title: '2026-09-10',
data: [{ id: 'a', time: '09:00', title: 'Consultation' }],
},
],
[],
);
return (
<CalendarProvider
range={range}
date={date}
onDateChanged={(next) => setDate(next)}
showTodayButton
todayLabel="Today"
>
<ExpandableCalendar closeOnDayPress>
<AgendaList<Item>
sections={sections}
itemHeightByType={ROW_HEIGHTS}
renderItem={({ item }) => (
<View style={{ height: ROW_HEIGHTS.Regular, justifyContent: 'center' }}>
<Text>{item.time} · {item.title}</Text>
</View>
)}
renderSectionHeader={(title) => <Text>{title}</Text>}
stickySectionHeadersEnabled
/>
</ExpandableCalendar>
</CalendarProvider>
);
}ExpandableCalendar's children are rendered in the body it pushes down — put
the agenda or the timeline there.
A day timeline instead
import { TimelineList } from 'react-native-agenda-kit';
<TimelineList
events={{
'2026-09-10': [
{ id: '1', start: '2026-09-10 09:00:00', end: '2026-09-10 09:30:00', title: 'Consultation' },
],
}}
showNowIndicator
scrollToFirst
timelineProps={{
format24h: true,
hourHeight: 60,
availableHours: { '2026-09-10': [{ start: 8, end: 18 }] },
onEventPress: (event) => console.log(event.id),
}}
/>Neighbouring dates must be present in events or a swipe shows a blank day.
Note availableHours takes the available windows, not the unavailable ones
— the one deliberate rename from react-native-calendars.
📖 Recipes for data loading, view switching, dark mode, refresh, localisation and more: docs/usage.md
Features
Every behaviour, its default, and the prop that switches it. The full switchboard — including what is deliberately always on, and why — is in docs/features.md.
ExpandableCalendar — week strip ⇄ month grid
| Feature | Default | Switch |
| --- | --- | --- |
| Drag the knob to expand | on | disablePan |
| Open at mount | closed | initialPosition="open" |
| Collapse on day press | on | closeOnDayPress={false} |
| Collapse when the list scrolls | on | closeOnListScroll={false} |
| Drag distance before it commits | 30 px | openThreshold, closeThreshold |
| Dots, selection, disabled days | off | markedDates |
| Month names over each 1st | off | monthShortLabels |
| Screen-reader labels | off | knobAccessibilityLabel, dayAccessibilityLabel |
| Toggle from your own button | — | ref.current.toggleCalendarPosition() |
AgendaList — windowed section list
| Feature | Default | Switch |
| --- | --- | --- |
| Declared row heights | — | itemHeightByType (required) |
| Row type resolution | item.itemCustomHeightType → 'Regular' | getItemType |
| Section header height | 32 px | sectionHeaderHeight |
| Sticky headers (a real overlay) | off | stickySectionHeadersEnabled |
| Pull to refresh | off | onRefresh + refreshing |
| Skeleton backdrop during a fling | off | backdrop, rowGroundColor |
| Drag / settle callbacks | off | onDragBegin, onSettle |
| Scroll deceleration | 0.985 | decelerationRate |
| Imperative scrolling | — | ref → scrollToDate, scrollToSection, scrollToOffset |
TimelineList — day view
| Feature | Default | Switch |
| --- | --- | --- |
| Now indicator | off | showNowIndicator |
| Open at now / first event / a time | top of day | scrollToNow, scrollToFirst, initialTime |
| 24-hour clock | on | timelineProps.format24h |
| Hour height | 100 px | timelineProps.hourHeight |
| Available-hours wash | off | timelineProps.availableHours (+ availableHoursColor) |
| Overlap packing | always on | overlapEventsSpacing, rightEdgeSpacing |
| Custom event blocks | default fill | renderEvent, onEventPress, or event.color |
| Day paging | always on | include neighbouring dates in events |
CalendarProvider — the date owner
| Feature | Default | Switch |
| --- | --- | --- |
| Today pill | off | showTodayButton + todayLabel |
| Lift the pill above a tab bar | 0 | todayBottomMargin |
| Date / month callbacks | — | onDateChanged, onMonthChange |
| Dark theme | light | themeBase={darkTheme} |
| Brand colours and fonts | built-in | theme |
Always on, on purpose
Worklet scroll handlers · declared row heights · windowing · echo-proofing the
controlled date · allowFontScaling={false} inside the kit · integer epoch-day
date maths. Why each one is not a prop →
Theming
Eleven colours and three font families. If a colour is not on this list, the kit does not draw it.
import { CalendarProvider, darkTheme } from 'react-native-agenda-kit';
// Hoist it: a fresh object every render rebuilds every stylesheet in the tree.
const THEME = {
colors: { accent: '#0F766E', accentFill: '#0F766E' },
fonts: { regular: 'Inter-Regular', medium: 'Inter-Medium', semiBold: 'Inter-SemiBold' },
};
<CalendarProvider theme={THEME} themeBase={darkTheme} {...rest}>lightTheme and darkTheme are exported, as are the static metrics
(SPACING, FONT_SIZE, RADIUS, ANIMATION, EASING) if you want to match
them elsewhere. AgendaKitThemeProvider is available if you would rather theme
above CalendarProvider.
📖 What each token paints, ink vs fill, and what is deliberately not themeable: docs/theming.md
Migrating from react-native-calendars
The component and prop names match deliberately. Six differences matter:
| | react-native-calendars | this kit |
| --- | --- | --- |
| Row heights | measured | declared via itemHeightByType (required) |
| Sticky headers | emulated | a real fixed overlay |
| unavailableHours | the unavailable windows | availableHours — the available ones |
| theme object | a large nested theme | eleven colours + three fonts |
| Strings | ships English defaults | ships none — pass todayLabel, monthShortLabels, a11y labels |
| Date range | pastScrollRange / futureScrollRange | one range from buildRange |
ExpandableCalendar still accepts firstDay, renderHeader, headerStyle,
minDate, maxDate, pastScrollRange, futureScrollRange, hideArrows,
hideDayNames, theme and friends — typed and ignored, so copied JSX
compiles while you delete them. No header band or day-names row is rendered;
render your own above the calendar.
📖 Step-by-step diffs: docs/migration.md
Notes and limits
AgendaListheights are contracts. A row taller than its declared type clips.TimelineListneeds neighbouring dates present ineventsto swipe into.- Text ignores OS font scaling inside the kit (
allowFontScaling={false}), because declared heights and scaled text cannot both be right. Scale youritemHeightByTypeyourself if you need to honour it. - Reduce Motion collapses every duration to 0; a running screen reader switches
ExpandableCalendarto a static open grid. - Tested on iOS and Android. Web is not supported.
Something not behaving? docs/troubleshooting.md lists every error message the kit can emit and what causes it.
Documentation
| | |
| --- | --- |
| Installation | Peers, the Babel plugin, rebuilding |
| Compatibility | Versions, architecture, platforms, a11y, time zones |
| Usage | Quick starts and recipes |
| Features | Every toggle, default and switch |
| API | Every prop, ref, helper and type |
| Theming | The eleven tokens |
| Migration | From react-native-calendars |
| Troubleshooting | Errors and failure modes |
Contributing
npm install
npm run typecheck
npm run buildIssues and pull requests: github.com/shivampandey-07/react-native-agenda-kit
License
MIT © Shivam Pandey
