Server-rendered calendar component for date selection with keyboard navigation.
The calendar grid is built entirely server-side using Elixir's Date module.
State (current month, selected date or range) lives in the parent LiveView.
Day clicks and month navigation fire phx-click events back to the LiveView.
Supports two selection modes:
:single— selects a singleDate.t()value:range— highlights an interval between:range_startand:range_end
Keyboard navigation (Arrow keys, Enter, Home, End) is provided by the
PhiaCalendar JS hook registered on the root element.
When to use
Use calendar/1 as the foundational date-picking primitive. Higher-level
components compose it:
date_picker/1— wrapscalendar/1in a popover with a trigger buttondate_range_picker/1— renders twocalendar/1instances side-by-side for range selection
Use calendar/1 directly when you want a permanently visible calendar
(e.g. a booking widget, an event scheduler, a date navigator).
ARIA
The grid uses role="grid". Day-of-week headers use role="columnheader".
Individual day cells use role="gridcell" with aria-selected and
aria-disabled reflecting state. Disabled buttons have the disabled HTML
attribute to prevent interaction and remove them from the tab order.
Single-date example
defmodule MyAppWeb.BookingLive do
use Phoenix.LiveView
def mount(_params, _session, socket) do
today = Date.utc_today()
{:ok, assign(socket,
current_month: Date.beginning_of_month(today),
selected_date: nil
)}
end
def handle_event("date-selected", %{"date" => iso}, socket) do
date = Date.from_iso8601!(iso)
{:noreply, assign(socket, selected_date: date)}
end
def handle_event("calendar-prev-month", %{"month" => iso}, socket) do
{:noreply, assign(socket, current_month: Date.from_iso8601!(iso))}
end
def handle_event("calendar-next-month", %{"month" => iso}, socket) do
{:noreply, assign(socket, current_month: Date.from_iso8601!(iso))}
end
end
<%!-- Template --%>
<.calendar
id="booking-calendar"
current_month={@current_month}
value={@selected_date}
on_change="date-selected"
min={Date.utc_today()}
/>
<p :if={@selected_date}>
Selected: {Calendar.strftime(@selected_date, "%B %d, %Y")}
</p>Range selection example
<.calendar
id="vacation-range"
current_month={@current_month}
mode="range"
range_start={@check_in}
range_end={@check_out}
on_change="date-selected"
min={Date.utc_today()}
/>Disabling specific dates
Pass disabled_dates to block holiday or unavailable dates:
<.calendar
id="appointment-picker"
value={@appointment_date}
disabled_dates={@fully_booked_dates}
min={Date.utc_today()}
max={Date.add(Date.utc_today(), 90)}
on_change="date-selected"
/>Hook setup
# app.js
import PhiaCalendar from "./hooks/calendar"
let liveSocket = new LiveSocket("/live", Socket, {
hooks: { PhiaCalendar }
})
Summary
Functions
Renders a server-rendered, accessible calendar grid for date selection.
Functions
Renders a server-rendered, accessible calendar grid for date selection.
All calendar geometry (weeks, leading/trailing days, cell states) is computed
server-side in build_weeks/1 and compute_cell_states/4 before the
template renders. This keeps the HEEx template declarative and free of logic.
The PhiaCalendar hook handles keyboard navigation: Arrow keys move focus
between day cells, Enter selects the focused cell, Home/End jump to the
first/last day of the displayed month.
Attributes
id(:string) - Unique calendar ID. Used as the hook anchor and as a prefix for the embedded calendar indate_picker/1. Must be unique per page.Defaults to
"calendar".value(:any) - Selected date (Date.t()) in:singlemode, ornilfor no selection. Defaults tonil.current_month(:any) - Currently displayed month (Date.t()). Whennil, defaults to the month containingvalue, or the current month ifvalueis also nil. The LiveView must update this assign in response to prev/next month events.Defaults to
nil.mode(:string) - Selection mode:"single"— highlights one selected date"range"— highlights the interval betweenrange_startandrange_end
Defaults to
"single". Must be one of"single", or"range".range_start(:any) - Range selection start date (Date.t()) — only used inmode="range". Defaults tonil.range_end(:any) - Range selection end date (Date.t()) — only used inmode="range". Defaults tonil.min(:any) - Minimum selectable date (Date.t()). Dates before this are disabled. Defaults tonil.max(:any) - Maximum selectable date (Date.t()). Dates after this are disabled. Defaults tonil.disabled_dates(:list) - Explicit list ofDate.t()values that cannot be selected regardless ofmin/max. Use for holidays, fully-booked slots, or blackout dates.Defaults to
[].on_change(:string) -phx-clickevent name fired when a day button is clicked. The LiveView receives%{"date" => "YYYY-MM-DD"}.Defaults to
"calendar-change".class(:string) - Additional CSS classes merged onto the root element viacn/1. Defaults tonil.Global attributes are accepted. HTML attributes forwarded to the root div element.