# Time Field

Edit an hour and minute with segments or a selector.

Daily summary time

[Complete TSX file](/examples/time-field/default.tsx)

```tsx
"use client";

import {
  Field,
  FieldDescription,
  FieldLabel,
} from "@/siteplane/components/field";
import { TimeField } from "@/siteplane/components/time-field";

export default function Example() {
  return (
    <Field className="w-full max-w-sm">
      <FieldLabel>Daily summary time</FieldLabel>
      <TimeField defaultValue={{ hour: 8, minute: 30 }} />
      <FieldDescription>Sent every weekday at this time.</FieldDescription>
    </Field>
  );
}
```

## Installation

Complete [one-time setup](/docs/installation/) first.

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/time-field
```

With npm: `npx shadcn@4.18.0 add @siteplane/time-field`. [Registry JSON](/registry/time-field.json) lists source files and dependencies.

For this page’s examples, also install:

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/button @siteplane/fieldset @siteplane/form @siteplane/input
```

Field is installed with Input.

## Usage

```tsx
import { TimeField } from "@/siteplane/components/time-field";

<TimeField aria-label="Meeting time" defaultValue={{ hour: 9, minute: 0 }} />;
```

## Controlled value

Pass `value` and `onValueChange` to keep the time in React state. `onValueChange` receives `undefined` when the field is cleared; store `null` for an empty controlled value, because `undefined` makes the field uncontrolled.

Controlled meeting time

[Complete TSX file](/examples/time-field/controlled.tsx)

```tsx
"use client";

import {
  TimeField,
  type TimeFieldValue,
} from "@/siteplane/components/time-field";
import { useState } from "react";

export default function Example() {
  const [value, setValue] = useState<TimeFieldValue | null>({
    hour: 9,
    minute: 30,
  });
  return (
    <TimeField
      aria-label="Meeting time"
      value={value}
      onValueChange={(next) => setValue(next ?? null)}
    />
  );
}
```

## Minute step

`minuteStep` sets the minute options in the time selector, here every 15 minutes. Typed minutes are not rounded, so check the step on the server if it matters.

Meeting start in 15-minute steps

[Complete TSX file](/examples/time-field/step.tsx)

```tsx
"use client";

import { Field, FieldLabel } from "@/siteplane/components/field";
import { TimeField } from "@/siteplane/components/time-field";

export default function Example() {
  return (
    <Field className="w-full max-w-sm">
      <FieldLabel>Meeting start</FieldLabel>
      <TimeField defaultValue={{ hour: 10, minute: 30 }} minuteStep={15} />
    </Field>
  );
}
```

## 12-hour clock

Set `hourCycle={12}` for an application that shows times such as 9:00 PM. The field adds an AM/PM segment after the minutes, which takes focus and arrow keys like the hour and the minute; typing A or P sets it. The selector names each hour with its day period, such as 09 PM. The hour keeps its leading zero, as in every other segment. The value and the submitted `HH:mm` stay in 24-hour time. The space before AM or PM is a plain space on the server and in the browser alike, so a prerendered page hydrates without a mismatch.

```tsx
import { TimeField } from "@/siteplane/components/time-field";

<TimeField
  aria-label="Quiet hours start"
  defaultValue={{ hour: 21, minute: 0 }}
  hourCycle={12}
/>;
```

## Disabled, read-only and invalid

A disabled `Field` also disables the time field, and `readOnly` shows a time that can't be changed on the regular field surface. For an error from your application, set `invalid` on `Field` and explain the problem in a `FieldError`.

Disabled, read-only and invalid time fields

[Complete TSX file](/examples/time-field/states.tsx)

```tsx
"use client";

import {
  Field,
  FieldDescription,
  FieldError,
  FieldLabel,
} from "@/siteplane/components/field";
import { TimeField } from "@/siteplane/components/time-field";

export default function Example() {
  return (
    <div className="grid w-full max-w-sm gap-4">
      <Field disabled>
        <FieldLabel>Start time</FieldLabel>
        <TimeField defaultValue={{ hour: 9, minute: 0 }} />
      </Field>
      <Field>
        <FieldLabel>Office opens</FieldLabel>
        <TimeField defaultValue={{ hour: 8, minute: 0 }} readOnly />
        <FieldDescription>Set by your workspace admin.</FieldDescription>
      </Field>
      <Field invalid>
        <FieldLabel>End time</FieldLabel>
        <TimeField defaultValue={{ hour: 8, minute: 30 }} />
        <FieldError match={true}>
          End time must be after the start time.
        </FieldError>
      </Field>
    </div>
  );
}
```

## Submission and reset

With `name`, TimeField submits a hidden 24-hour `HH:mm` string without date, seconds or time-zone offset; the example submits `{ "reminderTime": "09:30" }`. Without `value` or `defaultValue` the field starts empty and submits an empty string. With `required`, submitting an empty field is blocked, the field is focused and `FieldError` shows the required message. Native form reset restores `defaultValue`; reset controlled state yourself.

Time field in a form

[Complete TSX file](/examples/time-field/form.tsx)

```tsx
"use client";

import { Button } from "@/siteplane/components/button";
import { Field, FieldError, FieldLabel } from "@/siteplane/components/field";
import { Form } from "@/siteplane/components/form";
import { TimeField } from "@/siteplane/components/time-field";

export default function Example() {
  return (
    <Form
      className="grid w-full max-w-sm gap-4"
      onSubmit={(event) => {
        event.preventDefault();
      }}
    >
      <Field>
        <FieldLabel>Reminder time</FieldLabel>
        <TimeField
          defaultValue={{ hour: 9, minute: 30 }}
          name="reminderTime"
          required
        />
        <FieldError />
      </Field>
      <div className="flex gap-2">
        <Button type="submit">Submit</Button>
        <Button type="reset" variant="outline">
          Reset
        </Button>
      </div>
    </Form>
  );
}
```

## Decorative icon

Use `iconMode="decorative-start"` for compact typed entry, such as a start and end time. The clock is decorative and no selector opens; `iconMode="none"` removes the icon.

Working hours

[Complete TSX file](/examples/time-field/icons.tsx)

```tsx
"use client";

import { Field, FieldLabel } from "@/siteplane/components/field";
import { Fieldset, FieldsetLegend } from "@/siteplane/components/fieldset";
import { TimeField } from "@/siteplane/components/time-field";

export default function Example() {
  return (
    <Fieldset className="grid w-full max-w-sm grid-cols-2 gap-4">
      <FieldsetLegend className="col-span-2">Working hours</FieldsetLegend>
      <Field>
        <FieldLabel>Start time</FieldLabel>
        <TimeField
          className="w-full"
          defaultValue={{ hour: 9, minute: 0 }}
          iconMode="decorative-start"
        />
      </Field>
      <Field>
        <FieldLabel>End time</FieldLabel>
        <TimeField
          className="w-full"
          defaultValue={{ hour: 17, minute: 0 }}
          iconMode="decorative-start"
        />
      </Field>
    </Fieldset>
  );
}
```

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| aria-label | string | unset | Accessible name when no FieldLabel or aria-labelledby names the field. |
| aria-labelledby / aria-describedby | string | unset | External label and help ids; inside a Field, FieldLabel, FieldDescription and FieldError are connected automatically. |
| aria-invalid | AriaAttributes[aria-invalid] | unset | External invalid state. |
| value / defaultValue | TimeFieldValue \| null (value only) | uncontrolled / unset | Controlled or initial time; unset or null shows empty segments. |
| onValueChange | (value: TimeFieldValue \| undefined) => void | unset | Reports a completed time, or undefined when cleared. |
| disabled | boolean | false | Prevents editing/opening; a disabled Field also disables the field. |
| className | string | segment width | Outer control styling and width. By default the field is as wide as its segments and the selector button; a width utility such as w-full or w-48 replaces that. |
| readOnly / required | boolean | false | Read-only field on the regular surface; required blocks form submission while empty, focuses the field and shows the required message in FieldError. |
| size | sm \| md \| lg \| xl | provider / lg | Control size. |
| textSize | sm \| base | unset | Field text size; unset is 14px, or 16px at xl, like Input. |
| variant | default \| elevated \| inset \| soft | default | Input surface. |
| form | string | unset | Associated form ID. |
| name | string | unset | Hidden serialized input name; a Field name takes precedence. |
| iconMode | action-end \| decorative-start \| none | action-end | Selector button, decorative leading icon or no icon. |
| openPickerLabel | string | Open time selector | Selector button name. |
| incompleteLabel | string | Enter a complete time. | The error of a required field while its time is incomplete, shown by FieldError. |
| minuteStep | number | 5 | Minute selector options; normalized to an integer between 1 and 60. |
| hourCycle | 12 \| 24 | 24 | Clock of the segments and the selector. 12 adds the locale's AM/PM segment, named after aria-label like the hour and minute; the value stays in 24-hour time. |
| align | start \| center | start | Segment text layout; center shows once the field is wider than its segments, for example with w-full. |
| hourLabel / minuteLabel | string | aria-label + hour / minute | Segment names. A FieldLabel or aria-labelledby follows them; without a value here, the localized hour and minute names are used. |
| hoursLabel / minutesLabel | string | Hours / Minutes | Selector column labels. |

`size` and `variant` use the same scale and surfaces as Input; see [Input sizes](/docs/components/input/#sizes) and [variants](/docs/components/input/#variants).

Exported types: `TimeFieldProps`, `TimeFieldValue`, `TimeFieldHourCycle`. These describe the contracts above.

[Read the installed source](/source/time-field).

## Usage notes

TimeFieldValue is `{hour:number, minute:number}` in 24-hour time, also on a 12-hour clock. It has no date, seconds, locale or time-zone prop. Without `value` or `defaultValue` the field shows empty segments. The field is compact by default, as wide as a time takes; add `className="w-full"` to fill a form column or a grid cell. Inside a Field, FieldLabel names TimeField and clicking it focuses the hour segment; outside a Field, pass `aria-label` or `aria-labelledby`. Arrow keys change the focused hour, minute or AM/PM segment. Only the props listed above are supported, and TimeField renders its own segments, selector button and popover; it accepts no children or ref.

The segments read in the direction of the locale that [SiteplaneLocaleProvider](/docs/customization/providers/#locale) sets, which the arrow keys between them follow too, and are isolated from the text around them. An `en-US` time keeps its hour before its minute in a right-to-left page, while the field keeps the page's direction: its selector button sits at the page's inline end.
