DatePicker
A Calendar in a BottomSheet, bound to a value and a draft. You write every part.
Installation
npx axiom add date-pickerpnpm dlx axiom add date-pickeryarn dlx axiom add date-pickerbun x axiom add date-pickerAlso copies: calendar, bottom-sheet, button, chip, input and their dependencies.
Requires the calendar icon in your icon registry. The field borrows the colors of Input: DatePicker brings no tokens of its own.
Usage
import { BottomSheet } from "@/components/ui/bottom-sheet";
import { Calendar } from "@/components/ui/calendar";
import { DatePicker } from "@/components/ui/date-picker";
import { Field } from "@/components/ui/input";
<Field>
<Field.Label>Departure</Field.Label>
<DatePicker value={date} onChange={setDate}>
<DatePicker.Trigger />
<BottomSheet.Content>
<BottomSheet.Handle />
<BottomSheet.Header
title="Departure"
leading={<DatePicker.Cancel />}
trailing={<DatePicker.Done />}
/>
<View style={{ paddingHorizontal: 16, paddingBottom: 16 }}>
<DatePicker.Calendar minDate={today}>
<Calendar.Header>
<Calendar.Title />
<Calendar.Nav>
<Calendar.PrevButton />
<Calendar.NextButton />
</Calendar.Nav>
</Calendar.Header>
<Calendar.Grid>
<Calendar.Weekdays />
<Calendar.Days />
</Calendar.Grid>
</DatePicker.Calendar>
</View>
</BottomSheet.Content>
</DatePicker>
</Field>;The whole picker is in front of you: a Field, a trigger, a BottomSheet and a Calendar. To change it, change what you write: another header, presets above the calendar, a Clear button under it, any element as the trigger. DatePicker only adds what those pieces don’t know: the value the screen has, and the draft the user is editing.
How it’s built
date-picker
│
┌───────┴───────┐
↓ ↓
calendar bottom-sheetDatePicker is a BottomSheet.Root: BottomSheet.Content, BottomSheet.Header and BottomSheet.Trigger work inside it as usual. The calendar edits a draft. Done applies it, Cancel or a swipe down drops it, so the screen’s value only changes on confirmation.
Props
DatePicker
| Prop | Type | Default | Description |
|---|---|---|---|
mode | 'single' | 'range' | 'single' | One date, or a start and end date. DatePicker.Calendar follows it. |
value | Date | { from: Date; to?: Date } | null | — | The selected date or range. null shows the placeholder. |
onChange | (value) => void | — | Called with the new value on Done, on Clear, or on the first press with confirm="instant". |
confirm | 'done' | 'instant' | 'done' | done: the value changes on Done, Cancel reverts it. instant: the sheet closes on the first press (single mode only). |
haptic | HapticKind | false | 'selection' | Haptic played when the user picks a day or a preset. false turns it off. |
Parts
| Part | What it is |
|---|---|
DatePicker.Trigger | A field that shows the value and opens the sheet. Takes placeholder, format and invalid. Inside a Field, it announces its label and follows its state. |
DatePicker.Calendar | A Calendar.Root bound to the draft. Takes every other Calendar.Root prop (minDate, maxDate, isDateDisabled, minRange, weekStartsOn…) and the Calendar parts as children. |
DatePicker.Cancel | A ghost Button that drops the draft and closes. |
DatePicker.Done | A ghost Button that applies the draft and closes. Leave it out with confirm="instant". |
DatePicker.Clear | A ghost Button that sets the value to null. Disabled while the value is empty. |
DatePicker.Preset | A Chip that picks its value into the draft: “Today”, “Next weekend”. |
The action buttons take every Button prop except onPress, and their text as children. formatDateValue(value, locale) is exported for your own trigger.
Haptics
The picker plays a selection haptic when the user picks a day in the calendar or a preset. Pass haptic={false} to turn it off, or another kind to change it. Nothing plays until your app connects a haptics library: see Haptics.
Use cases
Date of a task
confirm="instant": one press sets the date and closes the sheet. Presets cover the common choices.
<DatePicker confirm="instant" value={task.due} onChange={(due) => update({ due })}>
<BottomSheet.Trigger asChild>
<Chip leading="calendar">
{task.due ? formatShort(task.due) : "Due date"}
</Chip>
</BottomSheet.Trigger>
<BottomSheet.Content>
<BottomSheet.Handle />
<BottomSheet.Header title="Due date" leading={<DatePicker.Cancel />} />
<View style={{ paddingHorizontal: 16, paddingBottom: 16, gap: 12 }}>
<View style={{ flexDirection: "row", gap: 8 }}>
<DatePicker.Preset value={today}>Today</DatePicker.Preset>
<DatePicker.Preset value={addDays(today, 1)}>Tomorrow</DatePicker.Preset>
<DatePicker.Preset value={nextMonday(today)}>Next week</DatePicker.Preset>
</View>
<DatePicker.Calendar>{monthParts}</DatePicker.Calendar>
<DatePicker.Clear fullWidth />
</View>
</BottomSheet.Content>
</DatePicker>Travel dates
mode="range" behind two fields that read as one. Done stays disabled until both dates are set.
<DatePicker mode="range" value={stay} onChange={setStay}>
<BottomSheet.Trigger asChild>
<StayField from={stay?.from} to={stay?.to} />
</BottomSheet.Trigger>
<BottomSheet.Content>
<BottomSheet.Handle />
<BottomSheet.Header
title="Your stay"
leading={<DatePicker.Cancel />}
trailing={<DatePicker.Done />}
/>
<View style={{ paddingHorizontal: 16, paddingBottom: 16 }}>
<DatePicker.Calendar minDate={today} minRange={1}>
{monthParts}
</DatePicker.Calendar>
</View>
</BottomSheet.Content>
</DatePicker>Form field with validation
A regular field in a form, with a label, maxDate and an error when the date is missing.
<Field>
<Field.Label>Date of birth</Field.Label>
<DatePicker value={birthdate} onChange={setBirthdate}>
<DatePicker.Trigger />
<BottomSheet.Content>
<BottomSheet.Handle />
<BottomSheet.Header
title="Date of birth"
leading={<DatePicker.Cancel />}
trailing={<DatePicker.Done />}
/>
<View style={{ paddingHorizontal: 16, paddingBottom: 16 }}>
<DatePicker.Calendar maxDate={subYears(today, 13)}>
{monthParts}
</DatePicker.Calendar>
</View>
</BottomSheet.Content>
</DatePicker>
{submitted && !birthdate ? (
<Field.Error>We need your date of birth to continue.</Field.Error>
) : null}
</Field>monthParts stands for the Calendar parts of the Usage example. Write them once in a small component of your app if every picker shows the same month.