DateTimeInput
Overview
The DateTimeInput combines a calendar and a time wheel spinner in a single popover — an iOS-style layout. The popover opens to the calendar with an iOS-style footer: the selected time as a tappable value, a Reset button (reverts your edits to the value the popover opened with), and a circular ✓ confirm button. Clicking the time floats the wheel spinner over the calendar (hours, minutes, and optionally seconds — the same wheels the TimeInput uses), so the popover never grows or scrolls; clicking the time again, clicking outside the wheel card, or pressing Escape hides it. It supports the full prop surface of both DateInput and TimeInput, plus a native <input type="datetime-local"> fallback for touch devices. A clickable launcher icon on the right opens the popover, and you can type directly in the field with segmented keyboard entry.
Import
import { DateTimeInput } from '@allxsmith/bestax-bulma';
Props
The DateTimeInput prop set is the union of DateInput and TimeInput props. Notable additions and overrides:
| Prop | Type | Default | Description |
|---|---|---|---|
value | Date | null | — | Controlled selected date-time. |
defaultValue | Date | null | — | Initial value for uncontrolled usage. |
onChange | (d: Date | null) => void | — | Fired when either the date or time portion changes. |
format | string | Intl.DateTimeFormatOptions | 'YYYY-MM-DD HH:mm' | Token format string or Intl.DateTimeFormat options. |
closeOnSelect | boolean | false | Off by default — users typically tweak both halves before committing. |
editable | boolean | true | Allow segmented keyboard typing (type the date-time directly across all segments). false makes the field picker-only. |
popover | boolean | true | Whether the calendar + time popover exists. false makes the field input-only. |
incrementMinutes | number | 1 | Step between minute-wheel values (1 = every minute, iOS-style). |
incrementSeconds | number | 1 | Step between second-wheel values (only with enableSeconds). |
iconLeftName | string | 'calendar-alt' | Decorative left icon glyph for the wrapping Control (shown by default). Set '' to hide. |
triggerIcon | boolean | true | Show a clickable launcher button on the right that toggles the popover. |
triggerIconName | string | 'chevron-down' | Glyph for the right launcher button. |
mobileNative | boolean | 'auto' | 'auto' | Use <input type="datetime-local"> on coarse-pointer devices. |
min / max | Date | — | Bounds enforced across both date and time. |
shouldDisableDate | (d: Date) => boolean | — | Disable specific dates. Blocked dates are also rejected during manual typing (the predicate receives the full candidate date-time — prefer day-based checks). |
unselectableTimes | (d: Date) => boolean | — | Block specific times. Blocked times are also rejected during manual typing. |
firstDayOfWeek | 0..6 | 0 | Calendar week start. |
hourFormat | '12' | '24' | '24' | Time format. |
enableSeconds | boolean | false | Show seconds column. Note: iOS Safari's native datetime-local picker UI does not include a seconds wheel; pass mobileNative={false} if you need one on iOS. |
| ... | All DateInput + TimeInput props | See those pages for the full list. | |
| ... | All standard HTML and Bulma helper props | (See Helper Props) |
When you pass an explicit token format, that format is the source of truth for the time wheels and the footer time pill: a 12-hour format (h/hh with A/a) drives a 12-hour wheel with an AM/PM column, and a 24-hour format (H/HH) drives a 24-hour wheel — regardless of hourFormat. So hourFormat only applies when you don't pass a format. (If format is an Intl.DateTimeFormat options object rather than a token string, the cycle can't be read from it and the wheel/pill fall back to hourFormat.)
Usage
Basic DateTimeInput
The popover opens to the calendar with an iOS-style footer — the selected time, a Reset button, and a circular ✓ to confirm. Click the time value to float the wheel spinner over the calendar.
<DateTimeInput label="Appointment" placeholder="YYYY-MM-DD HH:MM" />
Typing-first — the same example with openOnFocus={false}: focusing or clicking the field lets you type; open the popover with the launcher icon (or press ↓).
<DateTimeInput label="Appointment" placeholder="YYYY-MM-DD HH:MM" openOnFocus={false} />
Controlled
function example() { const [v, setV] = useState(new Date()); return ( <Block> <DateTimeInput label="Meeting" value={v} onChange={setV} /> <Paragraph mt="2">Selected: {v ? v.toString() : '—'}</Paragraph> </Block> ); }
Typing-first — the same controlled example with openOnFocus={false} added: click in and type freely, and reach for the launcher icon (or ↓) when you want the popover.
function example() { const [v, setV] = useState(new Date()); return ( <Block> <DateTimeInput label="Meeting" value={v} onChange={setV} openOnFocus={false} /> <Paragraph mt="2">Selected: {v ? v.toString() : '—'}</Paragraph> </Block> ); }
12-hour Format
<DateTimeInput label="12-hour" hourFormat="12" defaultValue={new Date()} mobileNative={false} />
The OS-native pickers use the device clock setting, so hourFormat is ignored there. This example forces mobileNative={false} so the 12-hour format shows on touch devices too.
Typing-first — the same 12-hour field with openOnFocus={false}: type across the segments (press a / p on the trailing meridiem) and open the popover with the launcher icon or ↓.
function example() { return ( <DateTimeInput label="12-hour entry" hourFormat="12" defaultValue={new Date(2024, 5, 7, 13, 45)} mobileNative={false} openOnFocus={false} /> ); }
With Seconds
<DateTimeInput label="With seconds" enableSeconds defaultValue={new Date()} mobileNative={false} />
This example forces mobileNative={false} so the seconds wheel shows on every device.
Android Chrome generally renders a seconds component in its native datetime-local picker when step < 60 (with some long-standing quirks — exact seconds-spinner behavior varies by Android version). iOS Safari has no seconds wheel under any circumstances. If you need a guaranteed seconds wheel, pass mobileNative={false} to force the custom wheel popover. See Mobile Native below for the full iOS-vs-Android picker support matrix.
Typing-first — the same seconds-enabled field with openOnFocus={false}: the typed walk gains a seconds segment (year → month → day → hours → minutes → seconds), and the launcher icon (or ↓) opens the popover.
function example() { return ( <DateTimeInput label="With a seconds segment" enableSeconds defaultValue={new Date(2024, 5, 7, 13, 45, 30)} mobileNative={false} openOnFocus={false} /> ); }
12-hour with Seconds
Combine hourFormat="12" with enableSeconds for an hh:mm:ss A field — the wheel gains hours, minutes, seconds, and AM/PM columns.
<DateTimeInput label="12-hour with seconds" hourFormat="12" enableSeconds defaultValue={new Date()} mobileNative={false} />
The OS-native pickers honor neither half: hourFormat follows the device clock setting, and iOS Safari has no seconds wheel at all. This example forces mobileNative={false} so the 12-hour seconds wheel shows on every device.
Typing-first — the full segment set with openOnFocus={false}: type through date, hh, mm, ss, toggle the trailing meridiem with a / p, and open the popover via the launcher icon or ↓.
function example() { return ( <DateTimeInput label="12-hour with seconds and AM/PM" hourFormat="12" enableSeconds defaultValue={new Date(2024, 5, 7, 13, 45, 30)} mobileNative={false} openOnFocus={false} /> ); }
Formats
The format prop takes a token string or Intl.DateTimeFormatOptions spanning the whole date-time. Padded token formats keep the field segmented-typeable; Intl formats are display-only unless you add a custom parse.
<Block display="flex" flexDirection="column" gap="4"> <DateTimeInput label="YYYY-MM-DD HH:mm (default)" defaultValue={new Date(2026, 4, 30, 13, 45)} mobileNative={false} /> <DateTimeInput label="MM/DD/YYYY hh:mm A" format="MM/DD/YYYY hh:mm A" defaultValue={new Date(2026, 4, 30, 13, 45)} mobileNative={false} /> <DateTimeInput label="DD.MM.YYYY HH:mm" format="DD.MM.YYYY HH:mm" defaultValue={new Date(2026, 4, 30, 13, 45)} mobileNative={false} /> <DateTimeInput label="Intl — display only" format={{ dateStyle: 'medium', timeStyle: 'short' }} editable={false} defaultValue={new Date(2026, 4, 30, 13, 45)} mobileNative={false} /> </Block>
format is ignored by the OS-native pickers (they use the device locale), so these examples set mobileNative={false} to show the formats on touch devices too.
Typing-first — a custom DD.MM.YYYY HH:mm format with openOnFocus={false}: segments follow the format order (day first) and typing ., space, or : jumps the separators — so 25.12.2026 09:30 flows straight through — with the launcher icon (or ↓) opening the popover.
function example() { return ( <DateTimeInput label="DD.MM.YYYY HH:mm with dot separators" format="DD.MM.YYYY HH:mm" defaultValue={new Date(2024, 5, 7, 13, 45)} mobileNative={false} openOnFocus={false} /> ); }
Launcher Icon
A clickable launcher sits on the right and toggles the popover — handy for input-mode (openOnFocus={false}) where you type the value and click the icon to open the picker. Override its glyph with triggerIconName, or hide it with triggerIcon={false} (the popover still opens on focus / click). The decorative left icon is independent: it shows by default, takes its glyph from iconLeftName, and is hidden with iconLeftName="".
<Block display="flex" flexDirection="column" gap="4"> <DateTimeInput label="Default (left icon + right launcher)" /> <DateTimeInput label="Custom launcher glyph" triggerIconName="calendar-check" /> <DateTimeInput label="No launcher" triggerIcon={false} /> <DateTimeInput label="Left icon hidden" iconLeftName="" /> </Block>
Typing-first — the same group with openOnFocus={false} on every instance, so clicking a field just lets you type and the launcher icon opens the popover; note the triggerIcon={false} instance has no launcher, leaving its popover keyboard-only via ↓.
<Block display="flex" flexDirection="column" gap="4"> <DateTimeInput label="Default (left icon + right launcher)" openOnFocus={false} /> <DateTimeInput label="Custom launcher glyph" triggerIconName="calendar-check" openOnFocus={false} /> <DateTimeInput label="No launcher (popover via ↓ only)" triggerIcon={false} openOnFocus={false} /> <DateTimeInput label="Left icon hidden" iconLeftName="" openOnFocus={false} /> </Block>
Min and Max
Bounds apply to the combined date-time.
function example() { const today = new Date(); const min = new Date(today); min.setHours(9, 0, 0, 0); const max = new Date(today); max.setHours(17, 0, 0, 0); return <DateTimeInput label="Office hours today" min={min} max={max} />; }
min/max in the pickerOn iOS Safari the picker UI lets the user pick any value; min/max only fire at form-submission validation (WebKit bug #225639, still open). Pass mobileNative={false} for iOS-side enforcement. Android Chrome's native picker does honor them.
Typing-first — the same bounds with openOnFocus={false}: keystrokes and ↑ / ↓ arrows never produce a value outside min/max, and the launcher icon (or ↓) opens the popover.
function example() { const now = new Date(); const min = new Date(now); min.setHours(9, 0, 0, 0); const max = new Date(now); max.setHours(17, 0, 0, 0); const noon = new Date(now); noon.setHours(12, 0, 0, 0); return ( <DateTimeInput label="Office hours today only — typed entry too" min={min} max={max} defaultValue={noon} openOnFocus={false} /> ); }
Disabled Dates
Blocked dates are disabled in the calendar and rejected during manual typing, the same way min/max are enforced.
<DateTimeInput label="No weekend appointments" shouldDisableDate={d => d.getDay() === 0 || d.getDay() === 6} mobileNative={false} />
HTML has no predicate equivalent, so the OS-native pickers can't block any dates. This example forces mobileNative={false} so the rule works on touch devices; in your app keep mobileNative="auto" and also validate in onChange.
Typing-first — the same predicate with openOnFocus={false}: a keystroke or arrow that lands on a blocked date is rejected (matching the disabled calendar cells), and the launcher icon (or ↓) opens the popover.
function example() { return ( <DateTimeInput label="Weekends rejected while typing" shouldDisableDate={d => d.getDay() === 0 || d.getDay() === 6} defaultValue={new Date(2024, 5, 7, 13, 45)} mobileNative={false} openOnFocus={false} /> ); }
Unselectable Times
Blocked times are skipped by the wheels and rejected during manual typing.
<DateTimeInput label="Lunch hour blocked" unselectableTimes={d => d.getHours() === 12} defaultValue={new Date()} mobileNative={false} />
Same as Disabled Dates — the OS-native pickers can't evaluate predicates. This example forces mobileNative={false} so the blocked hour works on touch devices too.
Typing-first — the same blocked hour with openOnFocus={false}: setting the hour segment to a blocked hour is vetoed by the unselectableTimes predicate (just as the wheels skip it), and the launcher icon (or ↓) opens the popover.
function example() { return ( <DateTimeInput label="Lunch hour rejected while typing" unselectableTimes={d => d.getHours() === 12} defaultValue={new Date(2024, 5, 7, 11, 30)} mobileNative={false} openOnFocus={false} /> ); }
Inline
<DateTimeInput label="Inline" inline defaultValue={new Date()} />
First Day of Week
<DateTimeInput label="Monday-first" firstDayOfWeek={1} defaultValue={new Date()} mobileNative={false} />
The OS-native calendars use the device locale for the week start, so firstDayOfWeek is ignored there. This example forces mobileNative={false} so the Monday-first grid shows on touch devices too.
Typing-first — the same Monday-first example with openOnFocus={false}: type in the field directly and bring up the popover with the launcher icon (or ↓) to see the week start.
<DateTimeInput label="Monday-first" firstDayOfWeek={1} defaultValue={new Date()} mobileNative={false} openOnFocus={false} />
Mobile Native
By default mobileNative='auto': on touch devices with a small viewport ((pointer: coarse) and (max-width: 768px)) the input swaps to a plain <input type="datetime-local"> so the OS-native picker handles the interaction. Pass true or false to override.
<DateTimeInput label="Native datetime-local" mobileNative={true} />
The OS-native fallback is just a <input type="datetime-local">, so it inherits each platform's behavior. The custom popover (mobileNative={false}) honors every prop on every device.
Honored on Android Chrome but NOT on iOS Safari:
min/max— Android Chrome dims out-of-range values in the picker; iOS lets the user pick any value, only firing the constraint at form-submission validation. (WebKit bug #225639, still open as of 2026.)incrementMinutes,incrementHours— Android Chrome respectsstep(e.g. only 0/15/30/45 minutes selectable whenstep=900). iOS shows every value regardless.enableSeconds— Android Chrome shows a seconds component whenstep < 60(with some long-standing quirks fordatetime-local). iOS has no seconds wheel under any circumstances.
Ignored on BOTH iOS Safari and Android Chrome (HTML-spec gaps):
shouldDisableDate,unselectableDates,unselectableTimes— HTML has no predicate/array equivalent; native pickers can't evaluate functions.firstDayOfWeek,dayNames,monthNames,nearbyMonthDays— both use the device's system locale.hourFormat— both use the device's system clock setting (12h/24h).format,locale— both use the device's system locale; per-input overrides are ignored.placeholder— neither renders placeholder text.
If any of these matter, pass mobileNative={false} to force the custom popover (works on every device), or duplicate the constraint in onChange / server-side validation.
Locale
<Block display="flex" flexDirection="column" gap="4"> <DateTimeInput label="ja-JP" locale="ja-JP" defaultValue={new Date()} mobileNative={false} /> <DateTimeInput label="fr-FR" locale="fr-FR" defaultValue={new Date()} mobileNative={false} /> </Block>
The OS-native pickers always use the device's system locale, so these examples set mobileNative={false} to show the per-input locale on touch devices too.
Typing-first — the same locales with openOnFocus={false} on each instance: focus to type the localized value, and use the launcher icon (or ↓) to open the popover.
<Block display="flex" flexDirection="column" gap="4"> <DateTimeInput label="ja-JP" locale="ja-JP" defaultValue={new Date()} mobileNative={false} openOnFocus={false} /> <DateTimeInput label="fr-FR" locale="fr-FR" defaultValue={new Date()} mobileNative={false} openOnFocus={false} /> </Block>
Sizes
<Block display="flex" flexDirection="column" gap="4"> <DateTimeInput label="Small" controlSize="small" size="small" /> <DateTimeInput label="Default" /> <DateTimeInput label="Medium" controlSize="medium" size="medium" /> <DateTimeInput label="Large" controlSize="large" size="large" /> </Block>
Typing-first — every size with openOnFocus={false}: clicking any field lets you type straight away, with the launcher icon (or ↓) opening the popover.
<Block display="flex" flexDirection="column" gap="4"> <DateTimeInput label="Small" controlSize="small" size="small" openOnFocus={false} /> <DateTimeInput label="Default" openOnFocus={false} /> <DateTimeInput label="Medium" controlSize="medium" size="medium" openOnFocus={false} /> <DateTimeInput label="Large" controlSize="large" size="large" openOnFocus={false} /> </Block>
Colors
<Block display="flex" flexDirection="column" gap="4"> <DateTimeInput label="Primary" color="primary" /> <DateTimeInput label="Info" color="info" /> <DateTimeInput label="Success" color="success" /> <DateTimeInput label="Warning" color="warning" /> <DateTimeInput label="Danger" color="danger" /> </Block>
Typing-first — the same colors with openOnFocus={false} on every instance: type directly in any field and open the popover with the launcher icon (or ↓).
<Block display="flex" flexDirection="column" gap="4"> <DateTimeInput label="Primary" color="primary" openOnFocus={false} /> <DateTimeInput label="Info" color="info" openOnFocus={false} /> <DateTimeInput label="Success" color="success" openOnFocus={false} /> <DateTimeInput label="Warning" color="warning" openOnFocus={false} /> <DateTimeInput label="Danger" color="danger" openOnFocus={false} /> </Block>
States
<Block display="flex" flexDirection="column" gap="4"> <DateTimeInput label="Disabled" disabled /> <DateTimeInput label="Read only" readOnly defaultValue={new Date()} /> <DateTimeInput label="Loading" isLoading /> </Block>
Context-Aware Rendering
Default (with label)
<DateTimeInput label="When" placeholder="YYYY-MM-DD HH:MM" />
With Field Wrapper
function example() { return ( <Field horizontal label="When"> <Field.Body> <Field> <DateTimeInput placeholder="YYYY-MM-DD HH:MM" /> </Field> </Field.Body> </Field> ); }
With Field and Control Wrappers
function example() { return ( <Field horizontal label="When"> <Field.Body> <Field> <Control iconLeftName="calendar-alt"> <DateTimeInput placeholder="YYYY-MM-DD HH:MM" /> </Control> </Field> </Field.Body> </Field> ); }
Manual Keyboard Entry
The single input spans the whole date-time: year → month → day → hours → minutes (plus seconds / AM-PM when enabled). Focus highlights the year; ↑ / ↓ adjust a segment, → / ← move between them, digits overwrite with auto-advance, and typing a -, space, or : jumps across the separators. Segment mode activates whenever format is a token string with padded tokens (YYYY, MM, DD, HH/hh, mm, ss, A); Intl.DateTimeFormatOptions formats and single-character tokens fall back to free-form text.
These examples use openOnFocus={false} so the popover doesn't cover the input.
With openOnFocus={false} (used here), clicking the field just lets you type — the popover does not appear on focus or click. Open the picker by clicking the launcher icon on the right (or pressing ↓). With the default openOnFocus={true}, focusing or clicking the field opens the popover immediately (you can still type while it's open).
Basic
function example() { return ( <DateTimeInput label="Type across date and time" defaultValue={new Date(2024, 5, 7, 13, 45)} openOnFocus={false} /> ); }
Controlled with live value
function example() { const [v, setV] = useState(() => new Date(2024, 5, 7, 13, 45)); return ( <Block> <DateTimeInput label="Arrow or type — value updates live" value={v} onChange={setV} openOnFocus={false} /> <Paragraph mt="2">Selected: {v ? v.toString() : '—'}</Paragraph> </Block> ); }
Free-form fallback
An Intl.DateTimeFormatOptions format has no segment map, so entry is free-form — focusing does not highlight a segment and the text parses on Enter or blur instead.
function example() { return ( <DateTimeInput label="Free-form (Intl format)" format={{ dateStyle: 'medium', timeStyle: 'short' }} defaultValue={new Date(2024, 5, 7, 13, 45)} openOnFocus={false} /> ); }
Picker vs Input Modes
editable controls whether segmented typing is allowed; popover controls whether the calendar + time panel exists. Both default to true.
editable | popover | Behavior |
|---|---|---|
true | true | Both — segmented typing + popover (default) |
false | true | Picker-only — typing inert, popover opens |
true | false | Input-only — segmented typing, no popover |
false | false | Static display |
Picker only
<DateTimeInput label="Picker only" editable={false} defaultValue={new Date()} />
Input only
<DateTimeInput label="Input only" popover={false} defaultValue={new Date()} />
Keyboard Navigation
On the input (segmented entry)
A single field spans year → month → day → hours → minutes (→ seconds → AM/PM when enabled). Focus highlights the year; segment mode activates whenever format is a token string with padded tokens (YYYY, MM, DD, HH, hh, mm, ss, A); Intl formats and single-character tokens fall back to free-form text entry.
| Key | Action |
|---|---|
↑ / ↓ | Increment / decrement the active segment (wraps in place) |
← / → | Move to previous / next segment |
0–9 | Overwrite the active segment; auto-advances when no further digit is valid |
a / A / p / P | Toggle AM/PM on the meridiem segment (12-hour formats) |
Separator (- / : . space) | Skip to the next segment without inserting the character |
Backspace | Clear the typed-digit buffer; if already cleared, move to the previous segment |
Tab | Clear segment selection so focus moves out naturally |
Escape | Close the popover |
On the popover
| Key | Action |
|---|---|
↓ | Open popover (when closed) |
Escape | Close popover |
← / → | Move focused date by ±1 day |
↑ / ↓ | Move focused date by ±7 days |
PageUp / PageDown | Move focused date by ±1 month |
Shift+PageUp/Down | Move focused date by ±1 year |
Home / End | Jump to start / end of week |
Enter / Space | Select focused date |
Tab | Move focus from calendar → time button → footer |
Activate the footer time button (Enter / Space) to float the wheels over the calendar. On a time wheel: ↑ / ↓ change the value, ← / → move between the hours / minutes / (seconds) columns, and Enter commits and closes. While the wheels are open, Escape collapses them (a second Escape closes the popover), and clicking anywhere outside the wheel card dismisses them.
Form Submission
| Prop | Description |
|---|---|
name | Form field name. |
form | Optional id of the form the input belongs to. |
required | Marks the field as required for native HTML form validation. |
function DateTimeInputFormDemo() { const [submitted, setSubmitted] = React.useState(''); return ( <form onSubmit={e => { e.preventDefault(); const fd = new FormData(e.currentTarget); setSubmitted(JSON.stringify(Array.from(fd.entries()), null, 2)); }} > <DateTimeInput name="when" label="When" required /> <div style={{ marginTop: '1rem' }}> <button type="submit" className="button is-primary"> Submit </button> </div> {submitted && <pre style={{ marginTop: '1rem' }}>{submitted}</pre>} </form> ); }
Accessibility
- Trigger uses
role="combobox"witharia-haspopup="dialog",aria-expanded, andaria-controls. - Popover panel has
role="dialog"with an accessible name. - Calendar uses
role="grid"; cells exposearia-selected,aria-disabled, andaria-current="date". - Each time wheel uses
role="spinbutton"witharia-valuemin,aria-valuemax,aria-valuenow, andaria-valuetext. - The footer's confirm button exposes an accessible label (
Done); the Reset button reverts your edits to the value the popover opened with. - Tab order naturally walks from calendar → time wheels → footer (Reset / ✓).
Related Components
Additional Resources
Pair DateTimeInput with closeOnSelect={false} (the default) so users can tweak both date and time before committing via OK — closing on the first date click would surprise them.