elements
Demos UI Feed Discuss Install
Style System
  • Style
  • Themes
  • Behaviors
Forms
  • Button
  • ButtonGroup
  • Checkbox
  • ColorPicker
  • Combobox
  • Field
  • FileUpload
  • Input
  • InputGroup
  • InputOTP
  • Label
  • NativeSelect
  • NumberInput
  • PasswordInput
  • RadioGroup
  • Select
  • Slider
  • Switch
  • TagsInput
  • Textarea
  • Toggle
  • ToggleGroup
Overlays
  • AlertDialog
  • Command
  • ContextMenu
  • Dialog
  • Drawer
  • DropdownMenu
  • HoverCard
  • Menubar
  • NavigationMenu
  • Popover
  • Sheet
  • Toast
  • Tooltip
Display
  • Accordion
  • Alert
  • AspectRatio
  • Avatar
  • Badge
  • Breadcrumb
  • Card
  • Collapsible
  • Empty
  • Item
  • Kbd
  • Pagination
  • Progress
  • Separator
  • Skeleton
  • Spinner
  • Table
  • Tabs
  • Tree
Layout
  • Carousel
  • Resizable
  • ScrollArea
  • Sidebar
Dates
  • Calendar
  • DatePicker
  • DateRangePicker
  • TimePicker
Data
  • AreaChart
  • BarChart
  • Chart
  • DataTable
  • Kanban
  • LineChart
  • PieChart
  • RadarChart
  • RadialChart
  • VirtualList
Chat
  • Attachment
  • Message
  • MessageList
  • PromptInput
Elements UI / Display / Tree

Tree

A hierarchical list that opens with the arrow keys, selects one node or many, and loads children on demand.

File explorer

Single selection. Arrows move, Right and Left open and close, Enter acts on a file. indentGuides draws a rule down each open folder.

  • src
    • components
    • lib
    • app.ts
  • public
  • package.json
  • README.md
selected: none / expanded: src / action: none
Code
<Tree label="Files" items={files} indentGuides
      selected={state.selected} expanded={state.expanded}
      onAction={(node) => open(node)} />

Usage

import { Tree, TreeNode, visibleNodes } from "@elements/ui/tree";

<Tree label="Files" items={files} selected={state.selected} expanded={state.open} />

const files: TreeNode[] = [
  { id: "src", label: "src", icon: "folder", children: [
    { id: "app", label: "app.ts", icon: "file" },
  ] },
  { id: "readme", label: "README.md", icon: "file" },
];

Examples

Multiple selection

Space or a click toggles, Shift+Arrow extends, Shift+Space selects a range, Ctrl+A selects all.

  • Fruits
    • Apple
    • Banana
    • Cherry
  • Vegetables
  • Grains
selected: apple
Code
<Tree label="Groceries" items={groceries} selection="multiple" selected={state.selected} />

Lazy children

hasChildren with no children loads them on first open. The node is busy meanwhile; an empty result makes it a leaf.

  • Orders
  • Archive (empty)
  • Notes
loads: none loaded: none
Code
<Tree label="Orders" items={[{ id: "orders", label: "Orders", hasChildren: true }]}
      loadChildren={(node) => fetchChildren(node.id)} />

Load errors

When loadChildren fails the node closes again and onLoadChildrenError reports it. Opening it retries.

  • Reports (offline)
attempts: 0 / errors: 0 / expand changes: 0
Code
<Tree label="Reports" items={reports} expanded={state.expanded}
      loadChildren={(node) => fetchChildren(node.id)}
      onLoadChildrenError={(node, error) => toast(error.message)} />

Selection follows focus

Moving focus selects. Disabled nodes take focus but cannot be chosen.

  • Inbox
  • Drafts
  • Locked folder
  • Trash
selected: inbox
Code
<Tree label="Mailboxes" items={mailboxes} selectionFollowsFocus selected={state.selected} />

Controlled

Bind selected and expanded. The change events fire only for the user's changes.

changes: select 0, expand 0
  • src
  • public
  • package.json
  • README.md
Code
<Tree items={files} selected={state.selected} expanded={state.expanded}
      onSelectedChange={(ids) => ...} onExpandedChange={(ids) => ...} />
<Button onclick={() => state.expanded = ["src", "components", "lib", "public"]}>Expand all</Button>

Checkbox tree

checkboxes puts a check on every node. A parent checks or clears all its leaves and shows mixed when some are checked; disabled leaves keep their state.

  • Billing
    • View invoices
    • Issue refunds
    • Change plans
  • People
  • Read the audit log
checked: invoices / changes: 0
Code
<Tree label="Permissions" items={permissions} checkboxes
      checked={state.checked} onCheckedChange={(ids) => save(ids)} />

Filter

filter shows the nodes whose label contains the text, the parents that lead to them opened.

  • src
  • public
  • package.json
  • README.md
Code
<input type="search" value={state.query} placeholder="Filter files">
<Tree label="Files" items={files} filter={state.query} />

Rename, add and remove

F2, or the Rename button writing renaming, renames a node in place: Enter saves, Escape cancels. A name with a slash is refused and the input stays open. The tree follows items, so adding and removing nodes is a change to the data.

  • docs
    • intro.md
    • setup.md
  • notes.txt
renamed: none
Code
<Tree label="Documents" items={state.items} selected={state.selected}
      renaming={state.renaming}
      canRename={() => true}
      onRename={(node, label) => save(node, label)} />

// save updates state.items, or returns false to refuse a name with a slash.
<Button onclick={() => state.items = [...state.items, newFile()]}>New file</Button>
<Button onclick={() => state.renaming = state.selected[0]}>Rename</Button>

Links

A node with href is a link: a click or Enter follows it, and it opens in a new tab like any link (APG navigation tree). typeahead is off here.

  • Guides
    • Getting started
    • Deploying
  • Reference
Code
<Tree label="Site" items={[
  { id: "guides", label: "Guides", children: [
    { id: "start", label: "Getting started", href: "/guides/start" },
  ] },
  { id: "reference", label: "Reference", href: "/reference" },
]} />

Focused node

focused is two-way: arrows write the node that takes focus and onFocusChange reports it; writing it moves the tab stop, and focus while the tree has it.

  • src
    • components
    • lib
    • app.ts
  • public
  • package.json
  • README.md
focused: none
Code
<Tree label="Files" items={files} focused={state.focused}
      onFocusChange={(id) => log(id)} />
<Button onclick={() => state.focused = "readme"}>Focus README.md</Button>

Active descendant

activeDescendant keeps focus on the tree and points aria-activedescendant at the node (APG's alternative to moving focus). The keys are the same.

  • src
    • components
    • lib
    • app.ts
  • public
  • package.json
  • README.md
selected: none / focused: none
Code
<Tree label="Files" items={files} activeDescendant />

Large trees

virtual renders only the rows in and near the view, so ten thousand nodes open, scroll and take arrow keys as fast as ten. Give the tree a height. visibleNodes lists the nodes it shows, for a count or a search.

shown: 100 of 10100 / selected: none
Code
<Tree virtual class="big-tree" label="Archive" items={folders}
      selected={state.selected} expanded={state.expanded} />

.big-tree { height: 20rem; }

// The nodes the tree shows, in order, with their levels:
visibleNodes(folders, state.expanded).length

Element ids

ids sets the tree's id and each node's, for a label or a link elsewhere on the page. renameLabel names the rename field, the one text the tree adds.

  • src
  • public
  • package.json
  • README.md
Code
<Tree label="Files" items={files} renameLabel="New name" canRename={() => true}
      ids={{ root: "project-files", node: (node) => `project-${node.id}` }} />

Horizontal

orientation=horizontal sets aria-orientation: Right and Left move between nodes, Down opens a folder or enters it, Up closes it or goes to its parent (APG).

  • src
  • public
  • package.json
  • README.md
Code
<Tree label="Files" items={files} orientation="horizontal" />

Right to left

Left opens and Right closes; chevrons mirror.

  • المستندات
  • صور
Code
<div dir="rtl"><Tree label="ملفات" items={files} /></div>
Reference Tree in the manual Every part, attribute, slot, key and data attribute.
Prev TabsNext Carousel
Product
Manual Pricing Compare
Company
History Contact
Legal
Privacy Policy Acceptable Use Terms License Agreement
© Elements Dev Corporation 2026