Sidebar
Grouped application navigation.
Basic use
use xgpui_luma_look_radix as radix;
use gpui::{Context};
struct Example {
control: gpui::Entity<xgpui_luma::controls::sidebar::SidebarControl>,
active_destination: String,
}
impl Example {
fn new(cx: &mut Context<Self>) -> Self {
let look = radix::Look::built_in();
let control = radix::sidebar_builder(&look, "navigation")
.on_select(cx, |app, id, cx| {
app.active_destination = id.to_string();
cx.notify();
})
.spawn(cx);
// Compose header, menus, and footer through the sidebar builder.
Self { control, active_destination: String::new() }
}
}Integration snippet, checked against the current SDK and Radix 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
sidebar_builder(&look, id) styles rows, sections, focus, drag feedback, collapsed-branch menus, and scrollbars. Configure SDK structure and presentation before spawning.
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 navigation presentation. The host owns pane width and visibility.
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 / payload | Callback |
|---|---|
PresentationChanged { presentation: SidebarPresentation } | .on_presentation_changed(cx, |app, presentation, cx| { … }) |
Select { id: SharedString } | .on_select(cx, |app, id, cx| { … }) |
ItemFocused { id: SharedString } | .on_item_focused(cx, |app, id, cx| { … }) |
HoverChanged { id: Option<SharedString> } | .on_hover_changed(cx, |app, id, cx| { … }) |
SubMenuToggle { id: SharedString, open: bool } | .on_sub_menu_toggle(cx, |app, id, open, 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 SidebarEventExt. Existing cx.subscribe observers remain supported.
Drag and drop
See Drag and drop for lifecycle ordering, callbacks, ownership, cancellation and the coverage matrix.
Enable .drag_drop(SidebarDragDrop::new()) on the SDK, Shadcn, or Radix builder. Group .id(...) supplies a stable destination identity; omitting it uses the menu ID. IDs must be unique. .allowed_groups(...) limits both source and destination groups; .draggable(false) fixes an item while allowing it to remain a branch destination.
Expanded rows offer before/after sibling gaps; the middle of a branch accepts a subtree as its last child. A group heading’s middle appends at the group root, including an empty menu. .sub(...) preserves branch identity even when empty. Collapsed branches accept children without hover expansion. Collapsed icon rails and their popovers are navigation-only. Header/footer items and structural section headings cannot be dragged; disabled items and disabled ancestor paths are rejected. Unlabelled groups remain reachable through their items; give empty destination groups a heading.
SidebarDropLocation { group_id, parent_id, before } identifies a group, optional parent, and optional next sibling. before=None appends. SidebarDragEvent uses the shared phases with this location in place of a flat gap. Group moves and reparenting emit a same-control reorder; removal/addition phases are available filters but are not emitted. Cycles, duplicate IDs, missing targets and stale sessions fail before mutation. Selection, expanded state, focus handles, tooltips, scroll and complete subtrees survive moves.
The SDK commits automatically. Use set_drop_handler for synchronous policy and answer with commit_drop or reject_drop; unanswered handlers reject. Replacing data, disabling DnD or changing presentation cancels an active session. The content viewport scrolls at its edges during an admitted drag. Custom templates attach RenderedNavNode.drag_drop to the row only and use SDK drag_boundary() around embedded interactive actions.
mod dnd_example {
use gpui::{Context, Entity};
use xgpui_luma::controls::sidebar::*;
use xgpui_luma_look_shadcn::{ShadcnLook, Sidebar};
struct Owner;
fn navigation(look: &ShadcnLook, cx: &mut Context<Owner>) -> Entity<SidebarControl> {
Sidebar::new("nav").look(look)
.drag_drop(SidebarDragDrop::new().allowed_groups(["properties"]))
.sidebar(sidebar("panel").content(sidebar_content().group(
sidebar_group().id("properties").label("Properties").menu(
sidebar_menu("properties-menu")
.item(sidebar_menu_item("layout", "Layout").sub(sidebar_menu_sub()
.item(sidebar_menu_item("width", "Width"))
.item(sidebar_menu_item("height", "Height"))))
.item(sidebar_menu_item("look", "Look").sub(sidebar_menu_sub()))
)
)))
.on_dropped(cx, |_, _, _, keys, location, changed, _| {
let _ = (keys, &location.group_id, &location.parent_id, &location.before, changed);
})
.spawn(cx)
}
}Committed changes also support shared drop-layout motion, including duration settings, reduced motion and documented virtualization/transfer limits.