DateTimePicker
A day and a time, in one popup. The calendar and the clock sit side by side at exactly the same height, so the popup is one rectangle rather than two of different sizes pushed together.
import { DateTimePicker } from 'neba';
<DateTimePicker label="Publish at" placeholder="Pick a moment" minuteStep={15} clearable />;Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variantshared | 'solid' | 'outline' | 'text' | 'outline' | Weight of the surface: filled, hairline, or none |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Height and type scale |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | 'default' | 'compact' | 'default' | Padding only — never the height, never the type scale |
| elevationshared | 0 | 1 | 2 | 3 | 0 | Drop shadow depth. 0 means no shadow at all |
| value | Date | null | — | The chosen moment. Use with onValueChange for a controlled picker |
| defaultValue | Date | null | — | The initial value, for an uncontrolled picker |
| onValueChange | (value: Date | null) => void | — | Called when the chosen moment changes |
| defaultMonth | Date | — | Which month the calendar opens on when there is no value. Defaults to this one |
| minDate | Date | null | — | The earliest moment that may be chosen. Unlike DatePicker's this is read at full precision: the day stays selectable in the calendar and the clock blocks the hours before it |
| maxDate | Date | null | — | The other end of the same span |
| shouldDisableDate | (date: Date) => boolean | — | Blocks individual days inside the range — weekends, holidays, a room already booked. The cell stays in the grid, unavailable |
| weekStartsOnshared | 0 | 1 | 2 | 3 | 4 | 5 | 6 | — | Which day the week starts on, Sunday being 0. Defaults to whatever the locale says |
| hour12 | boolean | — | A 12-hour dial with an AM/PM column. Defaults to whatever the locale does |
| showSeconds | boolean | false | Adds the seconds column |
| hourStep | number | 1 | How far apart the rows of the hour column are |
| minuteStep | number | 1 | The same, for minutes |
| secondStep | number | 1 | The same, for seconds |
| shouldDisableTime | (value: Date, unit: 'hour' | 'minute' | 'second' | 'meridiem') => boolean | — | Blocks individual rows. Called once per row per column with the instant that row would produce and the column it is in |
| showNowButton | boolean | true | Offers the shortcut to this moment in the footer |
| open | boolean | — | Whether the popup is open. Use with onOpenChange for a controlled one |
| defaultOpen | boolean | false | Whether it starts open |
| onOpenChange | (open: boolean) => void | — | Called when the popup opens or closes |
| locale | string | — | BCP 47 tag deciding the month and weekday names, the order of the header's two buttons, and how the trigger writes the value. Defaults to the browser's |
| format | Intl.DateTimeFormatOptions | — | How the trigger writes the value. Passed straight to Intl |
| placeholder | ReactNode | — | Shown in the trigger while nothing is chosen |
| clearable | boolean | false | Offers the × that empties the control |
| closeOnSelect | boolean | false | Closes the popup as soon as a day is chosen. A moment is a day *and* a time, so closing on the first of the two would leave the second unanswered |
| labels | Partial<PickerLabels> | — | The strings a screen reader hears. One object rather than eighteen props, because they are a set — the date names are not among them, those come from Intl |
| name | string | — | Identifies the field when a form is submitted, as YYYY-MM-DDTHH:MM — local, not UTC |
| fullWidth | boolean | false | Stretches to the width of the container |
| label | ReactNode | — | The label, wired to the control by Base UI's Field |
| description | ReactNode | — | Helper text |
| error | ReactNode | — | Error message. Its presence turns the control invalid and re-points the colour family at danger |
| invalid | boolean | !!error | Forces the invalid state without a message, for when a form library owns validity |
| readOnly | boolean | false | Shown but not changeable. Keeps its colour and edge, drains the saturation |
| disabled | boolean | false | Unavailable. Drops the colour family for neutral grey |
Everything else it does is DatePicker's and TimePicker's, unchanged: the same three calendar views, the same column behaviour, the same Date value. What is worth reading here is the part that is genuinely different.
The bounds are read at full precision
This is where a DateTimePicker earns being its own component rather than two fields in a row.
minDate on a DatePicker is day-granular — the 27th either exists or it does not. Here it is not, because the ordinary rule a scheduling form needs is "not before now", and now is a time as well as a day. So a minDate of 09:30 on the 27th leaves the 27th selectable in the calendar and greys out the morning in the clock.
The clock's own check is the span test TimePicker makes, moved onto the absolute timeline so it knows which day the columns are writing into. On the boundary day the hours before the minimum are blocked; on any later day none of them are.
One value, two halves, no order
Choosing a day changes the day and leaves the clock alone. Choosing an hour changes the clock and leaves the day alone. A picker that reset the time to midnight every time the date was corrected would make choosing a moment an ordered task, and nobody reads a popup in the order it was written.
That is also why closeOnSelect defaults to false and the footer carries Done: a moment is a day and a time, so closing on the first of the two would leave the second unanswered.
One glyph, not two
The trigger wears the calendar and not the clock. A control cannot say two things at once, and the date is the part a reader scans for.
Accessibility
The calendar is a role="grid" and the clock is a set of role="listbox" columns, exactly as they are in the two components this one is made of — see DatePicker and TimePicker. The trigger writes both halves into one string with Intl, so what a screen reader reads out is one sentence rather than two fields it has to join up.