Tree view

A hierarchy to expand, collapse, select and activate, from nested markup or from records.

<tp-tree-view>
Tree view
Default density Compact density Restart from the initial state Show the copyable source
src components button.ts card.ts dialog.ts index.ts docs getting-started.md package.json

Usage

Nest tp-tree-item elements: text forms the label, slot="leading" and slot="trailing" hold marks and actions, and nested items are the children. default-expanded and default-value take JSON arrays of item values and apply before the first render, so the tree appears already open without change events. The tree is one tab stop; arrow keys move, Right expands and Left collapses or returns to the parent.

The tab stop starts on the first visible selected item. A virtualized tree scrolls its first window to that item.

Forms: with name, each selected identifier is submitted as one entry, in tree order. Reset restores the default selection; required is invalid while nothing is selected.

Multiple selection with checkboxes

selection-mode="multiple" with checkbox-selection shows a checked indicator in every row. selection-propagation cascades a parent’s selection to its descendants and derives mixed parents.

Multiple: pressing or Space toggles; Shift extends from the anchor; Control/Command toggles without moving the anchor; Shift+Arrow extends while moving; Shift+Space selects the range; Control+Shift+Home/End selects to an end; Control+A selects every visible enabled item.

Permissions
Default density Compact density Restart from the initial state Show the copyable source
Billing View invoices Manage payment methods Projects Create projects Archive projects Workspace admin

Disabled items stay focusable but are never selected by user input.

Activation

Enter or a press fires a cancelable tp-action with the item’s value, separately from selection. Use it to open a file or navigate; canceling it keeps the press from selecting or expanding.

Opening items
Default density Compact density Restart from the initial state Show the copyable source
Engineering On-call rotation Code review People Nothing opened yet

Expansion trigger

By default pressing a row toggles it. expansion-trigger="indicator" reserves the row press for selection and expands only from the chevron, which suits folders that are also selectable.

Chevron only
Default density Compact density Restart from the initial state Show the copyable source
Europe Berlin Lisbon North America Toronto Austin

Records

For large or data-driven trees, set defaultItems (or controlled items) to records of { id, label, children }. Records mode renders flat rows, which enables virtualized and reorderable; give a virtualized tree a block size.

The default record shape is { id, label, children?, hasChildren?, disabled? }; any other shape works through getItemId, getItemLabel, getItemChildren, isItemDisabled and itemHasChildren. Identifiers are strings, unique within the tree. A repeated identifier keeps its first item and raises one tp-diagnostic (tree-view-duplicate:<id>).

From records, reorderable
Default density Compact density Restart from the initial state Show the copyable source

Drag a handle, or press Control+Enter (Command+Enter) on an item and move it with the arrow keys. Each move is proposed through a cancelable tp-items-change.

  • Pointer: drag the handle. Rows move as you drag; moving sideways changes the level by one indentation step per --tp-tree-view-indent.
  • Keyboard: on a focused item press Control+Enter (Command+Enter). Up/Down move it, Left/Right change its level, Space or Enter drop, Escape cancels. Each step is announced with the level and parent.

An item can land before, after or inside any item, never inside itself. Every move is proposed through the cancelable tp-items-change with detail.metadata.move = { itemId, fromParentId, fromIndex, toParentId, toIndex } and the next records in detail.value; with items controlled, apply them yourself. Parents whose children change are rebuilt with withItemChildren(record, children) (default: { ...record, children }); every other record keeps its identity. moveItem(id, { parentId, index }) proposes a move from code.

A tree you drag yourself can reuse the level rule. projectDepth(rows, index, requestedDepth, canNest) from @tweakpad/ui takes the visible rows in order as { id, depth, parentId } (the dragged row’s descendants removed), the dragged row’s index and the depth the pointer asks for. It returns { depth, minDepth, maxDepth, parentId, vetoed }: the depth clamped between the next row’s depth and one more than the previous row’s, and the parent at that depth. The optional canNest(parent, source) decides whether a projected parent accepts the dragged row; the depth is then lowered to the deepest level whose parent accepts it, the top level is always accepted, and vetoed is true when no level within the clamp is. dragDepth(offset, indentation, rtl) turns an inline pointer offset into depth steps, mirrored for right-to-left.

Custom rows

In records mode, renderItem(item, state) returns each row’s content, so the artwork can follow the row’s state. Here folders open their icon while expanded and show a child count in the trailing slot, built with html from lit. canDrop vetoes moves before they are offered, so files cannot hold other items, and Enter or a press on a row fires tp-action.

Rendered rows
Default density Compact density Restart from the initial state Show the copyable source
Press a file to open it

Large trees

virtualized mounts only the rows near the viewport, so tens of thousands of records stay fast. Keyboard navigation, typeahead and focusItem() mount a row before focusing it. Give the tree a block size so it has a viewport to fill.

Virtualized
Default density Compact density Restart from the initial state Show the copyable source

The archive holds about 25,000 records; only a few dozen rows exist in the document at a time.

Give the tree a height (block-size, or a flex/grid track); without one it uses the nearest scrolling ancestor. item-size is the row estimate in pixels (default: the first measured row, which includes the separation between rows); rows are measured as they mount, so variable heights work. The focused row stays mounted.

Controlled

To own selection and expansion yourself, set items, value and expanded and write back each accepted proposal from tp-value-change and tp-expanded-change. The tree then shows exactly your state; the methods still work and go through the same events.

Controlled lanes
Default density Compact density Restart from the initial state Show the copyable source
Selected: Refund policy Reveal selection

The selected article starts hidden inside collapsed groups; Reveal selection expands its ancestors through tp-expanded-change, which the owner accepts.

Loading children on demand

A record with hasChildren: true shows a chevron before its children exist. On first expansion the tree calls loadChildren(item, { signal }), shows a loading row while it waits, and inserts the records it returns. The signal aborts if the item is removed or the tree disconnects first, and a failed load offers a retry.

Lazy folders
Default density Compact density Restart from the initial state Show the copyable source

reloadChildren(id) discards loaded children so the next expansion fetches them again.

Mark items with hasChildren (records) or has-children (markup) and set loadChildren:

tree.loadChildren = async (item, { signal }) => { const response = await fetch(`/api/folders/${item.id}`, { signal }); return response.json(); // child records; markup trees append elements and return nothing };

A load that returns no children leaves the item expandable but empty.

Expanding from code

expandAll(), collapseAll(), expandItem(id) and collapseItem(id) change expansion through the same tp-expanded-change as the keyboard. revealItem(id, { focus: true }) opens the item’s ancestors, scrolls it into view and focuses it.

expandAll({ load: true }) also loads and expands their descendants, breadth-first. Identifiers of items that do not exist yet are kept and take effect when the items appear, so a default can name items that load lazily.

Expand, collapse and reveal
Default density Compact density Restart from the initial state Show the copyable source
Expand all Collapse all Reveal pricing.ts
app routes pricing.ts checkout.ts main.ts tests pricing.test.ts

Size and guides

Small, no guides
Default density Compact density Restart from the initial state Show the copyable source
Account Profile Security Notifications

Keyboard

KeyActionDown / UpNext / previous visible itemRightExpand; on an expanded item, go to its first childLeftCollapse; otherwise go to the parentHome / EndFirst / last visible item*Expand all siblingsEnterActivate (tp-action); selects in single modeSpaceSelect (single) or toggle (multiple)LettersTypeahead over visible labels

Right and Left swap in right-to-left documents. Keys typed into editable content inside an item are left alone, and controls placed in an item (a Button in trailing, for example) keep their own presses, Enter and Space.

The tree is one tab stop: the last focused item, else the first selected item, else the first enabled item. When the focused item is hidden by a collapse, focus moves to the collapsed ancestor; when it is removed, focus moves to the next visible item, else the previous one.

Accessibility

The root is a tree named by label (or aria-label), multiselectable in multiple mode. Each item is a treeitem with level, position, set size, expanded, selected, checked and busy states. There is one tab stop; arrows move focus. Indent, chevron and checked indicator are hidden from assistive technology. Loading, errors and reorder steps are announced politely. Reorderable items describe their keyboard shortcut.

Motion

Rapid reversal turns around from the current height; bulk changes (expandAll, controlled updates with several items) and very large expansions apply instantly.

API Reference

Every element also accepts motion-policy.

Attributes

AttributePropertyTypeDefaultDescriptiondefault-valuedefaultValuereadonly string[] | undefined—The ids selected at first render, when the tree owns its selection.expandedexpandedreadonly string[]—The expanded ids; set it to control expansion, then publish each accepted proposal back.default-expandeddefaultExpandedreadonly string[] | undefined—The ids expanded at first render, when the tree owns its expansion.selection-modeselectionModeTreeSelectionMode'single'How many items can be selected: none, one, or several with Shift and Ctrl or Cmd.selection-propagationselectionPropagationbooleanfalseSelecting a parent selects its descendants, and a parent shows checked or mixed from its children.checkbox-selectioncheckboxSelectionbooleanfalseShows a checkbox on each item; a press on the box toggles it without changing the rest of the selection.expansion-triggerexpansionTriggerTreeViewExpansionTrigger'item'What expands a parent: a press anywhere on its row, or only on the chevron, which leaves the row free to select or open.virtualizedvirtualizedbooleanfalseRenders only the rows near the viewport, for large trees built from records; give the tree a block size.item-sizeitemSizenumber | nullnullThe row height, in pixels, a virtualized tree estimates before measuring.overscanoverscannumber4How many rows a virtualized tree renders beyond each edge of the viewport.reorderablereorderablebooleanfalseLets people drag items, or move them with the keyboard, to a new place; the tree proposes each move through tp-items-change.sizesizeTreeViewSize'default'The row density.guidesguidesTreeViewGuides'line'How nesting is drawn: a guide line per level, or indentation alone.labellabelstring | nullnullThe accessible name of the tree, such as "Project files".

Properties

PropertyTypeDefaultDescriptiondefaultItemsreadonly T[] | undefined—The records the tree starts from, when it owns them; reordering then changes its own copy.loadChildrenTreeViewLoadChildren<T> | TreeViewLoadChildren<TpTreeItem> | nullnullLoads the children of an item that declares children but has none yet, on its first expansion; failures show a retry.canDrop((move: TreeMove) => boolean) | nullnullVetoes a move before it is proposed, for example into a read-only folder.messagesTreeViewMessages{}Replaces built-in text, such as the loading, retry and reorder announcements.getItemId(item: T) => string—The stable id of a record; defaults to its id.getItemLabel(item: T) => string—The text label of a record, for typeahead and announcements; defaults to its label, else its id.getItemChildren(item: T) => readonly T[] | null | undefined—The children of a record; defaults to its children.isItemDisabled(item: T) => boolean—Whether a record is disabled; defaults to its disabled.itemHasChildren(item: T) => boolean—Whether a record has children to load on first expansion; defaults to its hasChildren.withItemChildren(item: T, children: T[]) => T—Produces a record with new children when a reorder changes them.renderItemTreeViewRenderItem<T> | nullnullRenders the content of a record's row (icon, label, badge or actions); without it the row shows the label.itemsreadonly T[] | undefined—The records the tree shows; set it to control them, then publish each accepted reorder back.resolvedMessagesread-onlyRequired<TreeViewMessages>—messages merged over the defaults.visibleItemsread-onlyreadonly string[]—Identifiers of the visible rows in order.

Events

EventTypeDescriptiontp-value-changeTpValueChangeEvent<readonly string[]>Selection proposal (cancelable).tp-expanded-changeTpValueChangeEvent<readonly string[]>Expansion proposal (cancelable).tp-items-changeTpValueChangeEvent<readonly T[]>Records-mode reorder proposal (cancelable); detail.metadata.move.tp-actionCustomEvent<{ value: string; item: unknown; sourceEvent: Event; }>An item was pressed or Enter was pressed on it (cancelable).tp-loading-status-changeCustomEvent<{ error?: {} | null; itemId: string; status: ImageLoadStatus; }>Children loading status of an item.

Methods

MethodReturnsDescriptionloadingStatus(id)TreeLoadStateChildren loading status of an item.expandAll(options)Promise<void>Expand every known expandable item; with load, also load and expand descendants.collapseAll()voidCollapses every item.expandItem(id)voidExpands one item, loading its children first when needed.collapseItem(id)voidCollapses one item.toggleItem(id)voidExpands a collapsed item or collapses an expanded one.selectItem(id)voidAdds an item to the selection, or makes it the selection in single mode.deselectItem(id)voidRemoves an item from the selection.selectAll()voidSelects every enabled item, in multiple selection mode.clearSelection()voidEmpties the selection.revealItem(id, options)Promise<void>Expand the ancestors, scroll the item into view and optionally focus or select it.focusItem(id)Promise<void>Move focus to a visible item, mounting it first.reloadChildren(id)voidDiscard loaded children of an item and load them again.moveItem(id, target)booleanPropose a records-mode move. Returns whether it was accepted.

Slots

SlotDescriptiondefaultMarkup mode: top-level tp-tree-item elements.emptyContent shown when there are no items.

CSS parts

PartDescription::part(tree)The tree (role="tree"; a labelled group while there are no items).::part(viewport)The scroll container of the rows.::part(item)Records mode: each rendered item; its parts are exported.::part(segment)Records mode: the transient region of rows entering or leaving.::part(empty)The empty region.

<tp-tree-item>

Attributes

AttributePropertyTypeDefaultDescriptionvaluevaluestring''Item identifier (markup mode).labellabelstring | nullnullAccessible label; defaults to the text of the label slot.has-childrenhasChildrenbooleanfalseDeclares children that the tree's loadChildren supplies on first expansion.

Properties

PropertyTypeDefaultDescriptionstateTreeItemState | nullnullPublished by the owning tree.ownerTreeItemOwner | nullnullThe tree that publishes this item's state.groupElementread-onlyHTMLElement | null—The region holding the child items, once rendered.rowElementread-onlyHTMLElement | null—The pressable row, once rendered.presentationOwnerread-onlyHTMLElement | null—Axis values (size) come from the owning tree.childItemsread-onlyTpTreeItem[]—Child items authored inside this item (markup mode).labelTextread-onlystring—Text of the label slot, used when label is not set.

Slots

SlotDescriptiondefaultThe label; text and elements without a known slot name.leadingContent before the label, such as an icon.trailingContent after the label, such as a badge or an action.indicatorReplaces the default disclosure chevron.

CSS parts

PartDescription::part(row)The pressable line; data-selected, data-expanded, data-disabled.::part(indent)Indentation and guides.::part(indicator)The disclosure indicator region.::part(checkbox)The checked indicator; data-checked, data-indeterminate.::part(leading)The region before the label.::part(label)The label region.::part(trailing)The region after the label.::part(handle)The reorder handle region.::part(status)Loading or load-error line.::part(group)Markup mode: the region holding child items.