Range Calendar
A month grid for picking a date range.
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.
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>.
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 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>.
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()])Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
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()])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>.
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()])`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>.
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(),
]
})Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
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()])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>.
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(),
])Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
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()])Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
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()])Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
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()])Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
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()])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::{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()])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::{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()])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>.
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()])Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
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()])Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
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()])Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
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()])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, 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
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
| Builder | Values | Default | Description |
|---|---|---|---|
value( | RangeValue< | — | Controlled selected range seeded into the bound state entity on the first render. |
default_value( | RangeValue< | — | Seeds the uncontrolled range once. |
on_change( | Fn( | — | Reports only completed ranges; the pending first endpoint remains internal anchor state. |
focused_value( | DateValue | — | Controls the constrained focus date and realigns the visible range; movement waits for owner acceptance. |
on_focus_change( | Fn( | — | Reports pointer, hover and keyboard 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 | — | Uses the exact nonzero row count; zero falls back to the derived count. |
is_date_unavailable( | Fn( | — | 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:: | 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 range or one displayed unit. |
selection_alignment( | SelectionAlignment:: | SelectionAlignment:: | Explicit 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( | true | | false | Allows a completed range to span unavailable interior dates. |
is_disabled( | true | | false | Disables navigation, focus and selection and dims the calendar. |
is_read_only( | true | | false | Keeps focus and navigation while preventing range changes. |
is_invalid(Partial | true | | false | Applies danger accents, without v3's automatic invalid-range and 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 year picker open state. |
is_year_picker_open( | true | | — | Controls the internal year picker open state. |
on_year_picker_open_change( | Fn( | — | Reports internal year picker changes. |
offset(Partial | {months?: number} | — | Shifts all visible headings 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 custom content a numeric day label rather than a locale-formatted string. |
Not ported2 HeroUI entries not ported
| HeroUI v3 | Why it is not ported |
|---|---|
RangeCalendar.YearPickerTriggerHeading.format | Locale-aware heading formatting requires an internationalization layer. |
RangeCalendar.YearPickerGrid.format | Locale, era and calendar-system year formatting are unavailable. |
Parts
| Part | Description |
|---|---|
RangeCalendar | Root range state, layout and keyboard owner. |
RangeCalendar | Navigation and visible-range heading row. |
RangeCalendar | Heading for one visible month or linear range. |
RangeCalendar | Previous and next visible-section controls. |
RangeCalendar | One seven-column month, week or day grid. |
RangeCalendar | Weekday header container. |
RangeCalendar | Visible date-row container. |
RangeCalendar | One weekday label. |
RangeCalendar | Focusable date cell and range-track segment with custom render state. |
RangeCalendar | Optional three-pixel metadata dot. |
RangeCalendar | Heading control that toggles year-picker mode. |
RangeCalendar | Heading content inside the year trigger. |
RangeCalendar | One 12px down chevron that rotates 90 degrees over 150ms when the year picker opens. |
RangeCalendar | Scrollable three-column year surface overlaying the retained day area. |
RangeCalendar | Maps the year window into three-cell rows. |
RangeCalendar | Focusable year option returning to the day grid. |
States
| State | Builder | Description |
|---|---|---|
Selected rangePartial | eligible range membership | Draws the selectable soft range track. |
Range middlePartial | eligible interior range membership | The docs name a distinct middle state, but the dependency does not emit its attribute; derives interior membership directly. |
Selection startPartial | is_selection_start accent pill | Draws the accent leading cap. |
Selection endPartial | is_selection_end accent pill | Draws the accent trailing cap. |
Today | is_today accent.soft | Accent-soft fill and foreground with the accent-soft-hover fill on an unselected hovered today. |
Unavailable | is_unavailable | 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. |
HoveredPartial | on_hover | Moves anchored focus and previews the same range Enter will commit, but today lacks v3's accent-soft-hover fill. |
PressedPartial | PRESSED_SCALE_RANGE | Uses the exact 0.9 geometry, but without v3's 200ms scale interpolation. |
Focus visible | grid_focus | Roving 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. |
Disabled | is_disabled | Inert calendar or date with disabled opacity and in-month line-through. |
Read only | is_read_only | Retains focus and navigation without selection changes. |
Year picker openPartial | year_picker_open overlay | The scrollable year grid overlays the retained day layout, without the staggered crossfade. |