HextaUI

BetaGebaut auf Vercels UI SDK 0.1.0-beta.2. Die API kann sich ändern.

Date input

Ein Datumsfeld auf Vercels UI SDK, das Teil für Teil in der Reihenfolge der Locale getippt wird, mit optionaler Uhrzeit, Minimum und Maximum, gesperrten Tagen und ISO-Wert.

Abhängig von
  • @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

Fügt die Komponente, die HextaUI-Theme-Tokens und alle HextaUI-Komponenten hinzu, von denen sie abhängt.

UI SDK bringt etwas eigenes CSS in einem Cascade Layer namens ui-sdk mit. Deklariere diesen Layer ganz oben im globalen Stylesheet, über dem Tailwind-Import, damit deine Klassen gewinnen.

@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" />

Nutze ein Datumsfeld für Daten, die man auswendig kennt, etwa einen Geburtstag. Der Wert ist ein ISO-Datum wie "2026-11-02" und bleibt null, bis jeder Teil etwas enthält. Jeder Teil füllt sich beim Tippen und springt weiter, der Tag bleibt innerhalb seines Monats, und ein ganzes Datum lässt sich in jeden Teil einfügen. Zum Auswählen im Kalender nutze den Date Picker.

Das Feld ist eine Gruppe, also benenne es mit aria-labelledby oder aria-label. Ein Klick irgendwo in den Rahmen fokussiert den nächstgelegenen Teil.

DateInput
└── DateInputSegments
    └── DateInputSegment

DateInput rendert seine Teile selbst. Füge DateInputSegments selbst ein, um ein Symbol oder anderen Inhalt daneben zu setzen.

granularity mit "hour", "minute" oder "second" fügt die Uhrzeit hinzu, und der Wert wird zu lokalem Datum und Uhrzeit wie "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 }

Ein Datum vor min oder nach max markiert das Feld als ungültig, und sein Formular wird nicht gesendet.

"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 nimmt ISO-Daten, Bereiche { from, to }, Wochentage oder eine Funktion, und ein passendes Datum ist ungültig.

"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 bestimmt Reihenfolge und Trennzeichen. Die Ziffern sind immer 0 bis 9 und der Kalender immer gregorianisch, sodass der Wert überall dasselbe Datum bedeutet.

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 }

Ein verstecktes Feld sendet das ISO-Datum unter name, hier neben einem Time Input. required stoppt ein leeres oder halb getipptes Datum, und ein Zurücksetzen stellt den Standardwert her.

"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() setzt, leert oder fokussiert das Feld von überall. Fokussiere einen Teil über seine Einheit, etwa "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 }

Ein deaktiviertes Feld wird beim Fokussieren übersprungen und nicht gesendet.

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 }

Ein schreibgeschütztes Feld nimmt den Fokus an und wird gesendet, aber Tasten und Tippen ändern es nicht.

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 }

Die Teile behalten die Reihenfolge der Locale, und die Pfeiltasten folgen ihnen auf dem Bildschirm.

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 }
TasteAktion
0–9Füllt den Teil und springt zum nächsten.
/.Wechselt zum nächsten Teil.
↑↓Ändert den Teil um eins und läuft im Kreis. Ein leerer Teil beginnt bei heute.
PageUpPageDownÄndert in größeren Schritten: Jahr um 5, Monat um 3, Tag um 7.
HomeEndSpringt zum ersten oder letzten Wert des Teils.
←→Wechselt zum vorherigen oder nächsten Teil.
BackspaceLöscht die letzte Ziffer, leert dann den Teil und springt dann zurück.
EnterSendet das Formular mit der Eingabe ab.

Die Wurzel ist eine Gruppe. Jeder Teil ist ein Spinbutton, benannt nach seiner Einheit wie „month“, mit Wert und Bereich, und ein leerer Teil wird als „Empty“ vorgelesen. Der Text zwischen den Teilen ist für Screenreader verborgen.

Die Teile sind echte Eingabefelder, daher zeigen Handys einen Ziffernblock und Tippen funktioniert, bevor das Skript der Seite läuft. Auf Touchscreens wachsen sie auf 16px, damit Handys nicht zoomen, und der fokussierte Teil wird hervorgehoben statt eine Einfügemarke zu zeigen.

Jeder Teil umhüllt den gleichnamigen UI-SDK-Teil und nimmt alle seine Props an, auch render und einen className, der eine Funktion des Zustands sein kann.

PropTypStandard
value
string | null–
defaultValue
string | null–
onValueChangeLäuft, sobald jeder Teil etwas enthält, und mit null, wenn einer geleert wird.
(value: string | null, details) => void–
granularity
"day" | "hour" | "minute" | "second""day"
hourCycleStandardmäßig die Uhr der Locale.
12 | 24–
min
string–
max
string–
unavailableISO-Daten, Bereiche { from, to }, { weekday } oder eine Funktion.
DateMatchers–
localeStandardmäßig die Sprache der Seite.
string–
invalid
booleanfalse
disabled
booleanfalse
readOnly
booleanfalse
required
booleanfalse
name
string–
form
string–
controllerAus createDateInputController().
DateInputController–
childrenStandardmäßig DateInputSegments.
ReactNode | (state) => ReactNode–
render
ReactElement | (props, state) => ReactElement–
AttributBeschreibung
data-slot="date-input"Zum Ansprechen des Datumsfelds in CSS.
data-invalidVorhanden, wenn das Datum außerhalb von min und max liegt, gesperrt ist oder invalid gesetzt ist.
data-emptyVorhanden, solange alle Teile leer sind.
data-disabledVorhanden, wenn deaktiviert.
data-readonlyVorhanden, wenn schreibgeschützt.
data-requiredVorhanden, wenn erforderlich.

Rendert für jeden Teil ein DateInputSegment in der Reihenfolge der Locale, mit dem Text dazwischen.

PropTypStandard
segmentEines der segments aus dem Zustand der Wurzel.
DateInputSegmentData–
render
ReactElement | (props, state) => ReactElement–
AttributBeschreibung
data-slot="date-input-segment"Zum Ansprechen eines Teils in CSS.
data-typeyear, month, day, hour, minute, second, dayPeriod oder literal für den Text zwischen den Teilen.
data-emptyVorhanden, solange der Teil leer ist.