Slint Kit Documentation

Choices and values

Toggle Group for Slint

KitToggleGroup · Slint Kit 0.3.0

Group of actions with controlled multiple selection.

Group of actions with controlled multiple selection.
Native app built from the example below.

Try the component

Download the complete project, extract it and run cargo run --locked in its folder. Rust 1.92+; Slint Kit 0.3.0 from crates.io. You can also copy the four files below.

Choose Push to add it without losing Email. Remove a chip or toggle an accepted channel to remove it.

ui/app.slint

export { KitFormValidation, HtmlInputKind, NumberInputFormat, KitInputConstraints, KitValidationResult, KitValidityState } from "@kit/kit.slint";
// SPDX-License-Identifier: GPL-3.0-or-later
import { KitToggleGroup, OptionRow, ChoiceRow, ChipRow, SelectState, NavigationStep, SelectionAvailability, KitTheme, KitThemes } from "@kit/kit.slint";
export component App inherits Window {
    title: "KitToggleGroup example";
    preferred-width: 520px;
    preferred-height: 400px;
    max-width: 100000px;
    max-height: 100000px;
    background: root.theme.canvas;
    in-out property <KitTheme> theme: KitThemes.light;
    in property <[OptionRow]> rows;
    in property <[ChoiceRow]> choices;
    in property <[ChipRow]> chips;
    in property <SelectState> select-state;
    in property <string> result;
    callback toggle(int, int);
    callback navigate(NavigationStep);
    callback open();
    callback close();
    callback remove(int);
    VerticalLayout {
        padding: 24px;
        spacing: 16px;
        alignment: start;
        control := KitToggleGroup {
            theme: root.theme; rows: root.choices; generation: 1; active-token: root.select-state.active.token; label: "Delivery channels";
            navigate(step) => { root.navigate(step); }
            toggled(token, generation) => { root.toggle(token, generation); }
        }
        result := Text { text: root.result; color: root.theme.text-primary; }
    }

}

src/main.rs

// SPDX-License-Identifier: GPL-3.0-or-later
use slint::{ComponentHandle, ModelRc, VecModel};
use slint_kit::{MultiSelectController, Ownership, SelectOption, NavigationStep};
use std::{cell::RefCell, rc::Rc};
mod ui { slint::include_modules!(); }
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
enum Channel { Email, Push, Sms }
impl Channel {
    fn all() -> [Self; 3] { [Self::Email, Self::Push, Self::Sms] }
    fn token(self) -> i32 { match self { Self::Email => 11, Self::Push => 23, Self::Sms => 38 } }
    fn from_token(token: i32) -> Option<Self> { Self::all().into_iter().find(|key| key.token() == token) }
    fn label(self) -> &'static str { match self { Self::Email => "Email", Self::Push => "Push", Self::Sms => "SMS" } }
}
fn publish(app: &ui::App, controller: &MultiSelectController<Channel>, open: bool) {
    let rows = Channel::all().map(|key| ui::OptionRow { token: key.token(), generation: 1, label: key.label().into(), description: "".into(), disabled: false });
    let choices = Channel::all().map(|key| ui::ChoiceRow { token: key.token(), label: key.label().into(), disabled: false, selected: controller.selected().contains(&key) });
    let chips = controller.selected().iter().map(|key| ui::ChipRow { token: key.token(), label: key.label().into(), removable: true }).collect::<Vec<_>>();
    let active = controller.active().copied().unwrap_or(Channel::Email);
    app.set_rows(ModelRc::new(VecModel::from(rows.to_vec()))); app.set_choices(ModelRc::new(VecModel::from(choices.to_vec()))); app.set_chips(ModelRc::new(VecModel::from(chips)));
    app.set_select_state(ui::SelectState { open, generation: 1, active_index: Channel::all().iter().position(|key| *key == active).unwrap() as i32, active: ui::SelectionRef { present: true, token: active.token() }, selected: ui::SelectionRef { present: false, token: -1 }, selection_availability: ui::SelectionAvailability::Absent });
    app.set_result(format!("Selected: {}", controller.selected().iter().map(|key| key.label()).collect::<Vec<_>>().join(", ")).into());
}
fn step(value: ui::NavigationStep) -> NavigationStep { match value { ui::NavigationStep::Next => NavigationStep::Next, ui::NavigationStep::Previous => NavigationStep::Previous, ui::NavigationStep::First => NavigationStep::First, ui::NavigationStep::Last => NavigationStep::Last, ui::NavigationStep::PageNext => NavigationStep::PageNext, ui::NavigationStep::PagePrevious => NavigationStep::PagePrevious } }
fn main() -> Result<(), slint::PlatformError> {
    let app = ui::App::new()?;
    slint_kit::init_form_validation!(ui, app);
    let controller = Rc::new(RefCell::new(MultiSelectController::new(Channel::all().into_iter().map(|key| SelectOption::new(key, key.label())).collect(), vec![Channel::Email], Ownership::Local, None).expect("unique channel keys")));
    publish(&app, &controller.borrow(), false);
    let state = controller.clone(); let weak = app.as_weak();
    app.on_open(move || { if let Some(app) = weak.upgrade() { publish(&app, &state.borrow(), true); } });
    let state = controller.clone(); let weak = app.as_weak();
    app.on_close(move || { if let Some(app) = weak.upgrade() { publish(&app, &state.borrow(), false); } });
    let state = controller.clone(); let weak = app.as_weak();
    app.on_navigate(move |direction| { state.borrow_mut().navigate(step(direction), 3); if let Some(app) = weak.upgrade() { publish(&app, &state.borrow(), app.get_select_state().open); } });
    let state = controller.clone(); let weak = app.as_weak();
    app.on_toggle(move |token, generation| {
        if generation != 1 { return; }
        if let Some(key) = Channel::from_token(token) { let _ = state.borrow_mut().toggle(&key); }
        if let Some(app) = weak.upgrade() { publish(&app, &state.borrow(), true); }
    });
    let weak = app.as_weak();
    app.on_remove(move |token| {
        if let Some(key) = Channel::from_token(token) { if controller.borrow().selected().contains(&key) { let _ = controller.borrow_mut().toggle(&key); } }
        if let Some(app) = weak.upgrade() { publish(&app, &controller.borrow(), app.get_select_state().open); }
    });
    app.run()
}

Cargo.toml and build.rs

Cargo.toml

[package]
name = "slint-kit-toggle-group-example"
version = "0.1.0"
edition = "2024"
rust-version = "1.92"
license = "GPL-3.0-or-later"
publish = false

[dependencies]
slint-kit = "=0.3.0"
slint = { version = "=1.18.1", default-features = false, features = ["std", "compat-1-18", "backend-winit", "renderer-skia", "accessibility"] }

[build-dependencies]
slint-kit-build = "=0.3.0"

[workspace]

build.rs

fn main() { slint_kit_build::compile("ui/app.slint").expect("compile component example"); }

Files: Cargo.toml · build.rs · ui/app.slint · src/main.rs

Set each ChoiceRow.selected from your domain set and handle toggled(token, generation). Keep a separate active-token for keyboard navigation.

Each row can be selected independently. The API stays distinct from the radio group and imposes no exclusivity.

Choose between built-in options and custom composition

Shared options

Visual composition

The file below produces the screenshot. For controller-driven components, it shows the displayed state without supplying the Rust wiring. Create a project.

Show the screenshot source
Group of actions with controlled multiple selection.
Native screenshot of the same example, rendered with Skia.
// SPDX-License-Identifier: GPL-3.0-or-later
import { KitTheme, KitThemes, KitToggleGroup } from "@kit/kit.slint";

export component Example inherits Window {
    title: "KitToggleGroup example";
    preferred-width: 420px;
    preferred-height: 180px;
    max-width: 100000px;
    max-height: 100000px;
    background: root.theme.canvas;
    in-out property <KitTheme> theme: KitThemes.light;
    in-out property <string> status;
    VerticalLayout {
        padding: 24px; spacing: 16px; alignment: start;
        Text { text: "Toggle Group"; color: root.theme.text-primary; font-size: 20px * root.theme.text-scale; font-weight: 600; }
        KitToggleGroup { theme: root.theme; rows: [{ token: 11, label: "Email", selected: true, disabled: false }, { token: 23, label: "Push", selected: false, disabled: false }, { token: 38, label: "SMS", selected: false, disabled: true }]; generation: 1; active-token: 11; toggled(token, generation) => { root.status = "Choice token " + token; } }
        if root.status != "": Text { text: root.status; color: root.theme.text-secondary; font-size: 12px * root.theme.text-scale; wrap: word-wrap; }
    }
}

Download this example

Public API

Inherits Rectangle.

Properties

Name Type Direction
required bool in
required-message string in
custom-validity string in-out
validity KitValidityState out
will-validate bool out
validation-message string out
theme KitTheme in
control-size ControlSize in
access FieldAccess in
rows [ChoiceRow] in
generation int in
active-token int in
label string in
has-focus bool out

Types and variants

Type Declaration
struct ChoiceRow token: int, label: string, disabled: bool, selected: bool
enum ControlSize compact, medium, comfortable
enum FieldAccess editable, read-only, disabled
struct KitValidityState valid: bool, value-missing: bool, type-mismatch: bool, pattern-mismatch: bool, too-long: bool, too-short: bool, range-underflow: bool, range-overflow: bool, step-mismatch: bool, bad-input: bool, custom-error: bool,
enum NavigationStep next, previous, first, last, page-next, page-previous

Callbacks

Functions

See also