Standalone hierarchy for a chart of accounts, a folder tree or any outline: expand and collapse, filter as you type, and single or checkbox selection. Branches load their children on demand and rows move with the pointer or the keyboard. It is one tab stop, and the arrow keys walk the rows. aria-label or aria-labelledby on the host names it.
One highlighted row; selection holds its key and suiSelectionChange carries the new selection. The tree is a role="tree" of treeitem rows with aria-level and aria-selected, named by aria-label or aria-labelledby on the host. Metadata such as a balance sits flush right and is read as part of the row's name.
<div style="max-width: 380px"><sui-tree selection-mode="single" aria-label="Chart of accounts"></sui-tree></div>
checkbox mode multi-selects and each row carries aria-checked. The header box ticks every node the filter shows and reads as mixed while only some are ticked. The filter box narrows the tree as you type and opens every branch that holds a match.
// setup(root): properties and events
setup(root) {
const t = root.querySelector('#tr-none');
t.nodes = accounts();
t.expanded = ['assets', 'liab'];
}
Loading
loading puts one row in place of the tree, the filter box and the select-all header, since nothing is there to filter or tick. An empty tree shows emptyMessage, which says Nothing here yet; a filter that matches none shows emptyFilterMessage, No matches. Two states, and one sentence cannot be right about both.
A node with leaf: false is a branch whose children have not arrived. Opening it fires suiLoad with the node; answer by setting its loading flag, then its children, and assigning a new nodes array. The branch shows a spinner and reads aria-busy while it waits.
Open a branch to fetch it.
<div style="display:grid;gap:10px;max-width:380px">
<code id="tr-lazy-log" style="font:460 12px/1.6 var(--font-sans);color:var(--ink-muted)">Open a branch to fetch it.</code>
<sui-tree id="tr-lazy" selection-mode="single"></sui-tree>
</div>
drag-drop lets a row move by pointer or keyboard. Dropping on the top or bottom quarter of a row puts the node beside it; dropping on the middle makes it the last child of that row. Ctrl and the arrow keys do the same three moves, and every move writes nodes back and emits suiNodesChange.
expanded and selection are written before suiExpandedChange and suiSelectionChange fire, and each event carries the whole key array, numbers kept as numbers. Neither bubbles, so listen on the tree itself. Writing expanded from outside, as the buttons do, opens the branches without an event.
Enters the tree at the active row, else the first. The tree is one tab stop, and in checkbox mode the select-all header is the stop before it.
ArrowDownArrowUp
Moves focus to the next or previous visible row.
ArrowRight
Opens a closed branch. On an open branch, moves to its first child. Under dir="rtl" the pair swaps, so the arrow that points along the reading direction is always the forward one.
ArrowLeft
Closes an open branch. Otherwise moves to the parent row.
HomeEnd
Moves focus to the first or last visible row.
Enter
Selects the row, or toggles the box in checkbox mode. With selection-mode="none", opens or closes a branch.
Space
Selects the row, or toggles the box in checkbox mode. On the select-all header, ticks every node the filter shows, or clears them.
Ctrl+ArrowUpCtrl+ArrowDown
With drag-drop, moves the focused row above or below the sibling next to it (Cmd on a Mac).
Ctrl+ArrowRight
With drag-drop, makes the focused row the last child of the row above it.
Ctrl+ArrowLeft
With drag-drop, moves the focused row out of its parent, to sit just after it.
API
<sui-tree>
Properties
Property
Attribute
Type
Default
Description
dragDrop
drag-drop
boolean
false
Pointer drag and the Ctrl arrow keys move a row within the tree. Off by default, since most trees are read, not rearranged; the elements made for moving things (the reorder lists, the board and the calendar) drag by default and turn it off with noDrag. Not draggable, which is a native attribute. Reflected.
emptyFilterMessage
empty-filter-message
stringstring | undefined
Row shown when the filter matched nothing. An empty list and a search that found nothing are different states and one sentence cannot be right about both: "Nothing here yet." is wrong over a list of two hundred the reader has just narrowed to none.
emptyMessage
empty-message
stringstring | undefined
Row shown when the node list is empty. The empty slot replaces it.
expanded
property only
SuiKey[]
[]
Keys of expanded branches. Mutable; updated before suiExpandedChange fires. Property, not attribute.
filter
filter
boolean
false
Shows the filter box; branches with matching descendants open automatically while filtering. Reflected.
loading
loading
boolean
false
Shows the whole tree is still on its way: the box shows the loading row instead of rows, the filter box and the select-all header. Per-branch waits use the node's own loading. Reflected, so a page can style the wait.
loadingMessage
loading-message
stringstring | undefined
Text of the loading row.
nodes
property only
SuiTreeNode[]
[]
Node tree (key, label, icon, meta, children). Rendered as tree > treeitem (with aria-level and aria-expanded) > group. Mutable: a move writes the new tree back and emits suiNodesChange. Property, not attribute.
selection
property only
SuiKey[]
[]
Keys of the selected nodes, as every collection holds its selection: one key in single mode, any number in checkbox mode. Mutable; updated before suiSelectionChange fires. Property, not attribute.
none, single (one highlighted row, aria-selected) or checkbox (multi-select boxes, aria-checked, and a select-all header above the tree). Checkbox mode draws the 18px system box itself rather than a sui-checkbox, and the row, not the box, carries the state. Reflected.
Events
Event
Detail
React
Vue
Description
suiExpandedChange
SuiKey[]
onSuiExpandedChange
@sui-expanded-change
Fires when a branch opens or closes; detail is the array of expanded keys. Does not bubble.
suiLoad
SuiTreeNode
onSuiLoad
@sui-load
Asks the host for a branch's children: fires when the element opens a node marked leaf: false that has none yet and is not already loading. Detail is the node. Answer by setting its loading, then its children, and assigning a new nodes array. Writing expanded from outside opens a branch without asking, since the caller already knows which.
suiNodesChange
SuiTreeNode[]
onSuiNodesChange
@sui-nodes-change
Fires after a row moves; detail is the new nodes tree, which is written back first. Does not bubble.
suiSelectionChange
SuiKey[]
onSuiSelectionChange
@sui-selection-change
Fires when the selection changes; detail is the new selection. Does not bubble.
Methods
Method
Signature
Description
setFocus()
setFocus() => Promise<void>
Moves keyboard focus to the tree's single tab stop (the active row, else the first).
Slots
Slot
Description
empty
What to show in place of the empty sentence when nodes holds nothing: a line and a button offering the next step. The same slot name the grid, the list and the tree table take; a filter that found nothing keeps its own sentence.
Types
SuiKeytypeA record's or an item's key wherever the library identifies one: a row, a tree node, a list item, a selected value. Numbers stay numbers, so a numeric id round-trips through selection, the events and a framework binding without turning into a string.
No members.
SuiTreeNodeinterface
key
SuiKey
Identity for expanded and selection, kept as given: a numeric id stays a number.
label
string
icon?
string
Font Awesome class drawn before the label.
meta?
string
Metadata at the end of the row (a balance, a count); tabular numerals.
children?
SuiTreeNode[]
leaf?
boolean
false says the node is a branch whose children have not arrived: it draws the chevron and asks for them with suiLoad the first time it opens. Leave it unset for a settled node.
loading?
boolean
Draws a spinner where the chevron was and sets aria-busy while the host fetches this branch's children.
SuiTreeSelectionModetype
No members.
Theming
Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.