A headless, customizable horizontal strip calendar component for React Native with virtualized scrolling and flexible styling support.
- 📅 Flexible Date Range: Define custom start and end dates
- 🎨 Headless Design: Support for both StyleSheet and className (NativeWind) styling
- ⚡ Virtualized Scrolling: Smooth performance with large date ranges using
@legendapp/list - 🌍 Internationalization: Built-in support for multiple locales via
date-fns - 📱 React Native Compatible: Works with React Native and React Native Web
- 🎯 Customizable Rendering: Custom day and header rendering functions
- 📊 Marked Dates: Support for highlighting specific dates
- 🔄 Navigation Controls: Previous/Next week navigation with boundary checks
- 🧩 Compound Components: Modular design with Header, Week, Day, and Navigation components
- 📏 Flexible Layout: Support for custom spacing with
columnGapprop
npm install react-native-strip-calendar
# or
yarn add react-native-strip-calendar
# or
pnpm add react-native-strip-calendarThis library requires the following peer dependencies:
npm install react react-native react-native-webimport { StripCalendar } from 'react-native-strip-calendar';
export default function MyComponent() {
return (
<StripCalendar
startDate={new Date('2025-01-01')}
endDate={new Date('2025-12-31')}
selectedDate={format(new Date(), 'yyyy-MM-dd')}
onDateChange={(date) => console.log('Selected date:', date)}
>
<StripCalendar.Header>
{(dateString) => <Text>{dateString}</Text>}
</StripCalendar.Header>
<StripCalendar.Week />
<StripCalendar.PreviousButton>
{({ disabled }) => <Button disabled={disabled}>Previous</Button>}
</StripCalendar.PreviousButton>
<StripCalendar.NextButton>
{({ disabled }) => <Button disabled={disabled}>Next</Button>}
</StripCalendar.NextButton>
</StripCalendar>
);
}<StripCalendar
startDate={new Date('2025-01-01')}
endDate={new Date('2025-12-31')}
selectedDate={selectedDate}
onDateChange={setSelectedDate}
markedDates={['2025-01-15', '2025-02-14', '2025-03-08']}
containerHeight={120}
dayWidth={48}
>
<StripCalendar.Header>
{(dateString) => (
<Text style={styles.header}>
{format(new Date(dateString), 'MMMM yyyy')}
</Text>
)}
</StripCalendar.Header>
<StripCalendar.Week
columnGap={16}
dayProps={{
styles: {
base: {
container: {
width: 48,
height: 60,
alignItems: 'center',
justifyContent: 'center',
borderRadius: 20,
backgroundColor: '#f0f0f0',
marginHorizontal: 1,
marginVertical: 1,
},
dayName: {
fontSize: 9,
color: '#6b7280',
fontWeight: '500',
marginBottom: 2,
},
dayNumber: {
fontSize: 16,
color: '#374151',
fontWeight: '600',
},
},
today: {
container: {
backgroundColor: '#dbeafe',
borderWidth: 2,
borderColor: '#3b82f6',
},
},
selected: {
container: {
backgroundColor: '#3b82f6',
},
dayNumber: {
color: '#ffffff',
},
},
},
}}
/>
<StripCalendar.PreviousButton>
{({ disabled }) => (
<Pressable
style={[styles.button, disabled && styles.disabledButton]}
disabled={disabled}
>
<Text>‹</Text>
</Pressable>
)}
</StripCalendar.PreviousButton>
<StripCalendar.NextButton>
{({ disabled }) => (
<Pressable
style={[styles.button, disabled && styles.disabledButton]}
disabled={disabled}
>
<Text>›</Text>
</Pressable>
)}
</StripCalendar.NextButton>
</StripCalendar><StripCalendar
startDate={new Date('2025-01-01')}
endDate={new Date('2025-12-31')}
selectedDate={selectedDate}
onDateChange={setSelectedDate}
markedDates={['2025-01-15', '2025-02-14', '2025-03-08']}
containerHeight={120}
dayWidth={48}
>
<StripCalendar.Header>
{(dateString) => (
<Text style={styles.header}>
{format(new Date(dateString), 'MMMM yyyy')}
</Text>
)}
</StripCalendar.Header>
<StripCalendar.Week
columnGap={16}
dayProps={{
renderDay: ({ date, isMarked, dayName, dayNumber, onPress }) => (
<Pressable
style={[styles.customDay, isMarked && styles.markedDay]}
onPress={onPress}
>
<Text style={styles.dayName}>{dayName}</Text>
<Text style={styles.dayNumber}>{dayNumber}</Text>
{isMarked && <View style={styles.indicator} />}
</Pressable>
),
}}
/>
<StripCalendar.PreviousButton>
{({ disabled }) => (
<Pressable
style={[styles.button, disabled && styles.disabledButton]}
disabled={disabled}
>
<Text>‹</Text>
</Pressable>
)}
</StripCalendar.PreviousButton>
<StripCalendar.NextButton>
{({ disabled }) => (
<Pressable
style={[styles.button, disabled && styles.disabledButton]}
disabled={disabled}
>
<Text>›</Text>
</Pressable>
)}
</StripCalendar.NextButton>
</StripCalendar>| Prop | Type | Default | Description |
|---|---|---|---|
firstDay |
Day |
1 |
First day of the week (0 = Sunday, 1 = Monday) |
startDate |
Date |
undefined |
Start date of the calendar range |
endDate |
Date |
undefined |
End date of the calendar range |
minDate |
Date |
undefined |
Minimum selectable date |
maxDate |
Date |
undefined |
Maximum selectable date |
selectedDate |
string |
undefined |
Currently selected date (YYYY-MM-DD format) |
onDateChange |
(date: string) => void |
undefined |
Callback when date is selected |
containerHeight |
number |
60 |
Height of the calendar container |
dayWidth |
number |
48 |
Width of each day item |
markedDates |
string[] |
[] |
Array of dates to mark (YYYY-MM-DD format) |
classNames |
string |
undefined |
CSS class names for styling (NativeWind) |
styles |
StyleProp<ViewStyle> |
undefined |
StyleSheet styles for styling |
locale |
Locale |
enUS |
Locale for date formatting |
| Prop | Type | Default | Description |
|---|---|---|---|
dayProps |
DayProps |
- | Props for the Day component |
className |
object |
- | CSS class names for container and week |
style |
object |
- | StyleSheet styles for container and week |
columnGap |
number |
12 |
Gap between week columns |
weekHeight |
number |
- | Height of the week container (overrides containerHeight) |
Note: When renderDay is provided in dayProps, other properties on dayProps (like styles and classNames) are ignored. Use either dayProps for styling the default Day component or renderDay for completely custom day rendering.
| Prop | Type | Description |
|---|---|---|
date |
CalendarDate |
The date object to render |
classNames |
DayStateClassNames |
CSS class names for different day states |
styles |
DayStateStyles |
StyleSheet styles for different day states |
formatString |
object |
Custom format strings for day name and number |
renderDay |
(props) => ReactNode |
Custom day renderer function |
| Prop | Type | Description |
|---|---|---|
date |
CalendarDate |
The date object to render |
isSelected |
boolean |
Whether the date is selected |
isDisabled |
boolean |
Whether the date is disabled |
isMarked |
boolean |
Whether the date is marked |
dayName |
string |
Formatted day name (e.g., "Mon") |
dayNumber |
number |
Day number (1-31) |
onPress |
() => void |
Callback when day is pressed |
Renders the calendar header with the selected date.
<StripCalendar.Header>
{(dateString) => (
<Text style={styles.header}>
{format(new Date(dateString), 'MMMM yyyy')}
</Text>
)}
</StripCalendar.Header>Renders the week view with days. Supports two approaches:
<StripCalendar.Week
columnGap={16}
weekHeight={120}
className={{
container: 'list-container',
week: 'week-item',
}}
style={{
container: { padding: 16 },
week: { marginBottom: 8 },
}}
dayProps={{
styles: {
base: {
container: styles.dayContainer,
dayName: styles.dayName,
dayNumber: styles.dayNumber,
},
today: {
container: styles.todayContainer,
},
selected: {
container: styles.selectedContainer,
},
},
}}
/><StripCalendar.Week
columnGap={16}
weekHeight={120}
className={{
container: 'list-container',
week: 'week-item',
}}
style={{
container: { padding: 16 },
week: { marginBottom: 8 },
}}
dayProps={{
renderDay: ({ date, isMarked, dayName, dayNumber, onPress }) => (
<Pressable
style={[styles.customDay, isMarked && styles.markedDay]}
onPress={onPress}
>
<Text style={styles.dayName}>{dayName}</Text>
<Text style={styles.dayNumber}>{dayNumber}</Text>
{isMarked && <View style={styles.indicator} />}
</Pressable>
),
}}
/>Renders a single day (useful for custom layouts).
<StripCalendar.Day
date={dateObject}
styles={customDayStyles}
classNames={customDayClassNames}
/>Renders a button to navigate to the previous week.
<StripCalendar.PreviousButton>
{({ disabled }) => (
<Pressable disabled={disabled}>
<Text>‹</Text>
</Pressable>
)}
</StripCalendar.PreviousButton>Renders a button to navigate to the next week.
<StripCalendar.NextButton>
{({ disabled }) => (
<Pressable disabled={disabled}>
<Text>›</Text>
</Pressable>
)}
</StripCalendar.NextButton>Renders a button to navigate to today's date.
<StripCalendar.TodayButton>
<Pressable>
<Text>Today</Text>
</Pressable>
</StripCalendar.TodayButton>import { StyleSheet } from 'react-native';
<StripCalendar containerHeight={120} dayWidth={48}>
<StripCalendar.Week
columnGap={16}
dayProps={{
styles: {
base: {
container: styles.dayContainer,
dayName: styles.dayName,
dayNumber: styles.dayNumber,
},
today: {
container: styles.todayContainer,
},
selected: {
container: styles.selectedContainer,
},
},
}}
/>
</StripCalendar>;
const styles = StyleSheet.create({
dayContainer: {
width: 48,
height: 60,
alignItems: 'center',
justifyContent: 'center',
borderRadius: 20,
backgroundColor: '#f0f0f0',
marginHorizontal: 1,
marginVertical: 1,
},
dayName: {
fontSize: 9,
color: '#6b7280',
fontWeight: '500',
marginBottom: 2,
},
dayNumber: {
fontSize: 16,
color: '#374151',
fontWeight: '600',
},
todayContainer: {
backgroundColor: '#dbeafe',
borderWidth: 2,
borderColor: '#3b82f6',
},
selectedContainer: {
backgroundColor: '#3b82f6',
},
});<StripCalendar containerHeight={120} dayWidth={48}>
<StripCalendar.Week
columnGap={16}
dayProps={{
classNames: {
base: {
container:
'w-12 h-15 items-center justify-center rounded-xl bg-gray-100 mx-0.5 my-0.5',
dayName: 'text-xs text-gray-500 font-medium mb-0.5',
dayNumber: 'text-base text-gray-700 font-semibold',
},
today: {
container: 'bg-blue-100 border-2 border-blue-500',
},
selected: {
container: 'bg-blue-500',
dayNumber: 'text-white',
},
},
}}
/>
</StripCalendar><StripCalendar containerHeight={120} dayWidth={48}>
<StripCalendar.Week
columnGap={16}
dayProps={{
renderDay: ({ date, isMarked, dayName, dayNumber, onPress }) => (
<Pressable
style={[styles.customDay, isMarked && styles.markedDay]}
onPress={onPress}
>
<Text style={styles.dayName}>{dayName}</Text>
<Text style={styles.dayNumber}>{dayNumber}</Text>
{isMarked && <View style={styles.indicator} />}
</Pressable>
),
}}
/>
</StripCalendar>A custom hook that provides calendar state and navigation logic:
import { useHorizontalCalendar } from 'react-native-strip-calendar';
const {
weeksData,
selectedDate,
currentScrollIndex,
initialScrollIndex,
canGoNext,
canGoPrevious,
handleDateSelect,
goToNextWeek,
goToPreviousWeek,
goToToday,
scrollToWeek,
} = useHorizontalCalendar({
firstDay: 1,
startDate: new Date('2025-01-01'),
endDate: new Date('2025-12-31'),
minDate: new Date('2025-01-01'),
maxDate: new Date('2025-12-31'),
selectedDate: '2025-10-18',
onDateChange: (date) => console.log(date),
});import { ko } from 'date-fns/locale';
<StripCalendar
locale={ko}
firstDay={1} // Monday
/>;See the example directory for a complete working example.
# Install dependencies
pnpm install
# Build the library
pnpm build
# Run example
pnpm example:android
pnpm example:iosMIT
Contributions are welcome! Please feel free to submit a Pull Request.
