Component 50 of 60
Switch
Toggles a setting on or off.
Starting the preview. GPUI compiles to WebAssembly, so the first load takes a few seconds. This browser has neither WebGPU nor WebGL 2, so the live preview cannot run. The code on this page still works. The preview did not start.
fn switch_preview(&self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let switch_view = cx.entity().downgrade();
let row_view = cx.entity().downgrade();
div()
.flex()
.flex_col()
.gap(px(14.0))
.child(switch_row(
Switch::new("preview.switch")
.checked(self.checked)
.aria_label("Airplane mode")
.on_change(move |checked, _, _, cx| {
switch_view
.update(cx, |this, cx| {
this.checked = checked;
cx.notify();
})
.ok();
}),
div()
.id("preview.switch.interactive-label")
.debug_selector(|| "switch-label".into())
.cursor_pointer()
.on_click(move |_, _, cx| {
row_view
.update(cx, |this, cx| {
this.checked = !this.checked;
cx.notify();
})
.ok();
})
.child(if self.checked {
"Airplane mode on"
} else {
"Airplane mode"
}),
))
.child(switch_row(
Switch::new("preview.switch.checked")
.checked(true)
.aria_label("Notifications"),
"Notifications",
))
.child(switch_row(
Switch::new("preview.switch.disabled")
.checked(true)
.disabled(true)
.aria_label("Disabled setting"),
"Disabled",
))
.child(switch_row(
Switch::new("preview.switch.read-only")
.checked(true)
.read_only(true)
.aria_label("Read-only setting"),
"Read only",
))
}
Installation
After the one-time setup, run the command from your app root. The CLI
writes the modules and keeps src/ui/mod.rs in step.
gpuicn add switch
If you prefer to copy the files by hand, declare them yourself:
// src/ui/mod.rs
pub mod theme;
pub mod switch;
// src/main.rs
mod ui;
Usage
Put this in a view module and return example(checked) from its render method.
use crate::ui::switch::*;
use gpui_kit::IntoElement;
fn example(checked: bool) -> impl IntoElement {
Switch::new("airplane")
.checked(checked)
.aria_label("Airplane mode")
.on_change(|checked, _, _, _| println!("Airplane mode: {checked}"))
}
API reference
pub fn new(id: impl Into<ElementId>) -> Self
Creates a `Switch` with a stable caller-owned ID.
pub fn checked(mut self, checked: bool) -> Self
Sets the caller-owned checked state.
pub fn disabled(mut self, disabled: bool) -> Self
Disables interaction and applies the disabled appearance.
pub fn read_only(mut self, read_only: bool) -> Self
Prevents user changes while preserving focus and reading.
pub fn aria_label(mut self, label: impl Into<SharedString>) -> Self
Sets the accessible name of the control.
pub fn on_change(
mut self,
handler: impl Fn(bool, &ClickEvent, &mut Window, &mut App) + 'static,
) -> Self
Reports a requested value change; retain the next value in the owning view.
Installed source, 151 lines
//! Nova-styled controlled Switch using GPUI Kit.
use std::{rc::Rc, sync::Arc};
use gpui_kit::base::Switch as BaseSwitch;
use gpui_kit::{
App, ClickEvent, ElementId, InteractiveElement as _, IntoElement, ParentElement as _,
RenderOnce, SharedString, StatefulInteractiveElement as _, Styled, Window, div,
prelude::FluentBuilder as _, px,
};
use super::theme::{
UiTheme,
primitives::{disabled, focus, switch_colors},
transition_value,
};
type ChangeHandler = Rc<dyn Fn(bool, &ClickEvent, &mut Window, &mut App) + 'static>;
#[derive(IntoElement)]
/// A controlled Nova switch with a themed thumb transition.
pub struct Switch {
style: gpui_kit::StyleRefinement,
id: ElementId,
checked: bool,
disabled: bool,
read_only: bool,
aria_label: Option<SharedString>,
on_change: Option<ChangeHandler>,
}
impl Switch {
/// Creates a `Switch` with a stable caller-owned ID.
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
style: gpui_kit::StyleRefinement::default(),
id: id.into(),
checked: false,
disabled: false,
read_only: false,
aria_label: None,
on_change: None,
}
}
/// Sets the caller-owned checked state.
pub fn checked(mut self, checked: bool) -> Self {
self.checked = checked;
self
}
/// Disables interaction and applies the disabled appearance.
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
/// Prevents user changes while preserving focus and reading.
pub fn read_only(mut self, read_only: bool) -> Self {
self.read_only = read_only;
self
}
/// Sets the accessible name of the control.
pub fn aria_label(mut self, label: impl Into<SharedString>) -> Self {
self.aria_label = Some(label.into());
self
}
/// Reports a requested value change; retain the next value in the owning view.
pub fn on_change(
mut self,
handler: impl Fn(bool, &ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.on_change = Some(Rc::new(handler));
self
}
}
impl RenderOnce for Switch {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = UiTheme::read(cx).clone();
let spacing = theme.spacing.unit;
let colors = theme.colors;
let (track, _) = switch_colors(&theme, self.checked);
let focus_theme = theme.clone();
let thumb_id = ElementId::NamedChild(Arc::new(self.id.clone()), "thumb-motion".into());
let root = BaseSwitch::new(self.id)
.checked(self.checked)
.disabled(self.disabled)
.when(self.read_only, |root| root.aria_description("Read only"))
.relative()
.flex_shrink_0()
.w(spacing * 8_f32)
.h(spacing * 4.6_f32)
.rounded_full()
.border_1()
.border_color(colors.background.opacity(0.))
.bg(track)
.focus_visible(move |style| focus::ring(style, &focus_theme))
.when(!self.disabled && !self.read_only, |root| {
root.cursor_pointer()
})
.when(self.disabled, disabled::style)
.when_some(self.aria_label, |root, label| {
root.accessibility_label(label)
})
.child(AnimatedThumb {
id: thumb_id,
checked: self.checked,
})
.when(!self.read_only, |root| {
root.when_some(self.on_change, |root, handler| {
root.on_change(move |checked, event, window, cx| {
handler(checked, event, window, cx)
})
})
});
super::theme::apply_style(root, &self.style)
}
}
#[derive(IntoElement)]
struct AnimatedThumb {
id: ElementId,
checked: bool,
}
impl RenderOnce for AnimatedThumb {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = UiTheme::read(cx).clone();
let progress = transition_value(
self.id,
if self.checked { 1. } else { 0. },
theme.motion.fast,
window,
cx,
);
let thumb = theme.space(4.);
let inset = ((theme.space(4.6) - px(2.) - thumb) / 2.).max(px(0.));
let travel = (theme.space(8.) - px(2.) - thumb - inset * 2.).max(px(0.));
div()
.absolute()
.top(inset)
.left(inset + travel * progress)
.size(thumb)
.rounded_full()
.bg(switch_colors(&theme, self.checked).1)
}
}
impl gpui_kit::Styled for Switch {
fn style(&mut self) -> &mut gpui_kit::StyleRefinement {
&mut self.style
}
}
Platform notes
Visual reference: shadcn/ui 4.19.0, Neutral Nova. Implementation:
crates/ui/registry/switch/switch.rs, using GPUI Kit 0.6.4.
The application passes checked state and handles on_change. Kit supplies the switch primitive; gpuicn applies the Nova track, animated thumb, focus border and disabled appearance. Reduced motion snaps the transition.
The Rust source and Usage example define the supported API. Browser DOM/CSS behavior, exact exit animations and cross-platform screen-reader parity are not implied by the native component. See migration qualification for the checks completed on this revision.