Toggle Group for Slint
KitToggleGroup · Slint Kit 0.3.0
Group of actions with controlled multiple selection.

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
label— built-in property; custom composition example
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

// 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; }
}
}
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
invalid()navigate(NavigationStep)toggled(int, int)
Functions
check-validity() → boolreport-validity() → boolset-custom-validity(message: string)reset-validation()take-focus()