Date Range Picker
Pick a start and end date together.
use herogpui::components::date_picker::{DateRangePicker, DateRangeState};DateRangePicker on docs.rsSource at v0.14.0
Usage
Showing International Calendar, live: HeroGPUI compiled to WebAssembly. The canvas has no accessibility tree; each example's description and code below carry the same information.
Both fields and the popover grid share the locale override. Segment order follows that locale; selected values stay Gregorian ISO 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::{ActiveTheme, DateRangePicker, DateRangeState};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
gpui::div()
.flex()
.flex_col()
.w(px(256.))
.gap(px(12.))
.children(vec![DateRangePicker::new(
self.demo_range("drp-indian", cx),
)
.label("Stay")
.locale("en-US-u-ca-indian")
.trigger_hover_bg(cx.colors().accent.soft())
.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::{DateRangePicker, DateRangeState};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
gpui::div()
.flex()
.flex_col()
.w(px(256.))
.gap(px(12.))
.children(vec![DateRangePicker::new(
self.demo_range("drp-disabled", cx),
)
.label("Stay")
.is_disabled(true)
.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::{DateRangePicker, DateRangeState};
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![
{
// `value` seeds the caller's copy in, and
// `on_change` is how the caller gets one: a range
// picker reports the change with no arguments, so
// the range is read out of the state entity.
let state = self.demo_range("drp-controlled", cx);
let held = cx.entity().downgrade();
let (start, end) = {
let range = state.read(cx);
(range.start, range.end)
};
DateRangePicker::new(state.clone())
.label("Stay")
.value(start, end)
.on_change(move |_, cx| {
let (start, end) = {
let range = state.read(cx);
(range.start, range.end)
};
if let Some(gallery) = held.upgrade() {
gallery.update(cx, |gallery, cx| {
gallery.set_demo_text_value(
"drp-controlled",
match (start, end) {
(Some(a), Some(b)) => format!(
"{} to {}",
a.format_iso(),
b.format_iso()
),
_ => String::new(),
},
);
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::{DateRangePicker, DateRangeState};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
gpui::div()
.flex()
.flex_col()
.w(px(256.))
.gap(px(12.))
.children(vec![DateRangePicker::new(
self.demo_range("drp-invalid", cx),
)
.label("Stay")
.is_invalid(true)
.into_any_element()])Both ends follow the operating system's regional date order, separators, and numeric padding. Their state and submitted values stay ISO-formatted. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{DateRangePicker, DateRangeState};
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![
DateRangePicker::new(self.demo_range("drp-format", cx))
.label("Stay")
.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::{Button, DateRangePicker, DateRangeState, Form};
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![Form::new()
.child(
DateRangePicker::new(self.demo_range("drp-form", cx))
.label("Stay")
// v3 submits a range as two named fields.
.start_name("check_in")
.end_name("check_out"),
)
.child(Button::new("date-range-picker-form-submit").label("Book"))
.into_any_element()])Custom indicators replace the default calendar glyph and range separator without changing field or trigger behavior. 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, DateRangePicker, DateRangeState, 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![
DateRangePicker::new(self.demo_range("drp-indicator", cx))
.label("Stay")
.trigger_indicator(gpui::svg()
.size(px(16.))
.path(icons::CHECK)
.text_color(cx.colors().foreground)
.into_any_element())
.range_separator(gpui::div().child("to"))
.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::{Button, DateRangePicker, DateRangeState};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
let is_open = self.range_open;
let render_props_gallery = cx.entity().downgrade();
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![DateRangePicker::new(
self.demo_range("drp-render-props", cx),
)
.is_open(is_open)
.is_required(true)
.content(move |state| {
let gallery = render_props_gallery.clone();
gpui::div()
.flex()
.flex_col()
.items_start()
.gap(px(8.))
.child(format!(
"{} · {} · {}",
if state.is_required {
"required"
} else {
"optional"
},
if state.is_invalid { "invalid" } else { "valid" },
if state.is_open { "open" } else { "closed" },
))
.child(
Button::new("date-range-picker-render-props-toggle")
.label(if state.is_open { "Close" } else { "Open" })
.on_press(move |_, _, cx| {
if let Some(gallery) = gallery.upgrade() {
gallery.update(cx, |gallery, cx| {
gallery.range_open = !gallery.range_open;
cx.notify();
});
}
}),
)
.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, DateRangePicker, DateRangeState};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
let is_open = self.range_open;
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![gpui::div()
.w(px(320.))
.child(
DateRangePicker::new(self.demo_range("drp-usage", cx))
.label("Trip dates")
// v3's Usage seeds the range and bounds it.
.default_value((
Date::new(2025, 12, 8),
Date::new(2025, 12, 14),
))
.min_value(Date::new(2025, 1, 1))
.is_open(is_open)
.on_open_change(cx.listener(
|this, open: &bool, _, cx| {
this.range_open = *open;
cx.notify();
},
))
.on_change(|_, _cx| {}),
)
.into_any_element()])The range calendar popover accepts the shared 22-value placement vocabulary. Defaults to a centered bottom popover; this specimen opts into top placement. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{DateRangePicker, DateRangeState, Placement};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
gpui::div()
.flex()
.flex_col()
.w(px(256.))
.gap(px(12.))
.children(vec![
DateRangePicker::new(self.demo_range("drp-placement", cx))
.label("Top placement")
.placement(Placement::Top)
.default_open(true)
.into_any_element(),
])Anatomy
Rust types
Date Range Picker is assembled from these builders. The API reference lists each one.
Customization
Shared appearance belongs on ThemeBuilder::components and .recipe("name"). Typed builders change how Date Range Picker looks at one call site. Every builder also carries sx for placement, width and flex: it takes GPUI's own styling methods and refines them over the component's root after the theme, so an instance override wins. Behaviour builders live in the API reference below.
Styling
| Method | Values | Description |
|---|---|---|
placement(Placement) | Placement | The full 22-value union renders through the shared vocabulary with default eight-pixel popover offset: physical and aligned spellings plus the logical start/end aliases, which share their left/right side's pixels in this LTR-only port. The centered bottom value matches default. |
// The one slot for caller-owned low-level styling: GPUI's
// styling methods, refined over the root element last.
.sx(|el| {
el.bg(gpui::rgba(0xffa500ff)) // background
.text_color(gpui::rgba(0x000000ff)) // text
.w(gpui::px(13.)) // size
.h(gpui::px(12.))
})API reference
Against HeroUI v3.2.6: 31 entries ported, 6 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< | — | Seeds the controlled start and end into the caller-owned DateRangeState on the first render. |
default_value( | RangeValue< | — | Seeds the uncontrolled complete range once without fighting later edits. |
on_change(Partial | Fn( | — | Reports valid field edits and completed calendar ranges; the range is read from DateRangeState. |
is_open( | true | | — | Controls the range calendar popover without mutating local open state. |
default_open( | true | | false | Seeds uncontrolled popover state. |
on_open_change( | Fn( | — | Reports trigger, selection, Escape, outside-press and focus-departure changes. |
is_disabled( | true | | false | Removes both fields and the trigger from interaction and form submission. |
is_read_only( | true | | false | Keeps both dates focusable while suppressing edits, opening and selection. |
is_required( | true | | false | Blocks native submission until both named range ends are present and focuses the start field first. |
is_invalid( | true | | — | Forces invalid field and range-calendar state. |
validate(Partial | Fn( | — | A callback supplies one optional validation message for the complete range; structured ValidationError objects are not exposed. |
validation_behavior( | ValidationBehavior:: | ValidationBehavior:: | Selects blocking native validation or non-blocking ARIA-style validation for both fields. |
validation_errors(Partial | string[] | — | Server messages take precedence and mark both fields invalid; independently composed FieldError content is unavailable. |
min_value( | DateValue | — | Earliest valid field and calendar date. |
max_value( | DateValue | — | Latest valid field and calendar date. |
is_date_unavailable( | Fn( | — | Rejects individual dates inside the selected range. |
first_day_of_week( | string | Weekday:: | The range calendar grid defaults from the operating system's regional date preferences through CLDR week data; the local Weekday enum provides an explicit override. |
should_close_on_select(Partial | true | | true | The boolean form controls whether a completed calendar range dismisses the popover; callback-valued policy is not represented. |
auto_focus( | true | | false | Focuses the editable start field on first render. |
start_name( | string | — | Submits the displayed start date under this name. |
end_name( | string | — | Submits the displayed end date under this name. |
content( | AnyElement | | — | Replacement content receives disabled, invalid, read-only, required, focus and open state. |
trigger_indicator( | AnyElement | AnyElement:: | Replaces the glyph inside the existing trigger geometry and behavior. |
range_separator( | AnyElement | " - " | Replaces the text between the two editable date fields. |
placement( | Placement | Placement:: | The full 22-value union renders through the shared vocabulary with default eight-pixel popover offset: physical and aligned spellings plus the logical start/end aliases, which share their left/right side's pixels in this LTR-only port. The centered bottom value matches default. |
Not ported2 HeroUI entries not ported
| HeroUI v3 | Why it is not ported |
|---|---|
DateRangePicker.className / style | Browser styles and inline styles are unavailable. |
DateRangePicker.render | Root-element substitution has no GPUI equivalent. |
Parts
| Part | Description |
|---|---|
DateRangePicker | Root state owner and focus-within scope with two system-regionally formatted DateFields. Simultaneous pickers need distinct DateRangeState owners. |
DateRangePickerPartial | Separate keyboard and pointer action inside the shared range field. |
DateRangePicker | Default or caller-supplied 16px trigger glyph. |
DateRangePicker | Default or caller-supplied separator between the start and end fields. |
DateRangePickerPartial | RangeCalendar overlay positioned by the shared Placement vocabulary. |
States
| State | Builder | Description |
|---|---|---|
Open | controlled/open overlay state | Mounts the range calendar overlay and hands open state to replacement content. |
Focus within | close_on_blur/content focus scope | Tracks focus across both editable fields, the trigger and the calendar. |
Focus visible | focus_visible | Draws keyboard focus and hands keyboard-visible root focus to replacement content. |
Disabled | is_disabled(bool) | Applies disabled opacity and removes editing, opening and submission. |
Read only | is_read_only(bool) | Keeps both fields focusable while making field, trigger and calendar inert. |
Required | is_required(bool) | Marks the label and blocks native submission until both range ends exist. |
Invalid | resolved DateRangePicker validity | Combines controlled, server, custom, syntax and date-constraint validation. |