pub fn Tree(props: TreeProps) -> impl IntoViewExpand description
Hand-composed hierarchy for sidebars, pickers, and file lists where you control every row.
Wrap rows in nested [TreeItem] components with [TreeItemLayout] slots. Bind TreeExpansion and TreeSelection with Leptos signals for open and selected ids. When items come from an API, lazy loading, or virtualization, use RichTree instead — same selection and expansion APIs, data adapters instead of nested components.
§When to use
- Hand-authored hierarchies with custom slots (icons, aside actions, motion) - Sidebars and pickers where item count is modest - Full control over row layout via [
TreeItemLayout]
§Usage
- Wrap rows in
Tree+ nested [TreeItem] components. 2. BindTreeExpansion::open_itemsfor controlled expand/collapse. 3. ChooseTreeSelectionmode (single,multi,checkbox, ornone).
§Best Practices
§Do’s
- Use
TreeApiRefto focus items from external controls * Setdata-testidon preview wrappers for E2E coverage * PreferRichTreewhen items come from an API
§Don’ts
- Don’t render thousands of static [
TreeItem] nodes — useRichTreewith virtualization * Don’t rely on mount order for keyboard range selection — the registry orders visible items
§Examples
§Default nested tree
Branch pre-expanded via open_items; nested leaf under Documents.
use crate::{Tree, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
use orbital_base_components::SignalModel;
use std::collections::HashSet;
let open = RwSignal::new(HashSet::from(["root".to_string()]));
view! {
<div data-testid="tree-view-preview">
<Tree expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none()>
<TreeItem config=TreeItemConfig::branch("root")>
<TreeItemLayout slot>
<span data-testid="tree-node-docs">"Documents"</span>
</TreeItemLayout>
<TreeItem config=TreeItemConfig::leaf("readme")>
<TreeItemLayout slot>
<span data-testid="tree-node-readme">"readme.md"</span>
</TreeItemLayout>
</TreeItem>
</TreeItem>
</Tree>
</div>
}§Small size
Compact row height for dense sidebars.
use crate::{Tree, TreeAppearance, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection, TreeSize};
use orbital_base_components::SignalModel;
use std::collections::HashSet;
let open = RwSignal::new(HashSet::new());
let appearance = TreeAppearance::new(Signal::derive(|| TreeSize::Small));
view! {
<div data-testid="tree-small">
<Tree expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none() appearance=appearance>
<TreeItem config=TreeItemConfig::leaf("a")>
<TreeItemLayout slot>"Item"</TreeItemLayout>
</TreeItem>
</Tree>
</div>
}§Deep nesting
Three-level branch chain demonstrates nested subtrees.
use crate::{Tree, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
use orbital_base_components::SignalModel;
use std::collections::HashSet;
let open = RwSignal::new(HashSet::from(["l1".to_string(), "l2".to_string()]));
view! {
<div data-testid="tree-deep">
<Tree expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none()>
<TreeItem config=TreeItemConfig::branch("l1")>
<TreeItemLayout slot>"Level 1"</TreeItemLayout>
<TreeItem config=TreeItemConfig::branch("l2")>
<TreeItemLayout slot>"Level 2"</TreeItemLayout>
<TreeItem config=TreeItemConfig::leaf("l3")>
<TreeItemLayout slot>"Level 3"</TreeItemLayout>
</TreeItem>
</TreeItem>
</TreeItem>
</Tree>
</div>
}§Click to expand
Starts collapsed; clicking the branch toggles open_items.
use crate::{Tree, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
use orbital_base_components::SignalModel;
use std::collections::HashSet;
let open = RwSignal::new(HashSet::new());
view! {
<div data-testid="tree-toggle">
<Tree expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none()>
<TreeItem config=TreeItemConfig::branch("toggle")>
<TreeItemLayout slot>
<span data-testid="tree-branch-toggle">"Expand me"</span>
</TreeItemLayout>
<TreeItem config=TreeItemConfig::leaf("child")>
<TreeItemLayout slot>"Hidden until expanded"</TreeItemLayout>
</TreeItem>
</TreeItem>
</Tree>
</div>
}§Flat leaf list
All leaves — no chevrons.
use crate::{Tree, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
use orbital_base_components::SignalModel;
use std::collections::HashSet;
let open = RwSignal::new(HashSet::new());
view! {
<div data-testid="tree-flat">
<Tree expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none()>
<TreeItem config=TreeItemConfig::leaf("1")>
<TreeItemLayout slot>"One"</TreeItemLayout>
</TreeItem>
<TreeItem config=TreeItemConfig::leaf("2")>
<TreeItemLayout slot>"Two"</TreeItemLayout>
</TreeItem>
</Tree>
</div>
}§Single selection
Click rows to select one item at a time.
use crate::{Tree, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
use orbital_base_components::{SignalModel, TreeSelectionMode};
use std::collections::HashSet;
let open = RwSignal::new(HashSet::new());
let selected = RwSignal::new(HashSet::new());
view! {
<div data-testid="tree-selection">
<Tree
expansion=TreeExpansion::new(SignalModel::from(open))
selection=TreeSelection::single(SignalModel::from(selected))
>
<TreeItem config=TreeItemConfig::leaf("a")>
<TreeItemLayout slot>"Alpha"</TreeItemLayout>
</TreeItem>
<TreeItem config=TreeItemConfig::leaf("b")>
<TreeItemLayout slot>"Beta"</TreeItemLayout>
</TreeItem>
</Tree>
</div>
}§Theme surfaces
Items use theme background tokens.
use crate::{Tree, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
use orbital_base_components::SignalModel;
use std::collections::HashSet;
let open = RwSignal::new(HashSet::from(["theme".to_string()]));
view! {
<div data-testid="tree-theme">
<Tree expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none()>
<TreeItem config=TreeItemConfig::branch("theme")>
<TreeItemLayout slot>
<span data-testid="tree-theme-cell" style="background: var(--orb-color-surface-canvas); padding: 4px 8px;">"Themed row"</span>
</TreeItemLayout>
</TreeItem>
</Tree>
</div>
}§RichTree data-driven
Pass structured data instead of composing nested items.
use crate::{RichTree, RichTreeData, TreeExpansion, TreeSelection};
use orbital_base_components::SignalModel;
use std::collections::HashSet;
#[derive(Clone)]
struct Node { id: String, label: String, children: Vec<Node> }
let data = RichTreeData::new(vec![
Node { id: "docs".into(), label: "Documents".into(), children: vec![
Node { id: "readme".into(), label: "readme.md".into(), children: vec![] },
]},
]).get_id(|n| n.id.clone()).get_label(|n| n.label.clone()).get_children(|n| n.children.clone());
let open = RwSignal::new(HashSet::from(["docs".to_string()]));
view! {
<div data-testid="tree-rich">
<RichTree data=data expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none() />
</div>
}§Icons and aside actions
Leading icon, trailing badge, and hover-revealed aside actions.
use crate::{Button, ButtonAppearance, Tree, TreeExpansion, TreeItem, TreeItemConfig, TreeItemIconAfter, TreeItemIconBefore, TreeItemAside, TreeItemLayout, TreeSelection};
use orbital_base_components::SignalModel;
use std::collections::HashSet;
let open = RwSignal::new(HashSet::from(["docs".to_string()]));
view! {
<div data-testid="tree-icons-aside">
<Tree expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none()>
<TreeItem config=TreeItemConfig::branch("docs")>
<TreeItemLayout slot>
<TreeItemIconBefore slot>
<span>"📁"</span>
</TreeItemIconBefore>
"Documents"
<TreeItemIconAfter slot>
<span>"3"</span>
</TreeItemIconAfter>
<TreeItemAside slot>
<Button appearance=ButtonAppearance::Subtle icon=icondata::AiEllipsisOutlined />
</TreeItemAside>
</TreeItemLayout>
<TreeItem config=TreeItemConfig::leaf("readme")>
<TreeItemLayout slot>"readme.md"</TreeItemLayout>
</TreeItem>
</TreeItem>
</Tree>
</div>
}§Multi selection (ctrl + shift)
Ctrl/Meta toggles; Shift selects a visible range.
use crate::{Tree, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
use orbital_base_components::{SignalModel, TreeSelectionMode};
use std::collections::HashSet;
let open = RwSignal::new(HashSet::new());
let selected = RwSignal::new(HashSet::new());
view! {
<div data-testid="tree-multi">
<Tree expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::multi(SignalModel::from(selected))>
<TreeItem config=TreeItemConfig::leaf("a")><TreeItemLayout slot>"Alpha"</TreeItemLayout></TreeItem>
<TreeItem config=TreeItemConfig::leaf("b")><TreeItemLayout slot>"Beta"</TreeItemLayout></TreeItem>
<TreeItem config=TreeItemConfig::leaf("c")><TreeItemLayout slot>"Gamma"</TreeItemLayout></TreeItem>
</Tree>
</div>
}§Checkbox selection with cascade
Parent checkbox selects descendants when cascade is enabled.
use crate::{Tree, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
use orbital_base_components::SignalModel;
use std::collections::HashSet;
let open = RwSignal::new(HashSet::from(["root".to_string()]));
let selected = RwSignal::new(HashSet::new());
let selection = TreeSelection::checkbox(SignalModel::from(selected)).with_cascade(true);
view! {
<div data-testid="tree-checkbox">
<Tree expansion=TreeExpansion::new(SignalModel::from(open)) selection=selection>
<TreeItem config=TreeItemConfig::branch("root")>
<TreeItemLayout slot>"Root"</TreeItemLayout>
<TreeItem config=TreeItemConfig::leaf("child")><TreeItemLayout slot>"Child"</TreeItemLayout></TreeItem>
</TreeItem>
</Tree>
</div>
}§Disabled items (focusable)
Disabled rows remain focusable for screen-reader access.
use crate::{Tree, TreeBehavior, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
use orbital_base_components::SignalModel;
use std::collections::HashSet;
let open = RwSignal::new(HashSet::new());
let behavior = TreeBehavior::new()
.with_disabled_items(SignalModel::new(HashSet::from(["b".to_string()])))
.with_disabled_items_focusable(Signal::from(true));
view! {
<div data-testid="tree-disabled">
<Tree expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none() behavior=behavior>
<TreeItem config=TreeItemConfig::leaf("a")><TreeItemLayout slot>"Enabled"</TreeItemLayout></TreeItem>
<TreeItem config=TreeItemConfig::leaf("b")><TreeItemLayout slot>"Disabled"</TreeItemLayout></TreeItem>
</Tree>
</div>
}§Icon-container expansion
Only the chevron toggles expansion; row click selects.
use crate::{Tree, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
use orbital_base_components::{ExpansionTrigger, SignalModel};
use std::collections::HashSet;
let open = RwSignal::new(HashSet::new());
let selected = RwSignal::new(HashSet::new());
let expansion = TreeExpansion::new(SignalModel::from(open)).with_expansion_trigger(ExpansionTrigger::IconContainer);
view! {
<div data-testid="tree-icon-expand">
<Tree expansion=expansion selection=TreeSelection::single(SignalModel::from(selected))>
<TreeItem config=TreeItemConfig::branch("branch")>
<TreeItemLayout slot><span data-testid="tree-icon-branch">"Branch"</span></TreeItemLayout>
<TreeItem config=TreeItemConfig::leaf("leaf")><TreeItemLayout slot>"Leaf"</TreeItemLayout></TreeItem>
</TreeItem>
</Tree>
</div>
}§Expand / collapse all
External button uses TreeApiRef to toggle every branch.
use crate::{Button, Tree, TreeApiRef, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
use orbital_base_components::SignalModel;
use std::collections::HashSet;
let open = RwSignal::new(HashSet::new());
let api = TreeApiRef::default();
let api_for_button = api.clone();
view! {
<div data-testid="tree-expand-all">
<Button on_click=Callback::new(move |_| {
api_for_button.with(|handle| {
for (id, _) in handle.get_item_tree() {
handle.set_item_expansion(id, true);
}
});
})>"Expand all"</Button>
<Tree api_ref=api expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none()>
<TreeItem config=TreeItemConfig::branch("a")>
<TreeItemLayout slot>"A"</TreeItemLayout>
<TreeItem config=TreeItemConfig::leaf("a1")><TreeItemLayout slot>"A1"</TreeItemLayout></TreeItem>
</TreeItem>
</Tree>
</div>
}§Connector borders
Indentation guide lines between levels.
use crate::{Tree, TreeAppearance, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
use orbital_base_components::SignalModel;
use std::collections::HashSet;
let open = RwSignal::new(HashSet::from(["root".to_string()]));
let appearance = TreeAppearance::new(Signal::from(orbital_base_components::TreeSize::Medium)).with_connectors(Signal::from(true));
view! {
<div data-testid="tree-connectors">
<Tree appearance=appearance expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none()>
<TreeItem config=TreeItemConfig::branch("root")>
<TreeItemLayout slot>"Root"</TreeItemLayout>
<TreeItem config=TreeItemConfig::leaf("leaf")><TreeItemLayout slot>"Leaf"</TreeItemLayout></TreeItem>
</TreeItem>
</Tree>
</div>
}§Focus via button
Imperative focus through [TreeApiRef::focus_item].
use crate::{Button, Tree, TreeApiRef, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
use orbital_base_components::SignalModel;
use std::collections::HashSet;
let open = RwSignal::new(HashSet::new());
let api = TreeApiRef::default();
let api_for_button = api.clone();
view! {
<div data-testid="tree-focus-button">
<Button on_click=Callback::new(move |_| { api_for_button.with(|handle| handle.focus_item("target".into())); })>"Focus target"</Button>
<Tree api_ref=api expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none()>
<TreeItem config=TreeItemConfig::leaf("target")><TreeItemLayout slot>"Target row"</TreeItemLayout></TreeItem>
</Tree>
</div>
}§Keyboard navigation
Arrow keys move focus; typeahead jumps to matching labels.
use crate::{Tree, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
use orbital_base_components::SignalModel;
use std::collections::HashSet;
let open = RwSignal::new(HashSet::new());
view! {
<div data-testid="tree-keyboard">
<Tree expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none()>
<TreeItem config=TreeItemConfig::leaf("alpha")><TreeItemLayout slot>"Alpha"</TreeItemLayout></TreeItem>
<TreeItem config=TreeItemConfig::leaf("beta")><TreeItemLayout slot>"Beta"</TreeItemLayout></TreeItem>
</Tree>
</div>
}§Inline editing
Double-click or Enter on a focused row edits the label.
use crate::{Tree, TreeBehavior, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
use orbital_base_components::{Handler, SignalModel};
use std::collections::HashSet;
let open = RwSignal::new(HashSet::new());
let log = RwSignal::new(String::new());
let behavior = TreeBehavior::new()
.with_editable(Signal::from(true))
.with_on_label_change(Handler::on(move |(id, label)| log.set(format!("{id}:{label}"))));
view! {
<div data-testid="tree-editing">
<Tree behavior=behavior expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none()>
<TreeItem config=TreeItemConfig::leaf("edit")><TreeItemLayout slot>"Rename me"</TreeItemLayout></TreeItem>
</Tree>
<span data-testid="tree-edit-log">{move || log.get()}</span>
</div>
}§Drag reorder
Pointer drag reorders siblings when reorderable is enabled.
use crate::{Tree, TreeBehavior, TreeExpansion, TreeItem, TreeItemConfig, TreeItemLayout, TreeSelection};
use orbital_base_components::SignalModel;
use std::collections::HashSet;
let open = RwSignal::new(HashSet::new());
let behavior = TreeBehavior::new().with_reorderable(Signal::from(true));
view! {
<div data-testid="tree-reorder">
<Tree behavior=behavior expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none()>
<TreeItem config=TreeItemConfig::leaf("one")><TreeItemLayout slot>"One"</TreeItemLayout></TreeItem>
<TreeItem config=TreeItemConfig::leaf("two")><TreeItemLayout slot>"Two"</TreeItemLayout></TreeItem>
</Tree>
</div>
}§Custom collapse motion
Override the CSS transition name via [TreeItemCollapse].
use crate::{Tree, TreeExpansion, TreeItem, TreeItemCollapse, TreeItemConfig, TreeItemLayout, TreeSelection};
use orbital_base_components::SignalModel;
use std::collections::HashSet;
let open = RwSignal::new(HashSet::new());
view! {
<div data-testid="tree-custom-motion">
<Tree expansion=TreeExpansion::new(SignalModel::from(open)) selection=TreeSelection::none()>
<TreeItem config=TreeItemConfig::branch("motion")>
<TreeItemLayout slot>
"Animated branch"
<TreeItemCollapse slot motion_name="orbital-tree-custom-collapse".to_string() />
</TreeItemLayout>
<TreeItem config=TreeItemConfig::leaf("child")><TreeItemLayout slot>"Child"</TreeItemLayout></TreeItem>
</TreeItem>
</Tree>
</div>
}§Required Props
- children:
Children- [
TreeItem] children forming the expandable hierarchy.
- [
§Optional Props
- expansion:
impl Into<TreeExpansion>- Expansion state and triggers.
- selection:
impl Into<TreeSelection>- Selection mode and selected item ids.
- behavior:
impl Into<TreeBehavior>- Editing, reorder, and interaction callbacks.
- appearance:
impl Into<TreeAppearance>- Visual density and motion settings.
- api_ref:
TreeApiRef- Imperative tree API handle.
- class:
impl Into<MaybeProp<String>>- Extra CSS class names merged onto the tree root.