Skip to main content

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:

PropTypeDefaultDescription
valueDate | nullControlled selected date-time.
defaultValueDate | nullInitial value for uncontrolled usage.
onChange(d: Date | null) => voidFired when either the date or time portion changes.
formatstring | Intl.DateTimeFormatOptions'YYYY-MM-DD HH:mm'Token format string or Intl.DateTimeFormat options.
closeOnSelectbooleanfalseOff by default — users typically tweak both halves before committing.
editablebooleantrueAllow segmented keyboard typing (type the date-time directly across all segments). false makes the field picker-only.
popoverbooleantrueWhether the calendar + time popover exists. false makes the field input-only.
incrementMinutesnumber1Step between minute-wheel values (1 = every minute, iOS-style).
incrementSecondsnumber1Step between second-wheel values (only with enableSeconds).
iconLeftNamestring'calendar-alt'Decorative left icon glyph for the wrapping Control (shown by default). Set '' to hide.
triggerIconbooleantrueShow a clickable launcher button on the right that toggles the popover.
triggerIconNamestring'chevron-down'Glyph for the right launcher button.
mobileNativeboolean | 'auto''auto'Use <input type="datetime-local"> on coarse-pointer devices.
min / maxDateBounds enforced across both date and time.
shouldDisableDate(d: Date) => booleanDisable 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) => booleanBlock specific times. Blocked times are also rejected during manual typing.
firstDayOfWeek0..60Calendar week start.
hourFormat'12' | '24''24'Time format.
enableSecondsbooleanfalseShow 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 propsSee 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}
/>

Forced to the custom popover

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}
/>

iOS Safari has no seconds wheel (Android Chrome shows one with caveats)

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}
/>

Forced to the custom popover

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>

Forced to the custom popover

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} />;
}

iOS native does not enforce min/max in the picker

On 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}
/>

Forced to the custom popover

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}
/>

Forced to the custom popover

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}
/>

Forced to the custom popover

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} />

Native picker support varies — iOS lags Android

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 respects step (e.g. only 0/15/30/45 minutes selectable when step=900). iOS shows every value regardless.
  • enableSeconds — Android Chrome shows a seconds component when step < 60 (with some long-standing quirks for datetime-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>

Forced to the custom popover

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.

Opening the picker vs. typing

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.

editablepopoverBehavior
truetrueBoth — segmented typing + popover (default)
falsetruePicker-only — typing inert, popover opens
truefalseInput-only — segmented typing, no popover
falsefalseStatic 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.

KeyAction
/ Increment / decrement the active segment (wraps in place)
/ Move to previous / next segment
09Overwrite the active segment; auto-advances when no further digit is valid
a / A / p / PToggle AM/PM on the meridiem segment (12-hour formats)
Separator (- / : . space)Skip to the next segment without inserting the character
BackspaceClear the typed-digit buffer; if already cleared, move to the previous segment
TabClear segment selection so focus moves out naturally
EscapeClose the popover

On the popover

KeyAction
Open popover (when closed)
EscapeClose popover
/ Move focused date by ±1 day
/ Move focused date by ±7 days
PageUp / PageDownMove focused date by ±1 month
Shift+PageUp/DownMove focused date by ±1 year
Home / EndJump to start / end of week
Enter / SpaceSelect focused date
TabMove 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

PropDescription
nameForm field name.
formOptional id of the form the input belongs to.
requiredMarks 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" with aria-haspopup="dialog", aria-expanded, and aria-controls.
  • Popover panel has role="dialog" with an accessible name.
  • Calendar uses role="grid"; cells expose aria-selected, aria-disabled, and aria-current="date".
  • Each time wheel uses role="spinbutton" with aria-valuemin, aria-valuemax, aria-valuenow, and aria-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 / ✓).


Additional Resources

Pro Tip

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.