HextaUI

BêtaConstruit sur l’UI SDK 0.1.0-beta.2 de Vercel. Son API peut changer.

Date input

Un champ de date sur l’UI SDK de Vercel saisi partie par partie dans l’ordre de la langue, avec heure en option, minimum et maximum, jours indisponibles et une valeur ISO.

Dépend de
  • @ui-sdk/core
  • @ui-sdk/react
import { IconCalendar } from "@tabler/icons-react"

import { Label } from "@/components/ui/label"
import { DateInput, DateInputSegments } from "@/components/ui/ui-sdk-date-input"

function UiSdkDateInputDemo() {
  return (
    <div className="flex w-full max-w-44 flex-col gap-2">
      <Label id="birthday">Birthday</Label>
      <DateInput aria-labelledby="birthday" name="birthday">
        <IconCalendar aria-hidden="true" />
        <DateInputSegments />
      </DateInput>
    </div>
  )
}

export { UiSdkDateInputDemo }
pnpm dlx shadcn@latest add https://hextaui.com/r/ui-sdk-date-input.json

Ajoute le composant, les tokens de thème HextaUI et les composants HextaUI dont il dépend.

UI SDK ajoute un peu de CSS dans une couche de cascade nommée ui-sdk. Déclarez cette couche en haut de votre feuille de style globale, au-dessus de l’import Tailwind, pour que vos classes l’emportent.

@layer ui-sdk;

@import "tailwindcss";
import { DateInput } from "@/components/ui/ui-sdk-date-input"
<Label id="start-date">Start date</Label>
<DateInput aria-labelledby="start-date" defaultValue="2026-11-02" />

Utilisez un champ de date pour les dates connues par cœur, comme un anniversaire. La valeur est une date ISO comme "2026-11-02", et reste null tant que chaque partie n’est pas remplie. Chaque partie se remplit à la saisie puis passe à la suite, le jour reste dans son mois, et une date entière peut être collée dans n’importe quelle partie. Pour choisir dans un calendrier, utilisez le Date Picker.

Le champ est un groupe : nommez-le avec aria-labelledby ou aria-label. Un clic n’importe où dans le cadre cible la partie la plus proche.

DateInput
└── DateInputSegments
    └── DateInputSegment

DateInput affiche ses parties tout seul. Ajoutez DateInputSegments vous-même pour placer une icône ou un autre contenu à côté.

granularity réglé sur "hour", "minute" ou "second" ajoute l’heure, et la valeur devient une date et heure locales comme "2026-11-02T14:30".

import { Label } from "@/components/ui/label"
import { DateInput } from "@/components/ui/ui-sdk-date-input"

function UiSdkDateInputDateTime() {
  return (
    <div className="flex w-full max-w-60 flex-col gap-2">
      <Label id="departure">Departure</Label>
      <DateInput
        aria-labelledby="departure"
        granularity="minute"
        defaultValue="2026-11-02T14:30"
        locale="en-US"
      />
    </div>
  )
}

export { UiSdkDateInputDateTime }

Une date avant min ou après max marque le champ comme invalide, et son formulaire ne s’envoie pas.

"use client"

import * as React from "react"

import { Label } from "@/components/ui/label"
import { DateInput } from "@/components/ui/ui-sdk-date-input"

const min = "2026-01-01"
const max = "2026-12-31"

function UiSdkDateInputMinMax() {
  const [date, setDate] = React.useState<string | null>("2026-06-15")
  const outside = date !== null && (date < min || date > max)

  return (
    <div className="flex w-full max-w-44 flex-col gap-2">
      <Label id="check-in">Check-in</Label>
      <DateInput
        aria-labelledby="check-in"
        aria-describedby="check-in-message"
        min={min}
        max={max}
        defaultValue="2026-06-15"
        onValueChange={(value) => setDate(value)}
      />
      <p
        id="check-in-message"
        className={
          outside
            ? "m-0 text-xs text-destructive"
            : "m-0 text-xs text-muted-foreground"
        }
      >
        {outside ? "Pick a day in 2026." : "Any day in 2026."}
      </p>
    </div>
  )
}

export { UiSdkDateInputMinMax }

unavailable accepte des dates ISO, des plages { from, to }, des jours de la semaine ou une fonction, et une date concernée est invalide.

"use client"

import * as React from "react"

import { Label } from "@/components/ui/label"
import { DateInput } from "@/components/ui/ui-sdk-date-input"

function isWeekend(date: string) {
  const day = new Date(`${date}T12:00`).getDay()
  return day === 0 || day === 6
}

function UiSdkDateInputUnavailable() {
  const [date, setDate] = React.useState<string | null>("2026-11-02")
  const weekend = date !== null && isWeekend(date)

  return (
    <div className="flex w-full max-w-44 flex-col gap-2">
      <Label id="delivery-date">Delivery date</Label>
      <DateInput
        aria-labelledby="delivery-date"
        aria-describedby="delivery-date-message"
        defaultValue="2026-11-02"
        unavailable={{ weekday: [0, 6] }}
        onValueChange={(value) => setDate(value)}
      />
      <p
        id="delivery-date-message"
        className={
          weekend
            ? "m-0 text-xs text-destructive"
            : "m-0 text-xs text-muted-foreground"
        }
      >
        {weekend
          ? "We don’t deliver on weekends."
          : "We deliver Monday to Friday."}
      </p>
    </div>
  )
}

export { UiSdkDateInputUnavailable }

locale fixe l’ordre et les séparateurs. Les chiffres vont toujours de 0 à 9 et le calendrier est toujours grégorien, donc la valeur désigne la même date partout.

import { Label } from "@/components/ui/label"
import { DateInput } from "@/components/ui/ui-sdk-date-input"

const locales = [
  { locale: "en-GB", label: "Date (en-GB)" },
  { locale: "de-DE", label: "Datum (de-DE)" },
  { locale: "ja-JP", label: "日付 (ja-JP)" },
]

function UiSdkDateInputLocale() {
  return (
    <div className="flex w-full max-w-44 flex-col gap-4">
      {locales.map(({ locale, label }) => (
        <div key={locale} lang={locale} className="flex flex-col gap-2">
          <Label id={`date-${locale}`}>{label}</Label>
          <DateInput
            aria-labelledby={`date-${locale}`}
            locale={locale}
            defaultValue="2026-11-02"
          />
        </div>
      ))}
    </div>
  )
}

export { UiSdkDateInputLocale }

Un champ caché envoie la date ISO sous name, ici à côté d’un Time Input. required bloque une date vide ou à moitié saisie, et la réinitialisation rétablit la valeur par défaut.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { DateInput } from "@/components/ui/ui-sdk-date-input"
import { TimeInput } from "@/components/ui/ui-sdk-time-input"

function UiSdkDateInputForm() {
  const [submitted, setSubmitted] = React.useState<string | null>(null)

  return (
    <form
      className="flex w-full max-w-72 flex-col gap-3"
      onSubmit={(event) => {
        event.preventDefault()
        const data = new FormData(event.currentTarget)
        setSubmitted(`${data.get("date")} at ${data.get("time")}`)
      }}
    >
      <div className="grid grid-cols-[1fr_auto] gap-2">
        <div className="flex min-w-0 flex-col gap-2">
          <Label id="booking-date">Date</Label>
          <DateInput
            aria-labelledby="booking-date"
            name="date"
            defaultValue="2026-11-02"
            required
          />
        </div>
        <div className="flex flex-col gap-2">
          <Label id="booking-time">Time</Label>
          <TimeInput
            aria-labelledby="booking-time"
            name="time"
            defaultValue="09:30"
            required
          />
        </div>
      </div>
      <div className="flex items-center gap-3">
        <Button type="submit" size="sm">
          Book
        </Button>
        <Button type="reset" size="sm" variant="ghost">
          Reset
        </Button>
      </div>
      <p className="m-0 text-sm text-muted-foreground tabular-nums">
        {submitted ? `Submitted ${submitted}` : "Nothing submitted yet."}
      </p>
    </form>
  )
}

export { UiSdkDateInputForm }

createDateInputController() définit, vide ou cible le champ depuis n’importe où. Ciblez une partie par son unité, comme "year".

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import {
  DateInput,
  createDateInputController,
} from "@/components/ui/ui-sdk-date-input"

function UiSdkDateInputController() {
  const [controller] = React.useState(createDateInputController)

  return (
    <div className="flex w-full max-w-60 flex-col gap-2">
      <Label id="publish-on">Publish on</Label>
      <DateInput aria-labelledby="publish-on" controller={controller} />
      <div className="flex items-center gap-2 pt-1">
        <Button
          size="sm"
          variant="outline"
          onClick={() => controller.setValue("2026-12-25")}
        >
          Christmas
        </Button>
        <Button
          size="sm"
          variant="ghost"
          onClick={() => controller.focus("year")}
        >
          Edit year
        </Button>
        <Button size="sm" variant="ghost" onClick={() => controller.clear()}>
          Clear
        </Button>
      </div>
    </div>
  )
}

export { UiSdkDateInputController }

Un champ désactivé est ignoré par le focus et n’est pas envoyé.

import { Label } from "@/components/ui/label"
import { DateInput } from "@/components/ui/ui-sdk-date-input"

function UiSdkDateInputDisabled() {
  return (
    <div className="flex w-full max-w-44 flex-col gap-2">
      <Label id="start-date-disabled">Start date</Label>
      <DateInput
        aria-labelledby="start-date-disabled"
        defaultValue="2026-11-02"
        disabled
      />
    </div>
  )
}

export { UiSdkDateInputDisabled }

Un champ en lecture seule prend le focus et est envoyé, mais les touches et la saisie ne le modifient pas.

import { Label } from "@/components/ui/label"
import { DateInput } from "@/components/ui/ui-sdk-date-input"

function UiSdkDateInputReadOnly() {
  return (
    <div className="flex w-full max-w-44 flex-col gap-2">
      <Label id="signed-on">Signed on</Label>
      <DateInput
        aria-labelledby="signed-on"
        defaultValue="2026-11-02"
        readOnly
      />
    </div>
  )
}

export { UiSdkDateInputReadOnly }

Les parties gardent l’ordre de la langue, et les flèches les suivent à l’écran.

import { IconCalendar } from "@tabler/icons-react"

import { Label } from "@/components/ui/label"
import { DateInput, DateInputSegments } from "@/components/ui/ui-sdk-date-input"

function UiSdkDateInputRtl() {
  return (
    <div dir="rtl" lang="ar" className="flex w-full max-w-44 flex-col gap-2">
      <Label id="rtl-date">تاريخ البدء</Label>
      <DateInput
        aria-labelledby="rtl-date"
        locale="ar-EG"
        defaultValue="2026-11-02"
      >
        <IconCalendar aria-hidden="true" />
        <DateInputSegments />
      </DateInput>
    </div>
  )
}

export { UiSdkDateInputRtl }
ToucheAction
0–9Remplit la partie, puis passe à la suivante.
/.Passe à la partie suivante.
↑↓Modifie la partie d’une unité, en boucle. Une partie vide part d’aujourd’hui.
PageUpPageDownModifie par grands pas : une année de 5, un mois de 3, un jour de 7.
HomeEndVa à la première ou à la dernière valeur de la partie.
←→Passe à la partie précédente ou suivante.
BackspaceEfface le dernier chiffre, puis vide la partie, puis revient en arrière.
EnterEnvoie le formulaire avec la saisie.

La racine est un groupe. Chaque partie est un spinbutton nommé par son unité, comme « month », avec sa valeur et sa plage, et une partie vide se lit « Empty ». Le texte entre les parties est masqué aux lecteurs d’écran.

Les parties sont de vrais champs, donc les téléphones affichent un pavé numérique et la saisie fonctionne avant le script de la page. Elles passent à 16px sur écran tactile pour éviter le zoom, et la partie ciblée est surlignée au lieu d’afficher un curseur.

Chaque partie enveloppe la partie UI SDK du même nom et accepte toutes ses props, y compris render et un className qui peut être une fonction de son état.

PropTypePar défaut
value
string | null–
defaultValue
string | null–
onValueChangeS’exécute quand chaque partie est remplie, et avec null quand l’une est vidée.
(value: string | null, details) => void–
granularity
"day" | "hour" | "minute" | "second""day"
hourCyclePar défaut, l’horloge de la langue.
12 | 24–
min
string–
max
string–
unavailableDates ISO, plages { from, to }, { weekday } ou une fonction.
DateMatchers–
localePar défaut, la langue de la page.
string–
invalid
booleanfalse
disabled
booleanfalse
readOnly
booleanfalse
required
booleanfalse
name
string–
form
string–
controllerIssu de createDateInputController().
DateInputController–
childrenPar défaut, DateInputSegments.
ReactNode | (state) => ReactNode–
render
ReactElement | (props, state) => ReactElement–
AttributDescription
data-slot="date-input"Pour cibler le champ de date en CSS.
data-invalidPrésent quand la date est hors de min et max, indisponible, ou quand invalid est défini.
data-emptyPrésent tant que toutes les parties sont vides.
data-disabledPrésent lorsque l’élément est désactivé.
data-readonlyPrésent en lecture seule.
data-requiredPrésent lorsqu’elle est requise.

Affiche un DateInputSegment pour chaque partie, dans l’ordre de la langue, avec le texte entre elles.

PropTypePar défaut
segmentL’un des segments de l’état de la racine.
DateInputSegmentData–
render
ReactElement | (props, state) => ReactElement–
AttributDescription
data-slot="date-input-segment"Pour cibler une partie en CSS.
data-typeyear, month, day, hour, minute, second, dayPeriod, ou literal pour le texte entre les parties.
data-emptyPrésent tant que la partie est vide.