Slider

A slider component for selecting numeric values within a specified range. Supports both single value and range selection modes, horizontal and vertical orientations, custom styling, and step intervals.

Install

#Import

use gpui_kit::component::slider::{Slider, SliderState, SliderEvent, SliderValue};
// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { Slider } from "slider.slint";
use iced::widget::{column, container, mouse_area, row, scrollable, svg, text};
use iced::{Alignment, Element, Font, Length, Subscription, Task, Theme, font};

#Usage

#Volume

struct VolumeControl {
    volume_slider: Entity<SliderState>,
    volume: f32,
}

impl VolumeControl {
    fn new(cx: &mut Context<Self>) -> Self {
        let volume_slider = cx.new(|_| {
            SliderState::new()
                .min(0.0)
                .max(100.0)
                .step(1.0)
                .default_value(50.0)
        });

        let subscription = cx.subscribe(&volume_slider, |this, _, event: &SliderEvent, cx| {
            match event {
                SliderEvent::Change(value) => {
                    this.volume = value.start();
                    this.apply_volume_change();
                    cx.notify();
                }
            }
        });

        Self {
            volume_slider,
            volume: 50.0,
        }
    }

    fn apply_volume_change(&self) {
        // Apply volume change to audio system
        println!("Volume changed to: {}%", self.volume);
    }
}

impl Render for VolumeControl {
    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
        h_flex()
            .items_center()
            .gap_3()
            .child("🔊")
            .child(Slider::new(&self.volume_slider).flex_1())
            .child(format!("{}%", self.volume as i32))
    }
}
Slider {
    value <=> root.volume;
}
Text {
    text: "\{round(root.volume)}%";
    color: Theme.muted-foreground;
}
// The Slint example's volume: one slider bound to a number, with the number
// printed beside it. The value is the caller's — the slider reports what a
// drag asked for and the application writes it back.
let volume = column![
    row![
        slider(Slot::Volume, app.volume).width(Length::Fill),
        readout(app.volume, "%"),
    ]
    .spacing(12.0)
    .align_y(Alignment::Center),
]
.spacing(8.0);

#Basic Slider

let slider_state = cx.new(|_| {
    SliderState::new()
        .min(0.0)
        .max(100.0)
        .default_value(50.0)
        .step(1.0)
});

Slider::new(&slider_state)
VerticalLayout {
    spacing: 10px;
    alignment: start;
    Slider {
        value: 25;
    }
    Slider {
        value: 50;
    }
    Slider {
        value: 75;
    }
}
// The value is a plain number, so a slider backs any bounded quantity. Three
// sliders, each with the default 0..=100 range and step of one.
let basic = column![
    slider(Slot::BasicTop, app.basic_top),
    slider(Slot::BasicMiddle, app.basic_middle),
    slider(Slot::BasicBottom, app.basic_bottom),
]
.spacing(10.0);

#Slider with Event Handling

struct MyView {
    slider_state: Entity<SliderState>,
    current_value: f32,
}

impl MyView {
    fn new(cx: &mut Context<Self>) -> Self {
        let slider_state = cx.new(|_| {
            SliderState::new()
                .min(0.0)
                .max(100.0)
                .default_value(25.0)
                .step(5.0)
        });

        let subscription = cx.subscribe(&slider_state, |this, _, event: &SliderEvent, cx| {
            match event {
                SliderEvent::Change(value) => {
                    this.current_value = value.start();
                    cx.notify();
                }
            }
        });

        Self {
            slider_state,
            current_value: 25.0,
        }
    }
}

impl Render for MyView {
    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        v_flex()
            .gap_2()
            .child(Slider::new(&self.slider_state))
            .child(format!("Value: {}", self.current_value))
    }
}
SlintNo Slint version of this example yet.

#Range Slider

let range_slider = cx.new(|_| {
    SliderState::new()
        .min(0.0)
        .max(100.0)
        .default_value(20.0..80.0)  // Range from 20 to 80
        .step(1.0)
});

Slider::new(&range_slider)
SlintNo Slint version of this example yet.

#Vertical Slider

Slider::new(&slider_state)
    .vertical()
    .h(px(200.))
SlintNo Slint version of this example yet.

#Custom Step Intervals

// Integer steps
let integer_slider = cx.new(|_| {
    SliderState::new()
        .min(0.0)
        .max(10.0)
        .step(1.0)
        .default_value(5.0)
});

// Decimal steps
let decimal_slider = cx.new(|_| {
    SliderState::new()
        .min(0.0)
        .max(1.0)
        .step(0.01)
        .default_value(0.5)
});
Slider {
    value: 20;
    step: 10;
}
// A step of ten, so a drag snaps to 0, 10, 20 … and the arrow keys move by
// the same interval.
let stepped = column![
    slider(Slot::Stepped, app.stepped).step(10.0),
    muted_line(&format!(
        "Step 10 — the value is {}.",
        number_input::shown(app.stepped as f64)
    )),
]
.spacing(8.0);

#Min/Max Configuration

// Temperature slider
let temp_slider = cx.new(|_| {
    SliderState::new()
        .min(-10.0)
        .max(40.0)
        .default_value(20.0)
        .step(0.5)
});

// Percentage slider
let percent_slider = cx.new(|_| {
    SliderState::new()
        .min(0.0)
        .max(100.0)
        .default_value(75.0)
        .step(5.0)
});
Slider {
    value: 21;
    minimum: -10;
    maximum: 40;
}
// The bounds are the caller's: this one runs from -10 to 40, the temperature
// range the Slint group names.
let ranged = column![
    slider(Slot::Ranged, app.ranged)
        .minimum(-10.0)
        .maximum(40.0),
    muted_line(&format!(
        "From -10 to 40 at step 1 — the value is {}.",
        number_input::shown(app.ranged as f64)
    )),
]
.spacing(8.0);

#Disabled State

Slider::new(&slider_state)
    .disabled(true)
Slider {
    value: 30;
    enabled: false;
}
// A control with no message cannot move, which is how iced spells disabled;
// `disabled(true)` also draws the muted track and thumb, which iced cannot
// fade as one subtree.
let disabled = column![
    Slider::<Message>::new(30.0).disabled(true),
    muted_line("Disabled: the fill and the thumb's edge take a lower weight, and the pointer and the keys do nothing."),
]
.spacing(8.0);

#Custom Styling

Slider::new(&slider_state)
    .bg(cx.theme().success)
    .text_color(cx.theme().success_foreground)
    .rounded(px(4.))
VerticalLayout {
    spacing: 12px;
    alignment: start;
    Slider {
        value: 40;
    }
    Slider {
        value: 65;
    }
}
// The page's own sample names a fill, a thumb and a corner. Here each part
// is named: the first slider tightens its track to a 2-pixel corner and fills
// it in success over a light success tint, and the second reverses the fill
// in danger and gives the thumb a solid face. Neither takes the primary the
// default sliders above use.
let styled = column![
    slider(Slot::StyledTop, app.styled_top)
        .track_color(|tokens| theme::alpha(tokens.success, 0.2))
        .fill_color(|tokens| tokens.success)
        .thumb_color(|tokens| tokens.background)
        .thumb_border_color(|tokens| tokens.success)
        .radius(2.0),
    slider(Slot::StyledBottom, app.styled_bottom)
        .track_color(|tokens| theme::alpha(tokens.danger, 0.15))
        .fill_color(|tokens| tokens.danger)
        .thumb_color(|tokens| tokens.danger)
        .thumb_border_color(|tokens| tokens.background)
        .reverse(true),
    muted_line("Success and danger replace the primary on both track and thumb: the first tightens the track's corner, the second fills from the thumb to the end. Each part is a builder over the active tokens, so both themes keep the colours readable."),
]
.spacing(10.0);

#Scale

There have 2 types of scale for the slider:

  • Linear (default)
  • Logarithmic

The logarithmic scale is useful when the range of values is large and you want to give more precision to smaller values.

let log_slider = cx.new(|_| {
    SliderState::new()
        .min(1.0) // min must be greater than 0 for log scale
        .max(1000.0)
        .default_value(10.0)
        .step(1.0)
        .scale(SliderScale::Logarithmic)
});
SlintNo Slint version of this example yet.

In this case:

  • If slider at 25%, value will be approximately 5.62.
  • If slider at 50%, value will be approximately 31.62.
  • If slider at 75%, value will be approximately 177.83.
  • If slider at 100%, value will be 1000.0.

#Conversions

// From f32
let single_value: SliderValue = 42.0.into();

// From tuple
let range_value: SliderValue = (10.0, 90.0).into();

// From Range
let range_value: SliderValue = (10.0..90.0).into();
SlintNo Slint version of this example yet.

#SliderEvent

EventDescription
Change(SliderValue)Emitted continuously while the slider value is being changed
Release(SliderValue)Emitted once when the user releases the slider after interaction

#Styling

The slider component implements Styled trait and supports:

  • Background color for track and thumb
  • Text color for thumb
  • Border radius
  • Size customization

#Color Picker

struct ColorPicker {
    hue_slider: Entity<SliderState>,
    saturation_slider: Entity<SliderState>,
    lightness_slider: Entity<SliderState>,
    alpha_slider: Entity<SliderState>,
    current_color: Hsla,
}

impl ColorPicker {
    fn new(cx: &mut Context<Self>) -> Self {
        let hue_slider = cx.new(|_| {
            SliderState::new()
                .min(0.0)
                .max(1.0)
                .step(0.01)
                .default_value(0.5)
        });

        let saturation_slider = cx.new(|_| {
            SliderState::new()
                .min(0.0)
                .max(1.0)
                .step(0.01)
                .default_value(1.0)
        });

        // Subscribe to all sliders to update color
        let subscriptions = [&hue_slider, &saturation_slider /* ... */]
            .iter()
            .map(|slider| {
                cx.subscribe(slider, |this, _, event: &SliderEvent, cx| {
                    match event {
                        SliderEvent::Change(_) => {
                            this.update_color(cx);
                        }
                    }
                })
            })
            .collect::<Vec<_>>();

        Self {
            hue_slider,
            saturation_slider,
            // ... other fields
        }
    }

    fn update_color(&mut self, cx: &mut Context<Self>) {
        let h = self.hue_slider.read(cx).value().start();
        let s = self.saturation_slider.read(cx).value().start();
        // ... calculate color
        self.current_color = hsla(h, s, l, a);
        cx.notify();
    }
}

impl Render for ColorPicker {
    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        h_flex()
            .gap_4()
            .child(
                v_flex()
                    .gap_2()
                    .child("Hue")
                    .child(Slider::new(&self.hue_slider).vertical().h(px(120.)))
            )
            .child(
                v_flex()
                    .gap_2()
                    .child("Saturation")
                    .child(Slider::new(&self.saturation_slider).vertical().h(px(120.)))
            )
            // ... other sliders
    }
}
SlintNo Slint version of this example yet.

#Price Range Filter

struct PriceFilter {
    price_range: Entity<SliderState>,
    min_price: f32,
    max_price: f32,
}

impl PriceFilter {
    fn new(cx: &mut Context<Self>) -> Self {
        let price_range = cx.new(|_| {
            SliderState::new()
                .min(0.0)
                .max(1000.0)
                .step(10.0)
                .default_value(100.0..500.0)  // Range slider
        });

        let subscription = cx.subscribe(&price_range, |this, _, event: &SliderEvent, cx| {
            match event {
                SliderEvent::Change(value) => {
                    this.min_price = value.start();
                    this.max_price = value.end();
                    this.filter_products();
                    cx.notify();
                }
            }
        });

        Self {
            price_range,
            min_price: 100.0,
            max_price: 500.0,
        }
    }

    fn filter_products(&self) {
        println!("Filtering products: ${} - ${}", self.min_price, self.max_price);
    }
}

impl Render for PriceFilter {
    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
        v_flex()
            .gap_2()
            .child("Price Range")
            .child(Slider::new(&self.price_range))
            .child(format!("${} - ${}", self.min_price as i32, self.max_price as i32))
    }
}
SlintNo Slint version of this example yet.

#Temperature Slider with Custom Styling

struct TemperatureControl {
    temp_slider: Entity<SliderState>,
    temperature: f32,
}

impl Render for TemperatureControl {
    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let temp_color = if self.temperature < 10.0 {
            cx.theme().info  // Cold - blue
        } else if self.temperature > 25.0 {
            cx.theme().destructive  // Hot - red
        } else {
            cx.theme().success  // Comfortable - green
        };

        v_flex()
            .gap_3()
            .child("Temperature Control")
            .child(
                Slider::new(&self.temp_slider)
                    .bg(temp_color)
                    .text_color(cx.theme().background)
                    .rounded(px(8.))
            )
            .child(format!("{}°C", self.temperature as i32))
    }
}
SlintNo Slint version of this example yet.

#Keyboard Shortcuts

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// The four arrows are the control's own, read while the pointer is over it
// because iced reports no focus to a widget: `←`/`↓` and `→`/`↑` take the
// bounded step. `Home`, `End`, `Page Up` and `Page Down` are the
// application's, wired through the subscription above, and `Tab`/`Shift+Tab`
// are iced's focus traversal.
let keyboard = column![
    slider(Slot::Keyboard, app.keyboard),
    muted_line(&format!(
        "← ↓ → ↑ step, Home and End jump, Page Up and Page Down move by ten — the value is {}.",
        number_input::shown(app.keyboard as f64)
    )),
    muted_line("iced reports no focus to the application, so the subscription acts on this slider whatever is focused."),
]
.spacing(8.0);
KeyAction
← / ↓Decrease value by step
→ / ↑Increase value by step
Page DownDecrease by larger amount
Page UpIncrease by larger amount
HomeSet to minimum value
EndSet to maximum value
TabMove focus to next element
Shift + TabMove focus to previous element