component

Input Datetime

A segmented date / time input. Each unit — year, month, day, hour, minute, AM/PM — is its own focusable segment; separators are static, and the container shows a single focus ring via :focus-within. Type digits to fill a segment (it auto-advances when no further digit fits), use / to step a value (stepping an empty segment starts from the current date/time), and

/ to move between segments. Date segment order and separators follow the locale.

One component covers three shapes via granularity: "date" (default), "time", and "datetime". Set range for a start/end pair. The time portion follows the locale’s 12/24-hour cycle unless you force it with is24Hour.

The value is a Date (or a { start, end } DateRange when range is set), exposed as a bindable value + onChange with defaultValue for uncontrolled use, and serialized into hidden input(s) for native form submission — YYYY-MM-DD for date, HH:MM for time, YYYY-MM-DDTHH:MM for datetime.

Opt into a calendar picker with calendar: it adds a trailing calendar-icon button (joined to the field with a controls Group) that opens a date-picker popover, so the same field supports both typing and point-and-click selection.

Date

MMDDYYYY
---
import InputDatetime from "../InputDatetime.astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "md" })}>
  <InputDatetime id="astro-demo-input-datetime-default" />
</div>

Time

granularity="time" renders hour + minute, plus an AM/PM segment in 12-hour mode. is24Hour forces the 24-hour clock.

---
import InputDatetime from "../InputDatetime.astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "md" })}>
  <label class={stack({ gap: "2xs" })}>
    <span style="font-size: 0.75rem; opacity: 0.7;">12-hour (AM/PM)</span>
    <InputDatetime
      id="astro-demo-time-12"
      granularity="time"
      fullWidth={false}
    />
  </label>
  <label class={stack({ gap: "2xs" })}>
    <span style="font-size: 0.75rem; opacity: 0.7;">24-hour</span>
    <InputDatetime
      id="astro-demo-time-24"
      granularity="time"
      is24Hour
      fullWidth={false}
    />
  </label>
</div>

Datetime

granularity="datetime" composes the date and time segments into one input.

MMDDYYYYhhmm--
---
import InputDatetime from "../InputDatetime.astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "md" })}>
  <InputDatetime id="astro-demo-datetime" granularity="datetime" />
</div>

Calendar

calendar adds a trailing calendar-icon trigger that opens a date-picker popover. The calendar selects the date part; for granularity="datetime" the time stays typed in the segments (a missing time defaults to 00:00 on the first pick). It is shown for granularity "date" and "datetime", and for ranges — but never for "time" (a clock has no calendar). Picking a day fills the field and closes the popover; typing keeps the open calendar in sync, navigating to and highlighting the typed date.

MMDDYYYY
August 2026
SMTWTFS
---
import InputDatetime from "../InputDatetime.astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "md" })}>
  <InputDatetime id="astro-demo-input-datetime-calendar" calendar />
</div>

Calendar range

Combine calendar with range for a two-click range picker. The first click sets the start; the second commits { start, end } and closes the popover.

MMDDYYYYMMDDYYYY
August 2026
SMTWTFS
---
import InputDatetime from "../InputDatetime.astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "md" })}>
  <InputDatetime id="astro-demo-input-datetime-calendar-range" range calendar />
</div>

Calendar with time

With granularity="datetime", the calendar only sets the date — the typed time of day is preserved (or defaults to 00:00 on the first pick). Type a time, pick a day from the calendar, and the time stays put.

MMDDYYYYhhmm--
August 2026
SMTWTFS
---
import InputDatetime from "../InputDatetime.astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "md" })}>
  <InputDatetime
    id="astro-demo-input-datetime-calendar-datetime"
    calendar
    granularity="datetime"
  />
</div>

Range

range renders two groups joined by a separator (configurable via rangeSeparator). The value becomes { start, end }, and each side’s day clamps to its own month/year.

MMDDYYYYMMDDYYYY
---
import InputDatetime from "../InputDatetime.astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "md" })}>
  <InputDatetime id="astro-demo-range" range />
</div>

Formats

Date segment order and separators are derived from the locale via the platform’s Intl short-date pattern. Use separator to override the date-part separator.

---
import InputDatetime from "../InputDatetime.astro";
import { stack } from "@pindoba/styled-system/patterns";

const seed = new Date(2026, 4, 14);
---

<div class={stack({ gap: "md" })}>
  <label class={stack({ gap: "2xs" })}>
    <span style="font-size: 0.75rem; opacity: 0.7;">en-US — MM/DD/YYYY</span>
    <InputDatetime
      id="astro-demo-date-us"
      locale="en-US"
      value={seed}
      fullWidth={false}
    />
  </label>
  <label class={stack({ gap: "2xs" })}>
    <span style="font-size: 0.75rem; opacity: 0.7;">en-GB — DD/MM/YYYY</span>
    <InputDatetime
      id="astro-demo-date-gb"
      locale="en-GB"
      value={seed}
      fullWidth={false}
    />
  </label>
  <label class={stack({ gap: "2xs" })}>
    <span style="font-size: 0.75rem; opacity: 0.7;">pt-BR — DD/MM/YYYY</span>
    <InputDatetime
      id="astro-demo-date-br"
      locale="pt-BR"
      value={seed}
      fullWidth={false}
    />
  </label>
</div>

Required

required surfaces an error when the field is left empty on blur — data-error flips the input to the danger palette and emits onErrorChange (Svelte) / pindoba:invalid (Astro).

MMDDYYYY
---
import InputDatetime from "../InputDatetime.astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "md" })}>
  <InputDatetime id="astro-demo-required" required />
</div>

Disabled

disabled greys the field, drops the segments out of the tab order, and ignores all editing — in both the Svelte and Astro (DOM-attach) paths.

05142026
---
import InputDatetime from "../InputDatetime.astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "md" })}>
  <InputDatetime
    id="astro-demo-disabled"
    disabled
    value={new Date(2026, 4, 14)}
  />
</div>

Form submission

The component renders hidden <input>(s), so it submits inside a native <form> like any other control — name is the field key (range adds ${name}-end).

05142026

submitted: (nothing yet)

---
import InputDatetime from "../InputDatetime.astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<form id="astro-demo-form" class={stack({ gap: "md" })}>
  <InputDatetime
    id="astro-demo-form-input"
    name="appointment"
    value={new Date(2026, 4, 14)}
  />
  <button
    type="submit"
    style="align-self: flex-start; padding: 0.25rem 0.75rem; font-size: 0.875rem;"
  >
    Submit
  </button>
  <p
    data-submitted
    style="font-family: monospace; font-size: 0.75rem; opacity: 0.7;"
  >
    submitted: (nothing yet)
  </p>
</form>

<script>
  function boot() {
    document
      .querySelectorAll<HTMLFormElement>("#astro-demo-form")
      .forEach((form) => {
        if (form.dataset.bound) return;
        form.dataset.bound = "true";
        form.addEventListener("submit", (event) => {
          event.preventDefault();
          const data = new FormData(form);
          const out = form.querySelector("[data-submitted]");
          if (out) {
            out.textContent = `submitted: ${String(data.get("appointment") ?? "")}`;
          }
        });
      });
  }
  boot();
  document.addEventListener("astro:page-load", boot);
</script>
props · 23 shown · 23 total
ariaLabel
string

Accessible label for the group. Default derived from granularity.

calendar
boolean

Show a trailing calendar-icon trigger that opens a date-picker popover. Only takes effect for `granularity` `"date"` / `"datetime"` (never for `"time"`). Default `false`. Opt in to add the picker to a typed field.

class
string

Optional class string forwarded to the root element.

defaultValue svelte
DatenullDateRange

Initial uncontrolled value (used only to seed the controller).

disabled
boolean

Disabled flag — blocks editing and dims the input.

error svelte
InputDatetimeErrornull

Surfaced error kind. Bindable.

fullWidth
boolean
default true

Whether the input stretches to fill its container. Default: true.

granularity
"date""time""datetime"

Which units to edit. Default: `"date"`.

id
string

id forwarded to the root element.

is24Hour
boolean

Force 24-hour (no AM/PM) or 12-hour mode for the time portion. When omitted, the locale's default hour cycle decides.

locale
string

Locale tag driving date segment order, separators, and placeholders.

maxValue
Date

Latest allowed value (inclusive). Same semantics as `minValue`, at the other end of the window.

minValue
Date

Earliest allowed value (inclusive). Disables earlier days in the calendar popover AND validates typed input — a complete value below it surfaces the `out-of-range` error on blur (typing itself stays free). Compared at the input's granularity: date-only by Y/M/D, datetime to the minute, time by wall-clock time-of-day.

name
string

`name` for the hidden form input (range emits `name` + `name-end`).

onChange svelte
(value: Date | null | DateRange) => void

Fires when the value changes.

onErrorChange svelte
(error: InputDatetimeError | null) => void

Fires whenever the error transitions.

passThrough
InputDatetimePassThrough<RootEl>

Per-slot style/props passthrough.

range
boolean

Render a start/end range (value becomes a {@link DateRange}).

rangeSeparator
string

Separator joining the two range groups. Default `"–"`.

required
boolean

Treat an empty input as an error on blur.

separator
string

Override the locale-derived separator between date parts.

size
"sm""md""lg""xs"
default md

Visual size — drives the field height + density.

value svelte
DatenullDateRange

Currently selected value. `Date | null` for single inputs, `DateRange` when `range` is set. Bindable.

Type

  • Components
  • Blocks