gpuicn Components

Component 01 of 60

Accordion

A vertically stacked set of interactive headings that reveal content.

site/src/previews/accordion.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.

Accordion, 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 accordion

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

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

// src/main.rs
mod ui;

Usage

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

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

fn example(open: bool, window: &mut Window, cx: &mut App) -> impl IntoElement {
    accordion("faq", cx).child(
        accordion_item(cx)
            .open(open)
            .header(accordion_header(
                accordion_trigger("shipping", open, false, cx).child("When will it arrive?"),
            ))
            .panel(accordion_content(
                "shipping-content",
                open,
                "Within three working days.",
                window,
                cx,
            )),
    )
}

API reference

pub fn accordion(id: impl Into<ElementId>, cx: &App) -> Accordion

Creates the accordion collection. Each item receives its expanded state from the caller.

pub fn accordion_item(cx: &App) -> AccordionItem

Creates an item with the Nova divider.

pub fn accordion_header(trigger: AccordionTrigger) -> AccordionHeader

Creates a heading around a trigger.

pub fn accordion_trigger(
    id: impl Into<ElementId>,
    open: bool,
    disabled: bool,
    cx: &App,
) -> AccordionTrigger

Creates a keyboard-focusable disclosure trigger with a chevron.

pub fn accordion_content(
    id: impl Into<ElementId>,
    open: bool,
    content: impl IntoElement,
    window: &mut Window,
    cx: &mut App,
) -> AccordionPanel

Creates a controlled panel, retaining its content only while the exit motion runs.

Installed source, 147 lines
//! Nova accordion presentation with caller-owned disclosure state.
use super::theme::{UiTheme, primitives::disabled};
use gpui_icons::{LucideIcon, lucide};
pub use gpui_kit::base::{
    Accordion, AccordionHeader, AccordionItem, AccordionPanel, AccordionTrigger,
};
use gpui_kit::{
    App, ElementId, FontWeight, InteractiveElement as _, IntoElement, ParentElement as _, Styled,
    Window, div, prelude::FluentBuilder as _,
};

/// Creates the accordion collection. Each item receives its expanded state from the caller.
pub fn accordion(id: impl Into<ElementId>, cx: &App) -> Accordion {
    Accordion::new(id)
        .flex()
        .flex_col()
        .w_full()
        .font_family(UiTheme::read(cx).fonts.body.clone())
}
/// Creates an item with the Nova divider.
pub fn accordion_item(cx: &App) -> AccordionItem {
    AccordionItem::new()
        .border_b_1()
        .border_color(UiTheme::read(cx).colors.border)
}
/// Creates a heading around a trigger.
pub fn accordion_header(trigger: AccordionTrigger) -> AccordionHeader {
    AccordionHeader::new(trigger).flex()
}
/// Creates a keyboard-focusable disclosure trigger with a chevron.
pub fn accordion_trigger(
    id: impl Into<ElementId>,
    open: bool,
    disabled: bool,
    cx: &App,
) -> AccordionTrigger {
    let theme = UiTheme::read(cx);
    let colors = theme.colors;
    AccordionTrigger::new(id)
        .open(open)
        .disabled(disabled)
        .when(!disabled, |b| {
            b.tab_index(0).cursor_pointer().hover(|s| s.underline())
        })
        .when(disabled, |b| {
            disabled::style(b)
                .capture_any_mouse_down(|_, window, cx| {
                    window.prevent_default();
                    cx.stop_propagation();
                })
                .capture_any_mouse_up(|_, window, cx| {
                    window.prevent_default();
                    cx.stop_propagation();
                })
                .capture_key_down(|event, window, cx| {
                    if matches!(event.keystroke.key.as_str(), "enter" | "space") {
                        window.prevent_default();
                        cx.stop_propagation();
                    }
                })
        })
        .w_full()
        .flex()
        .flex_row_reverse()
        .items_start()
        .justify_between()
        .rounded(theme.radius.lg)
        .border_1()
        .border_color(colors.background.opacity(0.))
        .py(theme.space(2.5))
        .text_left()
        .font_family(theme.fonts.body.clone())
        .font_weight(FontWeight::MEDIUM)
        .text_size(theme.text(14.))
        .text_color(colors.foreground)
        .focus_visible(move |s| s.border_color(colors.ring))
        .child(super::theme::disclosure_icon(
            lucide(LucideIcon::ChevronDown)
                .size(theme.space(4.))
                .text_color(colors.muted_foreground),
            open,
        ))
}
/// Creates a controlled panel, retaining its content only while the exit motion runs.
pub fn accordion_content(
    id: impl Into<ElementId>,
    open: bool,
    content: impl IntoElement,
    window: &mut Window,
    cx: &mut App,
) -> AccordionPanel {
    let id = id.into();
    let presence = super::theme::presence(id.clone(), open, window, cx);
    let theme = UiTheme::read(cx);
    AccordionPanel::new()
        .open(open)
        .keep_mounted(presence.should_render() && (open || presence.progress > 0.))
        .child(gpui_kit::base::motion::MotionReveal::new(
            id,
            presence.progress,
            div()
                .w_full()
                .pb(theme.space(2.5))
                .font_family(theme.fonts.body.clone())
                .text_size(theme.text(14.))
                .text_color(theme.colors.foreground)
                .child(content)
                .into_any_element(),
        ))
}

#[cfg(test)]
mod tests {
    use super::*;
    use gpui_kit::{
        Context, Modifiers, Render, StatefulInteractiveElement as _, TestAppContext, point, px,
    };
    use std::{cell::Cell, rc::Rc};

    #[gpui_kit::test]
    fn disabled_trigger_blocks_caller_click_handlers(cx: &mut TestAppContext) {
        struct Probe {
            disabled: bool,
            clicks: Rc<Cell<usize>>,
        }
        impl Render for Probe {
            fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
                let clicks = self.clicks.clone();
                accordion_trigger("trigger", false, self.disabled, cx)
                    .size(px(100.))
                    .on_click(move |_, _, _| clicks.set(clicks.get() + 1))
            }
        }
        cx.update(|cx| UiTheme::set(cx, UiTheme::neutral_light()));
        for disabled in [false, true] {
            let clicks = Rc::new(Cell::new(0));
            let (_, visual) = cx.add_window_view({
                let clicks = clicks.clone();
                move |_, _| Probe { disabled, clicks }
            });
            visual.update(|window, cx| window.draw(cx).clear(cx));
            visual.simulate_click(point(px(10.), px(10.)), Modifiers::default());
            assert_eq!(clicks.get(), usize::from(!disabled));
        }
    }
}

Platform notes

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

Kit supplies the accordion parts; the application passes each expanded state and handles trigger clicks. gpuicn adds Nova borders, spacing, typography and chevrons. Single or multiple expansion is an application choice.

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.