Skip to main content

DateInput

Overview

The DateInput component is a form input that opens a popover calendar for date selection. A clickable launcher icon on the right opens the popover, and you can type directly in the field with segmented keyboard entry. It uses native Date and Intl only (no extra dependencies), supports min/max bounds, disabled-date predicates, custom token formats or Intl.DateTimeFormatOptions, locale-aware day/month names, an inline mode, and a native <input type="date"> fallback for touch devices.


Import

import { DateInput } from '@allxsmith/bestax-bulma';

Props

PropTypeDefaultDescription
valueDate | nullControlled selected date.
defaultValueDate | nullInitial date for uncontrolled usage.
onChange(d: Date | null) => voidFired when the value changes.
onOpen() => voidFired when the popover opens.
onClose() => voidFired when the popover closes.
minDateEarliest selectable date.
maxDateLatest selectable date.
shouldDisableDate(d: Date) => booleanPredicate to disable specific dates (e.g. weekends). Blocked dates are also rejected during manual typing.
unselectableDatesDate[]Convenience array of disabled dates; merged with shouldDisableDate. Matched by calendar day and also rejected during manual typing.
firstDayOfWeek0 | 1 | 2 | 3 | 4 | 5 | 60Day the week starts on (0 = Sunday).
dayNamesstring[]Override the 7 day-name labels (in calendar order, post-rotation).
monthNamesstring[]Override the 12 month-name labels.
nearbyMonthDaysbooleantrueShow dimmed dates from adjacent months in the grid.
placeholderstringPlaceholder text for the input.
formatstring | Intl.DateTimeFormatOptions'YYYY-MM-DD'Token format string or Intl.DateTimeFormat options.
parse(s: string) => Date | nullCustom parser (use when format is Intl.DateTimeFormatOptions).
localestringBCP-47 locale tag for day/month names and Intl formatting.
inlinebooleanfalseRender the calendar inline (no popover).
mobileNativeboolean | 'auto''auto'Use <input type="date"> on coarse-pointer + small-viewport devices.
editablebooleantrueAllow segmented keyboard typing in the input (type the date directly, auto-advancing across segments). false makes the field picker-only.
popoverbooleantrueWhether the calendar popover exists. false makes the field input-only (segmented typing, no popover).
openOnFocusbooleantrueOpen the popover when the input is focused.
closeOnSelectbooleantrueClose the popover after a date is selected.
position'bottom-left' | 'bottom-right' | 'top-left' | 'top-right' | 'auto''bottom-left'Popover anchor position relative to the input.
appendToBodybooleanfalseRender the popover into document.body via portal.
disabledbooleanfalseDisable the input.
readOnlybooleanfalseMake the input read-only.
color'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger'Bulma color modifier.
size'small' | 'medium' | 'large'Size variant.
isRoundedbooleanfalseRender the input with rounded corners.
iconLeftNamestring'calendar'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.
namestringForm field name. Forwarded to a hidden ISO-formatted input.
formstringForm id the input belongs to.
requiredbooleanfalseMarks the input as required.
labelReact.ReactNodeField label (component auto-wraps in a Field if not already inside).
horizontalbooleanfalseRender the field with horizontal layout.
messageReact.ReactNodeHelp/validation text below the input.
messageColor'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger'Color modifier for the help message.
classNamestringAdditional CSS classes for the input.
refReact.Ref<HTMLInputElement>Forwarded to the underlying <input>.
...All standard HTML and Bulma helper props(See Helper Props)

Usage

Basic DateInput

A simple date picker with a popover calendar.

function example() {
  return <DateInput label="Pick a date" placeholder="YYYY-MM-DD" />;
}

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 ).

function example() {
  return (
    <DateInput
      label="Type a date"
      placeholder="YYYY-MM-DD"
      openOnFocus={false}
    />
  );
}


Controlled

Manage state externally with value and onChange.

function example() {
  const [value, setValue] = useState(new Date());
  return (
    <Block>
      <DateInput label="Date" value={value} onChange={setValue} />
      <Paragraph mt="2">
        Selected: {value ? value.toDateString() : '—'}
      </Paragraph>
    </Block>
  );
}

Typing-first — identical, but with openOnFocus={false} so focusing just lets you type; the calendar waits behind the launcher icon (or ).

function example() {
  const [value, setValue] = useState(new Date());
  return (
    <Block>
      <DateInput
        label="Date"
        value={value}
        onChange={setValue}
        openOnFocus={false}
      />
      <Paragraph mt="2">
        Selected: {value ? value.toDateString() : '—'}
      </Paragraph>
    </Block>
  );
}


Inline

Skip the popover and render the calendar inline.

<DateInput label="Inline calendar" inline defaultValue={new Date()} />


Min and Max

Limit selectable dates to a range.

function example() {
  const today = new Date();
  const min = new Date(today.getFullYear(), today.getMonth(), 1);
  const max = new Date(today.getFullYear(), today.getMonth() + 1, 0);
  return (
    <DateInput
      label="This month only"
      min={min}
      max={max}
      defaultValue={today}
    />
  );
}

iOS native does not enforce min/max in the picker

On iOS Safari the calendar lets the user pick any date; 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}: every keystroke and / arrow is clamped to the range (out-of-range candidates are silently rejected), and the launcher icon (or ) opens the calendar.

function example() {
  const now = new Date();
  const min = new Date(now.getFullYear(), now.getMonth(), 1);
  const max = new Date(now.getFullYear(), now.getMonth() + 1, 0);
  return (
    <DateInput
      label="Typing is clamped to this month"
      min={min}
      max={max}
      defaultValue={new Date(now.getFullYear(), now.getMonth(), 15)}
      openOnFocus={false}
    />
  );
}


Disabled Dates

Disable specific dates with shouldDisableDate (predicate) or unselectableDates (array). Blocked dates are disabled in the calendar and rejected during manual typing, the same way min/max are enforced.

<DateInput
  label="No weekends"
  shouldDisableDate={d => d.getDay() === 0 || d.getDay() === 6}
  mobileNative={false}
/>

Forced to the custom calendar

HTML has no equivalent to shouldDisableDate or unselectableDates, so the OS-native pickers can't block any dates. This example forces mobileNative={false} so the rule works on touch devices too; in your app, keep the default mobileNative="auto" and also validate in onChange.

Typing-first — with openOnFocus={false} the predicate also vetoes manual entry: typing or arrowing to a weekend is rejected, and the calendar stays tucked behind the launcher icon (or ).

function example() {
  return (
    <DateInput
      label="Weekends rejected while typing"
      shouldDisableDate={d => d.getDay() === 0 || d.getDay() === 6}
      defaultValue={new Date(2024, 5, 7)}
      mobileNative={false}
      openOnFocus={false}
    />
  );
}


Custom Format

Use an alternative token format. Supported tokens: YYYY YY MM M DD D HH H hh h mm m ss s A a.

<DateInput
  label="Date of birth"
  format="DD/MM/YYYY"
  placeholder="DD/MM/YYYY"
  mobileNative={false}
/>

Forced to the custom calendar

The OS-native pickers use the device's locale format and don't render placeholder text. This example forces mobileNative={false} so the format/placeholder show on touch devices too.

Typing-first — the same format with openOnFocus={false}: type day-first (typing / jumps to the next segment) and reach for the launcher icon (or ) when you want the calendar.

function example() {
  return (
    <DateInput
      label="DD/MM/YYYY"
      format="DD/MM/YYYY"
      defaultValue={new Date(2024, 5, 7)}
      mobileNative={false}
      openOnFocus={false}
    />
  );
}


Formats

The format prop takes a token string or Intl.DateTimeFormatOptions. Padded tokens (YYYY, YY, MM, DD) keep the field segmented-typeable; Intl formats are display-only unless you also pass a custom parse.

<Block display="flex" flexDirection="column" gap="4">
  <DateInput
    label="YYYY-MM-DD (default)"
    defaultValue={new Date(2026, 4, 30)}
    openOnFocus={false}
    mobileNative={false}
  />
  <DateInput
    label="DD/MM/YYYY"
    format="DD/MM/YYYY"
    defaultValue={new Date(2026, 4, 30)}
    openOnFocus={false}
    mobileNative={false}
  />
  <DateInput
    label="MM-DD-YYYY"
    format="MM-DD-YYYY"
    defaultValue={new Date(2026, 4, 30)}
    openOnFocus={false}
    mobileNative={false}
  />
  <DateInput
    label="DD.MM.YY"
    format="DD.MM.YY"
    defaultValue={new Date(2026, 4, 30)}
    openOnFocus={false}
    mobileNative={false}
  />
</Block>

Forced to the custom calendar

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.

For an Intl.DateTimeFormatOptions format, supply a parse so typed text round-trips to a Date:

function example() {
  const parse = s => {
    const t = Date.parse(s);
    return isNaN(t) ? null : new Date(t);
  };
  return (
    <DateInput
      label="Intl long + custom parse"
      format={{ year: 'numeric', month: 'long', day: 'numeric' }}
      parse={parse}
      defaultValue={new Date(2026, 4, 30)}
      mobileNative={false}
    />
  );
}

Typing-first — adding openOnFocus={false} here shows free-form entry: Intl formats have no segments, so typed text is committed on Enter or blur, and the calendar opens only via the launcher icon (or ).

function example() {
  const parse = s => {
    const t = Date.parse(s);
    return isNaN(t) ? null : new Date(t);
  };
  return (
    <DateInput
      label="Intl long + custom parse — typing-first"
      format={{ year: 'numeric', month: 'long', day: 'numeric' }}
      parse={parse}
      defaultValue={new Date(2026, 4, 30)}
      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 calendar. 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">
  <DateInput label="Default (left icon + right launcher)" />
  <DateInput label="Custom launcher glyph" triggerIconName="calendar-day" />
  <DateInput label="No launcher" triggerIcon={false} />
  <DateInput label="Left icon hidden" iconLeftName="" />
</Block>

Typing-first — the same group with openOnFocus={false}, where the launcher icon earns its keep; note that the triggerIcon={false} instance has no launcher, so its popover is keyboard-only ().

<Block display="flex" flexDirection="column" gap="4">
  <DateInput label="Default (left icon + right launcher)" openOnFocus={false} />
  <DateInput
    label="Custom launcher glyph"
    triggerIconName="calendar-day"
    openOnFocus={false}
  />
  <DateInput
    label="No launcher — popover via ↓ only"
    triggerIcon={false}
    openOnFocus={false}
  />
  <DateInput label="Left icon hidden" iconLeftName="" openOnFocus={false} />
</Block>


Manual Keyboard Entry

Focus the input — the year segment highlights automatically and you can drive the whole date with the keyboard, never touching the calendar. Press / to change a segment, / to move between year, month, and day, or type digits directly. The caret auto-advances over the - separators (and typing a separator jumps too). Segment mode activates whenever format is a token string with padded tokens (YYYY, YY, MM, DD); Intl.DateTimeFormatOptions formats and single-character tokens (Y, M, D) fall back to free-form text entry.

These examples use openOnFocus={false} so the popover doesn't cover the input — set openOnFocus={true} (the default) and both UIs coexist.

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 (
    <DateInput
      label="Click in, then arrow or type"
      defaultValue={new Date(2024, 5, 7)}
      openOnFocus={false}
    />
  );
}


Digit entry and auto-advance

Type digits to overwrite the active segment. Auto-advance honors each segment's range: the month advances after a first digit ≥ 2 (no month 20+) but waits after 1 (for 10/11/12); the day advances after ≥ 4 but waits after 3 (for 30/31); the year buffers all four digits. Two-digit values clamp (month → 12, day → the month's length).

function example() {
  return (
    <DateInput
      label="Type digits — auto-advance across segments"
      defaultValue={new Date(2024, 5, 7)}
      openOnFocus={false}
    />
  );
}


Controlled with live value

function example() {
  const [v, setV] = useState(() => new Date(2024, 5, 7));
  return (
    <Block>
      <DateInput
        label="Arrow or type — value updates live"
        value={v}
        onChange={setV}
        openOnFocus={false}
      />
      <Paragraph mt="2">Selected: {v ? v.toDateString() : '—'}</Paragraph>
    </Block>
  );
}


Free-form fallback

When format is an Intl.DateTimeFormatOptions object (or uses single-char tokens), segment mode disables — focusing the input does not highlight a segment, and the input parses on blur instead.

function example() {
  return (
    <DateInput
      label="Free-form (Intl format)"
      format={{ year: 'numeric', month: 'long', day: 'numeric' }}
      defaultValue={new Date(2024, 5, 7)}
      openOnFocus={false}
    />
  );
}


Picker vs Input Modes

Two booleans choose how the field behaves. editable controls whether segmented typing is allowed; popover controls whether the calendar exists. Both default to true (type and pick). The four combinations:

editablepopoverBehavior
truetrueBoth — segmented typing + calendar (default)
falsetruePicker-only — typing inert, calendar opens
truefalseInput-only — segmented typing, no calendar
falsefalseStatic display

Picker only

Typing is disabled; the calendar still opens on click or focus.

<DateInput label="Picker only" editable={false} defaultValue={new Date()} />

Input only

Segmented typing with no calendar — handy in dense forms.

<DateInput label="Input only" popover={false} defaultValue={new Date()} />


Locale

Day and month names follow the supplied BCP-47 locale via Intl.DateTimeFormat.

<Block display="flex" flexDirection="column" gap="4">
  <DateInput
    label="ja-JP"
    locale="ja-JP"
    defaultValue={new Date()}
    mobileNative={false}
  />
  <DateInput
    label="fr-FR"
    locale="fr-FR"
    defaultValue={new Date()}
    mobileNative={false}
  />
  <DateInput
    label="de-DE"
    locale="de-DE"
    defaultValue={new Date()}
    mobileNative={false}
  />
</Block>

Forced to the custom calendar

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} added: type straight into each field, then compare the localized calendars via the launcher icon (or ).

<Block display="flex" flexDirection="column" gap="4">
  <DateInput
    label="ja-JP"
    locale="ja-JP"
    defaultValue={new Date()}
    mobileNative={false}
    openOnFocus={false}
  />
  <DateInput
    label="fr-FR"
    locale="fr-FR"
    defaultValue={new Date()}
    mobileNative={false}
    openOnFocus={false}
  />
  <DateInput
    label="de-DE"
    locale="de-DE"
    defaultValue={new Date()}
    mobileNative={false}
    openOnFocus={false}
  />
</Block>


First Day of Week

Set firstDayOfWeek to align the grid to Monday-first locales.

<DateInput label="Week starts Monday" firstDayOfWeek={1} mobileNative={false} />

Forced to the custom calendar

The OS-native calendars use the device locale for the week start, so firstDayOfWeek (and dayNames/monthNames/nearbyMonthDays) are ignored there. This example forces mobileNative={false} so the Monday-first grid shows on touch devices too.

Typing-first — the same example with openOnFocus={false}: type freely, then open the Monday-first grid with the launcher icon (or ).

<DateInput
  label="Week starts Monday — typing-first"
  firstDayOfWeek={1}
  mobileNative={false}
  openOnFocus={false}
/>


Mobile Native

Force the native <input type="date"> (auto-detected on coarse-pointer + small-viewport devices by default).

<DateInput label="Native picker" mobileNative={true} />

Native picker support varies — iOS lags Android

The OS-native fallback is just an <input type="date">, so it inherits each platform's behavior. The custom calendar 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 dates in the calendar; iOS lets the user pick any date, only firing the constraint at form-submission validation. (WebKit bug #225639, still open as of 2026.)

Ignored on BOTH iOS Safari and Android Chrome (HTML-spec gaps):

  • shouldDisableDate, unselectableDates — HTML has no predicate or array equivalent; native pickers can't evaluate functions.
  • firstDayOfWeek, dayNames, monthNames, nearbyMonthDays — both use the device's system locale and the OS's own calendar layout.
  • format, locale, parse — both use the device's system locale; per-input overrides are ignored.
  • placeholder — neither renders placeholder text on date inputs.

If any of these matter, pass mobileNative={false} to force the custom calendar popover (works on every device), or duplicate the constraint in onChange / server-side validation.


Sizes

<Block display="flex" flexDirection="column" gap="4">
  <DateInput label="Small" controlSize="small" size="small" />
  <DateInput label="Default" />
  <DateInput label="Medium" controlSize="medium" size="medium" />
  <DateInput label="Large" controlSize="large" size="large" />
</Block>

Typing-first — every size with openOnFocus={false} so focusing just lets you type; the launcher icon (or ) opens the popover.

<Block display="flex" flexDirection="column" gap="4">
  <DateInput
    label="Small"
    controlSize="small"
    size="small"
    openOnFocus={false}
  />
  <DateInput label="Default" openOnFocus={false} />
  <DateInput
    label="Medium"
    controlSize="medium"
    size="medium"
    openOnFocus={false}
  />
  <DateInput
    label="Large"
    controlSize="large"
    size="large"
    openOnFocus={false}
  />
</Block>


Colors

<Block display="flex" flexDirection="column" gap="4">
  <DateInput label="Primary" color="primary" />
  <DateInput label="Info" color="info" />
  <DateInput label="Success" color="success" />
  <DateInput label="Warning" color="warning" />
  <DateInput label="Danger" color="danger" />
</Block>

Typing-first — the same palette with openOnFocus={false}: click in to type, and use the launcher icon (or ) for the calendar.

<Block display="flex" flexDirection="column" gap="4">
  <DateInput label="Primary" color="primary" openOnFocus={false} />
  <DateInput label="Info" color="info" openOnFocus={false} />
  <DateInput label="Success" color="success" openOnFocus={false} />
  <DateInput label="Warning" color="warning" openOnFocus={false} />
  <DateInput label="Danger" color="danger" openOnFocus={false} />
</Block>


States

<Block display="flex" flexDirection="column" gap="4">
  <DateInput label="Disabled" disabled />
  <DateInput label="Read only" readOnly defaultValue={new Date()} />
  <DateInput label="Loading" isLoading />
</Block>


Horizontal Field

<DateInput label="Date of birth" horizontal placeholder="YYYY-MM-DD" />

Typing-first — the horizontal layout with openOnFocus={false}: focusing lets you type straight away, and the launcher icon (or ) opens the popover.

<DateInput
  label="Date of birth"
  horizontal
  placeholder="YYYY-MM-DD"
  openOnFocus={false}
/>


Context-Aware Rendering

The DateInput component is context-aware: it detects whether it is already inside a Field and adjusts its rendering accordingly. You can use it standalone with a label prop (it wraps itself in a Field), or inside a Field / Control (it skips rendering its own).

Default (with label)

The simplest usage — the component automatically renders its own Field wrapper.

<DateInput label="Date" placeholder="YYYY-MM-DD" />


With Field Wrapper

Wrap in a Field when you need manual layout control. The component detects it and skips rendering its own.

function example() {
  return (
    <Field horizontal label="Date">
      <Field.Body>
        <Field>
          <DateInput placeholder="YYYY-MM-DD" />
        </Field>
      </Field.Body>
    </Field>
  );
}


With Field and Control Wrappers

For full manual composition (e.g. custom icons), wrap in both Field and Control.

function example() {
  return (
    <Field horizontal label="Date">
      <Field.Body>
        <Field>
          <Control iconLeftName="calendar-alt">
            <DateInput placeholder="YYYY-MM-DD" />
          </Control>
        </Field>
      </Field.Body>
    </Field>
  );
}


Keyboard Navigation

On the input (segmented entry)

Focus the input — the year segment highlights automatically and the keyboard alone can drive the full date entry without ever opening the popover. Segment mode activates whenever format is a token string with padded tokens (YYYY, YY, MM, DD); Intl.DateTimeFormatOptions formats and single-character tokens (Y, M, D) fall back to free-form text entry that parses on blur.

KeyAction
/ Increment / decrement the active segment (year / month / day, wraps in place)
/ Move to previous / next segment
09Overwrite the active segment; auto-advances when no further digit is valid
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
EnterClose the popover when closeOnSelect (the value is already committed live)

Digit auto-advance honors each segment's range: the month advances after a first digit ≥ 2 (no month 20+) but waits after 1 (for 10/11/12); the day advances after ≥ 4 but waits after 3 (for 30/31); the year buffers all four digits. Two-digit values clamp (month → 12, day → the month's length).

On the popover calendar

KeyAction
Open popover (when closed)
EnterParse typed text / select focused
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
SpaceSelect focused date
TabMove focus to next control

Form Submission

DateInput participates in HTML form submission. Pass a name and the value is forwarded to a hidden <input> formatted as YYYY-MM-DD.

PropDescription
nameForm field name.
formOptional id of the form the input belongs to.
requiredMarks the field as required for native HTML form validation.
function DateInputFormDemo() {
  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));
      }}
    >
      <DateInput
        name="booking"
        label="Booking date"
        defaultValue={new Date()}
        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" with cells as role="gridcell".
  • Cells expose aria-selected, aria-disabled, and aria-current="date" for today.
  • Roving tabindex keeps a single grid cell focusable at a time.
  • Honors prefers-reduced-motion (skip popover fade-in).


Additional Resources

Pro Tip

Use inline instead of the popover when you have vertical room to spare — booking grids and dashboards feel more direct without the open/close ceremony.