Skip to content
PorabuildHeroGPUI component library
v0.14.0 · Apache-2.0

Date and time

Calendar

A month grid for picking a single date.

Rust
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.

International Calendars

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

Rust
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(),
    ])

Usage

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

Rust
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).

Default Value

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

Rust
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()])

Controlled

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
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).

Min and Max Dates

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
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()])

Unavailable Dates

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{Calendar, CalendarState, weekday_index};
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-unavailable", cx))
        // Weekends are struck through, which is v3's own example.
        .is_date_unavailable(|date| {
            let weekday = weekday_index(date);
            weekday == 0 || weekday == 6
        })
        .into_any_element()])

Weeks in Month

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
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()])

Multiple Selection

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
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).

Focused Value

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
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()])

Cell Indicators

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

Rust
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(),
    ])

Custom Navigation Icons

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{Calendar, CalendarState, icons};
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-nav", cx))
        .nav_icons(icons::ARROW_LEFT, icons::ARROW_RIGHT)
        .into_any_element()])

Real-World Example

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
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()])

Constraints

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

Rust
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(),
    ])

First day of week

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
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()])

Disabled

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

Rust
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()])

Read Only

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

Rust
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()])

Multiple Months

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

Rust
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()])

Week View

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
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()])

Day View

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

Rust
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()])

Year Picker

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
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()])

Heading Offset

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

Rust
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.

para
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

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

Calendar builders
BuilderValuesDefaultDescription
selection_mode(SelectionMode)SelectionMode::Single | SelectionMode::MultipleSelectionMode::SingleWhether one or many dates can be selected.
value(value) / values(values, cx)DateValue | null or DateValue[] | null—Controlled scalar or multiple selection.
default_value(value) / default_values(values)DateValue | null or DateValue[] | null—Initial uncontrolled scalar or multiple selection.
on_change(callback) / on_change_all(callback)(DateValue | null) or Fn(DateValue[] | null) -> ()—Reports scalar or complete multiple-selection changes.
focused_value(Date)DateValue—Controls the constrained focus date and realigns the visible range; movement waits for owner acceptance.
on_focus_change(callback)Fn(value: DateValue) -> ()—Reports keyboard or selectable-cell focus movement.
min_value(Date)DateValueCalendar-aware 1900-01-01Defaults to Gregorian 1900-01-01; an explicit minimum overrides it.
max_value(Date)DateValueCalendar-aware 2099-12-31Defaults to Gregorian 2099-12-31; an explicit maximum overrides it.
weeks_in_month(usize)number—Overrides the derived number of visible week rows.
is_date_unavailable(callback)Fn(date: DateValue) -> bool—Marks real in-month and adjacent-month dates unavailable.
first_day_of_week(Weekday)Weekday::Sun | Weekday::Mon | Weekday::Tue | Weekday::Wed | Weekday::Thu | Weekday::Fri | Weekday::SatLocale defaultDefaults 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::Visible | PageBehavior::SinglePageBehavior::VisiblePages by the visible duration or one displayed unit.
selection_alignment(SelectionAlignment)SelectionAlignment::Start | SelectionAlignment::Center | SelectionAlignment::EndSelectionAlignment::CenterAligns the initial visible range around the selection; picking preserves that displayed range.
is_disabled(bool)true | falsefalseDisables navigation, focus and selection and dims the whole calendar.
is_read_only(bool)true | falsefalseKeeps keyboard navigation while preventing pointer and selection changes.
is_invalid(bool)Partialtrue | falsefalseMarks selected and today treatments with an invalid accent, without v3's browser validation surface.
visible_duration(VisibleDuration){months?: number; weeks?: number; days?: number}{months: 1}Selects month, week or day view and its visible count.
default_year_picker_open(bool)true | falsefalseSeeds the uncontrolled internal year picker open.
is_year_picker_open(bool)true | false—Controls the internal year picker open state.
on_year_picker_open_change(callback)Fn(isOpen: bool) -> ()—Reports changes to the internal year picker.
offset(i32)Partial{months?: number}—Shifts every visible heading globally rather than configuring one composed heading.
visible_years(usize)numbermin-max span or 20Controls the bounded year window; otherwise the implicit 1900–2099 span is shown.
cell(render)Partialstring—Hands the day label to custom cell content; it is numeric rather than locale-formatted.
Not ported2 HeroUI entries not ported
Calendar builders not ported
HeroUI v3Why it is not ported
Calendar.YearPickerTriggerHeading.formatLocale-aware heading format options require an internationalization layer.
Calendar.YearPickerGrid.formatLocale, era and calendar-system year formatting are not available.

Parts

Calendar parts
PartDescription
CalendarRoot state, layout and keyboard-navigation owner. Simultaneous calendars need distinct CalendarState owners for internal identity and independent navigation.
CalendarHeader row containing navigation and the visible-range heading.
CalendarFormatted heading for one visible month or linear range.
CalendarPrevious and next section controls with replaceable icons.
CalendarOne seven-column month, week or day grid.
CalendarWeekday header container.
CalendarVisible date-row container.
CalendarOne weekday label.
CalendarFocusable date cell with state-driven custom content.
CalendarOptional three-pixel metadata indicator inside a cell.
CalendarFocusable heading control that toggles year-picker mode.
CalendarHeading content inside the year-picker trigger.
CalendarOne 12px down chevron that rotates 90 degrees over 150ms when the year picker opens.
CalendarScrollable three-column year surface overlaying the retained day area.
CalendarFragment-style body that maps the visible year window into three-cell rows.
CalendarFocusable year option that returns to the day grid after selection.

States

Calendar states
StateBuilderDescription
Selectedeligible membershipAccent fill and foreground on selectable selected dates.
Todayis_today accent.softAccent-soft fill and foreground with the accent-soft-hover fill on an unselected hovered today.
Unavailableis_unavailableKeyboard-focusable but pointer-inert date with muted, dimmed status-disabled styling and an operation-not-allowed cursor.
Outside monthoutside_monthReal adjacent-month date, -disabled and unselected at half opacity.
Hoveredhover(default)Default fill on selectable cells and accent-soft-hover on an unselected today.
Pressedpressed_with_backgroundSelectable cells scale to 0.95; selected cells use accent-hover, while today uses the default pressed fill.
Focus visiblegrid_focusRoving 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.
Disabledis_disabledInert calendar or date with disabled opacity and in-month line-through.
Read onlyis_read_onlyPointer-inert cells retain keyboard focus and navigation without selection.
Year picker openPartialyear_picker_open overlayThe scrollable year grid overlays the retained day layout, without the staggered crossfade.
Year cell selectedPartialactive_year accent branchAccent fill follows the roving year cursor and closes on activation.