A text input with a <.calendar> in a panel underneath it. The input shows a
formatted date you can type into; the form always posts ISO 8601.
Examples
<.date_picker name="due_on" label="Due date" value={~D[2026-03-14]} format="%d %b %Y" />Wired to a form field, it takes its name, value, errors and label the same way
<.field> does:
<.form for={@form} phx-change="validate" phx-submit="save">
<.date_picker field={@form[:due_on]} label="Due date" min={Date.utc_today()} />
</.form>Range mode posts from and to sub-fields, so params["stay"] comes back as
%{"from" => "2026-03-10", "to" => "2026-03-17"}:
<.date_picker
name="stay"
mode="range"
two_months
min={Date.utc_today()}
value={{~D[2026-03-10], ~D[2026-03-17]}}
/>What gets posted
The visible input is display-only: it is not named, so it never posts. A hidden
input carries the value in ISO 8601 (YYYY-MM-DD) whatever format you show,
because a strftime string is for humans and a form value is for your changeset.
Parse on blur
You can type in the input. On blur (or Enter) the picker tries, in order:
Date.from_iso8601/1, so2026-03-14always works- a lenient parse of the configured
format, so14 Mar 2026works whenformatis"%d %b %Y"
On success the hidden value and the calendar's month both move to the parsed date. On failure the input reverts to the last valid formatted value, so a half-typed date can never silently post as nothing. Enter commits the same way blur does, and when it lands a COMPLETE selection - both ends in range mode - it also closes the panel; a partial or failed parse keeps it open. Clicking a day keeps the same contract: the click that completes the selection (any single-mode click, a range's second end) closes the panel and hands focus back to the input; a range's anchor click keeps browsing. The calendar icon is a true toggle - it opens a closed panel and closes an open one.
While the panel is open, typing does not even wait for blur: the moment the
text parses to a complete selection, the calendar pages to it and paints it,
with the caret and your text left exactly where they are (14 Mar 2027
canonicalises to the configured format on blur or Enter, not mid-keystroke).
Incomplete text does nothing - previews never revert. Server-owned pickers
(on_select) keep the blur/Enter contract only, so your handler is never
fed per-keystroke half-states.
One wiring requirement that bites: with on_month_change left nil, month
navigation renders as patch links carrying month_param - which only pages
if your LiveView actually handles that param in handle_params/3 and feeds
it back through month. In a LiveView that ignores the param, prev/next do
nothing and a typed far-away date cannot bring its month into view. Either
handle the param, or pass on_month_change and assign the pushed month -
the typed-date month jump works through whichever wiring you chose.
With on_select set the server owns the value, so a parsed date is pushed to
you as that same event rather than written client-side - typing 12 Jun 1987
and clicking 12 June 1987 arrive at handle_event/3 identically. In range mode
a typed range arrives as two events in reading order, exactly as two clicks
would, so the handler you already have keeps working. Emptying the input pushes
on_clear when you have wired one; without one there is nothing the picker can
push, so the display reverts rather than sit there disagreeing with the value
that will actually post.
LiveView and dead views
With on_select set, clicking a day pushes that event with the ISO date and
your handle_event/3 owns the value. Without it, the PetalDatePicker hook
writes the hidden input itself and dispatches input/change, which is enough
for a plain form post or a phx-change form. The panel opens and closes with
Phoenix.LiveView.JS either way: Escape closes it and returns focus to the
input, and clicking outside dismisses it.
Summary
Functions
Renders a date input with a calendar panel. See the module docs above for what gets posted, the parse-on-blur contract, and the LiveView vs dead-view wirings.
Functions
Renders a date input with a calendar panel. See the module docs above for what gets posted, the parse-on-blur contract, and the LiveView vs dead-view wirings.
<.date_picker field={@form[:due_on]} label="Due date" format="%d %b %Y" />Attributes
id(:string) - the picker id; autogenerated if not set.field(Phoenix.HTML.FormField) - a form field; wires name, value, errors and label like <.field>. Defaults tonil.name(:any) - the posted field name; derived from field when given. Defaults tonil.value(:any) - a Date, an ISO string, or a {from, to} pair in range mode. Defaults tonil.label(:string) - the field label; omitted when nil. Defaults tonil.errors(:list) - error messages rendered under the input. Defaults to[].help_text(:string) - context shown under the input. Defaults tonil.mode(:string) - selection behaviour. Defaults to"single". Must be one of"single", or"range".format(:string) - strftime format for the display input (Calendar.strftime/2); the posted value is always ISO 8601. Defaults to"%Y-%m-%d".range_separator(:string) - what sits between the two dates in the display input in range mode. Defaults to" - ".placeholder(:string) - placeholder for the display input. Defaults tonil.clearable(:boolean) - show a clear button whenever the field holds text. Defaults tofalse.two_months(:boolean) - render two month panes side by side. A range reads naturally across the pair, and single mode earns it too - the flight-search pattern, where seeing next month saves a page before the date you are really after. Defaults tofalse.disabled(:boolean) - disable the input and the panel. Defaults tofalse.required(:boolean) - mark the field required. Defaults tofalse.month(:any) - the displayed month as any Date within it. Defaults tonil.min(:any) - earliest selectable Date (inclusive). Defaults tonil.max(:any) - latest selectable Date (inclusive). Defaults tonil.disabled_dates(:any) - a list of Dates, or a 1-arity function Date -> boolean. Defaults tonil.today(:any) - the date treated as today; defaults to Date.utc_today/0. Defaults tonil.starts_on(:integer) - first day of the week, ISO day numbers (1 = Monday, 7 = Sunday). Defaults to1. Must be one of1,2,3,4,5,6, or7.show_outside_days(:boolean) - render adjacent-month days in the grid. Defaults totrue.fixed_weeks(:boolean) - always render 6 week rows. Defaults tofalse.day_names(:list) - 7 short day labels ordered Monday-first. Defaults tonil.day_names_long(:list) - 7 full day labels ordered Monday-first. Defaults tonil.month_names(:list) - 12 month labels. Defaults tonil.on_select(:any) - event pushed when a day is clicked, with the ISO date. Defaults tonil.on_month_change(:any) - event pushed by the prev/next month buttons. Defaults tonil.on_clear(:any) - event pushed when the clear button is clicked. Defaults tonil.target(:any) - phx-target for the picker's events. Defaults tonil.open_label(:string) - accessible label for the toggle button. Defaults to"Choose date".clear_label(:string) - accessible label for the clear button. Defaults to"Clear date".class(:any) - extra classes for the wrapper. Defaults tonil.panel_class(:any) - extra classes for the panel. Defaults tonil.- Global attributes are accepted.