gpuicn Components

Component 57 of 60

Toggle Group

Groups two-state buttons with shared selection.

site/src/previews/toggle_group.rs

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.

Toggle Group, running in the browser. The second tab shows the preview's own source, which includes its layout and demo state. For the smallest working example, see Usage below. Download the whole showcase for its imports and helpers.

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 toggle-group

If you prefer to copy the files by hand, declare them yourself:

// src/ui/mod.rs
pub mod theme;
pub mod toggle_group;

// src/main.rs
mod ui;

Usage

Put this in a view module and return example(window, cx) from its render method.

use crate::ui::toggle_group::*;
use gpui_kit::{App, IntoElement, ParentElement, Window};

fn example(window: &mut Window, cx: &mut App) -> impl IntoElement {
    let value =
        window.use_keyed_state("styles", cx, |_, _| Vec::<gpui_kit::SharedString>::new());
    ToggleGroup::new("styles")
        .aria_label("Text styles")
        .multiple(true)
        .value(value.read(cx).clone())
        .item(
            ToggleGroupItem::new("bold", "bold")
                .aria_label("Bold")
                .child("B"),
        )
        .item(
            ToggleGroupItem::new("italic", "italic")
                .aria_label("Italic")
                .child("I"),
        )
        .on_change(move |next, _, cx| {
            value.update(cx, |value, cx| {
                *value = next;
                cx.notify();
            })
        })
}

API reference

pub fn new(id: impl Into<ElementId>, value: impl Into<SharedString>) -> Self

Creates a `ToggleGroupItem` with a stable caller-owned ID.

pub fn disabled(mut self, value: bool) -> Self

Disables interaction and applies the disabled appearance.

pub fn aria_label(mut self, label: impl Into<SharedString>) -> Self

Sets the accessible name of the control.

pub fn new(id: impl Into<ElementId>) -> Self

Creates a `ToggleGroup` with a stable caller-owned ID.

pub fn value(mut self, values: impl IntoIterator<Item = impl Into<SharedString>>) -> Self

Sets the caller-owned value displayed by this control.

pub fn aria_label(mut self, label: impl Into<SharedString>) -> Self

Sets the accessible name of the control.

pub fn disabled(mut self, value: bool) -> Self

Disables interaction and applies the disabled appearance.

pub fn multiple(mut self, value: bool) -> Self

Allows multiple selected values instead of one.

pub fn joined(mut self, value: bool) -> Self

Removes gaps between adjacent toggles.

pub fn item(mut self, item: ToggleGroupItem) -> Self

Appends an option to the group.

pub fn on_change(
        mut self,
        handler: impl Fn(Vec<SharedString>, &mut Window, &mut App) + 'static,
    ) -> Self

Reports a requested value change; retain the next value in the owning view.

Installed source, 207 lines
//! Nova toggle sets with controlled selection and arrow-key focus.
use super::theme::{
    RovingFocus, UiTheme, apply_style,
    primitives::{control, focus},
};
use gpui_kit::{
    AnyElement, App, ElementId, InteractiveElement as _, IntoElement, ParentElement, RenderOnce,
    SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled, Window,
    prelude::FluentBuilder as _, px,
};
use std::rc::Rc;

type ChangeHandler = Rc<dyn Fn(Vec<SharedString>, &mut Window, &mut App)>;
/// A labeled toggle option with custom child content.
pub struct ToggleGroupItem {
    id: ElementId,
    value: SharedString,
    label: Option<SharedString>,
    children: Vec<AnyElement>,
    disabled: bool,
    style: StyleRefinement,
}
impl ToggleGroupItem {
    /// Creates a `ToggleGroupItem` with a stable caller-owned ID.
    pub fn new(id: impl Into<ElementId>, value: impl Into<SharedString>) -> Self {
        Self {
            id: id.into(),
            value: value.into(),
            label: None,
            children: Vec::new(),
            disabled: false,
            style: Default::default(),
        }
    }
    /// Disables interaction and applies the disabled appearance.
    pub fn disabled(mut self, value: bool) -> Self {
        self.disabled = value;
        self
    }
    /// Sets the accessible name of the control.
    pub fn aria_label(mut self, label: impl Into<SharedString>) -> Self {
        self.label = Some(label.into());
        self
    }
}
impl Styled for ToggleGroupItem {
    fn style(&mut self) -> &mut StyleRefinement {
        &mut self.style
    }
}
impl ParentElement for ToggleGroupItem {
    fn extend(&mut self, children: impl IntoIterator<Item = AnyElement>) {
        self.children.extend(children);
    }
}
#[derive(IntoElement)]
/// A controlled collection of toggles with roving keyboard focus.
pub struct ToggleGroup {
    id: ElementId,
    value: Vec<SharedString>,
    label: Option<SharedString>,
    disabled: bool,
    multiple: bool,
    joined: bool,
    items: Vec<ToggleGroupItem>,
    on_change: Option<ChangeHandler>,
    style: StyleRefinement,
}
impl ToggleGroup {
    /// Creates a `ToggleGroup` with a stable caller-owned ID.
    pub fn new(id: impl Into<ElementId>) -> Self {
        Self {
            id: id.into(),
            value: Vec::new(),
            label: None,
            disabled: false,
            multiple: false,
            joined: true,
            items: Vec::new(),
            on_change: None,
            style: Default::default(),
        }
    }
    /// Sets the caller-owned value displayed by this control.
    pub fn value(mut self, values: impl IntoIterator<Item = impl Into<SharedString>>) -> Self {
        self.value = values.into_iter().map(Into::into).collect();
        self
    }
    /// Sets the accessible name of the control.
    pub fn aria_label(mut self, label: impl Into<SharedString>) -> Self {
        self.label = Some(label.into());
        self
    }
    /// Disables interaction and applies the disabled appearance.
    pub fn disabled(mut self, value: bool) -> Self {
        self.disabled = value;
        self
    }
    /// Allows multiple selected values instead of one.
    pub fn multiple(mut self, value: bool) -> Self {
        self.multiple = value;
        self
    }
    /// Removes gaps between adjacent toggles.
    pub fn joined(mut self, value: bool) -> Self {
        self.joined = value;
        self
    }
    /// Appends an option to the group.
    pub fn item(mut self, item: ToggleGroupItem) -> Self {
        self.items.push(item);
        self
    }
    /// Reports a requested value change; retain the next value in the owning view.
    pub fn on_change(
        mut self,
        handler: impl Fn(Vec<SharedString>, &mut Window, &mut App) + 'static,
    ) -> Self {
        self.on_change = Some(Rc::new(handler));
        self
    }
}
impl Styled for ToggleGroup {
    fn style(&mut self) -> &mut StyleRefinement {
        &mut self.style
    }
}
impl RenderOnce for ToggleGroup {
    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
        let theme = UiTheme::read(cx).clone();
        let control = theme.control();
        let focus = RovingFocus::new(
            self.id.clone(),
            &self
                .items
                .iter()
                .map(|i| (i.id.clone(), self.disabled || i.disabled))
                .collect::<Vec<_>>(),
            None,
            window,
            cx,
        );
        let count = self.items.len();
        let children = self
            .items
            .into_iter()
            .enumerate()
            .map(|(index, item)| {
                let pressed = self.value.contains(&item.value);
                let disabled = self.disabled || item.disabled;
                let handle = focus.handles[index].as_ref();
                let change = self.on_change.clone();
                let mut next = if self.multiple {
                    self.value.clone()
                } else {
                    Vec::new()
                };
                next.retain(|value| value != &item.value);
                if !pressed {
                    next.push(item.value);
                }
                let toggle = gpui_kit::base::Toggle::new(item.id)
                    .pressed(pressed)
                    .disabled(disabled)
                    .when_some(item.label, |toggle, label| {
                        toggle.accessibility_label(label)
                    })
                    .when_some(handle, |toggle, h| {
                        toggle.track_focus(h).tab_stop(h.tab_stop)
                    })
                    .flex()
                    .items_center()
                    .justify_center()
                    .gap(theme.space(1.))
                    .h(theme.space(8.))
                    .min_w(theme.space(8.))
                    .px(theme.space(2.5))
                    .rounded(if self.joined { px(0.) } else { theme.radius.lg })
                    .when(self.joined && index == 0, |t| t.rounded_l(theme.radius.lg))
                    .when(self.joined && index + 1 == count, |t| {
                        t.rounded_r(theme.radius.lg)
                    })
                    .map(|t| control::toggle(t, &theme, pressed, disabled, false))
                    .focus_visible({
                        let theme = theme.clone();
                        move |s| focus::ring(s, &theme)
                    })
                    .when(!disabled, |t| t.hover(move |s| s.bg(control.hover)))
                    .children(item.children)
                    .when_some(change, |toggle, handler| {
                        toggle.on_change(move |_, _, window, cx| handler(next.clone(), window, cx))
                    });
                apply_style(toggle, &item.style)
            })
            .collect::<Vec<_>>();
        let root = gpui_kit::base::ToggleGroup::new(self.id)
            .flex()
            .gap(theme.space(if self.joined { 0. } else { 2. }))
            .when_some(self.label, |root, label| root.aria_label(label))
            .on_key_down(move |event, window, cx| {
                focus.key(event, Some(gpui_kit::Axis::Horizontal), window, cx);
            })
            .children(children);
        apply_style(root, &self.style)
    }
}

Platform notes

Visual reference: shadcn/ui 4.19.0, Neutral Nova. Implementation: crates/ui/registry/toggle_group/toggle_group.rs, using GPUI Kit 0.6.4.

The application owns selected values and handles on_change. The group supports single or multiple selection and roving keyboard focus that skips disabled items. Kit toggle primitives supply each control.

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.