Skip to main content
astrocraft-ui/ components · 101

Date Field

Free · MIT

Headless, 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

Install command
npx astrocraft-ui add forms/date-field

Plain-CSS theme — no build step

Install command
npx astrocraft-ui add forms/date-field --theme css

Tailwind theme — needs Tailwind v4

Install command
npx astrocraft-ui add forms/date-field --theme tailwind

Plain-CSS theme, re-pointed onto Lumos tokens

Install command
npx astrocraft-ui add forms/date-field --theme css --bridge lumos

Tailwind theme, re-pointed onto Lumos tokens

Install command
npx astrocraft-ui add forms/date-field --theme tailwind --bridge lumos

Live 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.

11092026
09112026
0304PM
1504

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.

ComponentSlotsVariantsRuntime stateNative 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.

src/components/ui/forms/date-field/DateField.astro
---
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>
src/components/ui/forms/date-field/DateSegment.astro
---
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
>
src/components/ui/forms/date-field/index.ts
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.