<!-- Protocore Design System — DateTimePicker -->
# DateTimePicker

- **Category:** Inputs (`inputs`)
- **Slug:** `inputs/date-time-picker`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { DateTimePicker } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/inputs/date-time-picker

> Sunken field that opens a Calendar above a TimeInput in one overlay, so a date and a time are chosen together and returned as a single Date.

## When to use it

Use **DateTimePicker** when a single instant matters — a scheduled deploy, an event start. It composes **Calendar** and **TimeInput** so the two parts stay in sync as one `Date`. When only the date matters use **DatePicker**; when only the time matters use **TimePicker**.

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `className` | `string` | no | — | — |
| `clearable` | `boolean` | no | `false` | Show a clear button when a value is selected. |
| `defaultOpen` | `boolean` | no | — | Initial open state in uncontrolled mode. |
| `defaultValue` | `Date \| null` | no | — | Initial date-time in uncontrolled mode. |
| `disabled` | `boolean` | no | — | Disable the whole control. |
| `format` | `DateTimeFormatOptions \| ((date: Date) => string)` | no | — | How the value renders in the field. `Intl.DateTimeFormatOptions` or a function. |
| `hour12` | `boolean` | no | `false` | Use a 12-hour clock for the time field. |
| `invalid` | `boolean` | no | — | Mark the field invalid — danger border plus `aria-invalid`. |
| `isDateDisabled` | `((date: Date) => boolean)` | no | — | Predicate to disable arbitrary days. |
| `locale` | `string` | no | — | BCP-47 locale for the field label and calendar. |
| `max` | `Date` | no | — | Latest selectable day (inclusive). |
| `min` | `Date` | no | — | Earliest selectable day (inclusive). |
| `onOpenChange` | `((open: boolean) => void)` | no | — | Fires when the overlay opens or closes. |
| `onValueChange` | `((value: Date \| null) => void)` | no | — | Fires with the chosen `Date` (or `null` when cleared). |
| `open` | `boolean` | no | — | Controlled open state of the overlay. |
| `placeholder` | `string` | no | `Select date & time` | Text shown when no value is selected. |
| `size` | `enum` | no | `md` | Field height: `sm` (32) · `md` (36, default) · `lg` (40). |
| `style` | `CSSProperties` | no | — | — |
| `value` | `Date \| null` | no | — | Controlled selected date-time. Pass `null` for no selection. |
| `weekStartsOn` | `enum` | no | `1` | First column of the week: 0 = Sunday … 6 = Saturday. |
| `withSeconds` | `boolean` | no | `false` | Include a seconds segment in the time field. |

## Examples

### Basics

The trigger shows a formatted date-and-time; opening it reveals a `Calendar` above a `TimeInput`. Choosing a day keeps the current time, changing the time keeps the day. Controllable via `value` / `defaultValue` / `onValueChange` with a single `Date`.

```tsx
import { useState } from "react";
import { DateTimePicker } from "@protocore/pds";

export default function Basics() {
  const [value, setValue] = useState<Date | null>(new Date(2026, 6, 6, 9, 30, 0, 0));

  return (
    <div style={{ width: 280 }}>
      <DateTimePicker
        aria-label="Scheduled at"
        value={value}
        onValueChange={setValue}
        locale="en-GB"
      />
    </div>
  );
}
```

### Bounded, 12-hour & clearable

Wrap it in a `Field` for a label and hint. Constrain the calendar with `min` / `max`, switch the time to `hour12`, add `withSeconds` for precision, and set `clearable` to expose a reset once a value is chosen.

```tsx
import { useState } from "react";
import { DateTimePicker, Field } from "@protocore/pds";

export default function InField() {
  const [value, setValue] = useState<Date | null>(new Date(2026, 6, 6, 14, 0, 0, 0));

  return (
    <div style={{ width: 300 }}>
      <Field label="Deploy window" htmlFor="deploy-at" hint="Within the next 60 days.">
        <DateTimePicker
          id="deploy-at"
          value={value}
          onValueChange={setValue}
          min={new Date(2026, 6, 6)}
          max={new Date(2026, 8, 4)}
          hour12
          clearable
          locale="en-US"
        />
      </Field>
    </div>
  );
}
```

## Do & don't

**Do**

- Give the trigger an accessible name via a `Field` label or `aria-label`.
- Set `min` / `max` to the valid window so out-of-range days can't be picked.
- Match `hour12` and `locale` to the surrounding UI's conventions.
- Read the combined `Date` from `onValueChange` — it carries both parts.

**Don't**

- Don't pair a separate DatePicker and TimePicker when the two must move as one instant.
- Don't leave the trigger unlabelled when no visible field label wraps it.
- Don't reconstruct the value from the field string — use the `Date`.
- Don't nest it inside another popover trigger where overlays contend for focus.

## Accessibility

**Keyboard**

| Keys | Action |
| --- | --- |
| `Enter / Space` | Open the overlay from the focused trigger. |
| `Arrow keys` | Move between days in the calendar grid. |
| `Tab` | Move from the calendar into the time segments. |
| `Esc` | Close the overlay without changing the value. |

**Notes**

- The overlay is a Radix Popover: portalled, dismissed on outside-click and Escape, and returns focus to the trigger on close.
- The calendar is a `role="grid"`; the time field is a `role="group"` of spinbutton segments.
- `invalid` sets `aria-invalid` on the trigger; the trigger exposes `aria-expanded` for its open state.

## Related

`date-picker`, `time-input`, `time-picker`, `calendar`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
