Back to Components
📅

Date Picker

Medium

A beautiful modal date picker with calendar view, date range constraints, and custom theming. Built on react-native-calendars with additional styling improvements.

Inputv1.0.0Updated 2026-02-04

Live Preview

Open in Snack

Installation

1

Copy DatePicker.tsx to components

2

Copy utilities

3

Import and use in forms

⚠️ Native Setup Required:

  • No native setup required
  • Works on both iOS and Android

Source Code

DatePicker.tsx
import React, { useState, useRef, useEffect } from 'react';
import {
    View,
    Text,
    Modal,
    TouchableOpacity,
    StyleSheet,
    ScrollView,
    Dimensions,
    NativeSyntheticEvent,
    NativeScrollEvent,
} from 'react-native';
import Ionicons from 'react-native-vector-icons/Ionicons';

const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');

const wp = (percentage: number) => (percentage * SCREEN_WIDTH) / 100;
const hp = (percentage: number) => (percentage * SCREEN_HEIGHT) / 100;

// Default Constants
const DEFAULT_COLORS = {
    white: '#FFFFFF',
    darkgray: '#374151',
    WhiteSmoke: '#F5F5F5',
    LightGray: '#E5E7EB',
    BrightPink: '#FF0080',
    black: '#000000',
    Midgray: '#9CA3AF',
};

const fontFamily = {
    FONTS: {
        bold: 'System',
        Medium: 'System',
        Regular: 'System'
    }
};

const ITEM_HEIGHT = 50;

interface DatePickerProps {
    label?: string;
    value?: Date;
    onDateChange?: (date: Date) => void;
    placeholder?: string;
    minimumDate?: Date;
    maximumDate?: Date;
    disabled?: boolean;
}

const DatePicker = ({
    label,
    value,
    onDateChange,
    placeholder = 'MM/DD/YYYY',
    minimumDate,
    maximumDate,
    disabled = false,
}: DatePickerProps) => {
    const COLORS = DEFAULT_COLORS;
    const [visible, setVisible] = useState(false);
    const [selectedDate, setSelectedDate] = useState(value || new Date());

    useEffect(() => {
        if (value) {
            setSelectedDate(value);
        }
    }, [value]);

    const monthScrollRef = useRef<any>(null);
    const dayScrollRef = useRef<any>(null);
    const yearScrollRef = useRef<any>(null);

    const isProgrammaticScroll = useRef(false);

    const months = [
        'January', 'February', 'March', 'April', 'May', 'June',
        'July', 'August', 'September', 'October', 'November', 'December'
    ];

    const currentYearVal = new Date().getFullYear();
    const years = Array.from({ length: 100 }, (_, i) => currentYearVal - i);

    const getDaysInMonth = (month: number, year: number) => {
        return new Date(year, month + 1, 0).getDate();
    };

    const currentMonth = selectedDate.getMonth();
    const currentYear = selectedDate.getFullYear();
    const currentDay = selectedDate.getDate();

    const daysInMonth = getDaysInMonth(currentMonth, currentYear);
    const days = Array.from({ length: daysInMonth }, (_, i) => i + 1);

    const isDateDisabled = (month: number, day: number, year: number) => {
        const date = new Date(year, month, day);

        if (minimumDate) {
            const minDate = new Date(minimumDate);
            minDate.setHours(0, 0, 0, 0);
            if (date < minDate) return true;
        }

        if (maximumDate) {
            const maxDate = new Date(maximumDate);
            maxDate.setHours(0, 0, 0, 0);
            if (date > maxDate) return true;
        }

        return false;
    };

    const handleMonthChange = (monthIndex: number) => {
        const maxDay = getDaysInMonth(monthIndex, currentYear);
        const day = Math.min(currentDay, maxDay);
        setSelectedDate(new Date(currentYear, monthIndex, day));
    };

    const handleDayChange = (day: number) => {
        setSelectedDate(new Date(currentYear, currentMonth, day));
    };

    const handleYearChange = (year: number) => {
        const maxDay = getDaysInMonth(currentMonth, year);
        const day = Math.min(currentDay, maxDay);
        setSelectedDate(new Date(year, currentMonth, day));
    };

    const handleConfirm = () => {
        if (!isDateDisabled(currentMonth, currentDay, currentYear)) {
            onDateChange?.(selectedDate);
            setVisible(false);
        }
    };

    // 👉 Only scroll when modal opens
    useEffect(() => {
        if (!visible) return;

        isProgrammaticScroll.current = true;

        setTimeout(() => {
            if (monthScrollRef.current) {
                // @ts-ignore
                monthScrollRef.current.scrollTo({ y: currentMonth * ITEM_HEIGHT, animated: false });
            }
            if (dayScrollRef.current) {
                // @ts-ignore
                dayScrollRef.current.scrollTo({ y: (currentDay - 1) * ITEM_HEIGHT, animated: false });
            }

            const yearIndex = years.findIndex(y => y === currentYear);
            if (yearIndex >= 0 && yearScrollRef.current) {
                // @ts-ignore
                yearScrollRef.current.scrollTo({ y: yearIndex * ITEM_HEIGHT, animated: false });
            }

            setTimeout(() => {
                isProgrammaticScroll.current = false;
            }, 120);

        }, 150);

    }, [visible]);

    const handleMonthScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
        if (isProgrammaticScroll.current) return;

        const index = Math.round(e.nativeEvent.contentOffset.y / ITEM_HEIGHT);
        if (index >= 0 && index < months.length && index !== currentMonth) {
            handleMonthChange(index);
        }
    };

    const handleDayScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
        if (isProgrammaticScroll.current) return;

        const day = Math.round(e.nativeEvent.contentOffset.y / ITEM_HEIGHT) + 1;
        if (day >= 1 && day <= days.length && day !== currentDay) {
            handleDayChange(day);
        }
    };

    const handleYearScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
        if (isProgrammaticScroll.current) return;

        const index = Math.round(e.nativeEvent.contentOffset.y / ITEM_HEIGHT);
        if (index >= 0 && index < years.length && years[index] !== currentYear) {
            handleYearChange(years[index]);
        }
    };

    return (
        <View style={styles.wrapper}>
            {label && <Text style={styles.inputLabel}>{label}</Text>}
            <TouchableOpacity
                style={[styles.inputContainer, disabled && styles.disabledContainer]}
                onPress={() => !disabled && setVisible(true)}
                activeOpacity={0.7}
            >
                <Text style={[styles.inputText, !value && { color: COLORS.Midgray }]}>
                    {value ? value.toLocaleDateString() : placeholder}
                </Text>
                <Text style={{ fontSize: 16 }}>📅</Text>
            </TouchableOpacity>

            <Modal visible={visible} transparent animationType="fade" onRequestClose={() => setVisible(false)}>
                <View style={styles.modalOverlay}>
                    <View style={[styles.modalContainer, { backgroundColor: COLORS.WhiteSmoke }]}>

                        {/* Header */}
                        <View style={styles.header}>
                            <Text style={[styles.headerTitle, { color: COLORS.darkgray }]}>Select Date</Text>
                            <TouchableOpacity onPress={() => setVisible(false)}>
                                <Ionicons name="close" size={wp(6)} color={COLORS.darkgray} />
                            </TouchableOpacity>
                        </View>

                        {/* Picker */}
                        <View style={styles.pickerContainer}>
                            <View style={[styles.selectionHighlight, { backgroundColor: COLORS.white }]} />

                            {/* Month */}
                            <View style={styles.pickerColumn}>
                                <ScrollView
                                    ref={monthScrollRef}
                                    showsVerticalScrollIndicator={false}
                                    snapToInterval={ITEM_HEIGHT}
                                    decelerationRate="fast"
                                    onMomentumScrollEnd={handleMonthScroll}
                                >
                                    <View style={{ height: ITEM_HEIGHT * 2 }} />
                                    {months.map((m, i) => {
                                        const disabled = isDateDisabled(i, currentDay, currentYear);
                                        return (
                                            <View key={m} style={styles.pickerItem}>
                                                <Text style={[
                                                    styles.pickerText,
                                                    { color: COLORS.darkgray },
                                                    i === currentMonth && styles.selectedText && { color: COLORS.black },
                                                    disabled && styles.disabledText
                                                ]}>
                                                    {m}
                                                </Text>
                                            </View>
                                        );
                                    })}
                                    <View style={{ height: ITEM_HEIGHT * 2 }} />
                                </ScrollView>
                            </View>

                            {/* Day */}
                            <View style={styles.pickerColumn}>
                                <ScrollView
                                    ref={dayScrollRef}
                                    showsVerticalScrollIndicator={false}
                                    snapToInterval={ITEM_HEIGHT}
                                    decelerationRate="fast"
                                    onMomentumScrollEnd={handleDayScroll}
                                >
                                    <View style={{ height: ITEM_HEIGHT * 2 }} />
                                    {days.map(d => {
                                        const disabled = isDateDisabled(currentMonth, d, currentYear);
                                        return (
                                            <View key={d} style={styles.pickerItem}>
                                                <Text style={[
                                                    styles.pickerText,
                                                    { color: COLORS.darkgray },
                                                    d === currentDay && styles.selectedText && { color: COLORS.black },
                                                    disabled && styles.disabledText
                                                ]}>
                                                    {d}
                                                </Text>
                                            </View>
                                        );
                                    })}
                                    <View style={{ height: ITEM_HEIGHT * 2 }} />
                                </ScrollView>
                            </View>

                            {/* Year */}
                            <View style={styles.pickerColumn}>
                                <ScrollView
                                    ref={yearScrollRef}
                                    showsVerticalScrollIndicator={false}
                                    snapToInterval={ITEM_HEIGHT}
                                    decelerationRate="fast"
                                    onMomentumScrollEnd={handleYearScroll}
                                >
                                    <View style={{ height: ITEM_HEIGHT * 2 }} />
                                    {years.map(y => {
                                        const disabled = isDateDisabled(currentMonth, currentDay, y);
                                        return (
                                            <View key={y} style={styles.pickerItem}>
                                                <Text style={[
                                                    styles.pickerText,
                                                    { color: COLORS.darkgray },
                                                    y === currentYear && styles.selectedText && { color: COLORS.black },
                                                    disabled && styles.disabledText
                                                ]}>
                                                    {y}
                                                </Text>
                                            </View>
                                        );
                                    })}
                                    <View style={{ height: ITEM_HEIGHT * 2 }} />
                                </ScrollView>
                            </View>

                        </View>

                        {/* Buttons */}
                        <View style={styles.actionButtons}>
                            <TouchableOpacity style={[styles.button, { backgroundColor: COLORS.LightGray }]} onPress={() => setVisible(false)}>
                                <Text style={[styles.buttonText, { color: COLORS.darkgray }]}>Cancel</Text>
                            </TouchableOpacity>
                            <TouchableOpacity
                                style={[
                                    styles.button,
                                    { backgroundColor: COLORS.BrightPink },
                                    isDateDisabled(currentMonth, currentDay, currentYear) && { opacity: 0.5 }
                                ]}
                                onPress={handleConfirm}
                                disabled={isDateDisabled(currentMonth, currentDay, currentYear)}
                            >
                                <Text style={[styles.buttonText, { color: COLORS.white }]}>Confirm</Text>
                            </TouchableOpacity>
                        </View>

                    </View>
                </View>
            </Modal>
        </View>
    );
};

const styles = StyleSheet.create({
    wrapper: {
        marginVertical: 6,
    },
    inputLabel: {
        fontSize: 14,
        fontWeight: '600',
        marginBottom: 6,
        color: '#1a1a2e',
    },
    inputContainer: {
        flexDirection: 'row',
        alignItems: 'center',
        justifyContent: 'space-between',
        height: 48,
        paddingHorizontal: 16,
        borderRadius: 8,
        borderWidth: 1,
        borderColor: '#c0c0c0',
        backgroundColor: 'transparent',
    },
    disabledContainer: {
        backgroundColor: '#e0e0e0',
        opacity: 0.6,
    },
    inputText: {
        fontSize: 15,
        color: '#1a1a2e',
    },
    modalOverlay: {
        flex: 1,
        backgroundColor: 'rgba(0,0,0,0.5)',
        justifyContent: 'center',
        alignItems: 'center',
    },
    modalContainer: {
        width: wp(95),
        borderRadius: wp(4),
        padding: wp(4),
    },
    header: {
        flexDirection: 'row',
        justifyContent: 'space-between',
        marginBottom: hp(2),
    },
    headerTitle: {
        fontSize: hp(2.5),
        fontFamily: fontFamily.FONTS.bold,
    },
    pickerContainer: {
        flexDirection: 'row',
        height: ITEM_HEIGHT * 5,
        overflow: 'hidden',
        marginBottom: hp(3),
    },
    selectionHighlight: {
        position: 'absolute',
        top: ITEM_HEIGHT * 2,
        left: 0,
        right: 0,
        height: ITEM_HEIGHT,
        borderRadius: wp(2),
    },
    pickerColumn: {
        flex: 1,
    },
    pickerItem: {
        height: ITEM_HEIGHT,
        justifyContent: 'center',
        alignItems: 'center',
    },
    pickerText: {
        fontSize: hp(2),
        fontFamily: fontFamily.FONTS.Medium,
    },
    selectedText: {
        fontSize: hp(2.3),
        fontFamily: fontFamily.FONTS.bold,
    },
    disabledText: {
        opacity: 0.3,
        textDecorationLine: 'line-through'
    },
    actionButtons: {
        flexDirection: 'row',
        gap: wp(2),
    },
    button: {
        flex: 1,
        paddingVertical: hp(1.5),
        borderRadius: wp(2),
        alignItems: 'center',
    },
    buttonText: {
        fontSize: hp(2),
        fontFamily: fontFamily.FONTS.bold,
    },
});

export default DatePicker;

Usage Examples

Basic Usage

Simple date picker

Example 1
const [date, setDate] = useState<Date>();

<DatePicker
  label="Booking Date"
  value={date}
  onDateChange={setDate}
  minimumDate={new Date()}
  required
/>

Props

PropTypeDefaultDescription
valueDate | undefined-Currently selected date
onDateChange*(date: Date) => void-Callback when date selected
minimumDateDate-Earliest selectable date
maximumDateDate-Latest selectable date
labelstring-Label text above picker
requiredbooleanfalseShow required indicator

Features

  • Modal calendar interface
  • Date range constraints (min/max)
  • Custom color theming
  • Error state support
  • Required field indicator
  • Formatted date display

Dependencies

Required:

None