# Time Field (/ui/components/react/time-field)



<Playground component="time-field" />

## Usage [#usage]

```tsx
import { TimeField } from '@appica/ui-react/time-field'
```

```tsx
<TimeField value={time} onValueChange={setTime} />
```

`TimeField` is the time counterpart to [`DateField`](/ui/components/react/date-field): the `format` string is split into editable hour/minute/second (and optional AM/PM) segments, each a `spinbutton`. The value is always a 24-hour `HH:mm` or `HH:mm:ss` string (or `null` while empty), even when displayed in 12-hour form. It's a custom component — not based on Base UI — sharing the [`Input`](/ui/components/react/input) styling.

## Examples [#examples]

### Variants [#variants]

```tsx
import { TimeField } from '@appica/ui-react/time-field'

const variants = ['outline', 'soft'] as const

export default function TimeFieldVariants() {
  return (
    <div className="flex w-full max-w-40 flex-wrap items-center gap-4">
      {variants.map((variant) => (
        <TimeField key={variant} defaultValue="09:30" variant={variant} />
      ))}
    </div>
  )
}
```

### Sizes [#sizes]

```tsx
import { TimeField } from '@appica/ui-react/time-field'

const sizes = ['sm', 'md', 'lg'] as const

export default function TimeFieldSizes() {
  return (
    <div className="flex w-full max-w-40 flex-wrap items-center gap-4">
      {sizes.map((size) => (
        <TimeField key={size} defaultValue="09:30" size={size} />
      ))}
    </div>
  )
}
```

### Formats [#formats]

Choose 24-hour (`HH`) or 12-hour (`hh` with an `a` period segment), and add `ss` for seconds. The stored value stays 24-hour regardless of display.

```tsx
import { TimeField } from '@appica/ui-react/time-field'

const formats = ['HH:mm', 'HH:mm:ss', 'hh:mm a', 'hh:mm:ss a']

export default function TimeFieldFormats() {
  return (
    <div className="flex w-full max-w-40 flex-wrap items-center gap-4">
      {formats.map((format) => (
        <TimeField key={format} defaultValue="13:45:30" format={format} />
      ))}
    </div>
  )
}
```

### Controlled [#controlled]

Drive the value with `value` + `onValueChange`. The callback fires with a `"HH:mm"`/`"HH:mm:ss"` string once the required segments are filled, or `null` when cleared.

```tsx
'use client'

import * as React from 'react'
import { TimeField } from '@appica/ui-react/time-field'

export default function TimeFieldControlled() {
  const [time, setTime] = React.useState<string | null>('09:30')

  return (
    <div className="flex w-full max-w-40 flex-col gap-3">
      <TimeField value={time} onValueChange={setTime} />
      <p className="text-foreground-muted text-sm">
        Value: <span className="text-foreground font-medium">{time ?? 'null'}</span>
      </p>
    </div>
  )
}
```

### With an icon [#with-an-icon]

Use `startSlot` / `endSlot` to render adornments inside the field frame.

```tsx
import { TimeField } from '@appica/ui-react/time-field'
import { Clock } from '@appica/icons-react'

export default function TimeFieldWithIcon() {
  return <TimeField className="max-w-40" defaultValue="09:30" startSlot={<Clock />} />
}
```

### Disabled, read-only & error states [#disabled-read-only--error-states]

`disabled` greys out the field and blocks interaction; `readOnly` keeps it focusable and readable but prevents edits. Set `aria-invalid` to paint the error state — it's mirrored to `data-invalid`. The field also inherits `disabled` and the error state from a surrounding [`Field`](/ui/components/react/field), so wrapping it in a `Field` marked `invalid` produces the same styling and is the better choice when you're also rendering a validation message.

```tsx
import { TimeField } from '@appica/ui-react/time-field'

export default function TimeFieldStates() {
  return (
    <div className="flex w-full max-w-40 flex-wrap items-center gap-4">
      <TimeField defaultValue="09:30" disabled />
      <TimeField defaultValue="09:30" readOnly />
      <TimeField defaultValue="09:30" aria-invalid />
    </div>
  )
}
```

## RTL [#rtl]

Every Appica UI component supports right-to-left layouts out of the box. Set the `dir` attribute on a container (commonly your `<html>` element) so CSS logical properties resolve correctly, and wrap your tree in `DirectionProvider` so direction-aware behavior — roving focus, popup placement, and the like — follows the same direction.

```tsx
import { DirectionProvider } from '@appica/ui-react/providers/direction-provider'

export default function RootLayout({ children }) {
  return (
    <html lang="ar" dir="rtl">
      <body>
        <DirectionProvider dir="rtl">{children}</DirectionProvider>
      </body>
    </html>
  )
}
```

For setup details and caveats, see the [RTL guide](/ui/docs/react/rtl).

<RtlPreview component="time-field" />

## API reference [#api-reference]

| Prop                    | <ColMinWidth width="180">Type</ColMinWidth> | Default     | <ColMinWidth width="220">Description</ColMinWidth>                                     |
| ----------------------- | ------------------------------------------- | ----------- | -------------------------------------------------------------------------------------- |
| `value`                 | <Code>string \| null</Code>                 | —           | Controlled value, a 24-hour `"HH:mm[:ss]"` string. Pair with `onValueChange`.          |
| `defaultValue`          | <Code>string \| null</Code>                 | —           | Uncontrolled initial value.                                                            |
| `onValueChange`         | <Code>(time: string \| null) => void</Code> | —           | Fires with the time string when complete, or `null` when cleared.                      |
| `format`                | `string`                                    | `'HH:mm'`   | date-fns token string — `H`/`HH`, `h`/`hh`, `m`/`mm`, `s`/`ss`, `a`.                   |
| `variant`               | <Code>'outline' \| 'soft'</Code>            | `'outline'` | Field appearance — bordered or filled.                                                 |
| `size`                  | <Code>'sm' \| 'md' \| 'lg'</Code>           | `'md'`      | Height, padding, and text scale.                                                       |
| `startSlot` / `endSlot` | `ReactNode`                                 | —           | Adornments rendered inside the field frame.                                            |
| `disabled`              | `boolean`                                   | `false`     | Blocks interaction and removes the segments from the tab order.                        |
| `readOnly`              | `boolean`                                   | `false`     | Segments stay focusable and readable but can't be edited.                              |
| `required`              | `boolean`                                   | `false`     | Marks the hidden form input as required (needs `name`).                                |
| `name`                  | `string`                                    | —           | Renders a hidden `<input>` with the time value for form submission.                    |
| `unstyled`              | `boolean`                                   | `false`     | Drop the input appearance — for composing inside another field (used by `DatePicker`). |
| `className`             | `string`                                    | —           | Extra classes on the root, merged via `tailwind-merge`.                                |

Renders a `<div role="group">` and forwards the remaining `<div>` props (`id`, `aria-*`, `ref`, …). The error styling is driven by the `data-invalid` attribute; setting `aria-invalid` marks the field invalid — it sets `data-invalid` for styling and conveys the state to assistive technology.

## Accessibility [#accessibility]

* The field is a `role="group"`; each segment is a `role="spinbutton"` with an `aria-label` (hour/minute/second/period) and `aria-valuenow`/`aria-valuetext`.
* **←/→*&#x2A; move between segments, &#x2A;*↑/↓** step the focused segment (toggling AM/PM on the period), **Home/End** jump to the first/last, **Backspace/Delete** clear; typing fills and auto-advances. The period segment also accepts **a**/**p**.
* `disabled` removes the segments from the tab order; `readOnly` keeps them focusable but inert.
* Segment focus styling uses a transition wrapped in `motion-reduce:`, so it's skipped under reduced motion.
