react-native-strip-calendar
v0.1.8
Published
react native strip calendar
Readme
React Native Strip Calendar
A headless, customizable horizontal strip calendar component for React Native with virtualized scrolling and flexible styling support.
Features
- 📅 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
Installation
npm install react-native-strip-calendar
# or
yarn add react-native-strip-calendar
# or
pnpm add react-native-strip-calendarPeer Dependencies
This library requires the following peer dependencies:
npm install react react-native react-native-webBasic Usage
Simple Calendar
import { 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>
);
}With Custom Day Styling (using dayProps)
<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>With Custom Day Renderer (using renderDay)
<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>Props
StripCalendarProps
| 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 |
StripCalendar.Week Props
| 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.
DayProps
| 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 |
DayRenderProps
| 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 |
Compound Components
StripCalendar.Header
Renders the calendar header with the selected date.
<StripCalendar.Header>
{(dateString) => (
<Text style={styles.header}>
{format(new Date(dateString), 'MMMM yyyy')}
</Text>
)}
</StripCalendar.Header>StripCalendar.Week
Renders the week view with days. Supports two approaches:
Using dayProps (Default Day Component)
<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,
},
},
}}
/>Using renderDay (Custom Day Component)
<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>
),
}}
/>StripCalendar.Day
Renders a single day (useful for custom layouts).
<StripCalendar.Day
date={dateObject}
styles={customDayStyles}
classNames={customDayClassNames}
/>StripCalendar.PreviousButton
Renders a button to navigate to the previous week.
<StripCalendar.PreviousButton>
{({ disabled }) => (
<Pressable disabled={disabled}>
<Text>‹</Text>
</Pressable>
)}
</StripCalendar.PreviousButton>StripCalendar.NextButton
Renders a button to navigate to the next week.
<StripCalendar.NextButton>
{({ disabled }) => (
<Pressable disabled={disabled}>
<Text>›</Text>
</Pressable>
)}
</StripCalendar.NextButton>StripCalendar.TodayButton
Renders a button to navigate to today's date.
<StripCalendar.TodayButton>
<Pressable>
<Text>Today</Text>
</Pressable>
</StripCalendar.TodayButton>Styling
StyleSheet Approach
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',
},
});NativeWind/ClassName Approach
<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>Custom Rendering
Custom Day Renderer
<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>Hooks
useHorizontalCalendar
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),
});Internationalization
import { ko } from 'date-fns/locale';
<StripCalendar
locale={ko}
firstDay={1} // Monday
/>;Example
See the example directory for a complete working example.
Development
# Install dependencies
pnpm install
# Build the library
pnpm build
# Run example
pnpm example:android
pnpm example:iosLicense
MIT
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
