# Form field with validation

> Native form field with a required check, an error, reset and the submitted value.

A shadcn/ui block from the [@reui](https://reui.io) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/reui/c-time-picker-12).

- Page, with a live preview: https://shadcn.exchange/r/reui/c-time-picker-12
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/reui/c-time-picker-12-light-0dd42c477193c401.png
- Registry item: https://reui.io/r/new-york-v4/c-time-picker-12.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://reui.io/r/new-york-v4/c-time-picker-12.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `button`, `field`, `@reui/time-picker`

## Files

### `c-time-picker-12.tsx`

```tsx
"use client"

import { useId, useState } from "react"
import { TimePicker } from "@/components/reui/time-picker"

import { Button } from "@/components/ui/button"
import {
  Field,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLabel,
} from "@/components/ui/field"

export function Pattern() {
  const id = useId()
  const [invalid, setInvalid] = useState(false)
  const [booked, setBooked] = useState<string | null>(null)

  return (
    /* `noValidate`: the Field draws the error, not the browser bubble. */
    <form
      noValidate
      className="mx-auto w-full max-w-sm"
      onSubmit={(event) => {
        event.preventDefault()
        /* The picker submits `HH:mm` under its `name`, or "" while empty. */
        const time = String(
          new FormData(event.currentTarget).get("deliveryTime") ?? ""
        )
        setInvalid(!time)
        setBooked(time || null)
      }}
      onReset={() => {
        /* The picker returns to its default (empty) on the same reset. */
        setInvalid(false)
        setBooked(null)
      }}
    >
      <FieldGroup>
        <Field data-invalid={invalid || undefined}>
          <FieldLabel id={`${id}-label`} htmlFor={id}>
            Delivery time
          </FieldLabel>
          <TimePicker
            id={id}
            name="deliveryTime"
            required
            /* Sets aria-invalid on the trigger; the Field colors the label. */
            invalid={invalid}
            minuteStep={30}
            min="08:00"
            max="20:00"
            aria-labelledby={`${id}-label`}
            aria-describedby={`${id}-hint`}
            onValueChange={(next) => {
              /* A pick clears the error; emptying the field waits for Submit. */
              if (next) setInvalid(false)
            }}
          />
          {/* One id for both, so aria-describedby follows whichever is shown. */}
          {invalid ? (
            <FieldError id={`${id}-hint`}>
              Choose a delivery time to continue.
            </FieldError>
          ) : (
            <FieldDescription id={`${id}-hint`}>
              30-minute slots between 08:00 and 20:00.
            </FieldDescription>
          )}
        </Field>
        <div className="flex items-center gap-2">
          <Button type="submit">Submit</Button>
          <Button type="reset" variant="outline">
            Reset
          </Button>
          <p
            role="status"
            className="text-muted-foreground ms-auto min-w-0 truncate text-sm"
          >
            {booked && `Delivery booked for ${booked}`}
          </p>
        </div>
      </FieldGroup>
    </form>
  )
}
```
