Skip to content
omaris

Data Display

Tree View

A tree of rows you can open, close and walk with the keyboard.

import { TreeView } from 'omaris'
Learn

Examples

Basic

A file tree. → opens a folder and steps in, ← closes it and steps out.

src
lib
index.ts 12 KB
utils.ts 3 KB
app.css 28 KB
package.json 2 KB
<script lang="ts">	import { TreeView, type TreeNode } from 'omaris';​	const files: TreeNode[] = [		{			id: 'src',			label: 'src',			children: [				{					id: 'lib',					label: 'lib',					children: [						{ id: 'index', label: 'index.ts', meta: '12 KB' },						{ id: 'utils', label: 'utils.ts', meta: '3 KB' }					]				},				{ id: 'app', label: 'app.css', meta: '28 KB' }			]		},		{ id: 'pkg', label: 'package.json', meta: '2 KB' }	];​	let expanded = $state(['src', 'lib']);	let selected = $state<string | null>('utils');</script>​<div class="w-72 max-w-full rounded-shape-md border border-border p-2">	<TreeView items={files} bind:expanded bind:selected label="Files" /></div>

Checkable

Ticking a branch ticks its leaves, and a partly ticked branch shows a dash. checked holds only the leaf ids, so there is no half state to store.

Food
Grills
Stews
Drinks
Coffee
Tea

grills, coffee

<script lang="ts">	import { TreeView, type TreeNode } from 'omaris';​	const categories: TreeNode[] = [		{			id: 'food',			label: 'Food',			children: [				{ id: 'grills', label: 'Grills' },				{ id: 'stews', label: 'Stews' }			]		},		{			id: 'drinks',			label: 'Drinks',			children: [				{ id: 'coffee', label: 'Coffee' },				{ id: 'tea', label: 'Tea' }			]		}	];​	let checked = $state(['grills', 'coffee']);</script>​<div class="flex w-72 max-w-full flex-col gap-3">	<TreeView items={categories} checkable bind:checked defaultExpanded label="Categories" />	<p class="text-xs text-muted-foreground">{checked.join(', ') || 'nothing ticked'}</p></div>

Lazy

expandable draws the chevron before the children exist. onexpand is where they get fetched; loading swaps the chevron for a spinner meanwhile.

<script lang="ts">	import { TreeView, type TreeNode } from 'omaris';​	let items = $state<TreeNode[]>([		{ id: 'remote', label: 'remote', expandable: true },		{ id: 'local', label: 'local', children: [{ id: 'note', label: 'note.md' }] }	]);	let expanded = $state<string[]>([]);​	function load(node: TreeNode) {		if (node.id !== 'remote' || node.children) return;		items = items.map((entry) => (entry.id === 'remote' ? { ...entry, loading: true } : entry));		setTimeout(() => {			items = items.map((entry) =>				entry.id === 'remote'					? {							...entry,							loading: false,							children: [								{ id: 'origin', label: 'origin.json' },								{ id: 'upstream', label: 'upstream.json' }							]						}					: entry			);		}, 700);	}</script>​<div class="w-72 max-w-full">	<TreeView {items} bind:expanded onexpand={load} label="Sources" /></div>

Overridden

A wider indent, pill rows, an accent guide and a row body of your own.

Design open
Tokens
Motion
Code open
API
<script lang="ts">	import { TreeView, type TreeNode } from 'omaris';​	const items: TreeNode[] = [		{			id: 'design',			label: 'Design',			children: [				{ id: 'tokens', label: 'Tokens' },				{ id: 'motion', label: 'Motion' }			]		},		{ id: 'code', label: 'Code', children: [{ id: 'api', label: 'API' }] }	];</script>​<div class="w-72 max-w-full">	<TreeView		{items}		defaultExpanded		label="Sections"		class="[--tree-indent:2rem]"		classes={{ item: 'rounded-full', guide: 'bg-primary/30' }}	>		{#snippet row(node, { expanded })}			<span class="min-w-0 flex-1 truncate font-medium">{node.label}</span>			{#if expanded}				<span class="text-xs text-muted-foreground">open</span>			{/if}		{/snippet}	</TreeView></div>

When to use it

Use it for

  • Files and folders: a repository browser, an attachment picker, a media library.
  • Any nesting a person navigates rather than reads: a category tree, an org chart, nested filters, a JSON path, a chart of accounts.
  • A sidebar of sections whose depth varies from page to page, where a flat list would lose the relationships.
  • Picking a set out of a hierarchy, with checkable. Ticking a branch ticks everything under it; a partly ticked branch shows a dash.

Not for

  • One level of rows → List.
  • Sections of content on one page → Accordion. An accordion opens content; a tree opens more rows.
  • The app's own navigation → Navigation Drawer, which knows the current route.
  • Rows with several columns of data → Table.
  • A data structure for reading rather than walking → JSON Viewer.

Do

  • Keep expanded yourself when open branches should survive a navigation or come from a URL. bind:expanded is a plain array of ids.
  • Use expandable: true on a branch whose children are fetched, load them in onexpand, and set loading while they are on the way. The chevron becomes a spinner.
  • Put a size, a count or a status in meta, not in the label. It is right-aligned and stays out of the truncation.
  • Give the tree a label. It is the accessible name.
  • Turn guides off for a shallow tree and keep them past about three levels. Use size="sm" in a sidebar, where the tree is not the subject.

Don't

  • Render thousands of rows at once. A tree is for a hierarchy someone walks. Page or search instead.
  • Use checkable and onselect for two jobs at once. With checkboxes, clicking a row ticks it; a row that also navigates is ambiguous.
  • Nest more than about five levels. The indent runs out of screen first.
  • Reuse an id anywhere in the tree. expanded, selected and checked all key off it.

Quick reference

size
  • sm
  • md (default)

API

TreeView

A tree of rows you can open, close and walk with the keyboard.

Files are the obvious use and not the only one — a category tree, an org chart, a JSON path, a set of nested filters all have the same shape. It is data-driven rather than composed, because a tree is almost always rendered from something recursive and writing the recursion by hand at every call site is how the ARIA comes out wrong.

Keyboard: ↑↓ walk the open rows, → opens a branch then steps into it, ← closes it then steps out, Home/End jump, typing jumps to a matching row, Enter picks. One tab stop for the whole tree.

import { TreeView } from 'omaris'
<TreeView {items} bind:expanded bind:selected onselect={open} /><TreeView {items} checkable bind:checked />

Parts

Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.

root
No description in the source yet.
item
One row. data-selected and data-active mark its state.
toggle
The chevron. Rotates a quarter turn when the branch opens.
icon
The leading icon — a folder, a file, or your own.
label
The row's text. Truncates; there is no room to wrap a tree.
meta
The trailing meta text.
check
The checkbox drawn when checkable.
group
The children of an open branch.
guide
The hairline down the left of a branch's children.

Props

items

Defaults to []

TreeNode[]

The tree.

expanded bindable

Defaults to []

string[]

Ids of the open branches. Bindable.

selected bindable

Defaults to null

string | null

Id of the selected row, or null. Bindable.

checked bindable

Defaults to []

string[]

Ids of the ticked leaves — rows with no children. A branch's own state is worked out from them, so a half-ticked folder is a state you never have to store. Bindable.

checkable

Defaults to false

boolean

Give every row a checkbox and tick branches through their leaves.

size

Defaults to 'md'

TreeViewSize
sm
md
guides

Defaults to true

boolean

Draw the vertical rules down each open branch.

icons

Defaults to true

boolean

Use the built-in folder and file icons for rows with no icon.

defaultExpanded

Defaults to false

boolean

Open every branch on first render.

label
string

Accessible name for the tree.

onselect
(node: TreeNode) => void

Fires when a row is picked — clicked, or Enter on the keyboard.

onexpand
(node: TreeNode) => void

Fires when a branch opens. The place to fetch its children.

oncollapse
(node: TreeNode) => void

Fires when a branch closes.

row
Snippet<[TreeNode, { depth: number; expanded: boolean; selected: boolean }]>

Replaces a row's contents, after the chevron and the checkbox.

empty
Snippet

Shown when items is empty.

class
string
classes
TreeViewClasses

Per-part Tailwind overrides. class still covers the root.