A monthly calendar grid widget with date selection and keyboard navigation.
Displays a single month at a time with a title row showing the month name and year, a weekday header row (Su Mo Tu We Th Fr Sa), and up to six week rows. Today's date is highlighted automatically, and the selected date receives a distinct visual treatment.
Component tag
Tag :calendar, built by Drafter.App as {:calendar, opts}:
calendar(opts)There is no positional argument; every prop comes from opts.
from_component_opts/2 wraps :on_select with Drafter.Widget.Callback, so
it may be given as an atom event name.
Options
:selected_date-Date.t/0to highlight. Defaultnil. The cursor starts on this date, or onDate.utc_today/0when it isnil:on_select- atom event name or one-arity function receiving the confirmedDate.t/0. Defaultnil. Its return value is discarded, so it cannot emit an action; use it for its side effect:min_date- earliest navigableDate.t/0. Defaultnil, no constraint:max_date- latest navigableDate.t/0. Defaultnil, no constraint:style-map/0of inline style overrides. Default%{}:class- theme class atom or list of them, reachingmount/1as:classes. Default[]
update/2 re-reads :selected_date, :min_date, :max_date, :on_select,
:style and :classes. :cursor_date and :view_date are owned by the widget
and survive a re-render, so changing :selected_date after mount moves the
highlight but not the cursor or the displayed month.
Widget value
Drafter.get_widget_value/1 is not implemented for this widget; the chosen date
is reported through :on_select.
Key bindings
←/→— move cursor one day backward/forward↑/↓— move cursor one week backward/forwardEnter/Space— select the highlighted date and fire:on_select
A move that would leave the :min_date..:max_date range is ignored. Crossing a
month boundary scrolls the view to the new month. Every other key bubbles.
Usage
calendar(selected_date: ~D[2026-08-02], on_select: :date_chosen)
Summary
Functions
The registry tag for this widget.
Turns the {:calendar, opts} element into a props map for mount/1.
Callback implementation for Drafter.Widget.handle_event/2.
Moves the cursor or confirms the date under it.
Selects the date under the pressed cell.
Builds the calendar state from props.
Always 9: a title row, a weekday header row and six week rows.
Draws the month into rect.
Callback implementation for Drafter.Widget.unmount/1.
Folds fresh props into state.
Returns mount_props unchanged, so a re-render passes every option through to
update/2.
Types
Functions
@spec component_tag() :: :calendar
The registry tag for this widget.
iex> Drafter.Widget.Calendar.component_tag()
:calendar
@spec from_component_opts( term(), keyword() ) :: Drafter.Widget.props()
Turns the {:calendar, opts} element into a props map for mount/1.
The positional argument is ignored; every prop comes from opts. :class is
normalised into :classes and :on_select is wrapped by
Drafter.Widget.Callback.wrap_1/1.
iex> props = Drafter.Widget.Calendar.from_component_opts(nil, selected_date: ~D[2026-08-02])
iex> {props.selected_date, props.on_select, props.min_date, props.style, props.classes}
{~D[2026-08-02], nil, nil, %{}, []}
Callback implementation for Drafter.Widget.handle_event/2.
@spec handle_key(Drafter.Widget.key(), t()) :: {:ok, t()} | {:bubble, t()}
Moves the cursor or confirms the date under it.
Arrow keys return {:ok, state}, with the state unchanged when the target date
falls outside :min_date..:max_date. :enter and :" " set :selected_date
to the cursor date, call :on_select, and return {:ok, state}. Every other key
returns {:bubble, state}.
iex> cal = Drafter.Widget.Calendar.mount(%{selected_date: ~D[2026-08-02]})
iex> {:ok, moved} = Drafter.Widget.Calendar.handle_key(:down, cal)
iex> moved.cursor_date
~D[2026-08-09]
iex> cal = Drafter.Widget.Calendar.mount(%{selected_date: ~D[2026-08-02]})
iex> {:ok, moved} = Drafter.Widget.Calendar.handle_key(:left, cal)
iex> {moved.cursor_date, moved.view_date}
{~D[2026-08-01], ~D[2026-08-01]}
iex> cal = Drafter.Widget.Calendar.mount(%{selected_date: ~D[2026-08-02], min_date: ~D[2026-08-02]})
iex> {:ok, blocked} = Drafter.Widget.Calendar.handle_key(:left, cal)
iex> blocked.cursor_date
~D[2026-08-02]
iex> cal = Drafter.Widget.Calendar.mount(%{selected_date: ~D[2026-08-31]})
iex> {:ok, next} = Drafter.Widget.Calendar.handle_key(:right, cal)
iex> {next.cursor_date, next.view_date}
{~D[2026-09-01], ~D[2026-09-01]}
iex> cal = Drafter.Widget.Calendar.mount(%{selected_date: ~D[2026-08-02]})
iex> {:ok, chosen} = Drafter.Widget.Calendar.handle_key(:enter, cal)
iex> chosen.selected_date
~D[2026-08-02]
iex> cal = Drafter.Widget.Calendar.mount(%{selected_date: ~D[2026-08-02]})
iex> Drafter.Widget.Calendar.handle_key(:escape, cal) |> elem(0)
:bubble
Selects the date under the pressed cell.
Rows 2 through 7 are the week rows and every four columns are one weekday
column, so the cell is {div(x, 4), y - 2}. A press on the title or header row,
or beyond the seventh column, returns {:ok, state} unchanged. A hit moves the
cursor to that date, sets :selected_date, and calls :on_select; the
:min_date/:max_date range is not applied on this path.
@spec mount(Drafter.Widget.props()) :: t()
Builds the calendar state from props.
The cursor starts on :selected_date, or on Date.utc_today/0 when that is
nil, and the view opens on the first day of the cursor's month.
iex> cal = Drafter.Widget.Calendar.mount(%{selected_date: ~D[2026-08-02]})
iex> {cal.cursor_date, cal.view_date, cal.selected_date}
{~D[2026-08-02], ~D[2026-08-01], ~D[2026-08-02]}
iex> cal = Drafter.Widget.Calendar.mount(%{})
iex> {cal.selected_date, cal.min_date, cal.max_date, cal.classes, cal.style}
{nil, nil, nil, [], %{}}
@spec preferred_height( term(), keyword() ) :: pos_integer()
Always 9: a title row, a weekday header row and six week rows.
iex> Drafter.Widget.Calendar.preferred_height(nil, [])
9
@spec render(t(), Drafter.Widget.rect()) :: [Drafter.Draw.Strip.t()]
Draws the month into rect.
Always returns nine strips: a title row, a weekday header row, up to six week
rows, and blank rows making up the difference. rect.height is not consulted, and
rect.width is widened to at least 28 columns, four per weekday column.
Callback implementation for Drafter.Widget.unmount/1.
@spec update(Drafter.Widget.props(), t()) :: t()
Folds fresh props into state.
Re-reads :selected_date, :min_date, :max_date, :on_select, :style and
:classes. :cursor_date and :view_date are left untouched, so the displayed
month does not follow a new :selected_date.
iex> cal = Drafter.Widget.Calendar.mount(%{selected_date: ~D[2026-08-02]})
iex> updated = Drafter.Widget.Calendar.update(%{selected_date: ~D[2026-12-25]}, cal)
iex> {updated.selected_date, updated.view_date}
{~D[2026-12-25], ~D[2026-08-01]}
@spec update_props_from_mount(Drafter.Widget.props(), t(), keyword()) :: Drafter.Widget.props()
Returns mount_props unchanged, so a re-render passes every option through to
update/2.