DatePicker

A flexible date picker component with calendar interface that supports single date selection, date range selection, custom date formatting, disabled dates, and preset ranges.

Install

#Import

use gpui_kit::component::{
    date_picker::{DatePicker, DatePickerState, DateRangePreset, DatePickerEvent, DateTime},
    calendar::{Date, Matcher},
    time_field::{HourCycle, TimePrecision},
};
// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { DatePicker } from "date-picker.slint";
use iced::keyboard;
use iced::widget::{button, column, container, mouse_area, row, scrollable, svg, text};
use iced::{Alignment, Background, Border, Color, Element, Length, Subscription, Task, Theme};

#Usage

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// The whole of a date picker: the month the calendar shows, the chosen day,
// the open flag and the three reports. `new` takes the shown month, which the
// caller owns because iced keeps no state; `on_shift` moves it, `on_select`
// reports a day, and `on_open_change` reports every dismissal the popover
// answers — a trigger click, an outside click and Escape. Setting
// `with_time(true)` adds the time row under the calendar, and `size` and
// `block_weekends` are the Slint component's own two properties.
let usage = body_box(
    column![
        DatePicker::new(app.view[slot::USAGE_DATE].0, app.view[slot::USAGE_DATE].1)
            .date(app.date[slot::USAGE_DATE])
            .open(app.open[slot::USAGE_DATE])
            .placeholder("Select a date")
            .width(Length::Fixed(FIELD_WIDTH))
            .on_open_change(|open| Message::Opened {
                slot: slot::USAGE_DATE,
                open,
            })
            .on_select(|date| Message::Picked {
                slot: slot::USAGE_DATE,
                date,
            })
            .on_shift(|delta| Message::Shifted {
                slot: slot::USAGE_DATE,
                delta,
            }),
        picker(app, slot::USAGE_TIME),
        row![
            picker(app, slot::USAGE_SMALL),
            picker(app, slot::USAGE_MEDIUM),
            picker(app, slot::USAGE_LARGE),
        ]
        .spacing(12.0),
        muted_line(concat!(
            "The first field is a date; the second adds the time row. ",
            "Below it are the small, medium and large triggers."
        )),
    ]
    .spacing(12.0),
);

#Date

let date_picker = cx.new(|cx| DatePickerState::new(window, cx));

DatePicker::new(&date_picker)
DatePicker {
    year: 2026;
    month: 10;
    day: 2;
}

#With Initial Date

use chrono::Local;

let date_picker = cx.new(|cx| {
    let mut picker = DatePickerState::new(window, cx);
    picker.set_date(Local::now().naive_local().date(), window, cx);
    picker
});

DatePicker::new(&date_picker)
SlintNo Slint version of this example yet.

#Date Range Picker

use chrono::{Local, Days};

// Range mode picker
let range_picker = cx.new(|cx| DatePickerState::range(window, cx));

DatePicker::new(&range_picker)
    .number_of_months(2) // Show 2 months for easier range selection

// With initial range
let range_picker = cx.new(|cx| {
    let now = Local::now().naive_local().date();
    let mut picker = DatePickerState::new(window, cx);
    picker.set_date(
        (now, now.checked_add_days(Days::new(7)).unwrap()),
        window,
        cx,
    );
    picker
});

DatePicker::new(&range_picker)
    .number_of_months(2)
DatePicker {
    range: true;
    months: 2;
    empty: true;
    placeholder: "Select a range";
    year: 2026;
    month: 10;
    day: 2;
}

#Date and Time

Set a time_precision to edit the time of day as well. The popup then shows a time field below the calendar and stays open after a date is picked, so the time can be adjusted next. Every edit is reported as it happens. Clicking the selected date again — or double-clicking a date — confirms it and closes the popup, as do Enter, Escape and a click outside.

use chrono::NaiveTime;

let date_time_picker = cx.new(|cx| {
    DatePickerState::new(window, cx)
        .time_precision(TimePrecision::Minute) // or TimePrecision::Second
        .default_time(NaiveTime::from_hms_opt(9, 0, 0).unwrap())
});

DatePicker::new(&date_time_picker)
VerticalLayout {
    spacing: 8px;
    when := DatePicker {
        with-time: true;
        year: 2026;
        month: 10;
        day: 2;
        hour: 9;
        minute: 30;
    }
    Text {
        text: "Value: \{when.year}-\{when.month < 10 ? "0" : ""}\{when.month}-\{when.day < 10 ? "0" : ""}\{when.day} \{when.hour < 10 ? "0" : ""}\{when.hour}:\{when.minute < 10 ? "0" : ""}\{when.minute}";
        color: Theme.muted-foreground;
        font-size: Theme.font-size-sm;
    }
}

The time uses a 24-hour clock by default. Use hour_cycle for a 12-hour clock with an AM/PM segment:

DatePickerState::new(window, cx)
    .time_precision(TimePrecision::Minute)
    .hour_cycle(HourCycle::H12) // 09:30 PM
SlintNo Slint version of this example yet.

default_time is the time a date gets before the user edits it, 00:00 unless configured. The display format follows the precision and hour cycle (for example %Y/%m/%d %H:%M or %Y/%m/%d %I:%M %p) unless date_format is set.

Read and write the whole value with date_time and set_date_time; date and set_date still address the date part and keep the current time.

use chrono::Local;

date_time_picker.update(cx, |state, cx| {
    state.set_date_time(Local::now().naive_local(), window, cx);
});

if let DateTime::Single(Some(at)) = date_time_picker.read(cx).date_time() {
    println!("Selected {at}");
}
SlintNo Slint version of this example yet.

A range picker edits dates only, even with a time_precision. For a range with times, place two pickers side by side and validate the order in the owner:

h_flex()
    .gap_2()
    .child(DatePicker::new(&start_picker))
    .child("–")
    .child(DatePicker::new(&end_picker))
SlintNo Slint version of this example yet.

In the time field, Up/Down change the selected segment, Left/Right and Tab/Shift-Tab move between segments, digits type a value and advance to the next segment, a/p set AM or PM, and Backspace resets the segment.

#With Custom Date Format

let date_picker = cx.new(|cx| {
    DatePickerState::new(window, cx)
        .date_format("%Y-%m-%d") // ISO format
});

DatePicker::new(&date_picker)

// Other format examples:
// "%m/%d/%Y" -> 12/25/2023
// "%B %d, %Y" -> December 25, 2023
// "%d %b %Y" -> 25 Dec 2023
SlintNo Slint version of this example yet.

#With Placeholder

DatePicker::new(&date_picker)
    .placeholder("Select a date...")
SlintNo Slint version of this example yet.

#Cleanable Date Picker

DatePicker::new(&date_picker)
    .cleanable(true) // Show clear button when date is selected
SlintNo Slint version of this example yet.

#Different Sizes

DatePicker::new(&date_picker).large()
DatePicker::new(&date_picker) // medium (default)
DatePicker::new(&date_picker).small()
DatePicker {
    size: ControlSize.small;
    year: 2026;
    month: 10;
    day: 2;
}
DatePicker {
    year: 2026;
    month: 10;
    day: 2;
}
DatePicker {
    size: ControlSize.large;
    year: 2026;
    month: 10;
    day: 2;
}

#Disabled State

DatePicker::new(&date_picker).disabled(true)
SlintNo Slint version of this example yet.

#Custom Appearance

// Without default styling
DatePicker::new(&date_picker).appearance(false)

// Use in custom container
div()
    .border_b_2()
    .px_6()
    .py_3()
    .border_color(cx.theme().border)
    .bg(cx.theme().secondary)
    .child(DatePicker::new(&date_picker).appearance(false))
SlintNo Slint version of this example yet.

#Event Date Picker

let event_date = cx.new(|cx| {
    let mut picker = DatePickerState::new(window, cx)
        .date_format("%B %d, %Y")
        .disabled_matcher(calendar::Matcher::custom(|date| {
            // Disable past dates
            *date < Local::now().naive_local().date()
        }));
    picker
});

DatePicker::new(&event_date)
    .placeholder("Choose event date")
    .cleanable(true)
SlintNo Slint version of this example yet.

#Booking System Date Range

let booking_range = cx.new(|cx| DatePickerState::range(window, cx));

let booking_presets = vec![
    DateRangePreset::range("This Weekend", /* weekend dates */),
    DateRangePreset::range("Next Week", /* next week dates */),
    DateRangePreset::range("This Month", /* this month dates */),
];

DatePicker::new(&booking_range)
    .number_of_months(2)
    .presets(booking_presets)
    .placeholder("Select check-in and check-out dates")
SlintNo Slint version of this example yet.

#Financial Period Selector

let financial_period = cx.new(|cx| {
    DatePickerState::range(window, cx)
        .date_format("%Y-%m-%d")
});

DatePicker::new(&financial_period)
    .number_of_months(3)
    .presets(quarterly_presets)
    .placeholder("Select reporting period")
SlintNo Slint version of this example yet.

#Date Restrictions

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// `block_weekends` is the whole of the Slint restriction: a blocked day is
// lettered faintly and cannot be picked. GPUI's `Matcher` — a range, an
// interval or a custom predicate — is not in the Slint calendar, so the port
// does not invent it.
let restrictions = body_box(
    column![
        picker(app, slot::WEEKENDS),
        muted_line(
            "Weekends are blocked: the grid letters them faintly and a click \
             does nothing. A caller that needs a range or a predicate keeps \
             that rule where the value is applied.",
        ),
    ]
    .spacing(8.0),
);

#Disabled Weekends

use gpui_kit::component::calendar;

let date_picker = cx.new(|cx| {
    DatePickerState::new(window, cx)
        .disabled_matcher(vec![0, 6]) // Sunday=0, Saturday=6
});

DatePicker::new(&date_picker)
DatePicker {
    block-weekends: true;
    year: 2026;
    month: 10;
    day: 2;
}

#Disabled Date Range

use chrono::{Local, Days};

let now = Local::now().naive_local().date();

let date_picker = cx.new(|cx| {
    DatePickerState::new(window, cx)
        .disabled_matcher(calendar::Matcher::range(
            Some(now),
            now.checked_add_days(Days::new(7)),
        ))
});

DatePicker::new(&date_picker)
SlintNo Slint version of this example yet.

#Disabled Date Interval

let date_picker = cx.new(|cx| {
    DatePickerState::new(window, cx)
        .disabled_matcher(calendar::Matcher::interval(
            Some(now),
            now.checked_add_days(Days::new(5))
        ))
});

DatePicker::new(&date_picker)
SlintNo Slint version of this example yet.

#Custom Disabled Dates

// Disable first 5 days of each month
let date_picker = cx.new(|cx| {
    DatePickerState::new(window, cx)
        .disabled_matcher(calendar::Matcher::custom(|date| {
            date.day0() < 5
        }))
});

DatePicker::new(&date_picker)

// Disable all Mondays
let date_picker = cx.new(|cx| {
    DatePickerState::new(window, cx)
        .disabled_matcher(calendar::Matcher::custom(|date| {
            date.weekday() == chrono::Weekday::Mon
        }))
});
SlintNo Slint version of this example yet.

#Custom Year Range

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// The shown year is the caller's: this picker starts at 1990, and every
// arrow moves the view through `on_shift`, so a caller that wants a range
// clamps the shift in its own `update`. The Slint calendar has two month
// arrows and no year grid, so there is no year-selection mode to configure.
let years = body_box(
    column![
        picker(app, slot::BIRTHDAY),
        muted_line(
            "A birthday picker: the view starts at 1990 and the caller clamps \
             `on_shift` to whatever range it allows. There is no year-selection \
             grid — the Slint calendar has only the two month arrows.",
        ),
    ]
    .spacing(8.0),
);

By default, the date picker shows 50 years before and after the current year in year selection mode. Use set_year_range to configure a different range — for example, a birthday picker that goes back to 1900.

The range argument uses a half-open interval (start, end) where end is exclusive. Pass (1900, current_year + 1) to include current_year.

use chrono::Datelike;

// Birthday picker: allow years from 1900 to the current year (inclusive)
let birthday_picker = cx.new(|cx| {
    let current_year = chrono::Local::now().year();
    let mut picker = DatePickerState::new(window, cx)
        .date_format("%Y-%m-%d");
    picker.set_year_range((1900, current_year + 1), window, cx);
    picker
});

DatePicker::new(&birthday_picker)
    .cleanable(true)
    .placeholder("Select birthday")
SlintNo Slint version of this example yet.

set_year_range works for both single-date and range-mode pickers.

#Preset Ranges

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// Presets are the application's. GPUI's `presets(...)` is not in the Slint
// component, so the gallery builds them from ordinary buttons that set the
// same value a pick does — the component needs no property for it.
let presets = body_box(
    column![
        picker(app, slot::PRESET_SINGLE),
        row![
            preset_button(
                "Yesterday",
                Preset::Single(Date::new(2026, 10, 1)),
                slot::PRESET_SINGLE,
            ),
            preset_button(
                "Last Week",
                Preset::Single(Date::new(2026, 9, 25)),
                slot::PRESET_SINGLE,
            ),
            preset_button(
                "Last Month",
                Preset::Single(Date::new(2026, 9, 2)),
                slot::PRESET_SINGLE,
            ),
        ]
        .spacing(8.0),
        picker(app, slot::PRESET_RANGE),
        row![
            preset_button(
                "Last 7 Days",
                Preset::Range(Date::new(2026, 9, 26), Date::new(2026, 10, 2)),
                slot::PRESET_RANGE,
            ),
            preset_button(
                "Last 30 Days",
                Preset::Range(Date::new(2026, 9, 3), Date::new(2026, 10, 2)),
                slot::PRESET_RANGE,
            ),
            preset_button(
                "Last 90 Days",
                Preset::Range(Date::new(2026, 7, 5), Date::new(2026, 10, 2)),
                slot::PRESET_RANGE,
            ),
        ]
        .spacing(8.0),
        muted_line(
            "A preset is a button that writes the value the picker would have \
             reported, and it is the caller's to build.",
        ),
    ]
    .spacing(8.0),
);

#Single Date Presets

use chrono::{Utc, Duration};

let presets = vec![
    DateRangePreset::single(
        "Yesterday",
        (Utc::now() - Duration::days(1)).naive_local().date(),
    ),
    DateRangePreset::single(
        "Last Week",
        (Utc::now() - Duration::weeks(1)).naive_local().date(),
    ),
    DateRangePreset::single(
        "Last Month",
        (Utc::now() - Duration::days(30)).naive_local().date(),
    ),
];

DatePicker::new(&date_picker)
    .presets(presets)
SlintNo Slint version of this example yet.

#Date Range Presets

let range_presets = vec![
    DateRangePreset::range(
        "Last 7 Days",
        (Utc::now() - Duration::days(7)).naive_local().date(),
        Utc::now().naive_local().date(),
    ),
    DateRangePreset::range(
        "Last 30 Days",
        (Utc::now() - Duration::days(30)).naive_local().date(),
        Utc::now().naive_local().date(),
    ),
    DateRangePreset::range(
        "Last 90 Days",
        (Utc::now() - Duration::days(90)).naive_local().date(),
        Utc::now().naive_local().date(),
    ),
];

DatePicker::new(&date_picker)
    .number_of_months(2)
    .presets(range_presets)
SlintNo Slint version of this example yet.

#Handle Date Selection Events

let date_picker = cx.new(|cx| DatePickerState::new(window, cx));

cx.subscribe(&date_picker, |view, _, event, _| {
    match event {
        DatePickerEvent::Change(value) => {
            match value.date() {
                Date::Single(Some(selected_date)) => {
                    println!("Single date selected: {}", selected_date);
                }
                Date::Range(Some(start), Some(end)) => {
                    println!("Date range selected: {} to {}", start, end);
                }
                Date::Range(Some(start), None) => {
                    println!("Range start selected: {}", start);
                }
                _ => {
                    println!("Date cleared");
                }
            }
        }
    }
});
SlintNo Slint version of this example yet.
// `on_select` reports the day the calendar picked; the line under the field
// is the report made visible. The panel closes only because the caller
// answers `open`, which is also where a range's second end closes it.
let events = body_box(
    column![
        picker(app, slot::EVENTS),
        muted_line(format!(
            "Last pick: {}",
            app.last
                .map(|date| date.to_string())
                .unwrap_or_else(|| "nothing yet".to_string())
        )),
    ]
    .spacing(8.0),
);

#Multiple Months Display

// Show 2 months side by side (useful for date ranges)
DatePicker::new(&date_picker)
    .number_of_months(2)

// Show 3 months
DatePicker::new(&date_picker)
    .number_of_months(3)
SlintNo Slint version of this example yet.
// `months(2)` and `months(3)` are the Slint `columns`: two or three month
// grids side by side inside the one popover, which is what a range picker
// usually shows.
let months = body_box(
    column![
        picker(app, slot::MONTHS_TWO),
        picker(app, slot::MONTHS_THREE),
    ]
    .spacing(12.0),
);

#Advanced Examples

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// The range picker reports each end; the caller holds the anchor between the
// two clicks, so a maximum duration is enforced where the value is set rather
// than inside the component. The business-day picker is `block_weekends` on
// the two weekend columns.
let advanced = body_box(
    column![
        picker(app, slot::RANGE),
        picker(app, slot::BUSINESS),
        muted_line(
            "The range picker shows two months and marks the first click until \
             the second arrives; a caller that caps the span checks it in \
             `update`. The second picker blocks weekends.",
        ),
    ]
    .spacing(12.0),
);

#Business Days Only

use chrono::Weekday;

let business_days_picker = cx.new(|cx| {
    DatePickerState::new(window, cx)
        .disabled_matcher(calendar::Matcher::custom(|date| {
            matches!(date.weekday(), Weekday::Sat | Weekday::Sun)
        }))
});

DatePicker::new(&business_days_picker)
    .placeholder("Select business day")
SlintNo Slint version of this example yet.

#Date Range with Max Duration

use chrono::Days;

let max_30_days_picker = cx.new(|cx| DatePickerState::range(window, cx));

cx.subscribe(&max_30_days_picker, |view, picker, event, _| {
    match event {
        DatePickerEvent::Change(value) => {
            let Date::Range(Some(start), Some(end)) = value.date() else {
                return;
            };
            let duration = end.signed_duration_since(start).num_days();
            if duration > 30 {
                // Reset to start date only if range exceeds 30 days
                picker.update(cx, |state, cx| {
                    state.set_date(Date::Range(Some(start), None), window, cx);
                });
            }
        }
    }
});

DatePicker::new(&max_30_days_picker)
    .number_of_months(2)
    .placeholder("Select up to 30 days")
SlintNo Slint version of this example yet.

#Quarter Presets

use chrono::{NaiveDate, Datelike};

fn quarter_start(year: i32, quarter: u32) -> NaiveDate {
    let month = (quarter - 1) * 3 + 1;
    NaiveDate::from_ymd_opt(year, month, 1).unwrap()
}

fn quarter_end(year: i32, quarter: u32) -> NaiveDate {
    let month = quarter * 3;
    let start = NaiveDate::from_ymd_opt(year, month, 1).unwrap();
    NaiveDate::from_ymd_opt(year, month, start.days_in_month()).unwrap()
}

let year = Local::now().year();
let quarterly_presets = vec![
    DateRangePreset::range("Q1", quarter_start(year, 1), quarter_end(year, 1)),
    DateRangePreset::range("Q2", quarter_start(year, 2), quarter_end(year, 2)),
    DateRangePreset::range("Q3", quarter_start(year, 3), quarter_end(year, 3)),
    DateRangePreset::range("Q4", quarter_start(year, 4), quarter_end(year, 4)),
];

DatePicker::new(&date_picker)
    .presets(quarterly_presets)
SlintNo Slint version of this example yet.