DreamLake

Tree View

A hierarchical tree for scene graphs, file trees, and outlines. You supply a flat array of TreeDataItem nodes (each pointing at its parentId); the useTreeState hook derives the visible rows — with indent and guideline metadata — and manages expand-collapse state. Icons and label rendering are left to the caller. For trees with thousands of nodes, swap TreeView for VirtualTreeView — it shares the same props plus a height.

Basic

Pass the visibleData from useTreeState to data, and wire expandedItems, onToggleItem, and hasDescendants so collapsible folders get a chevron. getIcon returns whatever node icon you want, with the expanded flag for folder-open states.

src
components
Button.tsx
Input.tsx
hooks
useTree.ts
index.ts
README.md

Connectors

The branch into a row is an arc on every row, not only the last child — a column of connectors is one drawn path, and a corner that is square here and round there reads as two line systems in the same rail. A row with siblings below it keeps an unbroken vertical running past its branch; the arc leaves the trunk rather than replacing it.

The line is --tree-guide, not --faint. Rows hover and select, and a translucent hairline takes a tint from each of those, so one connector ended up drawn in several shades down a single column — reporting the row's state instead of the structure it is there to describe. --tree-guide is the same colour held still.

It is legible on the page and faint where it crosses a filled row (1.03:1 in light). That is the cost of a neutral line in a tree whose surfaces sit within 19 levels of each other — see Color → Lines that cross rows.

Selectable

Set isSelectable and control selection with selectedItemIds / onSelectionChange. With the default selectionMode="multi", Cmd/Ctrl-click toggles individual rows and Shift-click selects a range; selecting a parent implicitly covers its descendants.

Scene
Lights
KeyLight
FillLight
Meshes
Robot
Ground
Camera

Selected: robot

Searchable

TreeSearchBar is a controlled input with case-sensitive and regex toggles. Feed its state into useTreeSearch, which returns the filteredData (matches plus their ancestor paths) and a renderLabel that highlights the matched substring. Pass filteredData back through useTreeState and hand renderLabel to the TreeView.

app
routes
home.tsx
about.tsx
settings.tsx
lib
fetcher.ts
format.ts
config.ts

Quiet rows

Three opt-outs tune the tree for dense sidebars (the Dream Studio repos list uses all three). chevronPosition="trailing" moves a smaller, dimmed expand chevron to sit right after the label text — collapsed it points left, toward the label it reveals under. hoverSubtree={false} limits the hover wash to the row under the cursor instead of highlighting the whole subtree as one block. loneSelectionStyle="ring" draws a lone selected row with the same accent ring group selections use, leaving the row background transparent instead of the solid fill. A fourth, groupSelection="row", changes what selecting a group MEANS: only the clicked row is marked — descendants are untouched, and a selected group renders like any lone selection instead of a ring around its whole subtree.

studio
main
panel-integrations
uikit
docs-restructure

Selected runs — SelectionRun

When a list lets you pick several rows at once, wrap each contiguous stretch of selected rows in a SelectionRun. The rows keep their own surface and a single accent ring goes around the run, so five selected rows are one shape rather than five repeated fills.

Five filled rows say "five things happened". One ringed block says "this is the selection" — which is the thing a bulk action is about to act on.

Click the rows below: selecting a neighbour grows a run, and deselecting one in the middle splits it. A gap in the selection is a second run, not a taller one.

click a row — 3 selected

The hover block rounds by the same rule, and breaks where a selected row interrupts it. Hovering a group highlights the group and its descendants as one shape; if one of those descendants is selected it paints its own surface instead, so the hover band genuinely ends above it and resumes below. Each of the three pieces rounds all four corners. Treating the selected row as part of the band left square corners butted against it — the one place a tree row showed a raw corner.

Give the run the same corner radius as the rows it wraps. The ring is outset, so its inner curve is the run's radius: rows rounded any tighter pull away from it and leave a sliver at every corner. The default is var(--radius); override it with className if your rows are rounded differently.

The run owns the 2px gap between its rows, because that tight rhythm is part of reading them as one block. The ring is outset and the run sits above its neighbours, so a row below cannot clip it — but the list around it still has to leave 2px of side room, or the ring's corners go under whatever does the clipping. Native <div> attributes are forwarded.

Props

TreeView / VirtualTreeView

PropTypeDefaultDescription
dataTreeDataItemWithMeta<T>[]—Flattened rows with metadata — pass visibleData from useTreeState.
getIcon(item: T, expanded?: boolean) => ReactNode—Icon for each node. Receives the expanded flag for folder-open states.
expandedItemsSet<string>—IDs of expanded collapsible nodes.
onToggleItem(id: string) => void—Called when a chevron is clicked.
onItemHover(id: string | null) => void—Called on row mouse enter / leave.
hoveredIdstring | null—Controlled hovered row id.
isSelectablebooleanfalseEnables row selection.
selectedItemIdsSet<string>—Controlled set of selected node ids.
onSelectionChange(ids: Set<string>) => void—Called when the selection changes.
selectionMode'single' | 'multi''multi'multi enables Cmd/Ctrl-click toggle and Shift-click range.
hideExpandbooleanfalseHide expand chevrons entirely.
hasDescendants(id: string) => boolean() => falseWhether a node has children — pass the helper from useTreeState.
renderLabel(label: string, itemId: string) => ReactNodeidentityCustom label rendering — pass renderLabel from useTreeSearch for highlights.
renderContextMenu(item: T) => ReactNode—Right-click menu content for a row.
hoverSubtreebooleantrueHovering a group also highlights its whole subtree as one block. Set false for row-only hover.
loneSelectionStyle'fill' | 'ring''fill'Lone selected row: solid fill, or the accent ring group selections use (transparent background).
chevronPosition'leading' | 'trailing''leading'trailing renders a smaller, dimmed chevron after the label text; collapsed points left.
groupSelection'subtree' | 'row''subtree''row' = selecting a group marks only that row; descendants stay unmarked.
classNamestring—Extra classes on the container.
heightnumber | string'100%'VirtualTreeView only — container height for virtualization.
overscannumber5VirtualTreeView only — extra rows rendered outside the viewport.

TreeDataItem

FieldTypeDescription
idstringUnique node identifier.
parentIdstring | nullParent id, or null for roots.
labelstringDisplay text (also the search target).
isCollapsiblebooleanMarks a node as an expandable folder.
actionsReactNodeTrailing action content shown on the row.
disablebooleanDim and disable interaction for the row.
selectablebooleanSet false to exclude the row from selection.

TreeSearchBar

PropTypeDescription
searchQuerystringCurrent query text.
setSearchQuery(query: string) => voidUpdates the query.
isCaseSensitivebooleanCase-sensitive toggle state.
setIsCaseSensitive(value: boolean | ((prev: boolean) => boolean)) => voidToggles case sensitivity.
isRegexbooleanRegex toggle state.
setIsRegex(value: boolean | ((prev: boolean) => boolean)) => voidToggles regex mode.
isRegexValidbooleanWhether the current regex compiles — from useTreeSearch.
searchResultsCountnumberNumber of direct matches — from useTreeSearch.
classNamestringExtra classes on the container.

useTreeState({ data, defaultExpanded?, expandedItems?, onToggleItem? })

ReturnsTypeDescription
visibleDataTreeDataItemWithMeta<T>[]Flattened, metadata-tagged rows with collapsed subtrees hidden — pass to data.
dataWithMetaTreeDataItemWithMeta<T>[]All rows with metadata, ignoring collapse state.
expandedItemsSet<string>Currently expanded node ids.
toggleItem(id: string) => voidToggle one node's expanded state.
expandAll() => voidExpand every collapsible node.
collapseAll() => voidCollapse every node.
hasDescendants(id: string) => booleanWhether a node has children.

useTreeSearch({ data, searchQuery, isCaseSensitive?, isRegex? })

ReturnsTypeDescription
filteredDataT[]Matching nodes plus their ancestor paths — feed into useTreeState.
searchResultsCountnumberCount of direct label matches.
isRegexValidbooleanfalse when isRegex is on and the query is not a valid pattern.
renderLabel(label: string, itemId: string) => ReactNodeLabel renderer that highlights matched substrings — pass to TreeView.
hasActiveSearchbooleanWhether a non-empty query is active.