HextaUI

ベータVercel の UI SDK 0.1.0-beta.2 上に構築。API は変わる可能性があります。

Date input

Vercel の UI SDK で作る日付欄。ロケールの順に部分ごとに入力し、任意の時刻、最小値と最大値、選べない日、ISO 形式の値を備えます。

依存関係
  • @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

コンポーネント、HextaUIのテーマトークン、依存するHextaUIコンポーネントを追加します。

UI SDK は ui-sdk というカスケードレイヤーに少しだけ独自の CSS を追加します。自分のクラスが優先されるよう、グローバルスタイルシートの先頭、Tailwind の import より上でこのレイヤーを宣言してください。

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

誕生日のように覚えている日付には日付欄を使います。値は "2026-11-02" のような ISO 形式の日付で、すべての部分が埋まるまでは null のままです。各部分は入力に合わせて埋まって次へ進み、日はその月の範囲に収まり、日付全体をどの部分にも貼り付けられます。カレンダーから選ぶ場合は Date Picker を使ってください。

この欄はグループなので、aria-labelledby か aria-label で名前を付けてください。枠内のどこをクリックしても、いちばん近い部分にフォーカスします。

DateInput
└── DateInputSegments
    └── DateInputSegment

DateInput は部分を自動で描画します。横にアイコンなどを置くときは、DateInputSegments を自分で追加してください。

granularity を "hour"、"minute"、"second" にすると時刻が加わり、値は "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 }

min より前、または max より後の日付は欄を無効にし、フォームは送信されません。

"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 は ISO 形式の日付、{ from, to } の範囲、曜日、または関数を受け取り、該当する日付は無効になります。

"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 は順序と区切り文字を決めます。数字は常に 0〜9、暦は常にグレゴリオ暦なので、値はどこでも同じ日付を表します。

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 }

隠し入力が ISO 形式の日付を name で送信します。ここでは Time Input と並べています。required は空や途中までの日付を止め、リセットすると既定値に戻ります。

"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() で、どこからでも欄の値を設定・消去したりフォーカスしたりできます。部分は "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 }

無効な欄はフォーカスで飛ばされ、送信もされません。

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 }

読み取り専用の欄はフォーカスでき送信もされますが、キー操作や入力では変わりません。

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 }

部分はロケールの順序を保ち、矢印キーは画面上の並びに従います。

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 }
キーアクション
0–9部分を埋めて、次の部分に移ります。
/.次の部分に移動します。
↑↓部分を 1 ずつ変え、端で一周します。空の部分は今日から始まります。
PageUpPageDown大きく変えます。年は 5、月は 3、日は 7 ずつです。
HomeEnd部分の最初または最後の値に移動します。
←→前または次の部分に移動します。
Backspace最後の数字を消し、次に部分を空にし、さらに前の部分へ戻ります。
Enter入力した内容でフォームを送信します。

ルートはグループです。各部分は「month」のように単位で名前が付いた spinbutton で、値と範囲を持ち、空の部分は「Empty」と読み上げられます。部分の間の文字はスクリーンリーダーから隠れます。

各部分は実際の入力欄なので、スマートフォンでは数字キーパッドが出て、ページのスクリプトが動く前から入力できます。タッチ画面ではズームされないよう 16px に大きくなり、フォーカス中の部分はキャレットの代わりにハイライトされます。

各パーツは同名の UI SDK パーツをラップし、render やパーツの状態を受け取る関数にもできる className を含むすべての props を受け付けます。

プロパティ型デフォルト
value
string | null–
defaultValue
string | null–
onValueChangeすべての部分が埋まったときに呼ばれ、どれかが空になると null で呼ばれます。
(value: string | null, details) => void–
granularity
"day" | "hour" | "minute" | "second""day"
hourCycle既定はロケールの時計です。
12 | 24–
min
string–
max
string–
unavailableISO 形式の日付、{ from, to } の範囲、{ weekday }、または関数です。
DateMatchers–
locale既定はページの言語です。
string–
invalid
booleanfalse
disabled
booleanfalse
readOnly
booleanfalse
required
booleanfalse
name
string–
form
string–
controllercreateDateInputController() で作ります。
DateInputController–
children既定は DateInputSegments です。
ReactNode | (state) => ReactNode–
render
ReactElement | (props, state) => ReactElement–
属性説明
data-slot="date-input"CSS で日付欄を指定するためのものです。
data-invalid日付が min と max の範囲外のとき、選べない日のとき、または invalid が設定されたときに付きます。
data-emptyすべての部分が空の間に付きます。
data-disabled無効のときに存在します。
data-readonly読み取り専用のときに付与されます。
data-required必須のときに付与されます。

各部分の DateInputSegment を、ロケールの順に間の文字とともに描画します。

プロパティ型デフォルト
segmentルートの状態にある segments の 1 つです。
DateInputSegmentData–
render
ReactElement | (props, state) => ReactElement–
属性説明
data-slot="date-input-segment"CSS で部分を指定するためのものです。
data-typeyear、month、day、hour、minute、second、dayPeriod、または部分の間の文字を表す literal です。
data-empty部分が空の間に付きます。