Fluxon.Components.Calendar
(Fluxon v3.0.0)
Provides <.calendar> and <.calendar_range> components for inline date selection.
This module renders a standalone calendar interface that is always visible on the page, supporting single date, multiple date, and date range selection. It includes day, month, and year granularity modes, multiple navigation styles, min/max constraints, flexible disabled date patterns, locale support, and roving keyboard navigation.
Calendar vs DatePicker
Both components share the same calendar grid, but serve different layout needs:
| Feature | Calendar | DatePicker |
|---|---|---|
| Display | Always inline | Dropdown on click |
| Toggle | None | Button or typeable input |
| Time picker | Not supported | Supported |
| Display format | Not applicable | Configurable |
Use Calendar when the calendar should always be visible on the page. Use
Fluxon.Components.DatePicker when you need a compact input that opens a
calendar dropdown.
Usage
Render a calendar for single date selection:
<.calendar name="appointment" label="Appointment Date" />For multiple dates or date ranges, use the multiple attribute or the
<.calendar_range> component:
<.calendar name="holidays[]" label="Company Holidays" multiple />
<.calendar_range
start_name="check_in"
end_name="check_out"
label="Stay Period"
/>Selection Modes
Single Selection
The default mode selects one date at a time. Clicking a new date replaces the
previous selection. Clicking the selected date deselects it (unless allow_deselect
is false):
<.calendar name="date" label="Pick a date" />
<.calendar name="date" label="Required date" allow_deselect={false} />Multiple Selection
Enable with the multiple attribute. Each click toggles the date on or off.
Multiple hidden inputs are created for form submission, so the name attribute
should end with []:
<.calendar name="dates[]" label="Select dates" multiple />When allow_deselect is false, at least one date must remain selected.
Range Selection
Use calendar_range/1 for start/end date pairs. The first click sets the start
date, the second click sets the end date. If the end date is before the start,
they are automatically swapped. Clicking the start date after a complete range
clears both values:
<.calendar_range
start_name="check_in"
end_name="check_out"
label="Booking Period"
/>Dates between the start and end are visually highlighted with a background fill connecting the two selected endpoints.
Granularity
Controls the precision of the calendar grid. The granularity attribute
determines what unit the user selects and how the grid is laid out:
<.calendar name="date" granularity="day" />
<.calendar name="month" granularity="month" />
<.calendar name="year" granularity="year" />| Granularity | Grid | Navigation Step | Value Format | Use Case |
|---|---|---|---|---|
day | 7-column, 6 weeks | Month | 2025-01-15 | Appointment dates, deadlines |
month | 3-column, 12 months | Year | 2025-03 | Credit card expiry, billing periods |
year | 3-column, 12 years | Decade | 2025 | Fiscal years, birth years |
All selection modes (single, multiple, range) work with every granularity.
Focus moves through the grid with a roving tabindex, so only one cell is
tabbable at a time and Tab enters or leaves the grid as a single stop. Arrow
keys move focus and wrap across grid edges, advancing to the previous or next
period automatically: left and right step by one unit (a day, month, or year),
while up and down step by a full row, which is a week in day view and three
cells in month and year views. Enter or Space selects the focused cell.
Focus skips disabled cells, continuing in the arrow direction until it lands on
a selectable one.
Navigation
The navigation attribute controls the header UI. Choose based on how much control
users need over time traversal:
<.calendar name="date" navigation="default" />
<.calendar name="date" navigation="extended" />
<.calendar name="date" navigation="select" />| Style | Controls | Use Case |
|---|---|---|
default | Previous/next month arrows with month-year title | Most calendars where dates are near the current month |
extended | Adds previous/next year arrows | Booking systems spanning multiple years |
select | Month and year dropdown selects with next/previous arrows | Birth date pickers or historical date selection |
Navigation buttons are automatically disabled when they would move past min
or max boundaries.
Date Constraints
Min and Max
Restrict the selectable date range. Dates outside the range are visually dimmed, cannot be selected via click or keyboard, and navigation buttons are disabled at boundaries:
<!-- Only future dates -->
<.calendar name="date" min={Date.utc_today()} />
<!-- Fixed year range -->
<.calendar name="date" min={~D[2025-01-01]} max={~D[2025-12-31]} />Disabled Dates
The disabled_dates attribute accepts a list of patterns to disable specific
dates. Multiple patterns can be combined in a single list:
<!-- Disable weekends -->
<.calendar name="date" disabled_dates={[:weekends]} />
<!-- Disable holidays and recurring dates -->
<.calendar name="date" disabled_dates={[~D[2025-12-25], {:month_day, 1, 1}]} />Available patterns:
| Pattern | Example | Description |
|---|---|---|
Date | ~D[2025-01-15] | Specific date |
Date.Range | Date.range(~D[2025-01-01], ~D[2025-01-10]) | Contiguous date range |
:weekends | :weekends | Saturdays and Sundays |
:weekdays | :weekdays | Monday through Friday |
{:day, d} | {:day, 15} | Day of month, every month |
{:weekday, n} | {:weekday, 3} | Day of week (1=Mon, 7=Sun) |
{:week, n} | {:week, 33} | ISO week number |
{:month_day, m, d} | {:month_day, 12, 25} | Recurring annual date |
{:month, m} | {:month, 4} | Entire month |
{:year, y} | {:year, 2025} | Entire year |
Keyboard navigation automatically skips disabled dates, continuing in the arrow key direction until an enabled date is found.
Time Zones
The calendar is a wall-clock component. It emits and consumes the exact date the
user sees, with no timezone attached. Selected dates are never converted across
time zones, so what the user picks is what your handle_event receives, and a
submitted value is a plain Date you store as-is.
The highlighted "today" needs no configuration: when today is omitted the
client highlights the browser's local date, correct for the user's time zone for
single-locale and multi-zone apps alike. Set today only when the server must
control the value: static mode (no JavaScript runs to apply the highlight), a
highlight in the first server paint, deterministic rendering (tests, screenshots),
or anchoring to a non-clock date such as a business "today".
Time-zone-aware constraints
min, max, and disabled_dates are wall-clock values, enforced exactly as you
pass them. When a bound is relative to the current day (for example "today or
later"), compute it in the user's time zone so it matches the day they see. A
bound built from Date.utc_today() is the same for everyone, so near midnight it
can sit a day ahead of or behind the user's real day and block or allow a date it
should not. The usual approach is to send the browser time zone through connect
params and resolve the date on the server:
// app.js
let liveSocket = new LiveSocket("/live", Socket, {
params: {
_csrf_token: csrfToken,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
}
})# In your LiveView mount/3
def mount(_params, _session, socket) do
timezone = Phoenix.LiveView.get_connect_params(socket)["timezone"] || "Etc/UTC"
today = DateTime.now!(timezone) |> DateTime.to_date()
{:ok, assign(socket, today: today)}
end<.calendar name="date" min={@today} />If your schema records an absolute moment rather than a date, attach the time and
the user's zone on the server before converting to UTC. See
Fluxon.Components.DatePicker for the date and time workflow.
Static Mode
Renders a display-only calendar without navigation controls, hidden inputs, or the JavaScript hook. Selected dates are still visually highlighted. Useful for confirmation screens, read-only summaries, or print layouts:
<.calendar name="date" value={~D[2025-06-15]} static />In static mode, the calendar is read-only: every cell is disabled and removed from the tab order, no JavaScript is attached, and no hidden inputs are rendered.
Form Integration
Use the field attribute to bind to a Phoenix form field. The component
automatically derives name, value, and errors from the form field:
<.form :let={f} for={@changeset} phx-change="validate">
<.calendar field={f[:appointment_date]} label="Appointment Date" />
</.form>Range selection with form fields:
<.form :let={f} for={@changeset}>
<.calendar_range
start_field={f[:start_date]}
end_field={f[:end_date]}
label="Booking Period"
/>
</.form>For standalone usage without a form, use the name and value attributes directly:
<.calendar name="date" value={~D[2025-06-15]} />The component creates hidden <input> elements that participate in standard
form submission. In multiple mode, one hidden input is rendered per selected date.
In range mode, separate hidden inputs are created for start and end dates.
Locale
The locale attribute localizes weekday abbreviations, month names, header titles,
and accessible labels. Unsupported locales fall back to English:
<.calendar name="fecha" label="Fecha" locale="es" />
<.calendar name="date" label="Date" locale="fr" week_start={1} />
<.calendar name="datum" label="Datum" locale="de" navigation="select" />Supported locales include ar, bg, cs, da, de, el, en, es, fi,
fr, hi, hr, hu, it, ja, ko, nb, nl, pl, pt, pt-BR, ro,
ru, sk, sr, sv, th, tr, uk, vi, and zh.
Examples
Business Day Selector
<.calendar
name="business_date"
label="Meeting Date"
min={Date.utc_today()}
disabled_dates={[:weekends]}
allow_deselect={false}
/>Holiday Picker with Validation
<.form :let={f} for={@changeset} phx-change="validate" phx-submit="save">
<.calendar
field={f[:holidays]}
label="Company Holidays"
multiple
min={~D[2025-01-01]}
max={~D[2025-12-31]}
/>
</.form>Booking Range with Locale
<.calendar_range
start_name="check_in"
end_name="check_out"
label="Periodo de estancia"
locale="es"
min={Date.utc_today()}
week_start={1}
navigation="extended"
/>Month/Year Pickers
<.calendar name="birth_month" label="Birth Month" granularity="month" />
<.calendar
name="fiscal_year"
label="Fiscal Year"
granularity="year"
min={~D[2020-01-01]}
max={~D[2030-12-31]}
/>Read-Only Confirmation
<!-- Display selected date on a confirmation page -->
<.calendar name="appointment" value={@appointment_date} static />
Summary
Components
Renders an inline calendar for single or multiple date selection.
Renders an inline calendar for date range selection with separate start and end values.
Components
Renders an inline calendar for single or multiple date selection.
In single mode (default), clicking a date selects it and clicking again deselects it. In multiple mode, each click toggles the date on or off, and a separate hidden input is created for each selected value. The calendar is always visible on the page and supports all granularity modes, navigation styles, and date constraints.
Examples
<.calendar name="date" label="Select Date" />
<.calendar name="dates[]" label="Select Dates" multiple />
<.calendar
name="appointment"
label="Appointment"
min={Date.utc_today()}
disabled_dates={[:weekends]}
navigation="extended"
/>With a form field:
<.form :let={f} for={@changeset} phx-change="validate">
<.calendar field={f[:date]} label="Date" min={Date.utc_today()} />
</.form>Attributes
field(Phoenix.HTML.FormField) - Binds the calendar to a Phoenix form field for automatic name, value, and error derivation. When provided, thenameandvalueattributes are inferred from the form field. Validation errors display automatically when the field has been used.name(:any) - Sets the form input name for the calendar. Required when not using thefieldattribute. For multiple selection, append[]to the name (e.g.,"dates[]").value(:any) - Sets the currently selected date value. AcceptsDate,DateTime,NaiveDateTime, or ISO 8601 date strings. In multiple mode, accepts a list of date values.id(:any) - Sets the unique identifier for the calendar component. When not provided, defaults to the form field id (if usingfield) or thenameattribute. The id is used to generate sub-element ids like{id}-containerand{id}-calendar.Defaults to
nil.min(Date) - Specifies the earliest selectable date. Dates before this boundary are visually dimmed and cannot be selected via click or keyboard. Navigation buttons are disabled when they would move past this date. AcceptsDate,DateTime, or ISO 8601 date strings.Defaults to
nil.max(Date) - Specifies the latest selectable date. Dates after this boundary are visually dimmed and cannot be selected via click or keyboard. Navigation buttons are disabled when they would move past this date. AcceptsDate,DateTime, or ISO 8601 date strings.Defaults to
nil.today(:any) - Sets the date treated as "today", which controls the highlighted current day and the default visible month when no value is selected. AcceptsDate,DateTime,NaiveDateTime, or ISO 8601 strings, normalized to aDate.When omitted, the client highlights the browser's local date, so the current day is correct for the user's time zone with no configuration. Set this to let the server control the value instead: for static mode, a first-paint highlight, deterministic rendering (tests, screenshots), or anchoring to a non-clock date. See the "Time Zones" section for details.
Defaults to
nil.week_start(:integer) - Determines which day of the week appears in the first column of the day grid. When not provided, the value is derived from the component's locale.0: Sunday (common in the US).1: Monday (common in Europe and most ISO locales).6: Saturday (common in some Middle Eastern locales).
Values 0-6 map to Sunday through Saturday respectively.
Defaults to
nil.class(:any) - Additional CSS classes applied to the calendar wrapper element. Useful for controlling width, spacing, or custom styling of the calendar panel.Defaults to
nil.granularity(:string) - Controls the selection precision level and grid layout.day: 7-column grid showing individual days. Navigates by month. Best for appointment dates and deadlines.month: 3-column grid showing 12 months. Navigates by year. Best for billing periods and expiry dates.year: 3-column grid showing 12 years. Navigates by decade. Best for fiscal years and birth years.
Defaults to
"day". Must be one of"day","month", or"year".hide_outside_days(:boolean) - Controls whether the day grid fills the leading and trailing empty cells with days from the previous and next month. Only affectsdaygranularity.false(default): renders a padded six-week grid, with outside-month days dimmed. The grid keeps a constant height across months.true: renders only the current month's days. Leading and trailing cells are left blank and full outside-month weeks are dropped, so the grid uses four to six rows depending on the month and its height changes as you navigate.
Defaults to
false.navigation(:string) - Controls the navigation interface in the calendar header.default: previous/next month arrows with a month-year title. Best for most use cases where dates are near the current month.extended: adds previous/next year arrows for faster traversal. Best for booking systems spanning multiple years.select: month and year dropdown selects with navigation arrows. Best for birth date pickers or historical date selection.
Defaults to
"default". Must be one of"default","extended", or"select".disabled(:boolean) - Disables the entire calendar when set totrue. All date buttons and navigation controls become non-interactive. The calendar remains visible but no selection can be made.Defaults to
false.label(:string) - Sets the primary label text displayed above the calendar. Renders as a<label>element for accessibility.Defaults to
nil.sublabel(:string) - Specifies secondary text displayed inline beside the main label. Useful for adding optional context like "(optional)" or a brief clarification.Defaults to
nil.description(:string) - Provides a longer description rendered as a paragraph below the label. Useful for instructions or additional context about the expected selection.Defaults to
nil.help_text(:string) - Displays helper text below the calendar grid. Useful for formatting hints, selection guidance, or contextual information.Defaults to
nil.errors(:list) - Specifies error messages to display below the calendar. When using thefieldattribute, errors are automatically derived from form validation. Each error renders as a styled error message in red.Defaults to
[].disabled_dates(:list) - Specifies dates, date ranges, or patterns to disable. Disabled dates are visually dimmed with a strikethrough and cannot be selected via click or keyboard. Multiple patterns can be combined in a single list. Accepts:- Specific dates:
~D[2025-01-15] - Date ranges:
Date.range(~D[2025-01-01], ~D[2025-01-10]) - Day shortcuts:
:weekends,:weekdays - Day of month:
{:day, 15}(every month) - Weekday:
{:weekday, 3}(1=Monday, 7=Sunday) - ISO week:
{:week, 33} - Recurring annual dates:
{:month_day, 12, 25} - Month pattern:
{:month, 4}(entire month) - Year pattern:
{:year, 2025}(entire year)
Defaults to
[].- Specific dates:
allow_deselect(:boolean) - Controls whether clicking an already-selected date deselects it. Defaults totrue.- In single mode, clicking the selected date clears the value.
- In multiple mode, at least one date must remain selected when
false. - In range mode, clicking the start date of a complete range won't clear both values.
Set to
falsewhen a selection is always required.Defaults to
true.locale(:string) - Sets the locale for localizing weekday abbreviations, month names, header titles, and accessible labels. Defaults to"en". Unsupported locales fall back to English. Common values includear,de,es,fr,ja,ko,pt-BR, andzh.Defaults to
"en".static(:boolean) - Renders a non-interactive, display-only calendar when set totrue. Removes the navigation header, hidden form inputs, and the JavaScript hook. All day buttons are disabled and removed from the tab order. Selected dates remain visually highlighted. Useful for confirmation screens, read-only summaries, or print layouts.Defaults to
false.multiple(:boolean) - Enables multiple date selection when set totrue. Each click toggles a date on or off. A separate hidden input is created for each selected date, so thenameattribute should end with[](e.g.,"dates[]"). Defaults tofalse.Defaults to
false.
Renders an inline calendar for date range selection with separate start and end values.
The first click sets the start date, the second click sets the end date. If the
end date is earlier than the start, the values are automatically swapped. Dates
between the endpoints are visually highlighted with a connecting background fill.
Clicking the start date of a complete range clears both values (unless
allow_deselect is false). Two hidden inputs are created for form submission,
one for each endpoint.
Examples
<.calendar_range start_name="check_in" end_name="check_out" label="Stay" />
<.calendar_range
start_name="start"
end_name="end"
label="Project Timeline"
min={Date.utc_today()}
navigation="extended"
/>With form fields:
<.form :let={f} for={@changeset} phx-change="validate">
<.calendar_range
start_field={f[:start_date]}
end_field={f[:end_date]}
label="Booking Period"
/>
</.form>Attributes
start_field(Phoenix.HTML.FormField) - Binds the range start date to a Phoenix form field. When provided,start_nameandstart_valueare inferred from the field. Validation errors from both start and end fields are collected and displayed together.end_field(Phoenix.HTML.FormField) - Binds the range end date to a Phoenix form field. When provided,end_nameandend_valueare inferred from the field. Must be used together withstart_field.start_name(:any) - Sets the form input name for the start date. Required when not using thestart_fieldattribute. The hidden input for the start date uses this name for form submission.end_name(:any) - Sets the form input name for the end date. Required when not using theend_fieldattribute. The hidden input for the end date uses this name for form submission.start_value(:any) - Sets the initial start date value. AcceptsDate,DateTime,NaiveDateTime, or ISO 8601 date strings. The start date is visually highlighted with a filled circle at the beginning of the range.end_value(:any) - Sets the initial end date value. AcceptsDate,DateTime,NaiveDateTime, or ISO 8601 date strings. The end date is visually highlighted with a filled circle at the end of the range.id(:any) - Sets the unique identifier for the calendar component. When not provided, defaults to the form field id (if usingfield) or thenameattribute. The id is used to generate sub-element ids like{id}-containerand{id}-calendar.Defaults to
nil.min(Date) - Specifies the earliest selectable date. Dates before this boundary are visually dimmed and cannot be selected via click or keyboard. Navigation buttons are disabled when they would move past this date. AcceptsDate,DateTime, or ISO 8601 date strings.Defaults to
nil.max(Date) - Specifies the latest selectable date. Dates after this boundary are visually dimmed and cannot be selected via click or keyboard. Navigation buttons are disabled when they would move past this date. AcceptsDate,DateTime, or ISO 8601 date strings.Defaults to
nil.today(:any) - Sets the date treated as "today", which controls the highlighted current day and the default visible month when no value is selected. AcceptsDate,DateTime,NaiveDateTime, or ISO 8601 strings, normalized to aDate.When omitted, the client highlights the browser's local date, so the current day is correct for the user's time zone with no configuration. Set this to let the server control the value instead: for static mode, a first-paint highlight, deterministic rendering (tests, screenshots), or anchoring to a non-clock date. See the "Time Zones" section for details.
Defaults to
nil.week_start(:integer) - Determines which day of the week appears in the first column of the day grid. When not provided, the value is derived from the component's locale.0: Sunday (common in the US).1: Monday (common in Europe and most ISO locales).6: Saturday (common in some Middle Eastern locales).
Values 0-6 map to Sunday through Saturday respectively.
Defaults to
nil.class(:any) - Additional CSS classes applied to the calendar wrapper element. Useful for controlling width, spacing, or custom styling of the calendar panel.Defaults to
nil.granularity(:string) - Controls the selection precision level and grid layout.day: 7-column grid showing individual days. Navigates by month. Best for appointment dates and deadlines.month: 3-column grid showing 12 months. Navigates by year. Best for billing periods and expiry dates.year: 3-column grid showing 12 years. Navigates by decade. Best for fiscal years and birth years.
Defaults to
"day". Must be one of"day","month", or"year".hide_outside_days(:boolean) - Controls whether the day grid fills the leading and trailing empty cells with days from the previous and next month. Only affectsdaygranularity.false(default): renders a padded six-week grid, with outside-month days dimmed. The grid keeps a constant height across months.true: renders only the current month's days. Leading and trailing cells are left blank and full outside-month weeks are dropped, so the grid uses four to six rows depending on the month and its height changes as you navigate.
Defaults to
false.navigation(:string) - Controls the navigation interface in the calendar header.default: previous/next month arrows with a month-year title. Best for most use cases where dates are near the current month.extended: adds previous/next year arrows for faster traversal. Best for booking systems spanning multiple years.select: month and year dropdown selects with navigation arrows. Best for birth date pickers or historical date selection.
Defaults to
"default". Must be one of"default","extended", or"select".disabled(:boolean) - Disables the entire calendar when set totrue. All date buttons and navigation controls become non-interactive. The calendar remains visible but no selection can be made.Defaults to
false.label(:string) - Sets the primary label text displayed above the calendar. Renders as a<label>element for accessibility.Defaults to
nil.sublabel(:string) - Specifies secondary text displayed inline beside the main label. Useful for adding optional context like "(optional)" or a brief clarification.Defaults to
nil.description(:string) - Provides a longer description rendered as a paragraph below the label. Useful for instructions or additional context about the expected selection.Defaults to
nil.help_text(:string) - Displays helper text below the calendar grid. Useful for formatting hints, selection guidance, or contextual information.Defaults to
nil.errors(:list) - Specifies error messages to display below the calendar. When using thefieldattribute, errors are automatically derived from form validation. Each error renders as a styled error message in red.Defaults to
[].disabled_dates(:list) - Specifies dates, date ranges, or patterns to disable. Disabled dates are visually dimmed with a strikethrough and cannot be selected via click or keyboard. Multiple patterns can be combined in a single list. Accepts:- Specific dates:
~D[2025-01-15] - Date ranges:
Date.range(~D[2025-01-01], ~D[2025-01-10]) - Day shortcuts:
:weekends,:weekdays - Day of month:
{:day, 15}(every month) - Weekday:
{:weekday, 3}(1=Monday, 7=Sunday) - ISO week:
{:week, 33} - Recurring annual dates:
{:month_day, 12, 25} - Month pattern:
{:month, 4}(entire month) - Year pattern:
{:year, 2025}(entire year)
Defaults to
[].- Specific dates:
allow_deselect(:boolean) - Controls whether clicking an already-selected date deselects it. Defaults totrue.- In single mode, clicking the selected date clears the value.
- In multiple mode, at least one date must remain selected when
false. - In range mode, clicking the start date of a complete range won't clear both values.
Set to
falsewhen a selection is always required.Defaults to
true.locale(:string) - Sets the locale for localizing weekday abbreviations, month names, header titles, and accessible labels. Defaults to"en". Unsupported locales fall back to English. Common values includear,de,es,fr,ja,ko,pt-BR, andzh.Defaults to
"en".static(:boolean) - Renders a non-interactive, display-only calendar when set totrue. Removes the navigation header, hidden form inputs, and the JavaScript hook. All day buttons are disabled and removed from the tab order. Selected dates remain visually highlighted. Useful for confirmation screens, read-only summaries, or print layouts.Defaults to
false.