bx

Date Picker

A date picker component with range and presets.

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

DatePicker est un <Calendar> caché dans un popover, avec un bouton trigger qui affiche la date sélectionnée. La valeur est écrite dans un <input type="hidden" name="…"> pour être postée avec le reste du form — pas de JS custom nécessaire côté handler serveur, c'est un vrai name/value de form.

Contrat côté form :

  • Single datename="dueDate" + value="2026-06-13" (ISO). Un seul hidden input, POST donne dueDate=2026-06-13.
  • Rangemode="range" + value (début) + valueEnd (fin). Deux hidden inputs, name et nameEnd (défaut : {name}_end). POST donne dueDate=…&dueDate_end=….
  • required — pose required sur le hidden ; la validation HTML5 native déclenche si vide au submit.

Comportement côté client (island ui.js) :

  • Clic trigger → toggle popover (data-ui="toggle-pop").
  • Clic jour → set hidden input, met à jour le label du trigger, ferme le popover (single) ou attend la seconde date (range).
  • Clic prev/next ou dropdowns mois/année → reconstruit le calendrier au nouveau mois sans reload (voir Calendar).

Options utiles : placeholder (texte quand vide), locale="fr"|"en", weekStart (0 dimanche, 1 lundi défaut), minDate/maxDate (bornes ISO), triggerWidth (largeur du bouton, défaut w-[240px]).

Installation

Terminal
$ bext ui add date-picker

Usage

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

// Single date dans un form — POST envoie dueDate=2026-06-13
<form method="POST" action="/api/save">
  <DatePicker name="dueDate" value={value} locale="fr" />
  <button type="submit">Enregistrer</button>
</form>

// Range — POST envoie dueDate=… & dueDate_end=…
<DatePicker
  name="dueDate"
  mode="range"
  value={rangeStart}
  valueEnd={rangeEnd}
  placeholder="Choisir une période"
/>

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">                          // le trigger
  │   ├── <CalendarIcon />                                   // icône gauche
  │   └── <span>{value ? formatFR(value) : placeholder}</span>
  └── <div id="dp-…" data-overlay hidden>                   // le popover
      └── <Calendar value valueEnd locale weekStart … />    // la grille

API Reference

DatePicker

Prop Type Default Description
name string Nom du hidden input — c'est le nom qui apparaît dans le POST du form.
value string Valeur ISO "yyyy-mm-dd". Le hidden input reçoit cette valeur.
valueEnd string Fin du range (mode="range"). Le hidden input nameEnd reçoit cette valeur.
nameEnd string {name}_end Nom du deuxième hidden input pour le range end.
mode "single" | "range" "single" "single" : un seul clic ferme le popover. "range" : le premier clic pose value, le second pose valueEnd.
placeholder string "Choisir une date" (fr) / "Pick a date" (en) Texte du trigger quand vide.
required boolean Pose l'attribut required sur le hidden input → validation HTML5 native au submit.
disabled boolean Désactive le trigger.
id string dp-${name} id du popover panel. Sert au data-target du toggle-pop.
locale "fr" | "en" "fr" Locale pour le placeholder + le Calendar interne + le format d'affichage (dd/mm/yyyy vs yyyy-mm-dd).
weekStart 0 | 1 1 Voir Calendar.weekStart.
minDate string Borne basse ISO passée au Calendar interne.
maxDate string Borne haute ISO passée au Calendar interne.
triggerWidth string (utility) "w-[240px]" Classe utility Tailwind pour la largeur du trigger button.
className string Classes CSS sur le wrapper root
.

Data Attributes

Attributs qu'on peut cibler en CSS custom pour override le style par état.

Attribute On Values Description
data-ui="date-picker" root Marqueur racine.
data-name root string Reflète la prop name.
data-mode root "single" | "range" Lu par l'island pour le comportement clic jour (fermer vs attendre le second).
data-locale root "fr" | "en" Utilisé par l'island pour le format d'affichage post-sélection.
data-ui-dp-value hidden input Marqueur — l'island écrit la valeur ISO ici au clic jour.
data-ui-dp-end hidden input Idem pour valueEnd (mode range).
data-ui-dp-label span du trigger L'island met à jour le textContent avec la date formatée après clic.

Keyboard

Key Action
Tab Focus le trigger. Puis Tab intérieur : nav du Calendar.
Enter / Space Sur trigger fermé : ouvre le popover. Sur bouton jour : sélectionne.
Escape Ferme le popover ouvert.

Accessibility

Le trigger est un vrai <button type="button">, focusable et activable clavier. Le popover est un <div data-overlay hidden> — l'island le passe en visible au clic, et le referme sur clic extérieur ou Escape.

La valeur est écrite dans un <input type="hidden" name="…"> — c'est un vrai champ de form. Le navigateur inclut automatiquement le hidden dans le POST ; côté serveur c'est un simple form.get("dueDate"). La validation HTML5 (required) fonctionne nativement.

Le format d'affichage post-sélection suit la locale : 13/06/2026 (fr) ou 2026-06-13 (en). La valeur ISO envoyée au serveur reste normalisée.

Pour le champ aria-label du trigger, si pas de <label for="…"> englobant, ajouter aria-label="Date d'échéance" ou similaire pour que le lecteur d'écran annonce le rôle du champ.

Examples

Vide (placeholder)

tsx
<DatePicker name="dueDate" placeholder="Choisir une date" />

En anglais

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

Range (période)

tsx
<DatePicker
  name="dueDate"
  mode="range"
  value={start}
  valueEnd={end}
  placeholder="Choisir une période"
/>

Requis (validation HTML5)

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

Bornes minDate / maxDate

tsx
// Les jours hors bornes ne sont pas cliquables.
<DatePicker
  name="dueDate"
  value="2026-06-13"
  minDate="2026-06-05"
  maxDate="2026-06-25"
/>

Trigger large

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.