Siteplane UI

Time Field

Edit an hour and minute with segments or a selector.

Markdown
0830

Sent every weekday at this time.

"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

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

Complete the one-time setup first. Registry JSON lists source files and dependencies.

For this page’s examples, also install:

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

Field is installed with Input.

Usage

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.

0930
"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.

1030
"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.

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.

0900
0800

Set by your workspace admin.

0830
End time must be after the start time.
"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.

0930
"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
0900
1700
"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

Component properties
PropTypeDefaultDescription
aria-labelstringunsetAccessible name when no FieldLabel or aria-labelledby names the field.
aria-labelledby / aria-describedbystringunsetExternal label and help ids; inside a Field, FieldLabel, FieldDescription and FieldError are connected automatically.
aria-invalidAriaAttributes[aria-invalid]unsetExternal invalid state.
value / defaultValueTimeFieldValue | null (value only)uncontrolled / unsetControlled or initial time; unset or null shows empty segments.
onValueChange(value: TimeFieldValue | undefined) => voidunsetReports a completed time, or undefined when cleared.
disabledbooleanfalsePrevents editing/opening; a disabled Field also disables the field.
classNamestringsegment widthOuter 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 / requiredbooleanfalseRead-only field on the regular surface; required blocks form submission while empty, focuses the field and shows the required message in FieldError.
sizesm | md | lg | xlprovider / lgControl size.
textSizesm | baseunsetField text size; unset is 14px, or 16px at xl, like Input.
variantdefault | elevated | inset | softdefaultInput surface.
formstringunsetAssociated form ID.
namestringunsetHidden serialized input name; a Field name takes precedence.
iconModeaction-end | decorative-start | noneaction-endSelector button, decorative leading icon or no icon.
openPickerLabelstringOpen time selectorSelector button name.
incompleteLabelstringEnter a complete time.The error of a required field while its time is incomplete, shown by FieldError.
minuteStepnumber5Minute selector options; normalized to an integer between 1 and 60.
hourCycle12 | 2424Clock 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.
alignstart | centerstartSegment text layout; center shows once the field is wider than its segments, for example with w-full.
hourLabel / minuteLabelstringaria-label + hour / minuteSegment names. A FieldLabel or aria-labelledby follows them; without a value here, the localized hour and minute names are used.
hoursLabel / minutesLabelstringHours / MinutesSelector column labels.

size and variant use the same scale and surfaces as Input; see Input sizes and variants.

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

Read the installed source.

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 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.