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.json2. Use it
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
/** @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
{% 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
{# 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
tpl.ExecuteTemplate(w, "date-time-picker", map[string]any{
"Name": "bday", "Type": "date", "Min": "1900-01-01",
"AriaLabel": "Date of birth",
})View source
{{/*
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
<.date_time_picker name="bday" type="date" min="1900-01-01" aria-label="Date of birth" />View source
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
<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
<!--
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 [&.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" 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>Further reading
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 [&.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" 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 [&.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" 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 [&.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" 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 [&.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" 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.
<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 [&.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" 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>Further reading
API Reference
<DateTimePicker>
| Prop | Type | Default | Description |
|---|---|---|---|
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> |
value | string | — | 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 |
min | string | — | 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 |
max | string | — | Latest accepted value, in the value's format. The browser fails constraint validation when the value falls outside [min, max].MDNmax |
step | number|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 |
list | string | — | Id of a <datalist> of suggested values shown alongside the picker.MDN<datalist> |
readonly | boolean | false | Read-only — focusable but not editable. Per MDN, required has no effect on a readonly field. |
disabled | boolean | false | Disable the field — unfocusable, not submitted with the form. |
required | boolean | false | Native HTML required for form validation — submit is blocked while the field is empty. |
autofocus | boolean | false | Focus this field on initial page load (one per document). |
form | string | — | Associate the field with a <form> elsewhere in the document by id. |
ariaLabel | string | — | Accessible name when no visible <label>.MDNaria-label |
ariaLabelledby | string | — | Id of a visible element providing the accessible name.MDNaria-labelledby |
ariaDescribedby | string | — | Id of an element describing this control (announced after the name).MDNaria-describedby |
class | string | — | Extra Tailwind classes appended to the root element. |
hx-* | any | — | Any htmx attribute. Forwarded onto the underlying element.htmxAttribute reference |