package examples import (package examples import ( "github.com/axadrn/shadcn-templ/v2/components/button" "github.com/axadrn/shadcn-templ/v2/components/calendar" "github.com/axadrn/shadcn-templ/v2/components/icon" "github.com/axadrn/shadcn-templ/v2/components/popover") templ DatePickerDemo() { @popover.Popover(popover.Props{ID: "date-picker-demo"}) { {{ triggerAttrs := popover.Trigger(ctx) triggerAttrs["data-empty"] = "true" }} @button.Button(button.Props{ Variant: button.VariantOutline, Class: "w-[212px] justify-between text-left font-normal data-[empty=true]:text-muted-foreground", Attributes: triggerAttrs, }) { <span>Pick a date</span> <span data-icon="inline-end" class="contents"> @icon.ChevronDown() </span> } @popover.Content(popover.ContentProps{ Class: "w-auto p-0", Side: popover.SideBottom, Align: popover.AlignStart, }) { @calendar.Calendar() } } <script> (() => { const fmt = (d) => { const day = d.getDate(); const suf = day % 10 === 1 && day !== 11 ? "st" : day % 10 === 2 && day !== 12 ? "nd" : day % 10 === 3 && day !== 13 ? "rd" : "th"; return d.toLocaleDateString("en-US", { month: "long" }) + " " + day + suf + ", " + d.getFullYear(); }; // Delegated: the popover content is portaled to <body> at init, so it // is looked up per event instead of captured at parse time. document.addEventListener("calendar-change", (e) => { if (!(e.target instanceof Element) || !e.target.closest("#date-picker-demo")) return; const trigger = document.querySelector('[aria-controls="date-picker-demo"]'); trigger.querySelector("span").textContent = e.detail.value ? fmt(e.detail.value) : "Pick a date"; trigger.setAttribute("data-empty", e.detail.value ? "false" : "true"); }); })(); </script>}Installation
The Date Picker is built using a composition of the Popover and the Calendar components.
See installation instructions for the Popover and the Calendar components.
Usage
Listen for the calendar’s calendar-change event to show the selected date in the trigger, see the demo above for the full wiring.
Composition
A date picker is built from Popover and Calendar (there is no DatePicker root component):
Basic
A basic date picker component.
package examples import (package examples import ( "github.com/axadrn/shadcn-templ/v2/components/button" "github.com/axadrn/shadcn-templ/v2/components/calendar" "github.com/axadrn/shadcn-templ/v2/components/field" "github.com/axadrn/shadcn-templ/v2/components/popover") templ DatePickerBasic() { @field.Field(field.Props{Class: "mx-auto w-44"}) { @field.Label(field.LabelProps{For: "date-picker-simple"}) { Date } @popover.Popover(popover.Props{ID: "date-picker-basic"}) { {{ triggerAttrs := popover.Trigger(ctx) triggerAttrs["id"] = "date-picker-simple" }} @button.Button(button.Props{ Variant: button.VariantOutline, Class: "justify-start font-normal", Attributes: triggerAttrs, }) { <span>Pick a date</span> } @popover.Content(popover.ContentProps{ Class: "w-auto p-0", Side: popover.SideBottom, Align: popover.AlignStart, }) { @calendar.Calendar() } } } <script> (() => { const fmt = (d) => { const day = d.getDate(); const suf = day % 10 === 1 && day !== 11 ? "st" : day % 10 === 2 && day !== 12 ? "nd" : day % 10 === 3 && day !== 13 ? "rd" : "th"; return d.toLocaleDateString("en-US", { month: "long" }) + " " + day + suf + ", " + d.getFullYear(); }; // Delegated: the popover content is portaled to <body> at init, so it // is looked up per event instead of captured at parse time. document.addEventListener("calendar-change", (e) => { if (!(e.target instanceof Element) || !e.target.closest("#date-picker-basic")) return; const trigger = document.querySelector('[aria-controls="date-picker-basic"]'); trigger.querySelector("span").textContent = e.detail.value ? fmt(e.detail.value) : "Pick a date"; }); })(); </script>}Range Picker
A date picker component for selecting a range of dates.
package examples import (package examples import ( "time" "github.com/axadrn/shadcn-templ/v2/components/button" "github.com/axadrn/shadcn-templ/v2/components/calendar" "github.com/axadrn/shadcn-templ/v2/components/field" "github.com/axadrn/shadcn-templ/v2/components/icon" "github.com/axadrn/shadcn-templ/v2/components/popover") templ DatePickerRange() { {{ from := time.Date(time.Now().Year(), 1, 20, 0, 0, 0, 0, time.Local) to := from.AddDate(0, 0, 20) }} @field.Field(field.Props{Class: "mx-auto w-60"}) { @field.Label(field.LabelProps{For: "date-picker-range-trigger"}) { Date Picker Range } @popover.Popover(popover.Props{ID: "date-picker-range"}) { {{ triggerAttrs := popover.Trigger(ctx) triggerAttrs["id"] = "date-picker-range-trigger" }} @button.Button(button.Props{ Variant: button.VariantOutline, Class: "justify-start px-2.5 font-normal", Attributes: triggerAttrs, }) { <span data-icon="inline-start" class="contents"> @icon.Calendar() </span> <span> { from.Format("Jan 02, 2006") } - { to.Format("Jan 02, 2006") } </span> } @popover.Content(popover.ContentProps{ Class: "w-auto p-0", Side: popover.SideBottom, Align: popover.AlignStart, }) { @calendar.Calendar(calendar.Props{ Mode: calendar.ModeRange, Selected: from, EndValue: to, NumberOfMonths: 2, }) } } } <script> (() => { const fmt = (d) => d.toLocaleDateString("en-US", { month: "short", day: "2-digit", year: "numeric" }); // Delegated: the popover content is portaled to <body> at init, so it // is looked up per event instead of captured at parse time. document.addEventListener("calendar-change", (e) => { if (!(e.target instanceof Element) || !e.target.closest("#date-picker-range")) return; const trigger = document.querySelector('[aria-controls="date-picker-range"]'); const display = trigger.querySelector("span:not([data-icon])"); if (e.detail.value && e.detail.endValue) { display.textContent = fmt(e.detail.value) + " - " + fmt(e.detail.endValue); } else if (e.detail.value) { display.textContent = fmt(e.detail.value); } else { display.textContent = "Pick a date"; } }); })(); </script>}Date of Birth
A date picker component for selecting a date of birth. This component includes a dropdown caption layout for date and month selection.
package examples import (package examples import ( "github.com/axadrn/shadcn-templ/v2/components/button" "github.com/axadrn/shadcn-templ/v2/components/calendar" "github.com/axadrn/shadcn-templ/v2/components/field" "github.com/axadrn/shadcn-templ/v2/components/popover") templ DatePickerDob() { @field.Field(field.Props{Class: "mx-auto w-44"}) { @field.Label(field.LabelProps{For: "date-picker-dob-trigger"}) { Date of birth } @popover.Popover(popover.Props{ID: "date-picker-dob"}) { {{ triggerAttrs := popover.Trigger(ctx) triggerAttrs["id"] = "date-picker-dob-trigger" }} @button.Button(button.Props{ Variant: button.VariantOutline, Class: "justify-start font-normal", Attributes: triggerAttrs, }) { <span>Select date</span> } @popover.Content(popover.ContentProps{ Class: "w-auto overflow-hidden p-0", Side: popover.SideBottom, Align: popover.AlignStart, }) { @calendar.Calendar(calendar.Props{ CaptionLayout: calendar.CaptionLayoutDropdown, }) } } } <script> (() => { // Delegated: the popover content is portaled to <body> at init, so it // is looked up per event instead of captured at parse time. document.addEventListener("calendar-change", (e) => { if (!(e.target instanceof Element) || !e.target.closest("#date-picker-dob")) return; const trigger = document.querySelector('[aria-controls="date-picker-dob"]'); trigger.querySelector("span").textContent = e.detail.value ? e.detail.value.toLocaleDateString() : "Select date"; if (e.detail.value) window.tui.popover.close(e.target.closest("#date-picker-dob")); }); })(); </script>}Input
A date picker component with an input field for selecting a date.
package examples import (package examples import ( "time" "github.com/axadrn/shadcn-templ/v2/components/calendar" "github.com/axadrn/shadcn-templ/v2/components/field" "github.com/axadrn/shadcn-templ/v2/components/icon" "github.com/axadrn/shadcn-templ/v2/components/inputgroup" "github.com/axadrn/shadcn-templ/v2/components/popover") templ DatePickerInput() { {{ selected := time.Date(2025, 6, 1, 0, 0, 0, 0, time.Local) }} @field.Field(field.Props{Class: "mx-auto w-48"}) { @field.Label(field.LabelProps{For: "date-picker-input-field"}) { Subscription Date } @popover.Popover(popover.Props{ID: "date-picker-input"}) { {{ triggerAttrs := popover.Trigger(ctx) triggerAttrs["data-slot"] = "input-group-button" triggerAttrs["aria-label"] = "Select date" }} @inputgroup.InputGroup() { @inputgroup.Input(inputgroup.InputProps{ ID: "date-picker-input-field", Value: "June 01, 2025", Placeholder: "June 01, 2025", }) @inputgroup.Addon(inputgroup.AddonProps{Align: inputgroup.AlignInlineEnd}) { @inputgroup.Button(inputgroup.ButtonProps{ Size: inputgroup.ButtonSizeIconXs, Attributes: triggerAttrs, }) { @icon.Calendar() <span class="sr-only">Select date</span> } } } @popover.Content(popover.ContentProps{ Class: "w-auto overflow-hidden p-0", Side: popover.SideBottom, Align: popover.AlignEnd, SideOffset: 10, }) { @calendar.Calendar(calendar.Props{ ID: "date-picker-input-calendar", Selected: selected, }) } } } <script> (() => { const input = document.getElementById("date-picker-input-field"); const fmt = (d) => d.toLocaleDateString("en-US", { day: "2-digit", month: "long", year: "numeric" }); // Delegated: the popover content is portaled to <body> at init, so it // is looked up per event instead of captured at parse time. document.addEventListener("calendar-change", (e) => { if (!(e.target instanceof Element) || !e.target.closest("#date-picker-input")) return; if (!e.detail.value) return; input.value = fmt(e.detail.value); window.tui.popover.close(e.target.closest("#date-picker-input")); }); input.addEventListener("input", () => { const date = new Date(input.value); if (!isNaN(date.getTime())) { const iso = date.toLocaleDateString("sv-SE"); document.getElementById("date-picker-input-calendar").dispatchEvent( new CustomEvent("calendar-set", { bubbles: true, detail: { date: iso } }), ); } }); input.addEventListener("keydown", (e) => { if (e.key === "ArrowDown") { e.preventDefault(); window.tui.popover.open(document.getElementById("date-picker-input")); } }); })(); </script>}Time Picker
A date picker component with a time input field for selecting a time.
package examples import (package examples import ( "github.com/axadrn/shadcn-templ/v2/components/button" "github.com/axadrn/shadcn-templ/v2/components/calendar" "github.com/axadrn/shadcn-templ/v2/components/field" "github.com/axadrn/shadcn-templ/v2/components/icon" "github.com/axadrn/shadcn-templ/v2/components/input" "github.com/axadrn/shadcn-templ/v2/components/popover") templ DatePickerTime() { @field.Group(field.GroupProps{Class: "mx-auto max-w-xs flex-row"}) { @field.Field() { @field.Label(field.LabelProps{For: "date-picker-optional"}) { Date } @popover.Popover(popover.Props{ID: "date-picker-time"}) { {{ triggerAttrs := popover.Trigger(ctx) triggerAttrs["id"] = "date-picker-optional" }} @button.Button(button.Props{ Variant: button.VariantOutline, Class: "w-32 justify-between font-normal", Attributes: triggerAttrs, }) { <span>Select date</span> <span data-icon="inline-end" class="contents"> @icon.ChevronDown() </span> } @popover.Content(popover.ContentProps{ Class: "w-auto overflow-hidden p-0", Side: popover.SideBottom, Align: popover.AlignStart, }) { @calendar.Calendar(calendar.Props{ CaptionLayout: calendar.CaptionLayoutDropdown, }) } } } @field.Field(field.Props{Class: "w-32"}) { @field.Label(field.LabelProps{For: "time-picker-optional"}) { Time } @input.Input(input.Props{ ID: "time-picker-optional", Type: "time", Value: "10:30:00", Class: "appearance-none bg-background [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none", Attributes: templ.Attributes{ "step": "1", }, }) } } <script> (() => { const fmt = (d) => { const day = d.getDate(); const suf = day % 10 === 1 && day !== 11 ? "st" : day % 10 === 2 && day !== 12 ? "nd" : day % 10 === 3 && day !== 13 ? "rd" : "th"; return d.toLocaleDateString("en-US", { month: "long" }) + " " + day + suf + ", " + d.getFullYear(); }; // Delegated: the popover content is portaled to <body> at init, so it // is looked up per event instead of captured at parse time. document.addEventListener("calendar-change", (e) => { if (!(e.target instanceof Element) || !e.target.closest("#date-picker-time")) return; const content = e.target.closest("#date-picker-time"); const trigger = document.querySelector('[aria-controls="date-picker-time"]'); trigger.querySelector("span").textContent = e.detail.value ? fmt(e.detail.value) : "Select date"; if (e.detail.value) window.tui.popover.close(content); }); })(); </script>}