TimePicker
A time of day, chosen from columns. The bounds are checked against the span a row stands for, which is what keeps half past nine reachable when the minimum is 09:30.
import { TimePicker } from 'neba';
<TimePicker label="Starts at" placeholder="Pick a time" 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. A row of the clock is the same height |
| 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 time. A Date, so it carries a day as well — see referenceDate |
| defaultValue | Date | null | — | The initial value, for an uncontrolled picker |
| onValueChange | (value: Date | null) => void | — | Called when the chosen time changes |
| referenceDate | Date | today | The day a chosen time is written onto while there is no value yet |
| minTime | Date | null | — | The earliest time of day that may be chosen — only the clock is read. Compared against the span a row stands for, so 09:30 keeps the hour 9 and greys out the minutes before it |
| maxTime | Date | null | — | The other end of the same span |
| 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 the current time 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 column is touched. False by default, unlike DatePicker: a time is two answers, and closing after the first would make choosing 9:30 a matter of opening the popup twice |
| 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 HH:MM (HH:MM:SS with seconds shown) |
| 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 |
Columns, not a dial
Columns are the shape that answers what a time picker is actually asked. "Half past nine" is two glances, and "any time at all, on the hour" is a column you never touch. A clock face is prettier and needs a transform to read, which this library does not have.
The chosen row in each column is scrolled into view once, when the popup opens. That is the only imperative work in the component and it is not optional: a column of sixty minutes that opens at 00 while the value is 45 has hidden its own answer.
hour12 defaults to whatever the locale does, and the 12-hour column runs 12, 1, 2 … 11 — the order a dial is read in, not 0…11.
The value is a Date
Not a string and not a number of minutes, because everything else in this library that carries a moment is a Date, and because a bare time has nowhere to record that it crossed a daylight-saving boundary. referenceDate is the day a chosen time is written onto while the picker is still empty; it defaults to today and is held still for as long as the picker is mounted, so a popup left open across midnight does not quietly move the value onto a new day.
Examples
Steps, seconds and a 24-hour dial
Bounds
This is the detail that separates a working time picker from a frustrating one. minTime and maxTime are compared against the span a row covers, not against one instant inside it: with a minimum of 09:30 the hour 9 covers 09:00:00–09:59:59, which overlaps what is allowed, so it stays available and the minute column is where 00 through 25 grey out.
Comparing the whole candidate instead — which is the obvious implementation — hides the 9 entirely and makes half past nine unreachable.
shouldDisableTime is handed the instant a row would produce and the column it belongs to, so a rule may be as coarse as "no lunch hour" or as fine as one minute.
Why the popup stays open
closeOnSelect is false here and true on DatePicker, which is not an inconsistency. A time is two answers — the hour and the minute — and closing after the first would make choosing 9:30 a matter of opening the popup twice. The footer therefore carries a Done button, which is the thing to press that means "that is the one".
Accessibility
- Each column is a
role="listbox"ofrole="option"rows, namedHour,Minute,SecondandAM/PM. Those four names come fromlabelsand have English defaults. - The chosen row in each column carries
aria-selected; a blocked one carriesaria-disabledrather than thedisabledattribute, so it stays reachable and announces why it cannot be taken. - A live region beside the columns reads out the whole time whenever it changes. Three unlabelled lists of numbers say nothing on their own to someone reading the screen rather than looking at it.