Date Field
Free · MITHeadless, accessible, and styled entirely through the attribute surface below — bring your own CSS system, or start from the reference theme.
Install
Without a flag you get no styling at all — plain HTML carrying the attribute surface below. --theme copies the same theme in two dialects;--bridge lumos re-points either one onto Lumos for Astro's tokens (it needs a theme to re-point, and Lumos itself, already in your project).
Unstyled — behavior only, you write the CSS
npx astrocraft-ui add forms/date-fieldPlain-CSS theme — no build step
npx astrocraft-ui add forms/date-field --theme cssTailwind theme — needs Tailwind v4
npx astrocraft-ui add forms/date-field --theme tailwindPlain-CSS theme, re-pointed onto Lumos tokens
npx astrocraft-ui add forms/date-field --theme css --bridge lumosTailwind theme, re-pointed onto Lumos tokens
npx astrocraft-ui add forms/date-field --theme tailwind --bridge lumosLive demo
DateField, TimeField & TimePicker
Segmented fields: each part is a spinbutton with its own label. Type 4 in the day and it commits and moves on; type 1 and it waits, because the 11th–19th are still reachable. ↑/↓ step and wrap, ←/→ walk,Backspace clears. The two date fields differ only in locale — and therefore in the ORDER of their segments, which comes from Intl, not from us.
- 8:00 AM
- 8:30 AM
- 9:00 AM
- 9:30 AM
- 10:00 AM
- 10:30 AM
- 11:00 AM
- 11:30 AM
- 12:00 PM
- 12:30 PM
- 1:00 PM
- 1:30 PM
- 2:00 PM
- 2:30 PM
- 3:00 PM
- 3:30 PM
- 4:00 PM
- 4:30 PM
- 5:00 PM
- 5:30 PM
- 6:00 PM
Attribute surface
With no class-merging machinery, these attributes are the whole extension point. State is a data-attribute or a native attribute; visibility is the hiddenattribute. Generated from source by pnpm slots.
| Component | Slots | Variants | Runtime state | Native state |
|---|---|---|---|---|
| `DateField.astro` | `date-field` `date-field-literal` `date-field-value` | `data-size`: `sm` · `md` · `lg` `data-state`: `default` · `error` · `success` | — | — |
| `DateSegment.astro` | `date-segment` | — | — | — |
Source
What the command copies — 3 files, alias-free so the copy lands standing alone. This is the whole component, shown without its comments; the files you install keep them, with the reasoning behind every rule.
---
import type { HTMLAttributes } from "astro/types";
import {
fieldParts,
padSegment,
SEGMENT_LABELS,
SEGMENT_PLACEHOLDERS,
segmentRange,
type SegmentType,
} from "../../_segments";
import DateSegment from "./DateSegment.astro";
type Props = HTMLAttributes<"div"> & {
value?: string;
name?: string;
label?: string;
locale?: string;
size?: "sm" | "md" | "lg";
state?: "default" | "error" | "success";
labels?: Partial<Record<SegmentType, string>>;
placeholders?: Partial<Record<SegmentType, string>>;
};
const {
value = "",
name,
label = "Date",
locale,
size = "md",
state = "default",
labels,
placeholders,
class: className,
...rest
} = Astro.props;
const [year, month, day] = /^\d{4}-\d{2}-\d{2}$/.test(value)
? value.split("-").map(Number)
: [undefined, undefined, undefined];
const numbers: Partial<Record<SegmentType, number>> = { year, month, day };
---
<div
role="group"
aria-label={label}
data-slot="date-field"
data-value={value}
data-size={size}
data-state={state}
class={className}
{...rest}
>
{
fieldParts(locale, "date").map((part) =>
part.type === "literal" ? (
<span data-slot="date-field-literal" aria-hidden="true">
{part.value}
</span>
) : (
<DateSegment
segment={part.type}
label={labels?.[part.type] ?? SEGMENT_LABELS[part.type]}
placeholder={placeholders?.[part.type] ?? SEGMENT_PLACEHOLDERS[part.type]}
min={segmentRange(part.type).min}
max={segmentRange(part.type, { year, month }).max}
value={numbers[part.type]}
text={
numbers[part.type] === undefined
? undefined
: padSegment(part.type, numbers[part.type]!)
}
/>
),
)
}
{name && <input type="hidden" name={name} value={value} data-slot="date-field-value" />}
</div>
<script>
import { onReadyOnce } from "../../_once";
import { type SegmentValues, wireSegmentField } from "../../_segments";
function wire(root: HTMLElement) {
wireSegmentField(root, {
compose: ({ year, month, day }: SegmentValues) =>
year && month && day
? `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`
: "",
});
}
onReadyOnce('[data-slot="date-field"]', wire);
</script>
---
import type { HTMLAttributes } from "astro/types";
import type { SegmentType } from "../../_segments";
type Props = HTMLAttributes<"span"> & {
segment: SegmentType;
label: string;
placeholder: string;
min: number;
max: number;
value?: number;
text?: string;
am?: string;
pm?: string;
};
const {
segment,
label,
placeholder,
min,
max,
value,
text,
am,
pm,
class: className,
...rest
} = Astro.props;
---
<span
role="spinbutton"
tabindex="0"
inputmode="numeric"
aria-label={label}
aria-valuemin={min}
aria-valuemax={max}
aria-valuenow={value}
aria-valuetext={value === undefined ? undefined : text}
data-slot="date-segment"
data-segment={segment}
data-placeholder={placeholder}
data-am={am}
data-pm={pm}
class={className}
{...rest}>{value === undefined ? placeholder : text}</span
>
import DateField from "./DateField.astro";
import DateSegment from "./DateSegment.astro";
export { DateField, DateSegment };
export default DateField;
What you get
The component source, copied into your project by npx astrocraft-ui add forms/date-field — no package dependency, no CSS to fight. Whatever the paid blocks compose, this is it.