Calendar
A month grid for picking a single date.
use herogpui::components::calendar::{Calendar, CalendarState};Calendar on docs.rsSource at v0.14.0
Usage
Showing International Calendars, live: HeroGPUI compiled to WebAssembly. The canvas has no accessibility tree; each example's description and code below carry the same information.
Indian and Hebrew calendar grids use English labels here. Navigation follows each calendar while selected values remain Gregorian dates. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{Calendar, CalendarState, Date};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
gpui::div()
.flex()
.flex_wrap()
.items_center()
.justify_center()
.gap(px(12.))
.children(vec![
Calendar::new(self.demo_calendar("cal-indian", cx))
.locale("en-US-u-ca-indian")
.default_value(Date::new(2026, 1, 15))
.into_any_element(),
Calendar::new(self.demo_calendar("cal-hebrew", cx))
.locale("en-US-u-ca-hebrew")
.default_value(Date::new(2024, 3, 25))
.into_any_element(),
])Day, month and year labels use 14px/20px medium text; weekday labels use 12px/16px medium text. Enabled navigation hover fills ease over the 100ms curve while the deep press scale stays on the button. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{ActiveTheme, Calendar, CalendarState, Date};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{App, px};
use herogpui::gpui;
let picked = self.cal_picked;
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![
Calendar::new(self.demo_calendar("cal-usage", cx))
.day_hover_bg(cx.colors().accent.soft())
.nav_hover_bg(cx.colors().accent.soft())
.year_hover_bg(cx.colors().accent.soft())
.on_change(cx.listener(
|this, d: &Option<Date>, _, cx| {
this.cal_picked = *d;
cx.notify();
},
))
.into_any_element(),
para(
&match picked {
Some(d) => format!("Selected: {}", d.format_iso()),
None => "No date selected".to_owned(),
},
cx,
),
])Also calls para (included when you copy).
The seven 36px day columns align with the weekday headings without horizontal gaps. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{Calendar, CalendarState, Date};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![Calendar::new(self.demo_calendar("cal-default", cx))
.default_value(Date::new(2025, 12, 25))
.into_any_element()])Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{ActiveTheme, Calendar, CalendarState, Date};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{App, px};
use herogpui::gpui;
let picked = self.cal_picked;
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![
Calendar::new(self.demo_calendar("cal-controlled", cx))
.on_focus_change(cx.listener(|this, d: &Date, _, cx| {
this.set_demo_text_value("cal-focus", d.format_iso());
cx.notify();
}))
.on_change(cx.listener(
|this, d: &Option<Date>, _, cx| {
this.cal_picked = *d;
cx.notify();
},
))
.into_any_element(),
para(
&match picked {
Some(d) => format!("Value: {}", d.format_iso()),
None => "No value".to_owned(),
},
cx,
),
])Also calls para (included when you copy).
Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{Calendar, CalendarState, Date};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
let today = Date::today();
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![Calendar::new(self.demo_calendar("cal-minmax", cx))
.min_value(Date::new(today.year, today.month, 5))
.max_value(Date::new(today.year, today.month, 20))
.into_any_element()])Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{Calendar, CalendarState};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![Calendar::new(self.demo_calendar("cal-weeks", cx))
.weeks_in_month(6)
.into_any_element()])Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{ActiveTheme, Calendar, CalendarState, SelectionMode};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{App, px};
use herogpui::gpui;
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children({
let cal = self.demo_calendar("cal-multiple", cx);
// v3's `onChange` carries the whole selection in
// multiple mode; the summary below re-reads the state,
// and `window.refresh()` forces the repaint that makes
// the new frame visible.
let dates = cal.read(cx).selected_dates().to_vec();
let summary = if dates.is_empty() {
"No dates selected".to_owned()
} else {
format!(
"Selected: {}",
dates
.iter()
.map(|d| d.format_iso())
.collect::<Vec<_>>()
.join(", ")
)
};
vec![
Calendar::new(cal)
.selection_mode(SelectionMode::Multiple)
.on_change_all(|_, window, _| window.refresh())
.into_any_element(),
para(&summary, cx),
]
})Also calls para (included when you copy).
Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{Calendar, CalendarState, Date};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
let focused = self.calendar_focus;
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![Calendar::new(self.demo_calendar("cal-focused", cx))
.focused_value(focused)
.on_focus_change(cx.listener(
|this, date: &Date, _, cx| {
this.calendar_focus = *date;
cx.notify();
},
))
.into_any_element()])The marked days are the ones with events. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{Calendar, CalendarState};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![
Calendar::new(self.demo_calendar("cal-indicators", cx))
.cell_indicator(|date| {
[3, 7, 12, 15, 21, 28].contains(&date.day)
})
.into_any_element(),
])Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{Calendar, CalendarState, Date, Description, Surface, weekday_index};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
let today = Date::today();
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![Surface::new()
.padding(px(20.))
.gap(px(12.))
.child(gpui::div().child("Pick an appointment"))
.child(
Calendar::new(self.demo_calendar("cal-real", cx))
.min_value(today)
.is_date_unavailable(|date| {
let weekday = weekday_index(date);
weekday == 0 || weekday == 6
})
.cell_indicator(|date| date.day % 5 == 0),
)
.child(Description::new(
"Weekends are unavailable; a dot marks a day with slots left.",
))
.into_any_element()])MinValue/maxValue mute the days outside the range; isDateUnavailable strikes through the ones it rejects. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{Calendar, CalendarState, Date};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
let today = Date::today();
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![
Calendar::new(self.demo_calendar("cal-constraints", cx))
.min_value(Date::new(today.year, today.month, 5))
.max_value(Date::new(today.year, today.month, 24))
.is_date_unavailable(|d: Date| d.day.is_multiple_of(7))
.into_any_element(),
])Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{Calendar, CalendarState, Weekday};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![Calendar::new(self.demo_calendar("cal-first-day", cx))
.first_day_of_week(Weekday::Mon)
.weeks_in_month(6)
.into_any_element()])A disabled calendar keeps the current month readable but rejects presses, arrows, and leaves the tab order. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{Calendar, CalendarState};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![Calendar::new(self.demo_calendar("cal-disabled", cx))
.is_disabled(true)
.into_any_element()])A read-only calendar stays focusable and navigable but rejects mutations. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{Calendar, CalendarState};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![Calendar::new(self.demo_calendar("cal-readonly", cx))
.is_read_only(true)
.into_any_element()])Scroll horizontally to explore both months in narrow layouts. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{Calendar, CalendarState, VisibleDuration};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
gpui::div()
.flex()
.flex_col()
.w_full()
.gap(px(12.))
.children(vec![Calendar::new(self.demo_calendar("cal-months", cx))
.visible_duration(VisibleDuration::Months(2))
.into_any_element()])Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{Calendar, CalendarState, PageBehavior, VisibleDuration};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![Calendar::new(self.demo_calendar("cal-week-view", cx))
.visible_duration(VisibleDuration::Weeks(2))
.page_behavior(PageBehavior::Single)
.into_any_element()])Seven weekday columns show the requested dates, disabled leading dates and blank trailing cells. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{Calendar, CalendarState, VisibleDuration};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![Calendar::new(self.demo_calendar("cal-day-view", cx))
.visible_duration(VisibleDuration::Days(5))
.into_any_element()])Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{Calendar, CalendarState};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![Calendar::new(self.demo_calendar("cal-year-picker", cx))
.is_year_picker_open(self.cal_year_picker)
.on_year_picker_open_change(cx.listener(
|this, open: &bool, _, cx| {
this.cal_year_picker = *open;
cx.notify();
},
))
.into_any_element()])`Calendar.YearPickerTriggerHeading.offset` shifts the month heading -- also the year-picker trigger -- while the grid stays on the visible month. Both grids above show August; only the headings differ. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{Calendar, CalendarState, Date};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children({
let august = Date::new(2026, 8, 10);
vec![
gpui::div()
.flex()
.flex_wrap()
.items_center()
.justify_center()
.gap(px(12.))
.children(vec![
Calendar::new(self.demo_calendar("cal-heading-anchor", cx))
.default_value(august)
.into_any_element(),
Calendar::new(self.demo_calendar("cal-heading-offset", cx))
.default_value(august)
.offset(1)
.into_any_element(),
])
.into_any_element(),
]
})Helpers used by these examples
Plain functions over the public API, defined once here and copied with each example that calls them.
fn para(text: &str, cx: &App) -> gpui::AnyElement {
let colors = cx.colors();
gpui::div()
.text_size(px(14.5))
.line_height(px(24.))
.text_color(colors.foreground)
.max_w(px(720.))
.child(text.to_owned())
.into_any_element()
}Anatomy
Rust types
Calendar is assembled from these builders. The API reference lists each one.
API reference
Against HeroUI v3.2.6: 49 entries ported, 5 partial, 2 not ported. Partial rows are marked in the tables; entries not ported are listed under each table with the reason.
Builders
| Builder | Values | Default | Description |
|---|---|---|---|
selection_mode( | SelectionMode:: | SelectionMode:: | Whether one or many dates can be selected. |
value( | DateValue | | — | Controlled scalar or multiple selection. |
default_value( | DateValue | | — | Initial uncontrolled scalar or multiple selection. |
on_change( | ( | — | Reports scalar or complete multiple-selection changes. |
focused_value( | DateValue | — | Controls the constrained focus date and realigns the visible range; movement waits for owner acceptance. |
on_focus_change( | Fn( | — | Reports keyboard or selectable-cell focus movement. |
min_value( | DateValue | Calendar-aware 1900-01-01 | Defaults to Gregorian 1900-01-01; an explicit minimum overrides it. |
max_value( | DateValue | Calendar-aware 2099-12-31 | Defaults to Gregorian 2099-12-31; an explicit maximum overrides it. |
weeks_in_month( | number | — | Overrides the derived number of visible week rows. |
is_date_unavailable( | Fn( | — | Marks real in-month and adjacent-month dates unavailable. |
first_day_of_week( | Weekday:: | Locale default | Defaults from the operating system's regional date preferences through CLDR week data; an explicit weekday overrides it, with Sunday as the invalid-locale fallback. |
page_behavior( | PageBehavior:: | PageBehavior:: | Pages by the visible duration or one displayed unit. |
selection_alignment( | SelectionAlignment:: | SelectionAlignment:: | Aligns the initial visible range around the selection; picking preserves that displayed range. |
is_disabled( | true | | false | Disables navigation, focus and selection and dims the whole calendar. |
is_read_only( | true | | false | Keeps keyboard navigation while preventing pointer and selection changes. |
is_invalid(Partial | true | | false | Marks selected and today treatments with an invalid accent, without v3's browser validation surface. |
visible_duration( | {months?: number; weeks?: number; days?: number} | {months: 1} | Selects month, week or day view and its visible count. |
default_year_picker_open( | true | | false | Seeds the uncontrolled internal year picker open. |
is_year_picker_open( | true | | — | Controls the internal year picker open state. |
on_year_picker_open_change( | Fn( | — | Reports changes to the internal year picker. |
offset(Partial | {months?: number} | — | Shifts every visible heading globally rather than configuring one composed heading. |
visible_years( | number | min-max span or 20 | Controls the bounded year window; otherwise the implicit 1900–2099 span is shown. |
cell(Partial | string | — | Hands the day label to custom cell content; it is numeric rather than locale-formatted. |
Not ported2 HeroUI entries not ported
| HeroUI v3 | Why it is not ported |
|---|---|
Calendar.YearPickerTriggerHeading.format | Locale-aware heading format options require an internationalization layer. |
Calendar.YearPickerGrid.format | Locale, era and calendar-system year formatting are not available. |
Parts
| Part | Description |
|---|---|
Calendar | Root state, layout and keyboard-navigation owner. Simultaneous calendars need distinct CalendarState owners for internal identity and independent navigation. |
Calendar | Header row containing navigation and the visible-range heading. |
Calendar | Formatted heading for one visible month or linear range. |
Calendar | Previous and next section controls with replaceable icons. |
Calendar | One seven-column month, week or day grid. |
Calendar | Weekday header container. |
Calendar | Visible date-row container. |
Calendar | One weekday label. |
Calendar | Focusable date cell with state-driven custom content. |
Calendar | Optional three-pixel metadata indicator inside a cell. |
Calendar | Focusable heading control that toggles year-picker mode. |
Calendar | Heading content inside the year-picker trigger. |
Calendar | One 12px down chevron that rotates 90 degrees over 150ms when the year picker opens. |
Calendar | Scrollable three-column year surface overlaying the retained day area. |
Calendar | Fragment-style body that maps the visible year window into three-cell rows. |
Calendar | Focusable year option that returns to the day grid after selection. |
States
| State | Builder | Description |
|---|---|---|
Selected | eligible membership | Accent fill and foreground on selectable selected dates. |
Today | is_today accent.soft | Accent-soft fill and foreground with the accent-soft-hover fill on an unselected hovered today. |
Unavailable | is_unavailable | Keyboard-focusable but pointer-inert date with muted, dimmed status-disabled styling and an operation-not-allowed cursor. |
Outside month | outside_month | Real adjacent-month date, -disabled and unselected at half opacity. |
Hovered | hover(default) | Default fill on selectable cells and accent-soft-hover on an unselected today. |
Pressed | pressed_with_background | Selectable cells scale to 0.95; selected cells use accent-hover, while today uses the default pressed fill. |
Focus visible | grid_focus | Roving in-month focus ring driven by the calendar cursor. Month boundaries, paging, alignment and year selection follow the chosen calendar system while focus values stay Gregorian. |
Disabled | is_disabled | Inert calendar or date with disabled opacity and in-month line-through. |
Read only | is_read_only | Pointer-inert cells retain keyboard focus and navigation without selection. |
Year picker openPartial | year_picker_open overlay | The scrollable year grid overlays the retained day layout, without the staggered crossfade. |
Year cell selectedPartial | active_year accent branch | Accent fill follows the roving year cursor and closes on activation. |