DatePicker
A calendar in a popup, opened by a trigger you own and closed by the pick.
import { Button, ButtonVariant } from "@glint/components/button.slint";import { DatePicker } from "@glint/components/date-picker.slint";import { Tokens } from "@glint/theme/tokens.slint";import { IconSet } from "@lucide";
export component Demo inherits Window { width: 420px; height: 460px; background: Tokens.color-background;
in-out property <int> chosen: 0; // Glint formats no dates: the label is arithmetic on your own serials. // 1 May 2026 is serial 739737 on this host's scale, so subtracting the // day before it turns a serial back into a day of the month. property <string> label: root.chosen == 0 ? "Pick a date" : (root.chosen - 739736) + " May 2026";
VerticalLayout { padding: 24px; alignment: start;
HorizontalLayout { alignment: start;
picker := DatePicker { months: [{ label: "May 2026", day-count: 31, first-day-offset: 5, first-serial: 739737, }]; selected-serial <=> root.chosen;
Button { variant: ButtonVariant.outline; leading-icon: IconSet.CalendarDays; text: root.label; haspopup: true; // The pointer opens it for free; this is the keyboard path. clicked => { picker.show(); } } } } }}Usage
import { CalendarMonth } from "@glint/components/calendar.slint";import { DatePicker } from "@glint/components/date-picker.slint";import { Button } from "@glint/components/button.slint";
export component AppWindow inherits Window { in property <[CalendarMonth]> months; in-out property <int> chosen; in property <string> label;
picker := DatePicker { months: root.months; selected-serial <=> root.chosen; day-selected(serial) => { /* store it */ } prev-month => { /* roll your model back one month */ } next-month => { /* roll your model forward one month */ }
Button { text: root.label; haspopup: true; clicked => { picker.show(); } } }}The trigger is yours. Glint’s overlay components never own it: whatever
you slot into the picker is what opens the popup, and the picker publishes the
state a trigger needs to reflect — is-open, and the selection itself. A
pointer click anywhere on the trigger opens the popup for free; wire your
trigger’s keyboard activation to show() so the calendar is reachable without
a mouse.
Everything below the trigger is Calendar’s: the
months are yours to describe, a day is an absolute serial, and paging is
rolling your own model. The picker keeps no date model of its own — there is
none to keep — so every calendar property is forwarded rather than
aliased: a PopupWindow’s insides cannot be reached from outside it, which is
also why the calendar’s strings are declared here again and defaulted from the
same CalendarStrings global.
show() and close() mirror PopupWindow’s API (ADR-0006). Esc
and a click outside dismiss the calendar, and closing — by Esc, by
a click outside, or by picking a day — returns the keyboard focus to whatever
held it before, which is your trigger.
Examples
A single date
CalendarMode.single closes the popup on the pick: one day, one gesture, and
the focus back on the trigger ready for the next Enter.
import { Button, ButtonVariant } from "@glint/components/button.slint";import { CalendarMode } from "@glint/components/calendar.slint";import { DatePicker } from "@glint/components/date-picker.slint";import { Tokens } from "@glint/theme/tokens.slint";import { IconSet } from "@lucide";
export component Demo inherits Window { width: 420px; height: 460px; background: Tokens.color-background;
in-out property <int> chosen: 0; property <string> label: root.chosen == 0 ? "Pick a date" : (root.chosen - 739736) + " May 2026";
VerticalLayout { padding: 24px; spacing: 12px; alignment: start;
HorizontalLayout { alignment: start;
picker := DatePicker { mode: CalendarMode.single; months: [{ label: "May 2026", day-count: 31, first-day-offset: 5, first-serial: 739737, }]; selected-serial <=> root.chosen;
Button { // The trigger reflects the open state, because it is // yours to draw. variant: picker.is-open ? ButtonVariant.secondary : ButtonVariant.outline; leading-icon: IconSet.CalendarDays; text: root.label; haspopup: true; clicked => { picker.show(); } } } }
Text { text: picker.is-open ? "The calendar is open." : "The calendar is closed."; color: Tokens.color-muted-foreground; font-size: Tokens.typography-body-sm-size; } }}A range
CalendarMode.range keeps the popup up until both ends are in: the first pick
opens the interval and leaves the calendar on screen, the second closes both
the interval and the popup. The trigger is @children, so showing “5 – 9 May”
in it is yours — bind to range-from, range-to, and to preview-serial for
the end still under the pointer.
import { Button, ButtonVariant } from "@glint/components/button.slint";import { CalendarMode } from "@glint/components/calendar.slint";import { DatePicker } from "@glint/components/date-picker.slint";import { Tokens } from "@glint/theme/tokens.slint";import { IconSet } from "@lucide";
export component Demo inherits Window { width: 460px; height: 460px; background: Tokens.color-background;
in-out property <int> from-day: 0; in-out property <int> to-day: 0; property <int> forming: picker.preview-serial;
// Mapping a serial back to a date is the host's, because the scale is. // May 2026 runs 739737 … 739767; June opens the day after. pure function day-label(serial: int) -> string { return serial <= 739767 ? (serial - 739736) + " May" : (serial - 739767) + " June"; }
property <string> label: root.from-day == 0 ? "Pick your dates" : (root.to-day != 0 ? root.day-label(root.from-day) + " – " + root.day-label(root.to-day) : (root.forming != 0 ? root.day-label(root.from-day) + " – " + root.day-label(root.forming) + "?" : root.day-label(root.from-day) + " – …"));
VerticalLayout { padding: 24px; alignment: start;
HorizontalLayout { alignment: start;
picker := DatePicker { mode: CalendarMode.range; months: [ { label: "May 2026", day-count: 31, first-day-offset: 5, first-serial: 739737 }, { label: "June 2026", day-count: 30, first-day-offset: 1, first-serial: 739768 }, ]; content-width: 580px; range-from <=> root.from-day; range-to <=> root.to-day;
Button { variant: ButtonVariant.outline; leading-icon: IconSet.CalendarDays; text: root.label; haspopup: true; clicked => { picker.show(); } } } } }}Reaching a distant month from inside the popup
caption is forwarded like every other calendar property, so
CalendarCaption.dropdown puts the month and year pickers inside the popup —
which is what makes a birthday twenty years back a pick rather than two
hundred and forty chevron clicks. The dropdowns report an index and page
nothing themselves, exactly as they do on a bare calendar.
import { Button, ButtonVariant } from "@glint/components/button.slint";import { CalendarCaption } from "@glint/components/calendar.slint";import { DatePicker } from "@glint/components/date-picker.slint";import { Tokens } from "@glint/theme/tokens.slint";import { IconSet } from "@lucide";
export component Demo inherits Window { width: 440px; height: 460px; background: Tokens.color-background;
in-out property <string> note: "The caption inside the popup picks the month and the year.";
VerticalLayout { padding: 24px; spacing: 12px; alignment: start;
HorizontalLayout { alignment: start;
picker := DatePicker { caption: CalendarCaption.dropdown; months: [{ label: "May 2026", day-count: 31, first-day-offset: 5, first-serial: 739737, }]; month-options: [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ]; year-options: ["2024", "2025", "2026", "2027"]; month-index: 4; year-index: 2; month-picked(index) => { root.note = "Month " + index + " — roll your model to it."; } year-picked(index) => { root.note = "Year " + index + " — roll your model to it."; }
Button { variant: ButtonVariant.outline; leading-icon: IconSet.CalendarDays; text: "May 2026"; haspopup: true; clicked => { picker.show(); } } } }
Text { text: root.note; color: Tokens.color-muted-foreground; font-size: Tokens.typography-body-sm-size; } }}Bounds and unavailable days
can-prev-month, can-next-month and day-states pass straight through to
the calendar inside the popup. The picker computes none of them, for the
reason it computes nothing else: it holds no date model, so a host that
restricts a range works the two bounds out the same way it works out
day-count.
import { Button, ButtonVariant } from "@glint/components/button.slint";import { DatePicker } from "@glint/components/date-picker.slint";import { Tokens } from "@glint/theme/tokens.slint";import { IconSet } from "@lucide";
export component Demo inherits Window { width: 440px; height: 460px; background: Tokens.color-background;
in-out property <int> chosen: 0; property <string> label: root.chosen == 0 ? "Pick a delivery day" : (root.chosen - 739736) + " May 2026";
VerticalLayout { padding: 24px; spacing: 12px; alignment: start;
HorizontalLayout { alignment: start;
picker := DatePicker { months: [{ label: "May 2026", day-count: 31, first-day-offset: 5, first-serial: 739737, }]; // May is the only month this host offers. can-prev-month: false; can-next-month: false; // The first three days are already booked out. day-states: [ { disabled: true }, { disabled: true }, { disabled: true, note: "Fully booked" }, ]; selected-serial <=> root.chosen;
Button { variant: ButtonVariant.outline; leading-icon: IconSet.CalendarDays; text: root.label; haspopup: true; clicked => { picker.show(); } } } }
Text { text: "Both chevrons are at a bound, so they dim and leave the tab order."; color: Tokens.color-muted-foreground; font-size: Tokens.typography-body-sm-size; wrap: word-wrap; } }}API Reference
Properties
| Property | Type | Default | Description |
|---|---|---|---|
months | in [CalendarMonth] | [ }] | The months the popup calendar draws — see Calendar.months. |
day-states | in [CalendarDay] | no default | Per-day states, indexed from the first rendered day — see Calendar.day-states. |
weekday-labels | in [string] | CalendarStrings.weekday-initials | Column headers of the popup calendar — see Calendar.weekday-labels. A PopupWindow's insides are unreachable from here, so this cannot alias the Calendar's property; it takes its default from the same global the Calendar does and is passed down instead. Every string below is here for that reason. |
mode | in CalendarMode | CalendarMode.single | Whether a pick is a day or an end of an interval — see Calendar.mode. A range picker stays open until both ends are in. |
selected-serial | in-out int | 0 | Two-way; the serial of the selected day, or 0 for none. |
range-from | in-out int | 0 | Two-way; the ends of the selected interval, or 0 for none. The trigger is @children, so showing "5 – 9 May" in it is yours to do: bind to these two, and to preview-serial for the end still under the pointer. |
range-to | in-out int | 0 | |
preview-serial | out int | no default | The open end of a forming range — see Calendar.preview-serial. |
can-prev-month | in bool | true | Whether the popup calendar has a month to page to in each direction — forwarded straight to Calendar. The picker keeps no date model of its own (there is none to keep: Slint has no date arithmetic), so a host restricting a range computes the two bounds and passes them through, exactly as it computes day-count. |
can-next-month | in bool | true | |
prev-month-label | in string | CalendarStrings.previous-month | Accessible names for the popup's chevrons, and the week-number column heading — see Calendar. Repeated here for the same reason weekday-labels is: a PopupWindow's insides cannot be aliased from out here. |
next-month-label | in string | CalendarStrings.next-month | |
week-number-label | in string | CalendarStrings.week-number | |
in-range-note | in string | CalendarStrings.in-range | What a day inside a settled range announces — see Calendar.in-range-note. |
caption | in CalendarCaption | CalendarCaption.label | The popup calendar's caption, and the two models its dropdowns offer — see Calendar.caption. Forwarded rather than aliased, like everything else that lives inside the PopupWindow; a consumer that wants a year reachable from inside the popup sets these. |
month-options | in [string] | no default | |
year-options | in [string] | no default | |
month-index | in int | -1 | |
year-index | in int | -1 | |
month-picker-label | in string | CalendarStrings.month | |
year-picker-label | in string | CalendarStrings.year | |
content-width | in length | 296px | Pixel width of the popup panel. |
is-open | out bool | no default | True while the calendar is on screen. The trigger is @children, so reflecting the open state in it is yours to do — bind to this. |
Callbacks
| Callback | Description |
|---|---|
day-selected(int) | Fired with the chosen day's serial. In single mode the popup closes on it; in range mode it fires for each of the two picks and the popup stays up until the second one closes the interval. |
range-selected(int, int) | Fired with the two ends of a completed range, in order; the popup closes on it. |
prev-month() | Forwarded from the inner Calendar header chevrons and its caption dropdowns. |
next-month() | |
month-picked(int) | |
year-picked(int) |
Functions
| Function | Description |
|---|---|
show() | The overlay API mirrors PopupWindow's, per ADR-0006. Wire your trigger's keyboard activation to show(). |
close() | Hide the calendar. Focus returns to whatever held it before. |
Enums
| Enum | Values |
|---|---|
CalendarMode | single, range |
CalendarCaption | label, dropdown |
Accessibility
- The trigger’s accessibility is yours, because the trigger is. Name it,
and mark it as opening a popup —
haspopup: trueon a Button is that. - The grid takes the keyboard on its own way in. Nothing outside a
PopupWindowcan focus what is inside it, so the calendar grabs the keyboard as it is built. Without that the picker would open and answer no arrow, no page and no Enter — the pointer and an accessible action would be the only ways to pick a day. - Everything the calendar announces, it announces here: a month is a table, a day is a named cell that reports its index and whether it is selected, a disabled day refuses every path, and a note rides the description. See Calendar.
- Esc dismisses, and the focus comes back. The picker keeps no Esc handler of its own — dismissal is the shared overlay surface’s — so a picker opened inside a Popover takes the first Esc and the popover under it survives.
- A range picker stays up between the two picks, so a keyboard user finishes the interval in the surface they started it in.