# DatePicker

A calendar in a popup, opened by a trigger you own and closed by the pick.

```slint
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

```slint
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](/docs/components/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`.

```slint
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.

```slint
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.

```slint
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`.

```slint
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: true` on a [Button](/docs/components/button) is that.
- **The grid takes the keyboard on its own way in.** Nothing outside a `PopupWindow` can 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](/docs/components/calendar#accessibility).
- **`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](/docs/components/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.
