Data Display
Tree View
A tree of rows you can open, close and walk with the keyboard.
import { TreeView } from 'omaris' Examples
Basic
A file tree. → opens a folder and steps in, ← closes it and steps out.
<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.
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.
<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
expandedyourself when open branches should survive a navigation or come from a URL.bind:expandedis a plain array of ids. - Use
expandable: trueon a branch whose children are fetched, load them inonexpand, and setloadingwhile 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
guidesoff for a shallow tree and keep them past about three levels. Usesize="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
checkableandonselectfor 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
idanywhere in the tree.expanded,selectedandcheckedall key off it.
Quick reference
size smmd(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-selectedanddata-activemark 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
metatext. 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 bindableDefaults to []
string[] Ids of the open branches. Bindable.
selected bindableDefaults to null
string | null Id of the selected row, or null. Bindable.
checked bindableDefaults 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 smmd
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.