# Time input

> A time field on Vercel's UI SDK typed one part at a time, on a 12 or 24-hour clock with optional seconds, min and max, and a 24-hour value for forms.

Canonical: https://hextaui.com/docs/ui-sdk-time-input
Last updated: 2026-11-14

> **Beta.** Built on Vercel’s UI SDK 0.1.0-beta.2 (`@ui-sdk/react`), a public beta. Its API may change.

```tsx title="components/examples/ui-sdk-time-input/demo.tsx"
import { IconClock } from "@tabler/icons-react"

import { Label } from "@/components/ui/label"
import { TimeInput, TimeInputSegments } from "@/components/ui/ui-sdk-time-input"

function UiSdkTimeInputDemo() {
  return (
    <div className="flex w-full max-w-40 flex-col gap-2">
      <Label id="meeting-time">Meeting time</Label>
      <TimeInput aria-labelledby="meeting-time" defaultValue="09:30">
        <IconClock aria-hidden="true" />
        <TimeInputSegments />
      </TimeInput>
    </div>
  )
}

export { UiSdkTimeInputDemo }
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/ui-sdk-time-input.json
```

This adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

### Manual

Add the HextaUI theme tokens (https://hextaui.com/docs/installation#theme) to your global CSS if you haven't yet, then install the dependencies.

```bash
pnpm add @ui-sdk/react@0.1.0-beta.2 @ui-sdk/core@0.1.0-beta.2 cn
```

Copy and paste the following code into your project.

```tsx title="components/ui/ui-sdk-time-input.tsx"
"use client"

import * as React from "react"
import * as TimeInputPrimitive from "@ui-sdk/react/time-input"
import { cn } from "cn"

type ClassName<State> =
  string | ((state: State) => string | undefined) | undefined

function mergeClassName<State>(base: string, className: ClassName<State>) {
  return typeof className === "function"
    ? (state: State) => cn(base, className(state))
    : cn(base, className)
}

type TimeInputState = TimeInputPrimitive.TimeInputState

type TimeInputContextValue = {
  state: TimeInputState | null
  invalid: boolean
}

const TimeInputContext = React.createContext<TimeInputContextValue>({
  state: null,
  invalid: false,
})

type TimeInputProps = React.ComponentProps<
  typeof TimeInputPrimitive.TimeInput
> & {
  invalid?: boolean
}

function TimeInput({
  className,
  children,
  invalid = false,
  ...props
}: TimeInputProps) {
  return (
    <TimeInputPrimitive.TimeInput
      data-invalid={invalid ? "" : undefined}
      className={mergeClassName(
        "group/time-input flex h-9 w-full min-w-0 cursor-text items-center gap-1.5 rounded-md bg-transparent px-2.5 text-sm text-foreground inset-ring-(length:--hairline) inset-ring-input transition-[background-color,box-shadow] duration-150 ease-out-cubic has-[input:focus-visible]:ring-3 has-[input:focus-visible]:ring-focus-ring has-[input:focus-visible]:inset-ring-ring data-invalid:inset-ring-destructive data-invalid:has-[input:focus-visible]:ring-destructive/20 data-invalid:has-[input:focus-visible]:inset-ring-destructive data-readonly:cursor-default data-readonly:bg-muted/40 motion-reduce:transition-none dark:bg-input/30 dark:data-readonly:bg-input/20 forced-colors:border pointer-coarse:h-10 data-disabled:cursor-not-allowed data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&>svg]:text-muted-foreground [@media(hover:hover)]:hover:not-has-[input:focus-visible]:not-data-disabled:not-data-invalid:inset-ring-ring/70",
        className
      )}
      {...props}
    >
      {(state) => (
        <TimeInputContext.Provider value={{ state, invalid }}>
          {children === undefined ? (
            <TimeInputSegments />
          ) : typeof children === "function" ? (
            children(state)
          ) : (
            children
          )}
        </TimeInputContext.Provider>
      )}
    </TimeInputPrimitive.TimeInput>
  )
}

function TimeInputSegments() {
  const { state } = React.useContext(TimeInputContext)
  if (!state) return null
  return (
    <span data-slot="time-input-segments" className="flex min-w-0 items-center">
      {state.segments.map((segment) => (
        <TimeInputSegment key={segment.key} segment={segment} />
      ))}
    </span>
  )
}

function TimeInputSegment({
  className,
  segment,
  ...props
}: React.ComponentProps<typeof TimeInputPrimitive.TimeInputSegment>) {
  const { invalid } = React.useContext(TimeInputContext)
  if (!segment.editable) {
    return (
      <TimeInputPrimitive.TimeInputSegment
        segment={segment}
        className={mergeClassName(
          "whitespace-pre text-muted-foreground",
          className
        )}
        {...props}
      />
    )
  }
  return (
    <TimeInputPrimitive.TimeInputSegment
      segment={segment}
      aria-invalid={invalid || undefined}
      className={mergeClassName(
        "box-content w-[2ch] min-w-0 rounded-[calc(var(--radius-sm)-1px)] bg-transparent px-[2px] py-0.5 text-center tabular-nums caret-transparent outline-none selection:bg-transparent placeholder:text-muted-foreground read-only:cursor-default focus:bg-primary focus:text-primary-foreground focus:placeholder:text-primary-foreground/60 disabled:cursor-not-allowed data-[type=dayPeriod]:field-sizing-content data-[type=dayPeriod]:w-auto data-[type=dayPeriod]:min-w-[2ch] not-supports-[field-sizing:content]:data-[type=dayPeriod]:w-[2.75em] forced-colors:focus:bg-[Highlight] forced-colors:focus:text-[HighlightText] pointer-coarse:text-touch",
        className
      )}
      {...props}
    />
  )
}

const createTimeInputController = TimeInputPrimitive.createTimeInputController

export {
  TimeInput,
  TimeInputSegments,
  TimeInputSegment,
  createTimeInputController,
}
export type { TimeInputProps, TimeInputState }
export type {
  TimeInputController,
  TimeInputGranularity,
  TimeInputSegmentState,
  HourCycle,
} from "@ui-sdk/react/time-input"
```

Update the import paths to match your project setup.

## Usage

UI SDK adds a little CSS of its own in a cascade layer named `ui-sdk`. Declare that layer at the top of your global stylesheet, above the Tailwind import, so your classes win over it.

```css
@layer ui-sdk;

@import "tailwindcss";
```

```tsx
import { TimeInput } from "@/components/ui/ui-sdk-time-input"
```

```tsx
<Label id="meeting-time">Meeting time</Label>
<TimeInput aria-labelledby="meeting-time" defaultValue="09:30" />
```

The value is a 24-hour time like `"14:30"`, the same as a native time input’s, whatever clock the field shows. It stays `null` until every part holds something. Each part is its own small input: type digits and it moves on, or press A or P for the half of the day.

The field is a group, so name it with `aria-labelledby` or `aria-label`. Clicking anywhere in the box focuses the nearest part.

## Composition

```text
TimeInput
└── TimeInputSegments
    └── TimeInputSegment
```

`TimeInput` renders its parts on its own. Add `TimeInputSegments` yourself to put an icon or other content beside them.

## Examples

### Clock and seconds

The field shows the locale’s clock. `hourCycle` picks 12 or 24 hours, and `granularity` adds seconds or shows the hour alone. All of them submit 24-hour values.

```tsx title="components/examples/ui-sdk-time-input/clock.tsx"
import { Label } from "@/components/ui/label"
import { TimeInput } from "@/components/ui/ui-sdk-time-input"

function UiSdkTimeInputClock() {
  return (
    <div className="flex w-full max-w-40 flex-col gap-4">
      <div className="flex flex-col gap-2">
        <Label id="clock-12">12-hour clock</Label>
        <TimeInput
          aria-labelledby="clock-12"
          hourCycle={12}
          defaultValue="18:45"
          locale="en-US"
        />
      </div>
      <div className="flex flex-col gap-2">
        <Label id="clock-24">24-hour clock</Label>
        <TimeInput
          aria-labelledby="clock-24"
          hourCycle={24}
          defaultValue="18:45"
        />
      </div>
      <div className="flex flex-col gap-2">
        <Label id="clock-seconds">With seconds</Label>
        <TimeInput
          aria-labelledby="clock-seconds"
          granularity="second"
          hourCycle={24}
          defaultValue="18:45:30"
        />
      </div>
      <div className="flex flex-col gap-2">
        <Label id="clock-hour">Hour only</Label>
        <TimeInput
          aria-labelledby="clock-hour"
          granularity="hour"
          hourCycle={24}
          defaultValue="18:00"
        />
      </div>
    </div>
  )
}

export { UiSdkTimeInputClock }
```

### Min and max

A time before `min` or after `max` marks the field invalid, and its form won’t submit.

```tsx title="components/examples/ui-sdk-time-input/min-max.tsx"
"use client"

import * as React from "react"

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

const opens = "09:00"
const closes = "17:30"

function UiSdkTimeInputMinMax() {
  const [time, setTime] = React.useState<string | null>("16:00")
  const outside = time !== null && (time < opens || time > closes)

  return (
    <div className="flex w-full max-w-40 flex-col gap-2">
      <Label id="pickup-time">Pickup</Label>
      <TimeInput
        aria-labelledby="pickup-time"
        aria-describedby="pickup-time-message"
        defaultValue="16:00"
        min={opens}
        max={closes}
        hourCycle={24}
        onValueChange={(value) => setTime(value)}
      />
      <p
        id="pickup-time-message"
        className={
          outside
            ? "m-0 text-xs text-destructive"
            : "m-0 text-xs text-muted-foreground"
        }
      >
        {outside ? "We’re closed then." : "Open 09:00 to 17:30."}
      </p>
    </div>
  )
}

export { UiSdkTimeInputMinMax }
```

### Invalid

`invalid` marks the field for checks of your own, like an end time before the start.

```tsx title="components/examples/ui-sdk-time-input/range.tsx"
"use client"

import * as React from "react"

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

function UiSdkTimeInputRange() {
  const [start, setStart] = React.useState<string | null>("13:00")
  const [end, setEnd] = React.useState<string | null>("12:30")
  const invalid = start !== null && end !== null && end <= start

  return (
    <div className="flex w-full max-w-72 flex-col gap-2">
      <div className="grid grid-cols-2 gap-2">
        <div className="flex flex-col gap-2">
          <Label id="shift-start">Starts</Label>
          <TimeInput
            aria-labelledby="shift-start"
            defaultValue="13:00"
            onValueChange={(value) => setStart(value)}
          />
        </div>
        <div className="flex flex-col gap-2">
          <Label id="shift-end">Ends</Label>
          <TimeInput
            aria-labelledby="shift-end"
            aria-describedby="shift-message"
            defaultValue="12:30"
            invalid={invalid}
            onValueChange={(value) => setEnd(value)}
          />
        </div>
      </div>
      <p
        id="shift-message"
        className={
          invalid
            ? "m-0 text-xs text-destructive"
            : "m-0 text-xs text-muted-foreground"
        }
      >
        {invalid ? "The shift has to end after it starts." : "One shift a day."}
      </p>
    </div>
  )
}

export { UiSdkTimeInputRange }
```

### Locale

`locale` sets the order, the separators and the names for the half of the day. Without it, the field follows the page’s language once it’s interactive.

```tsx title="components/examples/ui-sdk-time-input/locale.tsx"
import { Label } from "@/components/ui/label"
import { TimeInput } from "@/components/ui/ui-sdk-time-input"

const locales = [
  { locale: "en-US", label: "Time (en-US)" },
  { locale: "de-DE", label: "Uhrzeit (de-DE)" },
  { locale: "ja-JP", label: "時刻 (ja-JP)" },
  { locale: "ko-KR", label: "시간 (ko-KR)" },
]

function UiSdkTimeInputLocale() {
  return (
    <div className="flex w-full max-w-40 flex-col gap-4">
      {locales.map(({ locale, label }) => (
        <div key={locale} lang={locale} className="flex flex-col gap-2">
          <Label id={`time-${locale}`}>{label}</Label>
          <TimeInput
            aria-labelledby={`time-${locale}`}
            locale={locale}
            defaultValue="18:45"
          />
        </div>
      ))}
    </div>
  )
}

export { UiSdkTimeInputLocale }
```

### Form

A hidden input submits the time under `name`. `required` stops an empty or half-typed time, and a reset brings back the default.

```tsx title="components/examples/ui-sdk-time-input/form.tsx"
"use client"

import * as React from "react"

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

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

  return (
    <form
      className="flex w-full max-w-40 flex-col gap-3"
      onSubmit={(event) => {
        event.preventDefault()
        const data = new FormData(event.currentTarget)
        setSubmitted(String(data.get("reminder")))
      }}
    >
      <div className="flex flex-col gap-2">
        <Label id="reminder-time">Remind me at</Label>
        <TimeInput
          aria-labelledby="reminder-time"
          name="reminder"
          defaultValue="08:15"
          required
        />
      </div>
      <div className="flex items-center gap-3">
        <Button type="submit" size="sm">
          Save
        </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 { UiSdkTimeInputForm }
```

### Controller

`createTimeInputController()` sets, clears or focuses the field from anywhere. Focus a part by its unit, like `"minute"`.

```tsx title="components/examples/ui-sdk-time-input/controller.tsx"
"use client"

import * as React from "react"

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

function now() {
  const date = new Date()
  return `${String(date.getHours()).padStart(2, "0")}:${String(
    date.getMinutes()
  ).padStart(2, "0")}`
}

function UiSdkTimeInputController() {
  const [controller] = React.useState(createTimeInputController)
  const [value, setValue] = React.useState<string | null>(null)

  return (
    <div className="flex w-full max-w-56 flex-col gap-2">
      <Label id="log-time">Logged at</Label>
      <TimeInput
        aria-labelledby="log-time"
        controller={controller}
        onValueChange={(next) => setValue(next)}
      />
      <div className="flex items-center gap-2 pt-1">
        <Button
          size="sm"
          variant="outline"
          onClick={() => controller.setValue(now())}
        >
          Now
        </Button>
        <Button
          size="sm"
          variant="ghost"
          onClick={() => controller.focus("minute")}
        >
          Edit minutes
        </Button>
        <Button size="sm" variant="ghost" onClick={() => controller.clear()}>
          Clear
        </Button>
      </div>
      <p className="m-0 text-xs text-muted-foreground tabular-nums">
        {value ? `Value ${value}` : "Value null"}
      </p>
    </div>
  )
}

export { UiSdkTimeInputController }
```

### Disabled

A disabled field is skipped by focus and doesn’t submit.

```tsx title="components/examples/ui-sdk-time-input/disabled.tsx"
import { Label } from "@/components/ui/label"
import { TimeInput } from "@/components/ui/ui-sdk-time-input"

function UiSdkTimeInputDisabled() {
  return (
    <div className="flex w-full max-w-40 flex-col gap-2">
      <Label id="closed-time">Last entry</Label>
      <TimeInput aria-labelledby="closed-time" defaultValue="22:00" disabled />
    </div>
  )
}

export { UiSdkTimeInputDisabled }
```

### Read-only

A read-only field takes focus and submits, but keys and typing don’t change it.

```tsx title="components/examples/ui-sdk-time-input/read-only.tsx"
import { Label } from "@/components/ui/label"
import { TimeInput } from "@/components/ui/ui-sdk-time-input"

function UiSdkTimeInputReadOnly() {
  return (
    <div className="flex w-full max-w-40 flex-col gap-2">
      <Label id="checkout-time">Checkout</Label>
      <TimeInput
        aria-labelledby="checkout-time"
        defaultValue="11:00"
        readOnly
      />
    </div>
  )
}

export { UiSdkTimeInputReadOnly }
```

### Right to left

The parts keep the locale’s order, and the arrow keys follow them on screen.

```tsx title="components/examples/ui-sdk-time-input/rtl.tsx"
import { IconClock } from "@tabler/icons-react"

import { Label } from "@/components/ui/label"
import { TimeInput, TimeInputSegments } from "@/components/ui/ui-sdk-time-input"

function UiSdkTimeInputRtl() {
  return (
    <div dir="rtl" lang="ar" className="flex w-full max-w-40 flex-col gap-2">
      <Label id="rtl-time">وقت الاجتماع</Label>
      <TimeInput aria-labelledby="rtl-time" locale="ar-EG" defaultValue="18:45">
        <IconClock aria-hidden="true" />
        <TimeInputSegments />
      </TimeInput>
    </div>
  )
}

export { UiSdkTimeInputRtl }
```

## Keyboard

| Key | Action |
| --- | --- |
| `0–9` | Fills the part, then moves to the next one. |
| `↑` `↓` | Steps the part by one, wrapping around. An empty part starts at the current time. |
| `PageUp` `PageDown` | Steps by more, like 15 minutes. |
| `Home` `End` | Goes to the part’s first or last value. |
| `←` `→` | Moves to the part before or after. |
| `Backspace` | Removes the last digit, then empties the part, then moves back. |
| `A` `P` | Chooses AM or PM on a 12-hour clock. |
| `Enter` | Submits the form with what’s typed. |

## Accessibility

The root is a group. Each part is a spinbutton named by its unit, like “hour”, with its value and range, and an empty part reads as “Empty”. The text between parts is hidden from screen readers.

Parts are real inputs, so phones show a number pad and typing works before the page’s script runs. They grow to 16px on touch screens so phones don’t zoom in, and the focused part is highlighted rather than showing a caret.

## API reference

Each part wraps the UI SDK part of the same name and accepts all of its props, including `render` and a `className` that can be a function of the part’s state.

### TimeInput

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string \| null` | – |  |
| `defaultValue` | `string \| null` | – |  |
| `onValueChange` | `(value: string \| null, details) => void` | – | Runs once every part holds something, and with null when one is emptied. |
| `hourCycle` | `12 \| 24` | – | Defaults to the locale’s clock. |
| `granularity` | `"hour" \| "minute" \| "second"` | `"minute"` |  |
| `min` | `string` | – |  |
| `max` | `string` | – |  |
| `locale` | `string` | – | Defaults to the page’s language. |
| `invalid` | `boolean` | `false` |  |
| `disabled` | `boolean` | `false` |  |
| `readOnly` | `boolean` | `false` |  |
| `required` | `boolean` | `false` |  |
| `name` | `string` | – |  |
| `form` | `string` | – |  |
| `controller` | `TimeInputController` | – | From createTimeInputController(). |
| `children` | `ReactNode \| (state) => ReactNode` | – | Defaults to TimeInputSegments. |
| `render` | `ReactElement \| (props, state) => ReactElement` | – |  |

| Attribute | Description |
| --- | --- |
| `data-slot="time-input"` | Target the time input in CSS. |
| `data-invalid` | Present when the time is outside min and max, or invalid is set. |
| `data-empty` | Present while every part is empty. |
| `data-disabled` | Present when disabled. |
| `data-readonly` | Present when read-only. |
| `data-required` | Present when required. |

### TimeInputSegments

Renders a TimeInputSegment for each part, in the locale’s order, with the text between them.

### TimeInputSegment

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `segment` | `TimeInputSegmentData` | – | One of the root state’s segments. |
| `render` | `ReactElement \| (props, state) => ReactElement` | – |  |

| Attribute | Description |
| --- | --- |
| `data-slot="time-input-segment"` | Target a part in CSS. |
| `data-type` | hour, minute, second, dayPeriod, or literal for the text between parts. |
| `data-empty` | Present while the part is empty. |

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt

## Related

- [Calendar](https://hextaui.com/docs/calendar) - A date grid for single, range and multiple selection, with sliding months, range previews and touch-sized days.
- [Checkbox](https://hextaui.com/docs/checkbox) - A checkbox whose check draws in, with indeterminate parents, groups and labels that share its hover.
- [Combobox](https://hextaui.com/docs/combobox) - A filterable select with chips, groups and async results, in a popup that resizes as you type.
- [Date picker](https://hextaui.com/docs/date-picker) - A button that opens a calendar in a popover, or a bottom sheet on phones, for single dates and ranges.
- [Field](https://hextaui.com/docs/field) - Labels, descriptions and errors wired to their control, with validation states and layouts for forms.
- [Input](https://hextaui.com/docs/input) - A text input with three sizes, invalid and read-only states, native validation styling and a 16px touch font so phones never zoom in.
