DateTimePicker
A day and a time of day, picked in one panel.
A day and a time of day, picked in one panel and carried in one Date.
A date field beside a time field is two decisions the reader has to make separately and then hold together — and the two halves can disagree, which is how a booking ends up on the right day at a time that has already passed. Here the calendar and the scale are the same panel over one value, so what is on screen is the answer rather than two thirds of it.
The layout follows from that: the calendar is the coarse choice and takes the room, the time sits under it behind a hairline, and one Done finishes both.
Installation
DateTimePicker ships with the library — no separate install.
import { DateTimePicker, Frame, Text } from 'panelui-native';Or copy the source into your project, to own and edit it:
npx panelui-cli@latest add date-time-pickerUsage
const [when, setWhen] = useState<Date>();
<DateTimePicker value={when} onValueChange={setWhen} />Composition
<DateTimePicker>
<DateTimePicker.Trigger>…</DateTimePicker.Trigger>
</DateTimePicker>DateTimePicker.Trigger— Wrap your own trigger in it to replace the default button. It isPopover.Trigger— the same partDatePickerandTimePickerre-export, so a field row written for one works for all three.
Examples
One value, filled in from either end
onValueChange fires on every change to either half, not on Done — Done closes the panel, it does not decide anything the caller has not already been told.
The halves can be filled in either order. A day picked first takes the top of the current hour until the time is touched; a time picked first means today, because a time is not a Date without a day and the day the reader is looking at is the only defensible guess. The alternative — emitting nothing until both halves have been touched — is a form that silently does nothing when you use it in the order it did not expect.
const [when, setWhen] = useState<Date>();
<DateTimePicker
value={when}
onValueChange={setWhen}
placeholder="Pick a date and time"
/>Where the panel goes
popover anchors it to the trigger and is the default. bottom-sheet is usually the better answer on a phone: a calendar and a scale stacked is a tall panel, and a sheet has the height to give it. dialog is for when the appointment is the decision on the screen. inline renders the panel bare, for composing into a Frame or a form.
<DateTimePicker presentation="bottom-sheet" />
<DateTimePicker presentation="dialog" />
{/* Bare, inside something of your own. */}
<Frame>
<Frame.Header><Frame.Title>Book a fitting</Frame.Title></Frame.Header>
<Frame.Panel>
<View className="p-3">
<DateTimePicker presentation="inline" value={when} onValueChange={setWhen} />
</View>
</Frame.Panel>
</Frame>Which face the time is picked on
ruler by default rather than the wheel. Under a month grid the panel is already tall, and the wheel is five rows of it; the ruler is one readout over a scale, reads at arm's length, and is the one face that fits under a calendar without the whole thing needing to scroll.
Where the exact minute matters more than the height, the other two faces are a prop away and produce the same value.
{/* The default. */}
<DateTimePicker layout="ruler" />
{/* Any minute in the day, two flicks away. */}
<DateTimePicker layout="wheel" minuteStep={5} hourCycle={24} />
{/* A face beside a list of slots. */}
<DateTimePicker layout="clock" minuteStep={30} />Bounding it on both axes
minDate and maxDate bound the calendar; minTime and maxTime bound the time of day, and minuteStep decides how finely it can be landed on. A picker that offers a slot nobody can be given is a picker that has to reject it afterwards, which is the same rejection arriving later and less usefully.
The bounds are independent, so this reads as "any day in the next three weeks, between nine and half past five" — which is what opening hours are.
<DateTimePicker
value={when}
onValueChange={setWhen}
minDate={today}
maxDate={threeWeeksOut}
minTime={{ hour: 9, minute: 0 }}
maxTime={{ hour: 17, minute: 30 }}
minuteStep={30}
/>A trigger of your own
Pass a child and it is cloned with an onPress that opens the panel, so a field row, a list item or an icon button can stand in for the default button without this component knowing what any of them look like.
<DateTimePicker value={when} onValueChange={setWhen}>
<Item onPress={() => {}}>
<Item.Content>
<Item.Title>Starts</Item.Title>
<Item.Description>{when ? format(when) : 'Not set'}</Item.Description>
</Item.Content>
<ChevronRightIcon size={16} />
</Item>
</DateTimePicker>API Reference
DateTimePicker
| Prop | Type | Default | Description |
|---|---|---|---|
value | Date | — | Controlled value. One Date carrying both halves. |
defaultValue | Date | — | Starting value when uncontrolled. |
onValueChange | (value: Date) => void | — | Fires on every change to either half, not on Done. Done closes the panel; it does not decide anything the caller has not already been told. |
layout | TimePickerLayout | 'ruler' | Which face the time is picked on. ruler is the one that fits here. |
presentation | DateTimePickerPresentation | 'popover' | Anchored panel, a sheet, a dialog, or the panel with nothing around it. |
open | boolean | — | Controlled open state of the panel. |
onOpenChange | (open: boolean) => void | — | |
hourCycle | HourCycle | 12 | 12 shows a meridiem, 24 does not. The value is 24-hour either way. |
minuteStep | number | — | Minutes between one selectable time and the next. |
minTime | TimeValue | — | Earliest selectable time of day, inclusive. |
maxTime | TimeValue | — | Latest selectable time of day, inclusive. |
placeholder | string | DEFAULT_PLACEHOLDER | What the trigger reads when nothing has been chosen. |
format | (value: Date) => string | — | Override how the chosen value is written on the trigger. |
disabled | boolean | false | Stop the trigger opening it, and the panel from being used. |
disabledDates | CalendarDisabled | — | Days that cannot be picked: a list, a span, or a rule. |
minDate | Date | — | Earliest selectable day. |
maxDate | Date | — | Latest selectable day. |
captionLayout | CalendarCaptionLayout | 'label' | dropdown swaps the month caption for month and year pickers. |
weekStartsOn | number | 0 | 0 is Sunday. |
locale | string | — | BCP 47 tag for the month names, the time and the trigger's own text. |
calendar | CalendarSystem | 'gregory' | Which calendar the months and day numbers are counted in. |
doneLabel | string | 'Done' | Label on the button that closes the panel. |
className | string | — |
Every part also accepts the underlying React Native props (ViewProps or TextProps) and a className for Tailwind utilities.
Notes
It does not close on the date
DatePicker closes as soon as a single day is tapped, because at that point there is nothing left to say. Here there is: the day is half the value, and closing on it would hide the other half at the moment it became relevant.
So the panel stays until Done — in every presentation, including the popover, which is the one place DatePicker has no Done button at all. Tapping outside still dismisses it, but that is how a popover is abandoned, and something has to say when both halves are settled.
The panel is a fixed width
That is what lets the two halves line up. A month grid and a time scale that each measured themselves would be two boxes of slightly different widths stacked on each other, and the seam shows at any size. The hairline between them is a hairline rather than a gap for the same reason: space alone reads as two controls that happen to be near each other.
The value is a Date, the time is not
The component's value is one Date carrying both halves, because that is what a caller stores and sends. Inside, and in minTime / maxTime, a time of day is { hour, minute } on a 24-hour clock — a Date cannot hold a time without also holding a day, and the two conversions that forces on every caller are where the daylight-saving bugs live. hourCycle decides whether a meridiem is shown; it does not change what is stored.
When two fields are still the right answer
If the day and the time are genuinely separate decisions — a recurring event's day-of-week and its time, a filter with an optional time — keep them apart. This component is for the case where one moment is being chosen and half of it is not an answer.