SvTreeSelect
A single-select dropdown that shows an indented, collapsible tree in its panel - the cascader / tree-select pattern. Pick a node from a hierarchy, optionally showing its full path in the trigger.
SvTreeSelect covers hierarchical choice (a category, a folder, an org unit)
without pulling in the full SvTree. It supports bind:value,
tracks its own expand/collapse state (seeded from expandedIds), and flattens the
visible nodes for a roving-focus keyboard model where ArrowRight / ArrowLeft
expand and collapse branches. The panel portals out of any scroll container and
colors follow the grid's --sg-* tokens.
Related: SvDropDownList · SvGridSelect · Selection overview
Installation
Add it with the CLI - this drops a ready-to-edit SvTreeSelect starter into your app:
Prefer to see it first? npx @svgrid/ui try tree-select opens it in a throwaway sandbox - no project needed.
Or install the package and import it directly. SvTreeSelect ships free in
@svgrid/grid (dependency-free):
import { SvTreeSelect } from '@svgrid/grid'
Example
Open the live example: Tree select (Selection)
<script lang="ts">
import { SvTreeSelect, type TreeSelectNode } from '@svgrid/grid'
const nodes: TreeSelectNode[] = [
{ value: 'eng', label: 'Engineering', children: [
{ value: 'fe', label: 'Frontend' },
{ value: 'be', label: 'Backend' },
] },
{ value: 'design', label: 'Design' },
]
let value = $state<string | number | null>(null)
</script>
<SvTreeSelect label="Team" {nodes} bind:value showPath />
Props
| Prop | Type | Default | Description |
|---|---|---|---|
nodes |
ReadonlyArray<TreeSelectNode> |
- | The hierarchy to choose from. |
value |
string | number | null |
null |
The selected node's value. Bindable (bind:value). |
onChange |
(value: string | number) => void |
- | Fires with the picked node's value. |
onCommit |
(value: string | number) => void |
- | Fires when a node is committed. |
onCancel |
() => void |
- | Fires when the panel is dismissed. |
placeholder |
string |
'Select…' |
Shown when nothing is selected. |
showPath |
boolean |
false |
Show the full node path (A / B / C) in the trigger. |
expandedIds |
ReadonlyArray<string | number> |
[] |
Node values expanded on first open. |
size |
sm | md | lg |
md |
Trigger height and font size. |
disabled |
boolean |
false |
Blocks interaction. |
label |
string |
- | Visible field label, wired to the control. |
hint |
string |
- | Helper text under the control. |
error |
string |
- | Error message; announced and styled when set. |
required |
boolean |
false |
Marks the field required. |
invalid |
boolean |
false |
Applies the invalid state. |
name |
string |
- | Emits a hidden input carrying the value for form posts. |
dir |
ltr | rtl | auto |
auto |
Text direction. |
ariaLabel |
string |
- | Accessible name when there is no visible label. |
id |
string |
- | Root id; label/hint/error ids derive from it. |
TreeSelectNode
type TreeSelectNode = {
value: string | number
label: string
children?: TreeSelectNode[]
disabled?: boolean
}
Examples
Full-path trigger
Turn on showPath so the trigger reads Engineering / Frontend instead of just
the leaf label - useful when leaf names repeat across branches.
Pre-expanded branches
Seed the open state so users land deep in the tree without clicking down to it:
<SvTreeSelect {nodes} bind:value expandedIds={['eng']} />
Disabled branches
Set disabled on a node to make it unpickable while still showing (and expanding)
its children.
Build the tree from flat rows
Hierarchies usually arrive flat (a parentId column). Fold them into
TreeSelectNode[] once, then bind the selected leaf:
<script lang="ts">
import { SvTreeSelect, type TreeSelectNode } from '@svgrid/grid'
const flat = [
{ id: 'eng', parent: null, name: 'Engineering' },
{ id: 'fe', parent: 'eng', name: 'Frontend' },
{ id: 'be', parent: 'eng', name: 'Backend' },
{ id: 'design', parent: null, name: 'Design' },
]
function toTree(parent: string | null): TreeSelectNode[] {
return flat
.filter((r) => r.parent === parent)
.map((r) => ({ value: r.id, label: r.name, children: toTree(r.id) }))
}
const nodes = toTree(null)
let value = $state<string | number | null>(null)
</script>
<SvTreeSelect label="Team" {nodes} bind:value showPath />
Tip:
expandedIdsonly seeds the open branches for the first open; after that the component tracks its own expand/collapse state, so you do not have to keep the prop in sync.
Accessibility
- The panel is a
role="tree"ofrole="treeitem"rows witharia-expandedandaria-selected; the active row is tracked viaaria-activedescendant. ArrowUp/ArrowDownmove,ArrowRight/ArrowLeftexpand / collapse,Enterpicks a leaf,Escapedismisses, and focus returns to the trigger.label,hint, anderrorare wired viaaria-describedby; passariaLabelwhen there is no visible label.
Sizes
Every control takes the same three sizes, so a dense toolbar and a roomy form can share components.
<script lang="ts">
import { SvTreeSelect } from '@svgrid/grid'
let treeSelect = $state('')
</script>
<SvTreeSelect bind:value={treeSelect} size="sm" />
<SvTreeSelect bind:value={treeSelect} size="md" />
<SvTreeSelect bind:value={treeSelect} size="lg" />
In a form
The shared field props behave the same on every editor: label names it, hint explains it, and error plus invalid mark it - which is why a validated form does not need per-component handling.
<script lang="ts">
import { SvTreeSelect } from '@svgrid/grid'
let treeSelect = $state('')
</script>
<SvTreeSelect
bind:value={treeSelect}
label="Label"
hint="A short hint"
required
/>
<SvTreeSelect
bind:value={treeSelect}
label="Label"
error="Something is wrong"
invalid
/>
See also
- Selection overview - the whole picker family at a glance.
- SvDropDownList - a flat single-select dropdown.
- SvGridSelect - pick a row across multiple columns.
Related articles
- SvGrid UI Components Tips and Tricks: Svelte 5 Buttons, Inputs, Tree, Forms - Practical tips for the SvGrid UI component kit - SvButton states, a Cmd+K command palette, a virtualized searchable tree, schema-driven forms, a parsing date/time field, and the shared field contract - each with a code snippet.