jixoai-ui
the terminal design language
loading the style sheet…
Skip to content
docs · print medium: screen

registry:ui · Data display

tree-view — the extensible ARIA tree

A generic native-ARIA tree (nested ul role=tree / group / treeitem, roving tabindex, the full arrow-key contract) with an extension surface instead of baked-in opinions: prefix/suffix slots and onPrefixSlotRender / onSuffixSlotRender resolvers render any icon library or action row from live context, onactivate lets an extension own the Enter/Space behavior (the shipped multiselect extension does exactly that), and the built-in variants stop where they must — toggler glyph, guide lines, indent. The seven demos below mirror the reui.io tree reference one-for-one.

prefix / suffix slots onXxxSlotRender resolvers multiselect extension disabled · variants roving tabindex

reference parity

The seven effects

The reui.io/components/tree reference ships seven effects; each one maps onto this architecture without forking it — plain rows, guide lines, custom indent with a prefix slot, the plus toggler, contextual type icons through the resolver, avatars through prefix + label snippets, and checkboxes through the multiselect extension.

1 · basic tree

Single-select CRM tree, chevron toggler, built-in file/folder icons (fileIcons). Folders toggle on click, leaves select — the file-tree lineage.

  • Leads
    • New Lead
    • Contacted Lead
    • Qualified Lead
  • Accounts
  • Activities
    • Calls
    • Meetings
    • Emails
tree-view-usage.svelte svelte
<script lang="ts">
  import TreeView, { type TreeNode } from '@ui/tree-view.svelte';
</script>

const leaf = (name: string): TreeNode => ({ name });
const crm: TreeNode[] = [
  { name: 'Leads', children: [leaf('New Lead'), leaf('Contacted Lead')] },
  { name: 'Accounts', children: [leaf('Acme Corp'), leaf('Globex Inc')] },
  // …Activities, Support — same shape
];

let selected = $state('Leads/New Lead');
</script>

<TreeView
  nodes={crm}
  defaultExpanded={['Leads', 'Accounts']}
  {selected}
  fileIcons
  onselect={(ctx) => (selected = ctx.id)}
/>

2 · indented lines

The built-in lines variant: one 1px guide rail per indent level, drawn only as tall as each group.

  • Leads
    • New Lead
    • Contacted Lead
    • Qualified Lead
  • Accounts
  • Activities
    • Calls
    • Meetings
    • Emails
usage.svelte svelte
<TreeView nodes={crm} defaultExpanded={['Leads', 'Accounts', 'Activities']} lines />

3 · custom indent · prefix slot

indent 32 plus the prefix snippet: folder glyphs swap with expand state, leaves get a file glyph — the static extension level.

  • Leads
    • New Lead
    • Contacted Lead
    • Qualified Lead
  • Accounts
  • Activities
    • Calls
    • Meetings
    • Emails
usage.svelte svelte
{#snippet crmPrefix(ctx)}
  {#if ctx.isFolder}
    <!-- the folder swaps glyphs with expand state; leaves show a file -->
    <Icon name={ctx.expanded ? 'folderOpen' : 'folder'} />
  {:else}
    <Icon name="file" />
  {/if}
{/snippet}

<TreeView
  nodes={crm}
  defaultExpanded={['Leads', 'Accounts', 'Activities']}
  indent={32}
  lines
  prefix={crmPrefix}
/>

4 · plus toggler

The plus toggler variant — expanded folders show −, collapsed show +.

  • Leads
    • New Lead
    • Contacted Lead
    • Qualified Lead
  • Accounts
  • Activities
    • Calls
    • Meetings
    • Emails
usage.svelte svelte
<TreeView
  nodes={crm}
  defaultExpanded={['Leads', 'Accounts', 'Activities']}
  indent={32}
  lines
  toggle="plus"
  prefix={crmPrefix}
/>

5 · file explorer · onPrefixSlotRender

The dynamic extension level: a resolver returns a snippet per node context — amber folders, blue code, purple css, yellow json.

  • my-project
    • src
      • app
        • page.tsx
        • layout.tsx
        • loading.tsx
      • components
        • button.tsx
        • card.tsx
        • dialog.tsx
      • globals.css
    • package.json
    • README.md
    • tsconfig.json
usage.svelte svelte
<!-- one snippet per file type; the resolver RETURNS one per node -->
{#snippet folderIcon(ctx)}
  <span class="text-amber-500"><Icon name={ctx.expanded ? 'folderOpen' : 'folder'} /></span>
{/snippet}
{#snippet tsIcon(ctx)}<span class="text-blue-500"><Icon name="fileCode" /></span>{/snippet}
{#snippet cssIcon(ctx)}<span class="text-purple-500"><Icon name="palette" /></span>{/snippet}
{#snippet jsonIcon(ctx)}<span class="text-yellow-500"><Icon name="braces" /></span>{/snippet}
{#snippet mdIcon(ctx)}<span class="text-muted-foreground"><Icon name="fileText" /></span>{/snippet}
{#snippet genericIcon(ctx)}<span class="text-muted-foreground"><Icon name="file" /></span>{/snippet}

<TreeView
  nodes={fileTree}
  defaultExpanded={['my-project', 'my-project/src', 'my-project/src/app']}
  onPrefixSlotRender={(ctx) => {
    if (ctx.isFolder) return folderIcon;
    switch (ctx.node.meta) {
      case 'ts':
      case 'tsx': return tsIcon;
      case 'css': return cssIcon;
      case 'json': return jsonIcon;
      case 'md': return mdIcon;
      default: return genericIcon;
    }
  }}
/>

6 · org chart · prefix + label

Avatars through the prefix snippet and a two-line label snippet — rows grow with their content.

  • Acme Inc. Company
    • Sarah Chen CEO
      • Alex Johnson CTO
        • Lisa Park Design Lead
usage.svelte svelte
{#snippet orgPrefix(ctx)}
  <Avatar name={ctx.node.name} src={ctx.node.meta?.avatar} size="sm" alt="" />
{/snippet}
{#snippet orgLabel(ctx)}
  <span class="flex flex-col items-start py-0.5 leading-tight">
    <span class="text-foreground text-[12px]">{ctx.node.name}</span>
    {#if ctx.node.meta?.role}
      <span class="text-muted-foreground text-[10px]">{ctx.node.meta.role}</span>
    {/if}
  </span>
{/snippet}

<TreeView
  nodes={org}
  defaultExpanded={['Acme Inc.', 'Acme Inc./Sarah Chen']}
  indent={24}
  prefix={orgPrefix}
  label={orgLabel}
/>

7 · permissions · multiselect

The shipped tree-view-multiselect extension: tri-state cascade, Space toggles the focused row, arrows keep walking.

  • User Management
    • View users
    • Create users
    • Edit users
    • Delete users
  • Content Management
    • View content
    • Publish content
    • Delete content
tree-view-multiselect-usage.svelte svelte
<script lang="ts">
  import TreeViewMulti from '@ui/tree-view-multiselect.svelte';
</script>

let checked = $state([
  'User Management/View users',
  'Content Management/View content',
  // …
]);
</script>

<TreeViewMulti
  nodes={permissions}
  defaultExpanded={['User Management', 'Content Management']}
  bind:checked
  oncheck={(ids) => console.log(ids)}
/>

tree-view · suffix actions + disabled

onSuffixSlotRender arms only the folders with Add / More icon buttons (revealed on row hover or focus), and the archive subtree ships disabled: focusable for screen readers, never activatable.

  • src
    • lib
      • icons.ts
      • site.ts
      • jixoai.css
    • routes
      • tokens.html
      • recipes.html
  • package.json
usage.svelte svelte
{#snippet folderActions(ctx)}
  <button class="jx-demo-act" title="Add" onclick={() => addInside(ctx.id)}>
    <Icon name="plus" />
  </button>
  <button class="jx-demo-act" title="More" onclick={() => moreFor(ctx.id)}>
    <Icon name="ellipsis" />
  </button>
{/snippet}

<TreeView
  nodes={projectTree}
  defaultExpanded={['src', 'src/lib', 'src/routes']}
  onSuffixSlotRender={((ctx: TreeItemCtx) => (ctx.isFolder ? folderActions : undefined)) as unknown as TreeSlotRender}
/>

law

The tree keyboard contract

The ARIA tree pattern is a keyboard contract before it is a widget: ↑/↓ move focus between visible items, → expands a collapsed directory (or jumps into its first child), ← collapses an expanded one (or returns to the parent), Home / End jump the ends, Enter / Space activates the item. Roving tabindex keeps exactly one tab stop; collapsed extents are inert, so hidden rows are untabbable by construction. Extensions hook the activation through onactivate and ctx.preventDefault() — the multiselect turns Space into a checkbox toggle without touching the walker.

usage
<script lang="ts">
  import TreeView, { type TreeNode } from '@ui/tree-view.svelte';
</script>

const leaf = (name: string): TreeNode => ({ name });
const crm: TreeNode[] = [
  { name: 'Leads', children: [leaf('New Lead'), leaf('Contacted Lead')] },
  { name: 'Accounts', children: [leaf('Acme Corp'), leaf('Globex Inc')] },
  // …Activities, Support — same shape
];

let selected = $state('Leads/New Lead');
</script>

<TreeView
  nodes={crm}
  defaultExpanded={['Leads', 'Accounts']}
  {selected}
  fileIcons
  onselect={(ctx) => (selected = ctx.id)}
/>

types

Tree-view variants

The built-ins stop where they must — toggler glyph, guide lines, indent — and the shipped multiselect extension adds tri-state cascade.

tree-view · variants

  • src
    • icons.ts
    • site.ts
  • package.json
  • src
    • icons.ts
    • site.ts
  • docs
    • tokens.html
  • package.json
  • src
    • icons.ts
    • site.ts
  • docs
    • tokens.html
  • package.json
tree-view-types-demo.svelte svelte
<script lang="ts">
  import TreeView, { type TreeNode } from '@ui/tree-view.svelte';
  import TreeViewMulti from '@ui/tree-view-multiselect.svelte';
</script>

const leaf = (name: string): TreeNode => ({ name });
const miniTree: TreeNode[] = [
  { name: 'src', children: [leaf('icons.ts'), leaf('site.ts')] },
  { name: 'docs', children: [leaf('tokens.html')] },
  leaf('package.json'),
];
let miniSelected = $state('src/icons.ts');
let miniChecked = $state<string[]>(['docs/tokens.html']);

<div class="grid w-full items-start gap-4 min-[900px]:grid-cols-3">
  <div class="border border-border p-3"><TreeView nodes={miniTree} defaultExpanded={['src']} selected={miniSelected} fileIcons onselect={(ctx) => (miniSelected = ctx.id)} /></div>
  <div class="border border-border p-3"><TreeView nodes={miniTree} defaultExpanded={['src', 'docs']} toggle="plus" lines indent={24} /></div>
  <div class="border border-border p-3"><TreeViewMulti nodes={miniTree} defaultExpanded={['src', 'docs']} bind:checked={miniChecked} /></div>
</div>

usage

Usage

Nodes are plain data (name/children/disabled/meta); selection stays consumer-owned through the controlled selected prop.

Tree-view usage
<script lang="ts">
  import TreeView, { type TreeNode } from '@ui/tree-view.svelte';
</script>

const leaf = (name: string): TreeNode => ({ name });
const crm: TreeNode[] = [
  { name: 'Leads', children: [leaf('New Lead'), leaf('Contacted Lead')] },
  { name: 'Accounts', children: [leaf('Acme Corp'), leaf('Globex Inc')] },
  // …Activities, Support — same shape
];

let selected = $state('Leads/New Lead');
</script>

<TreeView
  nodes={crm}
  defaultExpanded={['Leads', 'Accounts']}
  {selected}
  fileIcons
  onselect={(ctx) => (selected = ctx.id)}
/>

a11y

Accessibility

A native-ARIA tree: nested tree/group/treeitem roles, roving tabindex, and the full APG arrow contract.

Keyboard

KeyAction
↑ / ↓Move focus between visible items (roving tabindex)
→Expand a collapsed folder, or jump into its first child
←Collapse an expanded folder, or return to the parent
Home / EndJump to the first / last visible item
Enter / SpaceActivate the item — folders toggle, leaves select; extensions may preventDefault

ARIA

AttributeValueDescription
roletree / group / treeitemNested native-ARIA tree roles; one tab stop by roving tabindex.
aria-expandedfolders onlyReports the folder’s collapsed state.
aria-selectedleaves onlyMirrors the controlled selected path id.
aria-disableddisabled nodesFocusable for screen readers, never activatable (APG disabled treeitem).
aria-labelariaLabel propNames the tree (default "tree").

theming

Density and tokens

Row paint rides the ambient chain (measured: row height drifts 17.5/18/18.5/19.2px across the rungs — the density response flows; no fixed utility pins it); --jx-indent is the one geometry lever — it drives group padding and the lines guide rails together.

xs
  • src
    • icons.ts
    • site.ts
  • package.json
sm
  • src
    • icons.ts
    • site.ts
  • package.json
default
  • src
    • icons.ts
    • site.ts
  • package.json
lg
  • src
    • icons.ts
    • site.ts
  • package.json
TokenDefaultSource
--jx-indent16px (indent prop)component
guide rails1px var(--border)color
focus ring2px var(--ring)color
multiselect boxvar(--primary) fillcolor
type iconsaccent 60% mixcolor
suffix revealhover / focus-withinstructural

axes

Universal props

The eight-axis surface (explicit-props): size · shape · radius · density · color · theme · elevation · motion — each axis takes named steps, auto (inherit the ambient context; stamps nothing), an exact number (px · coefficient · dp · hue per axis), or query() for responsive/container-conditional values. The ul[role=tree] is the family's own DOM root (a FIRST-TIME no-own contract) — a dense file tree at size 12 is the use case; the multiselect sibling forwards the lanes to the composed root, and the rows/carets/language dots ride the ambient chain.

Tree view · universal props

  • Leads
    • New Lead
    • Contacted Lead
    • Qualified Lead
  • Accounts
  • Activities
    • Calls
    • Meetings
    • Emails
  • Leads
    • New Lead
    • Contacted Lead
    • Qualified Lead
  • Accounts
  • Activities
    • Calls
    • Meetings
    • Emails
tree-view-universal.svelte svelte
<TreeView nodes={tree} size={12} density="small" />

api

API

Slots and resolvers receive TreeItemCtx (node, id, depth, isFolder, expanded, selected, disabled, preventDefault); the multiselect extension reuses the same seams.

Properties

PropertyTypeDefaultDescription
nodes * TreeNode<T>[]—The tree data — name, children, disabled, consumer meta.
defaultExpanded string[][]Folder ids expanded on mount; uncontrolled afterwards.
selected string—Single-selection path id (consumer-owned; leaves select).
prefix Snippet<[TreeItemCtx<T>]>—Prefix column rendered for every node with the item ctx.
suffix Snippet<[TreeItemCtx<T>]>—Suffix column, hidden until row hover/focus-within (actions law).
onPrefixSlotRender TreeSlotRender<T>—Dynamic prefix: pick a snippet per node; wins over prefix.
onSuffixSlotRender TreeSlotRender<T>—Dynamic suffix: pick a snippet per node; wins over suffix.
label Snippet<[TreeItemCtx<T>]>node.nameLabel override.
onactivate (ctx: TreeItemCtx<T>) => void—Click / Enter / Space — runs first; ctx.preventDefault() cancels default.
ontoggle (ctx: TreeItemCtx<T>) => void—Folder expand/collapse; ctx.expanded is the NEW state.
onselect (ctx: TreeItemCtx<T>) => void—Leaf selection; the controlled selected prop stays the source of truth.
toggle 'chevron' | 'plus''chevron'Toggler glyph variant (built-in).
fileIcons booleanfalseBuilt-in file-tree icons in the prefix column; consumer snippets win per node.
lines booleanfalseVertical guide rails per indent level (built-in).
indent number16px per level; feeds --jx-indent.
ariaLabel string'tree'Names the tree.
class string''Extra classes on the root.

Universal props

PropertyTypeDefaultDescription
size'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopethe base scale — root font-size; parts size in em
shape'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto''auto' · ambient scopecorner geometry (CSS corner-shape; §14 degrade table)
radius'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopecorner size; auto = the concentric broadcast (§3)
density'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopespacing/leading scale over the kernel channels (§4)
color'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string'auto' · ambient scopethe hue axis of the fixed oklch primary system (§5)
theme'light' | 'dark' | 'system' | 'auto''auto' · ambient scopelight/dark profile; system = the JS-mutable global (§6)
elevation'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number'auto' · ambient scopeofficial M3 levels over the surface ladder (§7)
motion'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number'auto' · ambient scopeintensity across the motion kernels (§8)

TreeNode

PropertyTypeDefaultDescription
name * string—Row label source and path segment.
children TreeNode<T>[]—Presence makes the node a folder.
disabled boolean—Focusable but not activatable; freezes the subtree.
meta T—Consumer payload — flows untouched into ctx.node.meta.

TreeViewMulti — extension additions

PropertyTypeDefaultDescription
checked bindstring[][...defaultChecked]Checked path ids (folders included when fully on).
defaultChecked string[][]Initial ids when the consumer does not bind checked.
oncheck (ids: string[], ctx: TreeItemCtx<T>) => void—After every commit; ctx is the row that caused it.
jixoai-ui ui.jixoai.com the jixoai design language

Theme

Brand hue

0°

Auto cycle · 24h