Accordion

Expandable sections.

Try it

Interact with the control below. This preview runs the exact code sample shown on this page.

Open example separately ↗

Basic use

Radix · integration snippet
use xgpui_luma_look_radix as radix;
use gpui::{Context};
use gpui::{div, prelude::*};
use xgpui_luma::controls::accordion::{self, AccordionItem, AccordionTrigger, AccordionContent};

struct Example {
    control: xgpui_luma::controls::accordion::Accordion,
    details_expanded: bool,
}

impl Example {
    fn new(cx: &mut Context<Self>) -> Self {
        let look = cx.try_global::<radix::Look>().cloned().unwrap_or_else(radix::Look::built_in);
        let control = radix::accordion_builder(&look, "details")
            .item(AccordionItem::new(
                "about",
                AccordionTrigger::new("About"),
                AccordionContent::custom(|_, _| div().child("Project details").into_any_element()),
            ))
            .on_expanded_changed(cx, |app, _, expanded, cx| {
                app.details_expanded = expanded;
                cx.notify();
            })
            .spawn(cx);

        Self { control, details_expanded: false }
    }
}

Integration snippet, checked against the current SDK and Look crates. cx is the initialized owner context. The control retains its callbacks; callback owners are held weakly. Render self.control.clone() in the owner’s view.

Radix styling

accordion_builder(&look, id) installs Radix trigger, content, focus, and drag styling. SDK expansion, reordering, sizes, callbacks, and custom content remain available.

Pass an explicit Look, including forks. Theme adapters retain that binding; notify affected controls and their owners after palette, mode, or stylesheet edits. Resolved look values are snapshots and must be resolved again during render.

See the Radix control support table for adapter and utility status.

SDK behavior & customization

The control owns expansion, dividers, and clipping. Frame supplies optional outer decoration.

Relevant contracts: AccordionItemRenderModel, AccordionRenderModel, AccordionTemplateModifier, AccordionTemplateHandlers.

Use the module’s template contract for structural changes. Builder modifiers and item templates vary by control.

Events

Callbacks follow the existing SDK event stream. Registrations append, hold the owner weakly, and live until the control is released. cx.notify() requests an owner redraw. Compound payloads are borrowed; scalar values are passed by value.

Event / payloadCallback
ExpandedChanged { item_id: SharedString, expanded: bool }.on_expanded_changed(cx, |app, item_id, expanded, cx| { … })
FocusChanged { focused: bool }.on_focus_changed(cx, |app, focused, cx| { … })
ItemFocused { item_id: SharedString }.on_item_focused(cx, |app, item_id, cx| { … })
ItemHoverChanged { item_id: SharedString, hovered: bool }.on_item_hover_changed(cx, |app, item_id, hovered, cx| { … })
EnabledChanged { enabled: bool }.on_enabled_changed(cx, |app, enabled, cx| { … })

Use .on_event(cx, |app, event, cx| { … }) for all events. Factory-created controls support the same callbacks through AccordionEventExt. Existing cx.subscribe observers remain supported.

Drag and drop

Enable .reordering(true) on the SDK, Shadcn, or Radix builder to move whole items within one Accordion. The list-chevrons-up-down handle appears on header hover without shifting the layout. Only the handle initiates native dragging; its tooltip reads “Drag to reorder”. Trigger clicks, embedded editors and child drags remain independent. One canonical gap and a themed marker identify each insertion location, including before-first and append.

The SDK commits automatically. Use set_drop_handler to enforce synchronous host policy through commit_drop or reject_drop. Proposals use EntityId collection identities, stable SharedString keys and before=None for append. Unanswered proposals are rejected. Disabled items cannot initiate a drag; gaps beside them remain available.

See the shared DnD contract for callback registration, lifecycle ordering, validation and cancellation, and drop motion for duration, overrides and reduced motion. Accordion exposes AccordionDragEvent, AccordionDropProposal, on_drag_drop and all seven named lifecycle helpers on both builders and factory entities (AccordionEventExt). Same-control moves emit reorder phases; unsupported transfer phases are never synthesized.

SDK · Accordion reorder
use xgpui_luma::controls::accordion::Accordion;

struct Categories { accordion: Accordion, order: Vec<gpui::SharedString> }
impl Categories {
    fn new(cx: &mut Context<Self>) -> Self {
        let accordion = accordion::new("categories")
            .multiple()
            .reordering(true)
            .items(["base", "primary", "sidebar"].map(|id| AccordionItem::new(
                id, AccordionTrigger::new(id),
                AccordionContent::custom(|_, _| div().child("Retained category content").into_any_element()),
            )))
            .on_items_reordered(cx, |owner: &mut Self, _, _, _, cx| {
                owner.order = owner.accordion.read(cx).items().iter().map(|item| item.id().clone()).collect();
                cx.notify();
            })
            .spawn(cx);
        Self { accordion, order: vec![] }
    }
}

Reordering retains expansion, focused identity, disclosure transitions, content measurements and child entities by key. Transient hover/press state clears after a changed move. Drop position motion uses the shared SDK primitive; disclosure or incompatible geometry changes settle it. Existing motion_enabled(false) disables drop motion as well as disclosure motion.

The default root owns its scroll handle when constrained to a viewport. For an enclosing scroller, call set_drag_scroll_handle(Some(handle), cx) and track that same handle on the enclosing element; None restores the internal viewport. Edge scrolling uses that viewport and stops on cancellation or release.

Custom templates receive AccordionTemplateHandlers.drag. Preserve its root input bindings, put each supplied handle inside the corresponding header, measure whole items with AccordionDragBindings::measure_item, append its canonical targets, and wrap real keyed items with its motion handle inside motion.scope. Forward resolve_drag and drop_layout_motion when delegating templates. Default and Shadcn templates already do this.

Luma Studio’s left sidebar → Colors is the application test surface. Drag a category’s handle to reorder the whole category. Ordering, expansion, fields and pickers survive theme changes during the session; Expand All/Collapse All follow the reordered items. Token grouping is unchanged. Row reordering, cross-Accordion transfers and order persistence across restarts are outside this feature. Radix currently has no Accordion adapter.

Source

SDK module · Radix adapter