component
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.
---
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>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>granularity="datetime" composes the date and time segments into one input.
---
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 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.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
---
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>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.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
---
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>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.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
---
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 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.
---
import InputDatetime from "../InputDatetime.astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "md" })}>
<InputDatetime id="astro-demo-range" range />
</div>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 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).
---
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 greys the field, drops the segments out of the tab order, and ignores
all editing — in both the Svelte and Astro (DOM-attach) paths.
---
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>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).
---
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>Accessible label for the group. Default derived from granularity.
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.
Optional class string forwarded to the root element.
Initial uncontrolled value (used only to seed the controller).
Disabled flag — blocks editing and dims the input.
Surfaced error kind. Bindable.
trueWhether the input stretches to fill its container. Default: true.
Which units to edit. Default: `"date"`.
id forwarded to the root element.
Force 24-hour (no AM/PM) or 12-hour mode for the time portion. When omitted, the locale's default hour cycle decides.
Locale tag driving date segment order, separators, and placeholders.
Latest allowed value (inclusive). Same semantics as `minValue`, at the other end of the window.
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` for the hidden form input (range emits `name` + `name-end`).
Fires when the value changes.
Fires whenever the error transitions.
Per-slot style/props passthrough.
Render a start/end range (value becomes a {@link DateRange}).
Separator joining the two range groups. Default `"–"`.
Treat an empty input as an error on blur.
Override the locale-derived separator between date parts.
mdVisual size — drives the field height + density.
Currently selected value. `Date | null` for single inputs, `DateRange` when `range` is set. Bindable.