Skip to content
Glint UI

Calendar

A month-grid calendar over a model you supply — days, ranges and per-day states, named by an absolute day serial.

Usage

import { Calendar, CalendarMonth } from "@glint/components/calendar.slint";
export component AppWindow inherits Window {
// Everything about a month is your date library's arithmetic: how long it
// is, what weekday it opens on, and the serial of its first day.
in property <[CalendarMonth]> months;
in-out property <int> chosen;
callback page-back();
callback page-forward();
Calendar {
months: root.months;
selected-serial <=> root.chosen;
day-selected(serial) => { root.chosen = serial; }
// The component holds no date model, so paging is rolling yours.
prev-month => { root.page-back(); }
next-month => { root.page-forward(); }
// If your range is bounded, say where it ends.
can-prev-month: true;
can-next-month: true;
}
}

Slint 1.17 has no Date type and no date arithmetic, so the dates belong to you. You describe each month you want drawn and the calendar renders the grid, walks it, and reports what was picked. It formats nothing and assumes no calendar system, which is what makes a Persian or Hijri month cost exactly what a Gregorian one does.

A day is a serial. It is named by an absolute day number of your own, counted on whatever scale your date library gives you: chrono’s num_days_from_ce(), an epoch day count, anything monotonic and positive. A month carries the serial of its first day and every cell after it counts on from there.

That one number is what lets a single comparison answer “is this day inside the range?” across a month boundary, across two months drawn side by side, and for a range end that is not on screen at all (ADR-0031). A day-of-month cannot: 3 June is not “after” 28 May by it.

Serial 0 is never a day, so it is the empty selection — and what a blank leading or trailing cell carries, which is what makes those cells refuse a click that would otherwise land in the next month.

CalendarMonth is one month of the grid:

FieldWhat it is
labelThe caption, e.g. “May 2026” — it also names the month’s grid to assistive technology
day-countHow many days the month has
first-day-offsetWeekday of day 1, 0 = Sunday — the leading blanks
first-serialThe serial of day 1, one past the last serial of the month before it
week-numbersISO week numbers for the six rows; empty draws no column

CalendarDay is what you know about one day that the grid cannot work out:

FieldWhat it is
disabledAn unavailable day: it dims, refuses every activation and reports itself disabled, while keeping its cell and its place in the month
noteWhat is special about this day — “Booked”, “Public holiday”. It draws a dot under the number and rides the cell’s description

day-states is a flat array indexed from the first rendered day, not a set the cells search: Slint’s expression language has no loop, so membership can only be answered by position (ADR-0031). It degrades well — a read past the end is the default CalendarDay, so supplying fewer states than the grid draws leaves the rest ordinary days.

Examples

Picking a day

CalendarMode.single — the default — is one selected day: each pick replaces selected-serial and reports itself through day-selected(serial). Map the serial back through the model you built it from.

A range

CalendarMode.range takes two picks: the first opens the interval, the second closes it and reports the pair through range-selected(from, to). They are the ends in either order, so a user who draws backwards gets the range they drew rather than a dead end, and picking again once a range is complete starts the next one.

While an interval is forming, the day under the pointer is its open end — preview-serial, which paints the band and is what a trigger shows before the second pick lands. It is paint rather than state: a preview is something the pointer is doing, so only a settled range announces itself.

Two months at once

Give months more than one entry and they are drawn side by side from the one instance, each grid named for its own month. The serials have to be consecutive across them — May’s 31 days run out at 739767 and June opens at 739768 — which is what makes a range whose ends sit in two different grids one interval rather than two.

The chevrons page the whole strip, because you are the one rolling the model.

Unavailable days, and days with a note

day-states says what you know about each day. A disabled day dims, refuses the pointer, Enter and the accessible default action alike, and still keeps its cell and its index — the way every Glint control refuses. A note draws a dot under the number and is read after the day’s name, because the accessibility tree has no property for “this day is special” (ADR-0013).

The array is indexed from the first rendered day, so its first entry is 1 May.

Paging, and the bounds

The calendar has no notion of a bound of its own — the year/month model is yours — so a host that restricts a range says so with can-prev-month and can-next-month. Both default to true, so an unbounded calendar is unchanged. A chevron at a bound dims, fires nothing and reports itself disabled, the same contract Pagination’s steps carry at the ends of their range.

Here the model is three months and the chevrons roll it. Walking off the edge of the grid with the arrow keys pages too, and the focus lands on the day you aimed at once the new month arrives.

The caption dropdowns

CalendarCaption.label is the month’s own text. CalendarCaption.dropdown replaces it with a month picker and a year picker, so a distant month is one pick away instead of forty chevron clicks.

The dropdowns pick nothing: each reports the index it was given through month-picked(index) / year-picked(index) and you roll months to match — the same division can-prev-month already draws. Their open list is a card drawn inside the calendar rather than a popup of its own, because the calendar may already be inside one (ADR-0020), and closing it hands the keyboard to the day grid, which is where a user picking a month was going.

Week numbers

A month that carries week-numbers prepends them as a leading column under week-number-label; a month that carries none draws no column at all. There are six entries, one per row of the grid, and they are yours for the same reason the rest of the model is: an ISO week number is arithmetic the component can no more do than it can name a month.

A Monday-first week

weekday-labels is the column header, left to right — seven entries in the grid’s own order, which the defaults spell Sunday-first. Pass your own to translate them, to spell them out, or to start the week on Monday: rotate the labels and shift first-day-offset in your model to match, since the grid counts the offset in the same order it draws the header.

Every other string the calendar ships is a property too, and they all default through the CalendarStrings global below.

API Reference

Properties

PropertyTypeDefaultDescription
monthsin [CalendarMonth][ }]The months to draw, left to right. One is the ordinary calendar; more than one is the two-month range view, paged as a strip.
day-statesin [CalendarDay]no defaultPer-day states, indexed from the first rendered day — index 0 is months[0].first-serial. Shorter than the grid, or absent, is a grid of plain days: a read past the end is the default CalendarDay.
weekday-labelsin [string]CalendarStrings.weekday-initialsColumn headers, left to right — seven entries matching the grid's Sunday-first order. Defaults to the English initials (translatable); pass your own for another language or a Monday-first week (rotate the labels and shift first-day-offset in your model to match).
modein CalendarModeCalendarMode.singleWhether a pick is a day or an end of an interval.
selected-serialin-out int0Two-way; the serial of the selected day, or 0 for none. A range calendar leaves it alone — its selection is the two ends below.
range-fromin-out int0Two-way; the ends of the selected interval, or 0 for none. range-to is 0 while the range is still forming, which is what makes the next pick close the interval rather than start another.
range-toin-out int0
focused-serialin-out int0Two-way; where the keyboard stands. 0 falls back to the selection, and then to the first day on screen — so a consumer that never touches it gets a sensible starting cell, and one that does can place the walk.
can-prev-monthin booltrueWhether there is a month to page to in each direction. The component has no notion of a bound of its own — the year/month model belongs to the consumer, which is what lets a host page the calendar however it likes — so a host that restricts a range says so here. A chevron at a bound dims, fires nothing and reports itself disabled, the same contract Pagination's steps carry at the ends of their range. Both default to true, so a host that never sets them keeps two live chevrons and behaves exactly as before.
can-next-monthin booltrue
prev-month-labelin stringCalendarStrings.previous-monthAccessible names for the two chevrons. The month is not in them: a screen reader reads the button's name, and "previous month" is what it does regardless of which month is on screen.
next-month-labelin stringCalendarStrings.next-month
captionin CalendarCaptionCalendarCaption.labelWhat stands between the chevrons: the months' own captions, or the month and year dropdowns. The dropdowns drive the view rather than one of its grids, so a strip of months has one pair of them and each grid keeps its own name in the accessibility tree.
month-optionsin [string]no defaultWhat the two dropdowns offer, and which entry each stands on. The component neither builds nor formats these — same division as the rest of the model — and it picks no month either: it reports the index and the host rolls months to match.
year-optionsin [string]no default
month-indexin int-1
year-indexin int-1
month-picker-labelin stringCalendarStrings.monthThe dropdowns' own names, as against the values they stand on.
year-picker-labelin stringCalendarStrings.year
week-number-labelin stringCalendarStrings.week-numberHeading of the week-number column, for the months that carry one.
in-range-notein stringCalendarStrings.in-rangeWhat a day between the two ends of a settled range announces. The tree has no property for "inside the selection", so it rides the cell's description (ADR-0013).
auto-focusin boolfalseTake the keyboard as the grid is built. A Panel rider cannot reach in and focus it — focus() does not cross the PopupWindow boundary (panel.slint), and calling a public function on content inlined into one is rejected outright — so the surface asks for it with a property instead. DatePicker sets it, which is what makes the arrows, Home/End, the pages and Enter reachable at all inside its popup. Same shape Command uses for its search field.
preview-serialout intno defaultThe open end of a forming range: the day under the pointer while range-from is set and range-to is not. 0 the rest of the time — including over a complete range, which has both its ends already. It is what paints the preview, and what a trigger shows before the second pick lands.

Callbacks

CallbackDescription
day-selected(int)Fired when the user picks a day, by pointer, by Enter or through the accessible default action. Carries the day's serial; a day the host disabled fires nothing at all.
range-selected(int, int)Fired when the second end of a range lands, with the two ends in order. A range calendar reports both: day-selected for each pick, this once the interval is whole.
month-picked(int)Fired with the index of the month or year taken from the caption dropdown.
year-picked(int)
prev-month()Fired when the previous-month chevron is clicked, unless it is at a bound.
next-month()Fired when the next-month chevron is clicked, unless it is at a bound.

Enums

EnumValues
CalendarModesingle, range
CalendarCaptionlabel, dropdown

CalendarStrings

Every string the calendar ships as a default, in one place. A composite wrapping a Calendar cannot alias what is inside a PopupWindow, so DatePicker declares the same properties and defaults them from here — spelling them twice is how the two would drift apart.

Properties

PropertyTypeDefaultDescription
weekday-initialsout [string]no default
previous-monthout stringno default
next-monthout stringno default
week-numberout stringno default
monthout stringno default
yearout stringno default
in-rangeout stringno default

Accessibility

  • A month is a table, and a day is one of its cells. Each grid carries the table role under its own label, and every day of the month is a list-item naming itself — “14 May 2026” — with its index and the month’s own day count, so assistive technology can say “14 of 31”.
  • Selecting a day is its default action, which is what lets a screen reader pick one without a pointer. The cell reports itself selected, and in a range both ends do.
  • A blank is nothing at all. The leading and trailing cells outside the month are in no accessibility tree, take no click, and carry serial 0 — which the one refusal path turns away, so the six dead cells under May cannot pick June 1–6 by any route.
  • A disabled day stays in the grid. It keeps its cell and its index, reports itself disabled, and refuses the pointer, Enter and the accessible default action alike.
  • What has no property rides the description. A day’s note and, inside a settled range, in-range-note are read after the day’s name, because the tree has no property for either (ADR-0013).
  • The chevrons are named buttons. prev-month-label and next-month-label name them; the month is deliberately not in the name, because “previous month” is what the button does whichever month is on screen. At a bound a chevron dims, leaves the tab order and reports itself disabled.
  • The caption dropdowns are comboboxes, each named for what it is (“Month”, “Year”) and reporting the value it stands on. Their open list is a list whose rows are its items, so a screen reader can say “5 of 12”.
  • The grid is one tab stop, not forty-two. / move a day, / a week, Home / End reach the ends of the focused week without leaving the strip, and Page Up / Page Down are the chevrons under another name. Enter and Space pick whatever the walk landed on.
  • A walk off the strip pages. The focus goes to the day you aimed at and the host’s model catches up; at a bound the move is refused outright, because nothing will be drawn there.
  • Keyboard-only focus ring, around the day the arrows landed on rather than around the whole grid.