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

Date and time

Range Calendar

A month grid for picking a date range.

Rust
use herogpui::components::range_calendar::RangeCalendar;

RangeCalendar 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

An Indian-calendar range aligned to the end of a two-month view. The January 21-22 Gregorian selection stays in the second displayed 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::{ActiveTheme, Date, RangeCalendar, SelectionAlignment, 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![RangeCalendar::new(self.demo_range("rc-indian", cx))
        .locale("en-US-u-ca-indian")
        .day_hover_bg(cx.colors().accent.soft())
        .nav_hover_bg(cx.colors().accent.soft())
        .year_hover_bg(cx.colors().accent.soft())
        .default_value((Date::new(2026, 1, 21), Date::new(2026, 1, 22)))
        .visible_duration(VisibleDuration::Months(2))
        .selection_alignment(SelectionAlignment::End)
        .into_any_element()])

Disabled

Disabled day cells retain the same 14px/20px medium typography as selectable and selected 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::RangeCalendar;
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![RangeCalendar::new(
        self.demo_range("rc-disabled", cx),
    )
    .is_disabled(true)
    .into_any_element()])

Cell Indicators

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::RangeCalendar;
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![RangeCalendar::new(self.demo_range("rc-dots", cx))
        // `RangeCalendar.CellIndicator` marks a day with a dot,
        // the same part a `Calendar` draws.
        .cell_indicator(|d| d.day % 7 == 3)
        .into_any_element()])

Year Picker

Years scroll within the day-grid area. Opening and keyboard navigation reveal the focused year without expanding the calendar. 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::{RangeCalendar, 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![RangeCalendar::new(self.demo_range("rc-year", cx))
        .default_year_picker_open(true)
        // `firstDayOfWeek` reorders the seven columns.
        .first_day_of_week(Weekday::Mon)
        .into_any_element()])

Heading Offset

`RangeCalendar.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::{Date, RangeCalendar};
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), Date::new(2026, 8, 16));
        vec![
            gpui::div()
                .flex()
                .flex_wrap()
                .items_center()
                .justify_center()
                .gap(px(12.))
                .children(vec![
                                RangeCalendar::new(self.demo_range("rc-heading-anchor", cx))
                                    .default_value(august)
                                    .into_any_element(),
                                RangeCalendar::new(self.demo_range("rc-heading-offset", cx))
                                    .default_value(august)
                                    .offset(1)
                                    .into_any_element(),
                            ])
                .into_any_element(),
        ]
    })

Default 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::{Date, RangeCalendar};
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![RangeCalendar::new(
        self.demo_range("rc-default", cx),
    )
    .default_value((Date::new(2025, 12, 8), Date::new(2025, 12, 14)))
    .into_any_element()])

Controlled

The range lives in the state entity the caller owns. 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::{Date, RangeCalendar};
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![
        RangeCalendar::new(self.demo_range("rc-controlled", cx))
            .default_value((Date::new(2025, 12, 8), Date::new(2025, 12, 14)))
            .on_focus_change(cx.listener(|this, d: &Date, _, cx| {
                this.set_demo_text_value("rc-focus", d.format_iso());
                cx.notify();
            }))
            .into_any_element(),
    ])

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::{Date, RangeCalendar};
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![RangeCalendar::new(
        self.demo_range("rc-minmax", cx),
    )
    .min_value(Date::new(today.year, today.month, 5))
    .max_value(Date::new(today.year, today.month, 24))
    .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::{Date, RangeCalendar, Weekday, add_days, days_from_civil};
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({
        let blocked_ranges = [
            (add_days(&today, 2), add_days(&today, 5)),
            (add_days(&today, 12), add_days(&today, 13)),
        ];
        vec![
            RangeCalendar::new(self.demo_range("rc-unavailable", cx))
                .default_value((add_days(&today, 6), add_days(&today, 9)))
                .first_day_of_week(Weekday::Mon)
                .is_date_unavailable(move |date, _| {
                    let date = days_from_civil(&date);
                    blocked_ranges.iter().any(|(start, end)| {
                        date >= days_from_civil(start)
                            && date <= days_from_civil(end)
                    })
                })
                .into_any_element(),
            gpui::div()
                .child("Some days are unavailable")
                .into_any_element(),
        ]
    })

Anchor-Based Unavailable Dates

After the first date is selected, earlier dates become unavailable because the predicate receives that active anchor. 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::RangeCalendar;
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![
        RangeCalendar::new(self.demo_range("rc-anchor", cx))
            .is_date_unavailable(|date, anchor| {
                anchor.is_some_and(|anchor| {
                    (date.year, date.month, date.day)
                        < (anchor.year, anchor.month, anchor.day)
                })
            })
            .into_any_element(),
    ])

Allows Non-Contiguous Ranges

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::RangeCalendar;
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![RangeCalendar::new(
        self.demo_range("rc-noncontig", cx),
    )
    .is_date_unavailable(|date, _| date.day == 15)
    .allows_non_contiguous_ranges(true)
    .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::RangeCalendar;
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![RangeCalendar::new(self.demo_range("rc-weeks", cx))
        .weeks_in_month(6)
        .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::{RangeCalendar, 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![RangeCalendar::new(
        self.demo_range("rc-week-view", cx),
    )
    .visible_duration(VisibleDuration::Weeks(2))
    .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::{RangeCalendar, 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![RangeCalendar::new(
        self.demo_range("rc-day-view", cx),
    )
    .visible_duration(VisibleDuration::Days(5))
    .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::{RangeCalendar, VisibleDuration};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .w_full()
    .gap(px(12.))
    .children(vec![RangeCalendar::new(
        self.demo_range("rc-months", cx),
    )
    .visible_duration(VisibleDuration::Months(2))
    .into_any_element()])

Read Only

Browse dates without changing the supplied range. 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::{Date, RangeCalendar};
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![RangeCalendar::new(
        self.demo_range("rc-readonly", cx),
    )
    .value(Some(Date::new(2025, 12, 8)), Some(Date::new(2025, 12, 14)))
    .is_read_only(true)
    .into_any_element()])

Invalid

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::{Date, RangeCalendar};
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![RangeCalendar::new(
        self.demo_range("rc-invalid", cx),
    )
    .default_value((Date::new(2025, 12, 8), Date::new(2025, 12, 14)))
    .is_invalid(true)
    .into_any_element()])

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::{Date, RangeCalendar};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

let focused = self.range_calendar_focus;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![RangeCalendar::new(
        self.demo_range("rc-focused", cx),
    )
    .focused_value(focused)
    .on_focus_change(cx.listener(|this, date: &Date, _, cx| {
        this.range_calendar_focus = *date;
        cx.notify();
    },))
    .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::{Date, Description, RangeCalendar, Surface};
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("Choose your stay"))
        .child(
            RangeCalendar::new(self.demo_range("rc-real", cx))
                .min_value(today)
                .is_date_unavailable(|date, _| date.day == 20),
        )
        .child(Description::new("The 20th is fully booked."))
        .into_any_element()])

Usage

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, RangeCalendar};
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![RangeCalendar::new(self.demo_range("rc-usage", cx))
        .nav_hover_bg(cx.colors().accent.soft())
        .on_change(|_start, _end, _, _cx| {})
        .into_any_element()])

Anatomy

Rust types

RangeCalendar

Range Calendar is assembled from these builders. The API reference lists each one.

API reference

Against HeroUI v3.2.6: 48 entries ported, 10 partial, 2 not ported. Partial rows are marked in the tables; entries not ported are listed under each table with the reason.

Builders

Range Calendar builders
BuilderValuesDefaultDescription
value(start, end)RangeValue<DateValue> | null—Controlled selected range seeded into the bound state entity on the first render.
default_value((start, end))RangeValue<DateValue> | null—Seeds the uncontrolled range once.
on_change(callback)Fn(value: RangeValue<DateValue>) -> ()—Reports only completed ranges; the pending first endpoint remains internal anchor state.
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 pointer, hover and keyboard 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—Uses the exact nonzero row count; zero falls back to the derived count.
is_date_unavailable(callback)Fn(date: DateValue, anchorDate: CalendarDate | null) -> bool—Receives the active anchor for cells and both input paths; contiguous selection scans through one visible duration on each side plus the sentinel day beyond each limit, then merges discovered bounds into cells, focus and navigation.
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 range or one displayed unit.
selection_alignment(SelectionAlignment)SelectionAlignment::Start | SelectionAlignment::Center | SelectionAlignment::EndSelectionAlignment::CenterExplicit alignment wins; otherwise a range extending beyond the centered window automatically starts at the first visible unit. The first pick preserves that aligned view.
allows_non_contiguous_ranges(bool)true | falsefalseAllows a completed range to span unavailable interior dates.
is_disabled(bool)true | falsefalseDisables navigation, focus and selection and dims the calendar.
is_read_only(bool)true | falsefalseKeeps focus and navigation while preventing range changes.
is_invalid(bool)Partialtrue | falsefalseApplies danger accents, without v3's automatic invalid-range and 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 year picker open state.
is_year_picker_open(bool)true | false—Controls the internal year picker open state.
on_year_picker_open_change(callback)Fn(isOpen: bool) -> ()—Reports internal year picker changes.
offset(i32)Partial{months?: number}—Shifts all visible headings 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 custom content a numeric day label rather than a locale-formatted string.
Not ported2 HeroUI entries not ported
Range Calendar builders not ported
HeroUI v3Why it is not ported
RangeCalendar.YearPickerTriggerHeading.formatLocale-aware heading formatting requires an internationalization layer.
RangeCalendar.YearPickerGrid.formatLocale, era and calendar-system year formatting are unavailable.

Parts

Range Calendar parts
PartDescription
RangeCalendarRoot range state, layout and keyboard owner.
RangeCalendarNavigation and visible-range heading row.
RangeCalendarHeading for one visible month or linear range.
RangeCalendarPrevious and next visible-section controls.
RangeCalendarOne seven-column month, week or day grid.
RangeCalendarWeekday header container.
RangeCalendarVisible date-row container.
RangeCalendarOne weekday label.
RangeCalendarFocusable date cell and range-track segment with custom render state.
RangeCalendarOptional three-pixel metadata dot.
RangeCalendarHeading control that toggles year-picker mode.
RangeCalendarHeading content inside the year trigger.
RangeCalendarOne 12px down chevron that rotates 90 degrees over 150ms when the year picker opens.
RangeCalendarScrollable three-column year surface overlaying the retained day area.
RangeCalendarMaps the year window into three-cell rows.
RangeCalendarFocusable year option returning to the day grid.

States

Range Calendar states
StateBuilderDescription
Selected rangePartialeligible range membershipDraws the selectable soft range track.
Range middlePartialeligible interior range membershipThe docs name a distinct middle state, but the dependency does not emit its attribute; derives interior membership directly.
Selection startPartialis_selection_start accent pillDraws the accent leading cap.
Selection endPartialis_selection_end accent pillDraws the accent trailing cap.
Todayis_today accent.softAccent-soft fill and foreground with the accent-soft-hover fill on an unselected hovered today.
Unavailableis_unavailableFocusable 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.
HoveredPartialon_hoverMoves anchored focus and previews the same range Enter will commit, but today lacks v3's accent-soft-hover fill.
PressedPartialPRESSED_SCALE_RANGEUses the exact 0.9 geometry, but without v3's 200ms scale interpolation.
Focus visiblegrid_focusRoving in-month focus ring shared by hover and keyboard. 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_onlyRetains focus and navigation without selection changes.
Year picker openPartialyear_picker_open overlayThe scrollable year grid overlays the retained day layout, without the staggered crossfade.