bx

Date Picker

A date picker component with range and presets.

tsx
<DatePicker
  name="dueDate"
  value="2026-06-13"
  locale="en"
/>

DatePicker is a <Calendar> hidden in a popover, with a trigger button that shows the selected date. The value is written to an <input type="hidden" name="…"> so it is posted with the rest of the form. The server handler needs no custom JS: it is a real form name/value pair.

Form contract:

  • Single date: name="dueDate" + value="2026-06-13" (ISO). A single hidden input; the POST gives dueDate=2026-06-13.
  • Range: mode="range" + value (start) + valueEnd (end). Two hidden inputs, name and nameEnd (default: {name}_end). The POST gives dueDate=…&dueDate_end=….
  • required: sets required on the hidden input; native HTML5 validation fires if it is empty on submit.

Client-side behavior (ui.js island):

  • Click on the trigger → toggles the popover (data-ui="toggle-pop").
  • Click on a day → sets the hidden input, updates the trigger label, then closes the popover (single) or waits for the second date (range).
  • Click on prev/next or the month/year dropdowns → rebuilds the calendar for the new month without a reload (see Calendar).

Useful options: placeholder (text when empty), locale="fr"|"en", weekStart (0 Sunday, 1 Monday by default), minDate/maxDate (ISO bounds), triggerWidth (button width, default w-[240px]).

Installation

Terminal
$ bext ui add date-picker

Usage

tsx
import { DatePicker } from "@bext-stack/ui/primitives"

// Single date in a form: the POST sends dueDate=2026-06-13
<form method="POST" action="/api/save">
  <DatePicker name="dueDate" value={value} locale="en" />
  <button type="submit">Save</button>
</form>

// Range: the POST sends dueDate=… & dueDate_end=…
<DatePicker
  name="dueDate"
  mode="range"
  value={rangeStart}
  valueEnd={rangeEnd}
  placeholder="Pick a date range"
/>

Anatomy

<div data-ui="date-picker" data-name="…" data-mode="single|range">
  ├── <input type="hidden" name="…" value="yyyy-mm-dd" />
  ├── <input type="hidden" name="…_end" value="…" />        // range only
  ├── <button data-ui="toggle-pop">                          // the trigger
  │   ├── <CalendarIcon />                                   // left icon
  │   └── <span>{value ? formatFR(value) : placeholder}</span>
  └── <div id="dp-…" data-overlay hidden>                   // the popover
      └── <Calendar value valueEnd locale weekStart … />    // the grid

API Reference

DatePicker

Prop Type Default Description
name string — Name of the hidden input. This is the name that appears in the form POST.
value string — ISO value "yyyy-mm-dd". The hidden input receives this value.
valueEnd string — End of the range (mode="range"). The nameEnd hidden input receives this value.
nameEnd string {name}_end Name of the second hidden input, for the range end.
mode "single" | "range" "single" "single": a single click closes the popover. "range": the first click sets value, the second sets valueEnd.
placeholder string "Choisir une date" (fr) / "Pick a date" (en) Trigger text when empty.
required boolean — Sets the required attribute on the hidden input → native HTML5 validation on submit.
disabled boolean — Disables the trigger.
id string dp-${name} id of the popover panel. Used as the data-target of the toggle-pop.
locale "fr" | "en" "fr" Locale for the placeholder, the inner Calendar and the display format (dd/mm/yyyy vs yyyy-mm-dd).
weekStart 0 | 1 1 See Calendar.weekStart.
minDate string — Lower ISO bound passed to the inner Calendar.
maxDate string — Upper ISO bound passed to the inner Calendar.
triggerWidth string (utility) "w-[240px]" Tailwind utility class for the width of the trigger button.
className string — CSS classes on the root wrapper <div data-ui="date-picker">.

Data Attributes

Attributes you can target in custom CSS to override the style for each state.

Attribute On Values Description
data-ui="date-picker" root — Root marker.
data-name root string Reflects the name prop.
data-mode root "single" | "range" Read by the island to decide what a day click does (close vs wait for the second date).
data-locale root "fr" | "en" Used by the island for the display format after selection.
data-ui-dp-value hidden input — Marker: the island writes the ISO value here on day click.
data-ui-dp-end hidden input — Same for valueEnd (range mode).
data-ui-dp-label trigger span — The island updates the textContent with the formatted date after a click.

Keyboard

Key Action
Tab Focuses the trigger. Further Tab presses move through the Calendar.
Enter / Space On a closed trigger: opens the popover. On a day button: selects it.
Escape Closes the open popover.

Accessibility

The trigger is a real <button type="button">, focusable and keyboard-activatable. The popover is a <div data-overlay hidden>: the island makes it visible on click, and closes it on an outside click or Escape.

The value is written to an <input type="hidden" name="…">, which is a real form field. The browser includes the hidden input in the POST automatically; on the server it is a plain form.get("dueDate"). HTML5 validation (required) works natively.

The display format after selection follows the locale: 13/06/2026 (fr) or 2026-06-13 (en). The ISO value sent to the server stays normalized.

For the trigger's aria-label: if there is no enclosing <label for="…">, add aria-label="Due date" or similar so the screen reader announces the purpose of the field.

Examples

Empty (placeholder)

tsx
<DatePicker name="dueDate" placeholder="Pick a date" />

French locale

tsx
<DatePicker name="dueDate" value="2026-06-13" locale="fr" />

Range

tsx
<DatePicker
  name="dueDate"
  mode="range"
  value={start}
  valueEnd={end}
  placeholder="Pick a date range"
/>

Required (HTML5 validation)

tsx
<DatePicker name="dueDate" required placeholder="Required" />

minDate / maxDate bounds

tsx
// Days outside the bounds are not clickable.
<DatePicker
  name="dueDate"
  value="2026-06-13"
  minDate="2026-06-05"
  maxDate="2026-06-25"
/>

Wide trigger

tsx
<DatePicker name="dueDate" value="2026-06-13" triggerWidth="w-[360px]" />
Try it live
Open in play

Edit this component live in the bext playground. The PRISM + signals version is the bext-native idiom: fine-grained reactivity, no virtual DOM.

src/app/page.tsx 2 files · signals · runs in your browser
Pure PRISM + real bext signals: fine-grained, no re-render.