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.
"surface.ground"Background surface step for the field.
"default"Resting border variant. The line always occupies its 1px of layout, so swapping its color on focus/error never shifts content.
Border variant applied only on hover/press, composed over `border`.
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.
"tertiary"Surface emphasis ramp for the field's own panel. `tertiary` (the default) paints the neutral surface ramp regardless of `feedback`; `secondary` follows the feedback palette; `primary` fills with the accent surface.
Surfaced error kind. Bindable.
"neutral"Semantic feedback palette for the field's surface. Independent of the transient `data-error` state, which always flips the field to `danger`.
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.
Corner radius override. Without it the corner follows the size tier (`control.<size>`). `inner` renders a concentric corner computed from the nearest parent panel (parent radius − parent padding, floored at `xs`).
Border radius for the bottom-left and bottom-right corners.
Border radius for the top-left and bottom-left corners.
Border radius for the top-right and bottom-right corners.
Border radius for the top-left and top-right corners. Overrides `radius` on those corners only (flush-edge chains).
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.
"none"Drop shadow for the field's surface.
mdVisual size — drives the field height + density.
falseBlur + saturate whatever sits behind the field (for fields floating over content).
Currently selected value. `Date | null` for single inputs, `DateRange` when `range` is set. Bindable.