Toggle
A button-style toggle component that represents on/off or selected states. Unlike a traditional switch, toggles appear as buttons that can be pressed in or out. They’re perfect for toolbar buttons, filter options, or any binary choice that benefits from a button-like appearance.
#Import
use gpui_kit::component::button::{Toggle, ToggleGroup};// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { ToggleVariant, Toggle, ToggleOption, ToggleGroup } from "toggle.slint";use iced::widget::{Space, column, container, mouse_area, row, scrollable, svg, text};
use iced::{Alignment, Border, Element, Length, Task, Theme};#Usage
// The pressed value is the caller's: `new(selected)` reads it and
// `on_toggle` reports the requested new one, which the owner writes back.
let usage = row![
Toggle::new(app.usage)
.label("Bold")
.icon(glyph(BOLD, ControlSize::Medium, app.usage))
.on_toggle(Message::Usage),
Toggle::new(true).label("Pressed"),
Toggle::new(false).label("Unpressed"),
]
.spacing(8.0);#Toggle
Toggle::new("toggle1").
.label("Toggle me")
.checked(false)
.on_click(|checked, _, _| {
println!("Toggle is now: {}", checked);
})GridLayout {
spacing: 8px;
Toggle {
horizontal-stretch: 0;
icon: "bold";
}
Toggle {
horizontal-stretch: 0;
icon: "italic";
selected: true;
}
Toggle {
horizontal-stretch: 0;
icon: "underline";
}
}// An icon on its own is a square control; the icon set belongs to the
// application, so the element is the caller's where Slint names a file.
let icons = row![
Toggle::new(app.bold)
.icon(glyph(BOLD, ControlSize::Medium, app.bold))
.on_toggle(Message::Bold),
Toggle::new(app.italic)
.icon(glyph(ITALIC, ControlSize::Medium, app.italic))
.on_toggle(Message::Italic),
Toggle::new(app.underline)
.icon(glyph(UNDERLINE, ControlSize::Medium, app.underline))
.on_toggle(Message::Underline),
]
.spacing(8.0);Here, we can use on_click to handle toggle state changes. The callback receives the new checked state as a bool.
#Group
use gpui_kit::component::IconName;
Toggle::new("toggle2")
.icon(IconName::Eye)
.checked(true)
.on_click(|checked, _, _| {
println!("Visibility: {}", if *checked { "shown" } else { "hidden" });
})ToggleGroup {
options: [
{ text: "Day", icon: "sun" },
{ text: "Week", icon: "" },
{ text: "Month", icon: "" }
];
}#Controlled Toggle
struct MyView {
is_active: bool,
}
impl Render for MyView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
Toggle::new("active")
.label("Active")
.checked(self.is_active)
.on_click(cx.listener(|view, checked, _, cx| {
view.is_active = *checked;
cx.notify();
}))
}
}VerticalLayout {
spacing: 10px;
alignment: start;
HorizontalLayout {
spacing: 8px;
alignment: start;
Toggle {
text: "Bold";
icon: "bold";
selected <=> root.bold;
}
Button {
text: root.bold ? "Turn off" : "Turn on";
clicked => {
root.bold = !root.bold;
}
}
}
Text {
text: "Bold is " + (root.bold ? "on" : "off");
color: Theme.muted-foreground;
font-size: Theme.font-size-xs;
}
}// The host owns the value, so anything else on the page can move it too.
let controlled = column![
row![
Toggle::new(app.controlled)
.label("Bold")
.icon(glyph(BOLD, ControlSize::Medium, app.controlled))
.on_toggle(Message::Controlled),
Button::new(if app.controlled {
"Turn off"
} else {
"Turn on"
})
.on_press(Message::FlipControlled),
]
.spacing(8.0)
.align_y(Alignment::Center),
text(format!(
"Bold is {}",
if app.controlled { "on" } else { "off" }
)),
]
.spacing(10.0);#Toggle Variants
// Ghost toggle (default)
Toggle::new("ghost-toggle")
.ghost()
.label("Ghost")
// Outline toggle
Toggle::new("outline-toggle")
.outline()
.label("Outline")HorizontalLayout {
spacing: 8px;
alignment: start;
Toggle {
text: "Default";
}
Toggle {
text: "Default on";
selected: true;
}
Toggle {
text: "Outline";
variant: ToggleVariant.outline;
}
Toggle {
text: "Outline on";
variant: ToggleVariant.outline;
selected: true;
}
}// `default` stays flat until it is hovered or pressed; `outline` keeps a
// frame at rest, which is what a toolbar wants.
let variants = row![
Toggle::new(app.variant_default)
.label("Default")
.on_toggle(Message::VariantDefault),
Toggle::new(app.variant_default_on)
.label("Default on")
.on_toggle(Message::VariantDefaultOn),
Toggle::new(app.variant_outline)
.variant(Variant::Outline)
.label("Outline")
.on_toggle(Message::VariantOutline),
Toggle::new(app.variant_outline_on)
.variant(Variant::Outline)
.label("Outline on")
.on_toggle(Message::VariantOutlineOn),
]
.spacing(8.0);#Different Sizes
// Extra small
Toggle::new("xs-toggle")
.icon(IconName::Star)
.xsmall()
// Small
Toggle::new("small-toggle")
.label("Small")
.small()
// Medium (default)
Toggle::new("medium-toggle")
.label("Medium")
// Large
Toggle::new("large-toggle")
.label("Large")
.large()Toggle {
text: "Small";
size: ControlSize.small;
selected: true;
}
Toggle {
text: "Medium";
selected: true;
}
Toggle {
text: "Large";
size: ControlSize.large;
selected: true;
}let sizes = row![
Toggle::new(app.size_small)
.size(ControlSize::Small)
.label("Small")
.on_toggle(Message::SizeSmall),
Toggle::new(app.size_medium)
.label("Medium")
.on_toggle(Message::SizeMedium),
Toggle::new(app.size_large)
.size(ControlSize::Large)
.label("Large")
.on_toggle(Message::SizeLarge),
]
.spacing(8.0);#Disabled State
// Disabled unchecked
Toggle::new("disabled-toggle")
.label("Disabled")
.disabled(true)
.checked(false)
// Disabled checked
Toggle::new("disabled-checked-toggle")
.label("Selected (Disabled)")
.disabled(true)
.checked(true)Toggle {
text: "Disabled";
enabled: false;
}
Toggle {
text: "Disabled on";
enabled: false;
selected: true;
}let disabled = column![
Toggle::new(false).label("Disabled"),
Toggle::new(true).label("Disabled on"),
]
.spacing(10.0);#Toolbar with Toggle Buttons
struct EditorToolbar {
bold: bool,
italic: bool,
underline: bool,
strikethrough: bool,
}
h_flex()
.gap_1()
.p_2()
.bg(cx.theme().background)
.border_1()
.border_color(cx.theme().border)
.child(
ToggleGroup::new("formatting")
.small()
.child(Toggle::new(0).icon(IconName::Bold).checked(self.bold))
.child(Toggle::new(1).icon(IconName::Italic).checked(self.italic))
.child(Toggle::new(2).icon(IconName::Underline).checked(self.underline))
.child(Toggle::new(3).icon(IconName::Strikethrough).checked(self.strikethrough))
.on_click(cx.listener(|view, states, _, cx| {
view.bold = states[0];
view.italic = states[1];
view.underline = states[2];
view.strikethrough = states[3];
cx.notify();
}))
)Rectangle {
width: 100%;
height: 44px;
border-width: 1px;
border-color: Theme.border;
border-radius: Theme.radius-md;
HorizontalLayout {
padding: 6px;
spacing: 4px;
alignment: start;
Toggle {
icon: "bold";
variant: ToggleVariant.outline;
selected <=> root.bold;
}
Toggle {
icon: "italic";
variant: ToggleVariant.outline;
selected <=> root.italic;
}
Toggle {
icon: "underline";
variant: ToggleVariant.outline;
selected <=> root.underline;
}
Rectangle {
width: 1px;
height: 20px;
background: Theme.border;
}
Toggle {
text: "Comment";
icon: "message-circle";
variant: ToggleVariant.outline;
}
}
}// A row of icon toggles on a strip: the outlined variant is what the
// toolbar wants, and the divider is a one-pixel rule between groups.
let toolbar = container(
row![
Toggle::new(app.toolbar_bold)
.variant(Variant::Outline)
.icon(glyph(BOLD, ControlSize::Medium, app.toolbar_bold))
.on_toggle(Message::ToolbarBold),
Toggle::new(app.toolbar_italic)
.variant(Variant::Outline)
.icon(glyph(ITALIC, ControlSize::Medium, app.toolbar_italic))
.on_toggle(Message::ToolbarItalic),
Toggle::new(app.toolbar_underline)
.variant(Variant::Outline)
.icon(glyph(UNDERLINE, ControlSize::Medium, app.toolbar_underline))
.on_toggle(Message::ToolbarUnderline),
container(Space::new().width(1.0).height(20.0)).style(|theme: &Theme| {
container::Style {
background: Some(Tokens::of(theme).border.into()),
..container::Style::default()
}
}),
Toggle::new(app.toolbar_comment)
.variant(Variant::Outline)
.label("Comment")
.icon(glyph(MESSAGE, ControlSize::Medium, app.toolbar_comment))
.on_toggle(Message::ToolbarComment),
]
.spacing(4.0)
.align_y(Alignment::Center),
)
.width(Length::Fill)
.height(44.0)
.padding(6.0)
.align_y(Alignment::Center)
.style(|theme: &Theme| container::Style {
border: Border {
color: Tokens::of(theme).border,
width: 1.0,
radius: theme::RADIUS_MD.into(),
},
..container::Style::default()
});#Filter Interface
struct FilterPanel {
show_completed: bool,
show_pending: bool,
show_cancelled: bool,
show_urgent: bool,
}
v_flex()
.gap_3()
.p_4()
.child(Label::new("Filter by status"))
.child(
ToggleGroup::new("status-filters")
.outline()
.child(Toggle::new(0).label("Completed").checked(self.show_completed))
.child(Toggle::new(1).label("Pending").checked(self.show_pending))
.child(Toggle::new(2).label("Cancelled").checked(self.show_cancelled))
.on_click(cx.listener(|view, states, _, cx| {
view.show_completed = states[0];
view.show_pending = states[1];
view.show_cancelled = states[2];
cx.notify();
}))
)
.child(
Toggle::new("urgent-filter")
.label("Show urgent only")
.checked(self.show_urgent)
.on_click(cx.listener(|view, checked, _, cx| {
view.show_urgent = *checked;
cx.notify();
}))
)#Settings with Individual Toggles
struct NotificationSettings {
email_notifications: bool,
push_notifications: bool,
marketing_emails: bool,
}
v_flex()
.gap_4()
.child(
h_flex()
.items_center()
.justify_between()
.child(
v_flex()
.child(Label::new("Email notifications"))
.child(
Label::new("Receive notifications via email")
.text_color(cx.theme().muted_foreground)
.text_sm()
)
)
.child(
Toggle::new("email-notifications")
.icon(IconName::Mail)
.checked(self.email_notifications)
.on_click(cx.listener(|view, checked, _, cx| {
view.email_notifications = *checked;
cx.notify();
}))
)
)
.child(
h_flex()
.items_center()
.justify_between()
.child(Label::new("Push notifications"))
.child(
Toggle::new("push-notifications")
.icon(IconName::Bell)
.checked(self.push_notifications)
.on_click(cx.listener(|view, checked, _, cx| {
view.push_notifications = *checked;
cx.notify();
}))
)
)#Multi-select Options
struct SelectionView {
selected_categories: Vec<bool>,
}
impl SelectionView {
fn categories() -> Vec<&'static str> {
vec!["Technology", "Design", "Business", "Science", "Art"]
}
}
v_flex()
.gap_3()
.child(Label::new("Select categories of interest"))
.child(
ToggleGroup::new("categories")
.children(
Self::categories()
.into_iter()
.enumerate()
.map(|(i, category)| {
Toggle::new(i)
.label(category)
.checked(self.selected_categories.get(i).copied().unwrap_or(false))
})
)
.on_click(cx.listener(|view, states, _, cx| {
view.selected_categories = states.clone();
cx.notify();
}))
)#Toggle vs Switch
| Feature | Toggle | Switch |
|---|---|---|
| Appearance | Button-like, can be pressed in/out | Traditional switch with sliding indicator |
| Use Cases | Toolbar buttons, filters, binary options | Settings, preferences, on/off states |
| Visual Style | Rectangular button shape | Rounded switch track with thumb |
| State Indication | Background color change, pressed appearance | Position of sliding thumb |
| Multiple Selection | Supports groups with multiple selection | Individual switches only |
Use Toggle when you want:
- Button-like appearance for binary states
- Grouping multiple related options
- Toolbar or filter interfaces
- Options that feel like “selections” rather than “settings”
Use Switch when you want:
- Traditional on/off control appearance
- Settings or preferences interface
- Clear visual indication of state with sliding animation
- Individual boolean controls
#Integration with ToggleGroup
Toggle buttons can be grouped together using ToggleGroup for related options:
#Basic Toggle Group
ToggleGroup::new("filter-group")
.child(Toggle::new(0).icon(IconName::Bell))
.child(Toggle::new(1).icon(IconName::Bot))
.child(Toggle::new(2).icon(IconName::Inbox))
.child(Toggle::new(3).label("Other"))
.on_click(|checkeds, _, _| {
println!("Selected toggles: {:?}", checkeds);
})The on_click callback receives a Vec<bool> representing the new checked state of each toggle in the group.
#Toggle Group with Controlled State
struct FilterView {
notifications: bool,
bots: bool,
inbox: bool,
other: bool,
}
impl Render for FilterView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
ToggleGroup::new("filters")
.child(Toggle::new(0).icon(IconName::Bell).checked(self.notifications))
.child(Toggle::new(1).icon(IconName::Bot).checked(self.bots))
.child(Toggle::new(2).icon(IconName::Inbox).checked(self.inbox))
.child(Toggle::new(3).label("Other").checked(self.other))
.on_click(cx.listener(|view, checkeds, _, cx| {
view.notifications = checkeds[0];
view.bots = checkeds[1];
view.inbox = checkeds[2];
view.other = checkeds[3];
cx.notify();
}))
}
}#Toggle Group Variants and Sizes
// Outline variant, small size
ToggleGroup::new("compact-filters")
.outline()
.small()
.child(Toggle::new(0).icon(IconName::Filter))
.child(Toggle::new(1).icon(IconName::Sort))
.child(Toggle::new(2).icon(IconName::Search))
// Ghost variant (default), extra small
ToggleGroup::new("mini-toolbar")
.xsmall()
.child(Toggle::new(0).icon(IconName::Bold))
.child(Toggle::new(1).icon(IconName::Italic))
.child(Toggle::new(2).icon(IconName::Underline))VerticalLayout {
spacing: 12px;
alignment: start;
ToggleGroup {
size: ControlSize.small;
options: [
{ text: "Day", icon: "" },
{ text: "Week", icon: "" },
{ text: "Month", icon: "" }
];
}
ToggleGroup {
options: [
{ text: "Day", icon: "" },
{ text: "Week", icon: "" },
{ text: "Month", icon: "" }
];
}
ToggleGroup {
size: ControlSize.large;
options: [
{ text: "Day", icon: "" },
{ text: "Week", icon: "" },
{ text: "Month", icon: "" }
];
}
}#Segmented Toggle Group
Use segmented() when a group should render as a connected segmented control. The
group still uses the same multi-toggle behavior: on_click receives a Vec<bool>
with the new checked state for each item.
ToggleGroup::new("formatting")
.segmented()
.outline()
.child(Toggle::new(0).label("Bold").checked(self.bold))
.child(Toggle::new(1).label("Italic").checked(self.italic))
.child(Toggle::new(2).label("Code").checked(self.code))
.on_click(cx.listener(|view, states, _, cx| {
view.bold = states[0];
view.italic = states[1];
view.code = states[2];
cx.notify();
}))By default, segmented groups use a zero gap so adjacent items share one outline. Pass a non-zero gap when you want the segmented sizing and variants but separated items:
use gpui_kit::px;
ToggleGroup::new("quick-actions")
.segmented()
.outline()
.gap(px(8.))
.small()
.child(Toggle::new(0).label("Star"))
.child(Toggle::new(1).label("Watch"))
.child(Toggle::new(2).label("Pin"))If you need mutually exclusive behavior, keep that state in your view model and
set only one child to checked(true) until a dedicated single-selection API is
available.
#Event Handling
#Individual Toggle Events
Toggle::new("subscribe-toggle")
.label("Subscribe")
.on_click(|checked, window, cx| {
if *checked {
// Handle subscription logic
println!("Subscribed!");
} else {
// Handle unsubscription logic
println!("Unsubscribed!");
}
})VerticalLayout {
spacing: 10px;
alignment: start;
HorizontalLayout {
spacing: 8px;
alignment: start;
Toggle {
text: "Bold";
icon: "bold";
changed(selected) => {
root.last = selected ? "Bold on" : "Bold off";
}
}
Toggle {
text: "Italic";
icon: "italic";
changed(selected) => {
root.last = selected ? "Italic on" : "Italic off";
}
}
}
Text {
text: "Last change: " + root.last;
color: Theme.muted-foreground;
font-size: Theme.font-size-xs;
}
}// `on_toggle` reports the state it moved to, so a host can record or
// forward it without reading the property back.
let events = column![
row![
Toggle::new(app.event_bold)
.label("Bold")
.icon(glyph(BOLD, ControlSize::Medium, app.event_bold))
.on_toggle(Message::EventBold),
Toggle::new(app.event_italic)
.label("Italic")
.icon(glyph(ITALIC, ControlSize::Medium, app.event_italic))
.on_toggle(Message::EventItalic),
]
.spacing(8.0),
text(format!(
"Last change: {}",
app.event_last.unwrap_or("nothing yet")
)),
]
.spacing(10.0);#Best Practices
- Use meaningful labels: Choose clear, descriptive text for toggle labels
- Group related options: Use ToggleGroup for logically related binary choices
- Provide visual feedback: The checked state should be clearly distinguishable
- Consider context: Use toggles for options that feel like “selections” rather than “settings”
- Maintain state consistency: Ensure toggle state reflects the actual application state
- Accessible labels: Provide tooltips or ARIA labels for icon-only toggles