Date range picker with dual-calendar server-side rendering.
Renders two side-by-side monthly calendar grids for selecting a start and
end date. The calendar grid is built server-side using Elixir's Date
module. All state (current view month, selected from/to) lives in the
parent LiveView. Day clicks and month navigation fire phx-click events.
When to use
Use date_range_picker/1 whenever users need to select a date interval:
- Hotel booking (check-in / check-out)
- Report date range (start period / end period)
- Project timeline (kickoff / deadline)
- Vacation request (from / to)
- Analytics dashboard date filter
For single-date selection, use date_picker/1 instead.
State management
The LiveView manages a two-click selection protocol:
- First click sets
from, clearsto - Second click (on a date ≥
from) setsto - Any subsequent click resets: sets new
from, clearsto
Complete example — booking range
defmodule MyAppWeb.BookingLive do
use Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok, assign(socket,
view_month: Date.beginning_of_month(Date.utc_today()),
date_from: nil,
date_to: nil
)}
end
def handle_event("select-date", %{"date" => date_str}, socket) do
date = Date.from_iso8601!(date_str)
socket =
cond do
# No start date yet — set it
is_nil(socket.assigns.date_from) ->
assign(socket, date_from: date, date_to: nil)
# Start set, no end, and the clicked date is >= start — set end
is_nil(socket.assigns.date_to) and Date.compare(date, socket.assigns.date_from) != :lt ->
assign(socket, date_to: date)
# Otherwise reset: start a new range
true ->
assign(socket, date_from: date, date_to: nil)
end
{:noreply, socket}
end
def handle_event("change-month", %{"dir" => "next"}, socket) do
{:noreply, assign(socket, view_month: Date.shift(socket.assigns.view_month, month: 1))}
end
def handle_event("change-month", %{"dir" => "prev"}, socket) do
{:noreply, assign(socket, view_month: Date.shift(socket.assigns.view_month, month: -1))}
end
end
<%!-- Template --%>
<.date_range_picker
id="booking-range"
view_month={@view_month}
from={@date_from}
to={@date_to}
on_change="select-date"
on_month_change="change-month"
min_date={Date.utc_today()}
/>
<p :if={@date_from && @date_to}>
Booking: {Calendar.strftime(@date_from, "%B %d")} –
{Calendar.strftime(@date_to, "%B %d, %Y")}
({Date.diff(@date_to, @date_from)} nights)
</p>Range highlight
When both from and to are set, days between the endpoints receive a
bg-accent background. The endpoint buttons themselves receive
bg-primary text-primary-foreground and rounded corners that connect
visually to the range stripe (rounded-r-none on from, rounded-l-none
on to).
Summary
Functions
Renders a dual-calendar date range picker.
Functions
Renders a dual-calendar date range picker.
Displays two side-by-side monthly grids. The left grid shows view_month;
the right grid shows the month immediately following. Both grids share the
same from/to selection state and highlight the range consistently.
Month navigation applies to both calendars simultaneously — advancing or retreating one month at a time.
Attributes
id(:string) (required) - Unique DOM id for the root element.view_month(:any) (required) -Date.t()representing the first of the two displayed months. The second month is derived automatically asview_month + 1. Update this assign in response toon_month_changeevents.from(:any) - Selected range start date (Date.t()) ornilbefore the user's first click. Defaults tonil.to(:any) - Selected range end date (Date.t()) ornilbefore the user's second click. Defaults tonil.on_change(:string) -phx-clickevent name fired when a day cell is clicked. The LiveView receives%{"date" => "YYYY-MM-DD"}.Defaults to
"date-range-changed".on_month_change(:string) -phx-clickevent name for month navigation buttons. The LiveView receives%{"dir" => "next" | "prev"}.Defaults to
"date-range-month".min_date(:any) - Minimum selectable date (Date.t()). Days before this are rendered disabled. Defaults tonil.max_date(:any) - Maximum selectable date (Date.t()). Days after this are rendered disabled. Defaults tonil.locale(:string) - Locale for month/day labels (currently informational — labels are hardcoded in English). Defaults to"en".class(:string) - Additional CSS classes for the root wrapper. Defaults tonil.Global attributes are accepted. HTML attributes forwarded to the root div.