shshadcn-htmx

Components

Date Time Picker

A family of native <input type="date | time | datetime-local | month | week"> fields with shadcn polish. The browser supplies the calendar / clock picker, segment editing and min/max/step validation — no JS calendar library. Whatever the user's locale, the submitted value is always normalised and machine-readable.

Installation

One file per stack. Use the shadcn CLI for JSX, or copy the source for your template engine. There is no JavaScript to wire up.

1. Install via the shadcn CLI

npx shadcn@latest add http://localhost/r/date-time-picker.json

2. Use it

components/ui/date-time-picker.tsx
import { DateTimePicker } from "@/components/ui/date-time-picker"

<DateTimePicker name="bday" type="date" min="1900-01-01" ariaLabel="Date of birth" />
Or copy the source manually
components/ui/date-time-picker.tsx
/** @jsxImportSource hono/jsx */
import { cn, type ClassValue } from "@/registry/lib/cn"

// Date Time Picker — shadcn-htmx, htmx v4 + Tailwind v4.
//
// A family of native temporal fields. The control IS a real
// <input type="date|time|datetime-local|month|week"> — there is no JS calendar
// library and no userland popup. The browser ships the picker UI, the keyboard
// editing of each segment, locale-aware display, and constraint validation; we
// only restyle to match the rest of the form controls.
//
// Per MDN every variant normalises its submitted value to a fixed,
// machine-readable, locale-independent format regardless of how it is shown:
//   - date            → "yyyy-mm-dd"            step is days  (default 1)
//   - time            → "HH:mm" / "HH:mm:ss"    step is seconds (default 60);
//                        min/max have a *periodic domain* (may cross midnight)
//   - datetime-local  → "yyyy-mm-ddTHH:mm"      (a local date + time, no zone)
//   - month           → "YYYY-MM"
//   - week            → "yyyy-Www"              (ISO 8601 week number)
//
// Sources read for this component:
//   repos/mdn/files/en-us/web/html/reference/elements/input/date/index.md
//   repos/mdn/files/en-us/web/html/reference/elements/input/time/index.md
//   repos/mdn/files/en-us/web/html/reference/elements/input/datetime-local/index.md
//   repos/mdn/files/en-us/web/html/reference/elements/input/month/index.md
//   repos/mdn/files/en-us/web/html/reference/elements/input/week/index.md
//   htmx attrs verified against repos/htmx/www/reference.md
// Style analogue: registry/ui/input.tsx (same base + tokens, kept in sync).

export type DateTimeType = "date" | "time" | "datetime-local" | "month" | "week"

// Shared <Input> base — kept byte-for-byte in sync with registry/ui/input.tsx
// so a temporal field looks identical to every other control. We append rules
// to tame the engine-specific picker affordances:
//   - the calendar/clock indicator inherits the foreground colour and shows a
//     pointer cursor (it is the only chrome the browser exposes to CSS);
//   - the inner spin/edit fields drop their padding so the value sits flush.
const base =
  "flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none " +
  "selection:bg-primary selection:text-primary-foreground " +
  "placeholder:text-muted-foreground " +
  "disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 " +
  "md:text-sm dark:bg-input/30 " +
  "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 " +
  "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 " +
  // htmx-request: dim while a request triggered by this field is in flight.
  "[&.htmx-request]:opacity-70 " +
  // Style the native calendar/clock picker indicator (Chromium only exposes
  // this pseudo-element). Tint it to the foreground + show it is clickable.
  "[&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:opacity-60 [&::-webkit-calendar-picker-indicator]:hover:opacity-100 dark:[&::-webkit-calendar-picker-indicator]:invert " +
  "[&::-webkit-datetime-edit]:px-0 [&::-webkit-datetime-edit-fields-wrapper]:px-0"

export function dateTimePickerClasses(opts?: { class?: ClassValue }): string {
  return cn(base, opts?.class)
}

type DateTimePickerProps = {
  // Which native temporal control to render. Drives the picker UI, the value
  // format and the meaning of min/max/step.
  type?: DateTimeType

  id?: string
  name?: string
  // Initial value, in the variant's normalised format (e.g. "2026-06-02").
  value?: string
  required?: boolean
  disabled?: boolean
  readonly?: boolean
  autofocus?: boolean
  form?: string
  // Id of a <datalist> of suggested values.
  list?: string

  // Temporal constraints, enforced natively by the browser. Each must be a
  // string in the same normalised format as the value (e.g. min="09:00").
  min?: string
  max?: string
  // Granularity. date: days (default 1); time/datetime-local: seconds
  // (default 60 → minutes, "1" → seconds); month: months; week: weeks.
  // "any" removes stepping.
  step?: number | string

  // ARIA / labelling. A native input already exposes its value to AT; pair it
  // with a <label for> or supply an accessible name here.
  ariaLabel?: string
  ariaLabelledby?: string
  ariaDescribedby?: string
  ariaInvalid?: boolean | "grammar" | "spelling"
  ariaRequired?: boolean

  class?: ClassValue

  // htmx v4 passthrough — fires on the input's native change event by default
  // (the picker dispatches it on selection). Use hx-trigger to override.
  // See repos/htmx/www/reference.md.
  [key: `hx-${string}`]: any
  [key: `data-${string}`]: any
  [key: `aria-${string}`]: any
}

export function DateTimePicker(props: DateTimePickerProps) {
  const {
    type = "date",
    class: className,
    ariaLabel,
    ariaLabelledby,
    ariaDescribedby,
    ariaInvalid,
    ariaRequired,
    ...rest
  } = props

  return (
    <input
      type={type}
      class={dateTimePickerClasses({ class: className })}
      aria-label={ariaLabel}
      aria-labelledby={ariaLabelledby}
      aria-describedby={ariaDescribedby}
      aria-invalid={ariaInvalid === undefined ? undefined : String(ariaInvalid)}
      aria-required={ariaRequired === undefined ? undefined : String(ariaRequired)}
      data-slot="date-time-picker"
      {...rest}
    />
  )
}

1. Save the file

Copy date-time-picker.html into templates/components/.

2. Use it

templates/components/date-time-picker.html
{% from "components/date-time-picker.html" import date_time_picker %}

{{ date_time_picker(name="bday", type="date", min="1900-01-01", aria_label="Date of birth") }}
View source
templates/components/date-time-picker.html
{# Date Time Picker macro — shadcn-htmx, htmx v4 + Tailwind v4.
   Mirrors registry/ui/date-time-picker.tsx for Python/Flask/FastAPI/Django/Jinja2.

   The control is a native <input type="date|time|datetime-local|month|week">.
   The browser ships the picker UI, segment editing, locale-aware display and
   constraint validation; the submitted value is always normalised
   (date → yyyy-mm-dd, time → HH:mm[:ss], month → YYYY-MM, week → yyyy-Www).
   No JS — this is a zero-script component.

   Usage:
       {% from "components/date-time-picker.html" import date_time_picker %}
       {{ date_time_picker(name="bday", type="date", min="1900-01-01") }}
       {{ date_time_picker(name="slot", type="time", min="09:00", max="18:00", step=900) }}

   All hx-* attributes pass through via **attrs (underscores become dashes).
   See repos/mdn/files/en-us/web/html/reference/elements/input/ for native
   attribute semantics. #}

{% macro date_time_picker(
    type="date",
    id=none,
    name=none,
    value=none,
    required=false,
    disabled=false,
    readonly=false,
    autofocus=false,
    form=none,
    list=none,
    min=none,
    max=none,
    step=none,
    aria_label=none,
    aria_labelledby=none,
    aria_describedby=none,
    aria_invalid=none,
    aria_required=none,
    extra_class="",
    **attrs
) %}
{%- set base -%}
flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&.htmx-request]:opacity-70 [&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:opacity-60 [&::-webkit-calendar-picker-indicator]:hover:opacity-100 dark:[&::-webkit-calendar-picker-indicator]:invert [&::-webkit-datetime-edit]:px-0 [&::-webkit-datetime-edit-fields-wrapper]:px-0
{%- endset -%}
<input type="{{ type }}"
       class="{{ base }} {{ extra_class }}"
       {%- if id %} id="{{ id }}"{% endif %}
       {%- if name %} name="{{ name }}"{% endif %}
       {%- if value is not none %} value="{{ value }}"{% endif %}
       {%- if required %} required{% endif %}
       {%- if disabled %} disabled{% endif %}
       {%- if readonly %} readonly{% endif %}
       {%- if autofocus %} autofocus{% endif %}
       {%- if form %} form="{{ form }}"{% endif %}
       {%- if list %} list="{{ list }}"{% endif %}
       {%- if min is not none %} min="{{ min }}"{% endif %}
       {%- if max is not none %} max="{{ max }}"{% endif %}
       {%- if step is not none %} step="{{ step }}"{% endif %}
       {%- if aria_label %} aria-label="{{ aria_label }}"{% endif %}
       {%- if aria_labelledby %} aria-labelledby="{{ aria_labelledby }}"{% endif %}
       {%- if aria_describedby %} aria-describedby="{{ aria_describedby }}"{% endif %}
       {%- if aria_invalid is not none %} aria-invalid="{{ aria_invalid|string|lower }}"{% endif %}
       {%- if aria_required is not none %} aria-required="{{ aria_required|string|lower }}"{% endif %}
       data-slot="date-time-picker"
       {%- for k, v in attrs.items() %} {{ k|replace('_', '-') }}="{{ v }}"{% endfor -%}
>
{% endmacro %}

1. Save the file

Add date-time-picker.tmpl alongside your templates.

2. Use it

components/date-time-picker.tmpl
tpl.ExecuteTemplate(w, "date-time-picker", map[string]any{
    "Name": "bday", "Type": "date", "Min": "1900-01-01",
    "AriaLabel": "Date of birth",
})
View source
components/date-time-picker.tmpl
{{/*
  Date Time Picker template — shadcn-htmx, htmx v4 + Tailwind v4.
  Mirrors registry/ui/date-time-picker.tsx for Go projects using html/template.

  The control is a native <input type="date|time|datetime-local|month|week">.
  The browser ships the picker UI, segment editing, locale-aware display and
  constraint validation; the submitted value is always normalised
  (date → yyyy-mm-dd, time → HH:mm[:ss], month → YYYY-MM, week → yyyy-Www).
  No JS — this is a zero-script component.

  Args (via dict):
      Type    string  // date | time | datetime-local | month | week
      ID, Name, Value string
      Required, Disabled, Readonly, Autofocus bool
      Form, List      string
      Min, Max, Step  string  // temporal constraints, in the value's format
      AriaLabel, AriaLabelledby, AriaDescribedby string
      AriaInvalid, AriaRequired string  // "true" | "false"
      Attrs   map[string]string // hx-*, data-*, …

      {{template "date-time-picker" (dict "Name" "bday" "Type" "date" "Min" "1900-01-01")}}

  See repos/mdn/files/en-us/web/html/reference/elements/input/ for native
  attribute semantics.
*/}}

{{define "date-time-picker"}}
{{- $type := or .Type "date" -}}
{{- $base := "flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&.htmx-request]:opacity-70 [&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:opacity-60 [&::-webkit-calendar-picker-indicator]:hover:opacity-100 dark:[&::-webkit-calendar-picker-indicator]:invert [&::-webkit-datetime-edit]:px-0 [&::-webkit-datetime-edit-fields-wrapper]:px-0" -}}
<input type="{{$type}}"
       class="{{$base}}"
       {{- if .ID}} id="{{.ID}}"{{end}}
       {{- if .Name}} name="{{.Name}}"{{end}}
       {{- if .Value}} value="{{.Value}}"{{end}}
       {{- if .Required}} required{{end}}
       {{- if .Disabled}} disabled{{end}}
       {{- if .Readonly}} readonly{{end}}
       {{- if .Autofocus}} autofocus{{end}}
       {{- if .Form}} form="{{.Form}}"{{end}}
       {{- if .List}} list="{{.List}}"{{end}}
       {{- if .Min}} min="{{.Min}}"{{end}}
       {{- if .Max}} max="{{.Max}}"{{end}}
       {{- if .Step}} step="{{.Step}}"{{end}}
       {{- if .AriaLabel}} aria-label="{{.AriaLabel}}"{{end}}
       {{- if .AriaLabelledby}} aria-labelledby="{{.AriaLabelledby}}"{{end}}
       {{- if .AriaDescribedby}} aria-describedby="{{.AriaDescribedby}}"{{end}}
       {{- if .AriaInvalid}} aria-invalid="{{.AriaInvalid}}"{{end}}
       {{- if .AriaRequired}} aria-required="{{.AriaRequired}}"{{end}}
       data-slot="date-time-picker"
       {{- range $k, $v := .Attrs}} {{$k}}="{{$v}}"{{end -}}
>
{{end}}

1. Save the file

Drop date_time_picker.ex into lib/my_app_web/components/.

2. Use it

lib/my_app_web/components/date_time_picker.ex
<.date_time_picker name="bday" type="date" min="1900-01-01" aria-label="Date of birth" />
View source
lib/my_app_web/components/date_time_picker.ex
defmodule ShadcnHtmx.Components.DateTimePicker do
  @moduledoc """
  Date Time Picker — shadcn-htmx, htmx v4 + Tailwind v4 for Phoenix.

  Mirrors registry/ui/date-time-picker.tsx. The control is a native
  `<input type="date|time|datetime-local|month|week">`, so the picker UI,
  segment editing, locale-aware display and constraint validation all come from
  the platform. The submitted value is always normalised (date → yyyy-mm-dd,
  time → HH:mm[:ss], month → YYYY-MM, week → yyyy-Www). No JS.

  Works with plain HEEx and LiveView forms; htmx attributes and any other input
  attribute pass through via `:rest`.

  ## Examples

      <.date_time_picker name="bday" type="date" min="1900-01-01" />
      <.date_time_picker name="slot" type="time" min="09:00" max="18:00" step="900" />

  See repos/mdn/files/en-us/web/html/reference/elements/input/ for native
  attribute semantics.
  """

  use Phoenix.Component

  @base "flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs " <>
          "transition-[color,box-shadow] outline-none " <>
          "selection:bg-primary selection:text-primary-foreground " <>
          "placeholder:text-muted-foreground " <>
          "disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 " <>
          "md:text-sm dark:bg-input/30 " <>
          "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 " <>
          "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 " <>
          "[&.htmx-request]:opacity-70 " <>
          "[&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:opacity-60 [&::-webkit-calendar-picker-indicator]:hover:opacity-100 dark:[&::-webkit-calendar-picker-indicator]:invert " <>
          "[&::-webkit-datetime-edit]:px-0 [&::-webkit-datetime-edit-fields-wrapper]:px-0"

  attr :type, :string,
    default: "date",
    values: ~w(date time datetime-local month week)

  attr :class, :string, default: nil

  attr :rest, :global,
    include:
      ~w(hx-get hx-post hx-put hx-patch hx-delete hx-target hx-swap hx-trigger hx-indicator hx-vals hx-include hx-disable
         id name value required disabled readonly autofocus form list
         min max step
         aria-label aria-labelledby aria-describedby aria-invalid aria-required)

  def date_time_picker(assigns) do
    assigns = assign(assigns, :base_class, @base)

    ~H"""
    <input
      type={@type}
      class={[@base_class, @class]}
      data-slot="date-time-picker"
      {@rest}
    />
    """
  end
end

1. Save the file

Paste the markup; relies only on theme tokens. No script needed.

2. Use it

snippets/date-time-picker.html
<input type="date" name="bday" min="1900-01-01" data-slot="date-time-picker"
       class="flex h-9 w-full min-w-0 rounded-md border border-input … focus-visible:ring-ring/50">
View source
snippets/date-time-picker.html
<!--
  shadcn-htmx — raw Date Time Picker snippets.

  Mirrors registry/ui/date-time-picker.tsx. Drop onto any page that loads
  Tailwind CSS v4 and the shadcn theme variables (background, foreground, input,
  ring, destructive, primary). See app/styles/input.css for the defaults.

  The control is a native <input type="date|time|datetime-local|month|week">.
  The browser ships the picker UI, segment editing, locale-aware display and
  constraint validation (min/max/step/required). The submitted value is always
  normalised regardless of how it is displayed:
    date           → yyyy-mm-dd
    time           → HH:mm or HH:mm:ss
    datetime-local → yyyy-mm-ddTHH:mm
    month          → YYYY-MM
    week           → yyyy-Www  (ISO 8601 week number)
  No JavaScript — this is a zero-script component.

  BASE (shared by every variant):
    flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent
    px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none
    selection:bg-primary selection:text-primary-foreground
    placeholder:text-muted-foreground
    disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50
    md:text-sm dark:bg-input/30
    focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50
    aria-invalid:border-destructive aria-invalid:ring-destructive/20
    dark:aria-invalid:ring-destructive/40 [&.htmx-request]:opacity-70
    [&::-webkit-calendar-picker-indicator]:cursor-pointer
    [&::-webkit-calendar-picker-indicator]:opacity-60
    [&::-webkit-calendar-picker-indicator]:hover:opacity-100
    dark:[&::-webkit-calendar-picker-indicator]:invert
    [&::-webkit-datetime-edit]:px-0
    [&::-webkit-datetime-edit-fields-wrapper]:px-0
-->

<!-- ─── Date — value normalises to yyyy-mm-dd ───────────────────────── -->
<input type="date" name="bday" min="1900-01-01" max="2026-12-31" data-slot="date-time-picker"
  class="flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&.htmx-request]:opacity-70 [&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:opacity-60 [&::-webkit-calendar-picker-indicator]:hover:opacity-100 dark:[&::-webkit-calendar-picker-indicator]:invert [&::-webkit-datetime-edit]:px-0 [&::-webkit-datetime-edit-fields-wrapper]:px-0">

<!-- ─── Time — step="900" snaps to 15-minute slots (seconds units) ──── -->
<input type="time" name="slot" min="09:00" max="18:00" step="900" required data-slot="date-time-picker"
  class="flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&.htmx-request]:opacity-70 [&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:opacity-60 [&::-webkit-calendar-picker-indicator]:hover:opacity-100 dark:[&::-webkit-calendar-picker-indicator]:invert [&::-webkit-datetime-edit]:px-0 [&::-webkit-datetime-edit-fields-wrapper]:px-0">

<!-- ─── Datetime-local — value normalises to yyyy-mm-ddTHH:mm ───────── -->
<input type="datetime-local" name="start" data-slot="date-time-picker"
  class="flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&.htmx-request]:opacity-70 [&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:opacity-60 [&::-webkit-calendar-picker-indicator]:hover:opacity-100 dark:[&::-webkit-calendar-picker-indicator]:invert [&::-webkit-datetime-edit]:px-0 [&::-webkit-datetime-edit-fields-wrapper]:px-0">

<!-- ─── Month — value normalises to YYYY-MM ─────────────────────────── -->
<input type="month" name="period" data-slot="date-time-picker"
  class="flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&.htmx-request]:opacity-70 [&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:opacity-60 [&::-webkit-calendar-picker-indicator]:hover:opacity-100 dark:[&::-webkit-calendar-picker-indicator]:invert [&::-webkit-datetime-edit]:px-0 [&::-webkit-datetime-edit-fields-wrapper]:px-0">

<!-- ─── Week — value normalises to yyyy-Www (ISO 8601) ──────────────── -->
<input type="week" name="reporting-week" data-slot="date-time-picker"
  class="flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&.htmx-request]:opacity-70 [&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:opacity-60 [&::-webkit-calendar-picker-indicator]:hover:opacity-100 dark:[&::-webkit-calendar-picker-indicator]:invert [&::-webkit-datetime-edit]:px-0 [&::-webkit-datetime-edit-fields-wrapper]:px-0">

<!-- ─── Invalid — pair with an error message via aria-describedby ───── -->
<div>
  <input type="date" name="bday" aria-invalid="true" aria-describedby="bday-error" data-slot="date-time-picker"
    class="flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:opacity-60 [&::-webkit-calendar-picker-indicator]:hover:opacity-100 dark:[&::-webkit-calendar-picker-indicator]:invert">
  <p id="bday-error" class="mt-1 text-sm text-destructive">Choose a date in the past.</p>
</div>

<!-- ─── Disabled — also removes from tab order ──────────────────────── -->
<input type="date" value="2026-06-02" disabled data-slot="date-time-picker"
  class="flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30">

Examples

Date with min / max

A native date field bounded to a range. The browser enforces min/max and normalises the submitted value to yyyy-mm-dd regardless of the user's locale display.

The displayed format follows the user's browser locale, but the value posted to the server is always yyyy-mm-dd. Setting min and max greys out the out-of-range days in the picker and fails constraint validation on submit — no JS required.

<DateTimePicker name="trip-start" type="date"
            value="2026-07-22" min="2026-01-01" max="2026-12-31"
            ariaLabel="Trip start" />
{{ date_time_picker(name="trip-start", type="date",
                value="2026-07-22", min="2026-01-01", max="2026-12-31",
                aria_label="Trip start") }}
{{template "date-time-picker" (dict
  "Name" "trip-start" "Type" "date" "Value" "2026-07-22"
  "Min" "2026-01-01" "Max" "2026-12-31" "AriaLabel" "Trip start")}}
<.date_time_picker name="trip-start" type="date"
              value="2026-07-22" min="2026-01-01" max="2026-12-31"
              aria-label="Trip start" />
<div class="grid w-full max-w-xs gap-2">
  <label class="text-xs font-medium" for="ex-basic-date">Trip start</label>
  <input type="date" class="flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&amp;.htmx-request]:opacity-70 [&amp;::-webkit-calendar-picker-indicator]:cursor-pointer [&amp;::-webkit-calendar-picker-indicator]:opacity-60 [&amp;::-webkit-calendar-picker-indicator]:hover:opacity-100 dark:[&amp;::-webkit-calendar-picker-indicator]:invert [&amp;::-webkit-datetime-edit]:px-0 [&amp;::-webkit-datetime-edit-fields-wrapper]:px-0" data-slot="date-time-picker" id="ex-basic-date" name="trip-start" value="2026-07-22" min="2026-01-01" max="2026-12-31"/>
</div>

All five types

One component, five native controls. Each posts a different normalised value format and offers the matching picker UI.

time uses step in seconds (so step="1" reveals a seconds segment), its min/max can even cross midnight (a periodic domain), while week posts an ISO-8601 week string like 2026-W23.

<DateTimePicker type="time" name="slot" min="09:00" max="18:00" step="900" />
<DateTimePicker type="datetime-local" name="starts" />
<DateTimePicker type="month" name="period" />
<DateTimePicker type="week" name="reporting-week" />
{{ date_time_picker(type="time", name="slot", min="09:00", max="18:00", step=900) }}
{{ date_time_picker(type="datetime-local", name="starts") }}
{{ date_time_picker(type="month", name="period") }}
{{ date_time_picker(type="week", name="reporting-week") }}
{{template "date-time-picker" (dict "Type" "time" "Name" "slot" "Min" "09:00" "Max" "18:00" "Step" "900")}}
{{template "date-time-picker" (dict "Type" "datetime-local" "Name" "starts")}}
{{template "date-time-picker" (dict "Type" "month" "Name" "period")}}
{{template "date-time-picker" (dict "Type" "week" "Name" "reporting-week")}}
<.date_time_picker type="time" name="slot" min="09:00" max="18:00" step="900" />
<.date_time_picker type="datetime-local" name="starts" />
<.date_time_picker type="month" name="period" />
<.date_time_picker type="week" name="reporting-week" />
<div class="grid w-full max-w-sm gap-4">
  <div class="grid gap-2">
    <label class="text-xs font-medium" for="ex-t-time">Time (15-min slots)</label>
    <input type="time" class="flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&amp;.htmx-request]:opacity-70 [&amp;::-webkit-calendar-picker-indicator]:cursor-pointer [&amp;::-webkit-calendar-picker-indicator]:opacity-60 [&amp;::-webkit-calendar-picker-indicator]:hover:opacity-100 dark:[&amp;::-webkit-calendar-picker-indicator]:invert [&amp;::-webkit-datetime-edit]:px-0 [&amp;::-webkit-datetime-edit-fields-wrapper]:px-0" data-slot="date-time-picker" id="ex-t-time" name="slot" min="09:00" max="18:00" step="900" value="09:30"/>
  </div>
  <div class="grid gap-2">
    <label class="text-xs font-medium" for="ex-t-dtl">Starts at</label>
    <input type="datetime-local" class="flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&amp;.htmx-request]:opacity-70 [&amp;::-webkit-calendar-picker-indicator]:cursor-pointer [&amp;::-webkit-calendar-picker-indicator]:opacity-60 [&amp;::-webkit-calendar-picker-indicator]:hover:opacity-100 dark:[&amp;::-webkit-calendar-picker-indicator]:invert [&amp;::-webkit-datetime-edit]:px-0 [&amp;::-webkit-datetime-edit-fields-wrapper]:px-0" data-slot="date-time-picker" id="ex-t-dtl" name="starts" value="2026-06-02T14:00"/>
  </div>
  <div class="grid gap-2">
    <label class="text-xs font-medium" for="ex-t-month">Billing month</label>
    <input type="month" class="flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&amp;.htmx-request]:opacity-70 [&amp;::-webkit-calendar-picker-indicator]:cursor-pointer [&amp;::-webkit-calendar-picker-indicator]:opacity-60 [&amp;::-webkit-calendar-picker-indicator]:hover:opacity-100 dark:[&amp;::-webkit-calendar-picker-indicator]:invert [&amp;::-webkit-datetime-edit]:px-0 [&amp;::-webkit-datetime-edit-fields-wrapper]:px-0" data-slot="date-time-picker" id="ex-t-month" name="period" value="2026-06"/>
  </div>
  <div class="grid gap-2">
    <label class="text-xs font-medium" for="ex-t-week">Reporting week</label>
    <input type="week" class="flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&amp;.htmx-request]:opacity-70 [&amp;::-webkit-calendar-picker-indicator]:cursor-pointer [&amp;::-webkit-calendar-picker-indicator]:opacity-60 [&amp;::-webkit-calendar-picker-indicator]:hover:opacity-100 dark:[&amp;::-webkit-calendar-picker-indicator]:invert [&amp;::-webkit-datetime-edit]:px-0 [&amp;::-webkit-datetime-edit-fields-wrapper]:px-0" data-slot="date-time-picker" id="ex-t-week" name="reporting-week" value="2026-W23"/>
  </div>
</div>

htmx — available slots

Pick a date and htmx GETs the open time slots for that day. The native change event fires the request; the server returns the list.

The date field fires htmx on its native change event — dispatched when the user commits a selection from the picker. hx-get sends the normalised yyyy-mm-dd value and hx-target swaps the server's answer into the slots region. No client state.

Pick a date to see open times.
<DateTimePicker name="date" type="date" min="2026-06-01"
            hx-get="/api/slots" hx-target="#slots"
            hx-swap="innerHTML" hx-trigger="change" />
<div id="slots" aria-live="polite"></div>
{{ date_time_picker(name="date", type="date", min="2026-06-01",
                hx_get="/api/slots", hx_target="#slots",
                hx_swap="innerHTML", hx_trigger="change") }}
<div id="slots" aria-live="polite"></div>
{{template "date-time-picker" (dict
  "Name" "date" "Type" "date" "Min" "2026-06-01"
  "Attrs" (dict
    "hx-get" "/api/slots" "hx-target" "#slots"
    "hx-swap" "innerHTML" "hx-trigger" "change"
  ))}}
<.date_time_picker name="date" type="date" min="2026-06-01"
              hx-get="/api/slots" hx-target="#slots"
              hx-swap="innerHTML" hx-trigger="change" />
<div class="grid w-full max-w-xs gap-3">
  <label class="text-xs font-medium" for="ex-htmx-date">Booking date</label>
  <input type="date" class="flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&amp;.htmx-request]:opacity-70 [&amp;::-webkit-calendar-picker-indicator]:cursor-pointer [&amp;::-webkit-calendar-picker-indicator]:opacity-60 [&amp;::-webkit-calendar-picker-indicator]:hover:opacity-100 dark:[&amp;::-webkit-calendar-picker-indicator]:invert [&amp;::-webkit-datetime-edit]:px-0 [&amp;::-webkit-datetime-edit-fields-wrapper]:px-0" data-slot="date-time-picker" id="ex-htmx-date" name="date" value="2026-06-02" min="2026-06-01" hx-get="/date-time-picker/slots" hx-target="#ex-htmx-slots" hx-swap="innerHTML" hx-trigger="change"/>
  <div id="ex-htmx-slots" class="text-sm text-muted-foreground" aria-live="polite">Pick a date to see open times.</div>
</div>

API Reference

<DateTimePicker>

PropTypeDefaultDescription
type"date"|"time"|"datetime-local"|"month"|"week""date"
Which native temporal control to render. Drives the picker UI, the normalised value format and the units of min/max/step.MDN<input type>
valuestring
Initial value in the variant's normalised, locale-independent format — date yyyy-mm-dd, time HH:mm[:ss], datetime-local yyyy-mm-ddTHH:mm, month YYYY-MM, week yyyy-Www.MDNDate and time formats
minstring
Earliest accepted value, in the value's format. For time the domain is periodic, so a min later than max is valid and the range crosses midnight.MDNmin
maxstring
Latest accepted value, in the value's format. The browser fails constraint validation when the value falls outside [min, max].MDNmax
stepnumber|string
Granularity. date: days (default 1); time / datetime-local: seconds (default 60 → minutes, "1" reveals a seconds segment); month: months; week: weeks. "any" removes stepping.MDNstep
liststring
Id of a <datalist> of suggested values shown alongside the picker.MDN<datalist>
readonlybooleanfalse
Read-only — focusable but not editable. Per MDN, required has no effect on a readonly field.
disabledbooleanfalse
Disable the field — unfocusable, not submitted with the form.
requiredbooleanfalse
Native HTML required for form validation — submit is blocked while the field is empty.
autofocusbooleanfalse
Focus this field on initial page load (one per document).
formstring
Associate the field with a <form> elsewhere in the document by id.
ariaLabelstring
Accessible name when no visible <label>.MDNaria-label
ariaLabelledbystring
Id of a visible element providing the accessible name.MDNaria-labelledby
ariaDescribedbystring
Id of an element describing this control (announced after the name).MDNaria-describedby
classstring
Extra Tailwind classes appended to the root element.
hx-*any
Any htmx attribute. Forwarded onto the underlying element.htmxAttribute reference