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.
#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
// 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)#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 PMdefault_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}");
}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))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#With Placeholder
DatePicker::new(&date_picker)
.placeholder("Select a date...")#Cleanable Date Picker
DatePicker::new(&date_picker)
.cleanable(true) // Show clear button when date is selected#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)#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))#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)#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")#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")#Date Restrictions
// `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)#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)#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
}))
});#Custom Year Range
// 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")set_year_range works for both single-date and range-mode pickers.
#Preset Ranges
// 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)#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)#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");
}
}
}
}
});// `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)// `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
// 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")#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")#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)