Slider
Select a value from a range by dragging.
use herogpui::components::slider::Slider;Slider on docs.rsSource at v0.14.0
Usage
Showing Usage, live: HeroGPUI compiled to WebAssembly. The canvas has no accessibility tree; each example's description and code below carry the same information.
Labels and values use 14px text with a 20px line height, independent of the surrounding text style. An element expression with window: &mut Window and cx: &mut App in scope.
use herogpui::prelude::Slider;
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
gpui::div()
.w(px(320.))
.child(
Slider::new("slider-main", 30.)
.default_value(30.)
.label("Volume")
.show_value(true),
)Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{NumberFormat, Slider};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
let value = self.slider_value;
gpui::div()
.w(px(320.))
.child(Slider::new("slider-fmt", value)
.label("Budget")
.show_value(true)
.format_options(NumberFormat::currency("EUR"))
.on_change(cx.listener(|this, v: &f32, _, cx| {
this.slider_value = *v;
cx.notify();
})))A range slider is built from its parts: a `Label`, an `Output`, and a `Track` whose `thumb` closure is handed the state so it can draw one thumb per value. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::Slider;
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
let value = self.slider_value;
gpui::div()
.w(px(320.))
.child(Slider::new("slider-anatomy", 25.)
.default_values([25., 75.])
.label("Price range")
.show_value(true)
.thumb(|index, value| {
gpui::div()
.id(("sl-anatomy-thumb", index))
.size(px(18.))
.rounded_full()
.border_2()
.border_color(gpui::white())
.bg(gpui::rgb(0x0085F5))
// The closure is handed the value the slider
// already computed for this thumb, so the
// caller never re-derives it.
.flex()
.items_center()
.justify_center()
.child(
gpui::div()
.absolute()
.top(px(-20.))
.text_size(px(11.))
.child(format!("{value:.0}")),
)
.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, Slider};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{App, px};
use herogpui::gpui;
let volume = self.demo_value("sl-controlled", 40.);
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![
gpui::div()
.w(px(320.))
.child(Slider::new("slider-controlled", volume)
.label("Volume")
.show_value(true)
.on_change(cx.listener(|this, v: &f32, _, cx| {
this.set_demo_value("sl-controlled", *v);
cx.notify();
})))
.into_any_element(),
para(&format!("Value: {volume:.0}"), cx),
])Also calls para (included when you copy).
An element expression with window: &mut Window and cx: &mut App in scope.
use herogpui::prelude::Slider;
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![
gpui::div()
.w(px(320.))
.child(Slider::new("slider-fmt-pct", 0.35)
.default_value(0.35)
.min_value(0.)
.max_value(1.)
.step(0.01)
.label("Opacity")
.show_value(true)
.format_options(herogpui::core::NumberFormat::percent()))
.into_any_element(),
gpui::div()
.w(px(320.))
.child(Slider::new("slider-fmt-cur", 1200.)
.default_value(1200.)
.min_value(0.)
.max_value(5000.)
.step(50.)
.label("Budget")
.show_value(true)
.format_options(herogpui::core::NumberFormat::currency("USD")))
.into_any_element(),
])The output closure receives every live value and its formatted thumb label. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{Chip, ChipLabel, ChipVariant, Color, Slider};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
let volume = self.demo_value("sl-controlled", 40.);
gpui::div()
.w(px(320.))
.child(
Slider::new("slider-output", volume)
.label("Brightness")
.output(|values, labels| {
Chip::new()
.variant(ChipVariant::Soft)
.color(Color::Accent)
.radius(px(4.))
.text_size(px(12.))
.child(ChipLabel::new().child(format!(
"{:.0}% ({})",
values[0], labels[0]
)))
.into_any_element()
})
.on_change(cx.listener(|this, v: &f32, _, cx| {
this.set_demo_value("sl-controlled", *v);
cx.notify();
})),
)Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::Slider;
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
let value = self.slider_value;
gpui::div()
.w(px(320.))
.child(Slider::new("slider-range", value)
.label("Price range")
// An empty controlled range falls back to one
// thumb; keep that fallback interactive too.
.default_value(value)
.values(self.slider_range.clone())
.on_change_all(cx.listener(|this, vs: &[f32], _, cx| {
this.slider_range = vs.to_vec();
cx.notify();
})))An uncontrolled range specimen: two thumbs seeded at 100 and 500 over a 0-1000 span, stepping by 50 and formatted as US dollars. An element expression with window: &mut Window and cx: &mut App in scope.
use herogpui::prelude::Slider;
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
gpui::div()
.w(px(320.))
.child(Slider::new("slider-range-slider", 100.)
.default_values([100., 500.])
.min_value(0.)
.max_value(1000.)
.step(50.)
.label("Price Range")
.show_value(true)
.format_options(herogpui::core::NumberFormat::currency("USD")))The plainest slider: one thumb, a label, and the value readout, uncontrolled. An element expression with window: &mut Window and cx: &mut App in scope.
use herogpui::prelude::Slider;
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
gpui::div()
.w(px(320.))
.child(Slider::new("slider-basic", 100.)
.min_value(0.)
.max_value(200.)
.default_value(100.)
.label("Storage")
.show_value(true))A disabled slider keeps its value readable but answers no drag, wheel, or keys. An element expression with window: &mut Window and cx: &mut App in scope.
use herogpui::prelude::Slider;
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
gpui::div()
.w(px(320.))
.child(Slider::new("slider-disabled", 30.)
.default_value(30.)
.label("Volume")
.show_value(true)
.is_disabled(true))Continuous seeking brackets pointer updates with drag hooks; arbitrary presets retain keyboard navigation. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::Slider;
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
let seek_start = cx.entity().downgrade();
let seek_end = seek_start.clone();
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![
gpui::div()
.w(px(320.))
.child(Slider::new("slider-continuous", 0.5)
.default_value(0.5).max_value(1.).continuous(true)
.sx(|el| el.rounded_full()).show_value(true)
.label(if self.demo_flag("sl-seeking", false) { "Preview paused" } else { "Preview playing" })
.on_drag_start(move |_, cx| { let _ = seek_start.update(cx, |this, cx| { this.set_demo_flag("sl-seeking", true); cx.notify(); }); })
.on_drag_end(move |_, cx| { let _ = seek_end.update(cx, |this, cx| { this.set_demo_flag("sl-seeking", false); cx.notify(); }); }))
.into_any_element(),
gpui::div()
.w(px(320.))
.child(Slider::new("slider-presets", 1.)
.default_value(1.).min_value(0.25).max_value(2.)
.steps([0.25, 0.5, 1., 1.5, 2.]).show_value(true))
.into_any_element(),
])`size` is HeroGPUI's additive scale: `Md` is v3's 20px rail with its two-layer thumb, and `Sm` is a 6px pill with a single 12px knob. An element expression with window: &mut Window and cx: &mut App in scope.
use herogpui::prelude::{Slider, SliderSize};
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![
gpui::div()
.w(px(320.))
.child(Slider::new("slider-size-md", 40.)
.default_value(40.)
.label("Md")
.show_value(true))
.into_any_element(),
gpui::div()
.w(px(320.))
.child(Slider::new("slider-size-sm", 40.)
.default_value(40.)
.size(SliderSize::Sm)
.label("Sm")
.show_value(true))
.into_any_element(),
])The vertical specimen grows upward with its label attached. An element expression with window: &mut Window and cx: &mut App in scope.
use herogpui::prelude::{Orientation, Slider};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
gpui::div()
.w(px(320.))
.child(Slider::new("slider-vert-orientation", 30.)
.default_value(30.)
.label("Volume")
.show_value(true)
.orientation(Orientation::Vertical))`Slider.Thumb.isDisabled` fixes one thumb — dimmed, out of the roving tab stop, answering no drag or keys — while the other thumb keeps moving: the contrast a whole-slider `isDisabled` cannot show. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::Slider;
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
let value = self.slider_value;
gpui::div()
.w(px(320.))
.child(Slider::new("slider-lock", value)
.label("Price range")
.default_value(value)
.values(self.slider_range.clone())
.disabled_keys([0])
.on_change_all(cx.listener(|this, vs: &[f32], _, cx| {
this.slider_range = vs.to_vec();
cx.notify();
})))`Slider.Thumb.name` names each end of a range. `form_fields` hands the pair to the `Form` — it is told its fields, with no context propagation — so a submission carries one value per named thumb. 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, Button, Form, FormData, Slider};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{App, px};
use herogpui::gpui;
let value = self.slider_value;
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![
gpui::div()
.w(px(320.))
.child({
// v3 renders one `<input name=…>` per thumb; the form reads
// them back through `form_fields`, as DateRangePicker does.
let slider = Slider::new("slider-form", value)
.label("Price range")
.default_value(value)
.values(self.slider_range.clone())
.start_name("min")
.end_name("max")
.on_change_all(cx.listener(|this, vs: &[f32], _, cx| {
this.slider_range = vs.to_vec();
cx.notify();
}));
let mut form = Form::new();
for field in slider.form_fields() {
form = form.field(field);
}
let form =
form.on_submit(cx.listener(|this, data: &FormData, _, cx| {
this.input_submitted = format!(
"min={}, max={}",
data.text("min").unwrap_or_default(),
data.text("max").unwrap_or_default(),
);
cx.notify();
}));
let submit = form.submit_handler();
form.child(slider.into_any_element())
.child(
Button::new("slider-form-submit")
.label("Save")
.on_press(move |_, window, cx| submit(window, cx)),
)
.into_any_element()
})
.into_any_element(),
para(
&if self.input_submitted.is_empty() {
"Nothing submitted yet".to_owned()
} else {
format!("Submitted: {}", self.input_submitted)
},
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::{Orientation, Slider};
let value = self.slider_value;
Slider::new("slider-vert", value)
.orientation(Orientation::Vertical)
.on_change(cx.listener(|this, v: &f32, _, cx| {
this.slider_value = *v;
cx.notify();
}))Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::Slider;
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
let value = self.slider_value;
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![
gpui::div()
.w(px(320.))
.child(Slider::new("slider-step", value)
.step(10.0)
.label("Step 10")
.show_value(true)
.on_change(cx.listener(|this, v: &f32, _, cx| {
this.slider_value = *v;
cx.notify();
})))
.into_any_element(),
gpui::div()
.w(px(320.))
.child(Slider::new("slider-step-disabled", value)
.is_disabled(true)
.label("Disabled"))
.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
Slider 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 Slider 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 |
|---|---|---|
orientation(Orientation) | Orientation::Horizontal | Orientation::Vertical | The direction in which values increase. |
// 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: 29 entries ported, 4 partial, 15 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( | number | | — | The current value or values (controlled). Only the active single/range value mode owns state. |
default_value( | number | | — | The initial value or values (uncontrolled). Use this seed for interactive examples that do not store onChange values, including custom number formatting. |
on_change( | Fn( | — | Handler called whenever a thumb changes value. |
on_change_end( | Fn( | — | Handler called once when a drag or keyboard step completes. |
min_value( | number | 0 | The minimum value. |
max_value( | number | 100 | The maximum value. |
step( | number | 1 | The increment between valid values. |
format_options(Partial | Intl. | — | Formats the displayed value; supports decimal, percent and currency without CLDR locale data. |
orientation( | Orientation:: | Orientation:: | The direction in which values increase. |
is_disabled( | true | | — | Disables every thumb and removes the slider from the tab order. |
label(Partial | AnyElement | | — | Compound slider content; the monolithic port exposes its label, output and thumb customizations as builders. |
output( | AnyElement | | — | Custom output content handed every live value and its formatted thumb label. |
thumb(Partial | AnyElement | | — | Track content; the thumb closure receives each computed index and value. |
disabled_keys( | true | | — | Disables this thumb only and removes its form field and tab stop. |
thumb_names( | string | — | Name of this thumb's submitted form value. |
Not ported15 HeroUI entries not ported
| HeroUI v3 | Why it is not ported |
|---|---|
Slider.aria-label | Accessible label for an otherwise unlabelled slider. |
Slider.aria-labelledby | ID of the element that labels the slider. |
Slider.className | For the root. Native sx pixel corners also reach the track caps and both Md thumb layers; joined single-thumb cap/fill edges remain square. |
Slider.render | Overrides the root element. |
Slider.Output.className | For the output. |
Slider.Output.render | Overrides the output element. |
Slider.Track.className | For the track. |
Slider.Track.render | Overrides the track element. |
Slider.Fill.className | For the fill. |
Slider.Fill.style | Inline styles for the fill. |
Slider.Thumb.className | For the thumb. |
Slider.Thumb.render | Overrides the thumb element. |
Render Props.state | Full slider state passed to Output and Track render functions. |
Render Props.orientation | Current orientation supplied to a render function. |
Render Props.isDisabled | Current disabled state supplied to a render function. |
Parts
| Part | Description |
|---|---|
Slider | Root value, form, pointer and keyboard state owner. |
SliderPartial | Optional text label rendered above or beside the track. |
Slider | Formatted current values or custom content rendered from the live value array. |
Slider | Pointer and keyboard surface containing fill and thumbs. |
Slider | Accent span from the minimum or between the outermost thumbs. |
Slider | One draggable, focusable value handle per value. |
States
| State | Builder | Description |
|---|---|---|
Horizontal | Orientation::Horizontal | Values increase from left to right. |
Vertical | Orientation::Vertical | Values increase from bottom to top. |
Focus visible | ring_if_focused | The active enabled thumb carries the keyboard focus ring. |
Dragging | DragState | Pointer drag state updates the value and scales the inner thumb to 90% over the 250ms ease-out ramp; reduced motion settles directly. |
Disabled slider | Slider::is_disabled | All thumbs are inert, dimmed and removed from form submission and tab order. |
Disabled thumb | Slider::disabled_keys | One thumb is inert and omitted while its siblings remain interactive. |
Fill start | fill_from == 0 | The accent fill reaches the low edge. |
Fill end | fill_to == 1 | The accent fill reaches the high edge. |