Tree Select
Pass options with a children array to enable tree mode. Vue Pick detects nested data automatically, no extra prop needed. Click the chevron to expand or collapse a branch; clicking the row itself selects the node.
Opening a tree with a value already selected expands whatever branches are needed to reveal it, and scrolls it into view.
Use childrenKey if your data nests under a different field name. See the Data Shape guide.
Expanding on open
defaultExpandLevel pre-expands branches. A value of 1 expands top-level branches; 2 expands two levels deep, and so on.
<VPick :options="options" :default-expand-level="1" />The third control below pairs it with disableBranchNodes, which is the usual combination: open the categories, but only let leaves be picked.
Unselectable branches
disableBranchNodes makes branch nodes non-selectable, so only leaves can be picked.
<VPick :options="options" disable-branch-nodes />Clicking the row toggles the branch open or closed instead, so the whole row stays a useful target rather than only the chevron. The row keeps aria-disabled="true", since it still is not selectable as an option.
Branch rows read as section headings once they are unselectable, which is when --vpick-option-branch-weight and the branch padding variables are usually worth setting. See Theming.
Branches with no children
A node is a branch when you give it a children array. An empty array still counts, so a category that filters down to nothing stays a branch rather than turning into a selectable option:
{ label: "Archived", value: "archived", children: [] } // branch, currently empty
{ label: "Archived", value: "archived" } // leafExpanding an empty branch shows noChildrenText. That row is inert: it has no option role, and arrow keys skip it. Use the no-children slot when you need markup rather than plain text.
Cascade
Combine multiple with tree options to get cascade selection: clicking a branch checks all its descendants, clicking again unchecks them. An indeterminate dash appears when only some children are selected.
Use cascade: false to opt out and get independent node selection instead.
valueConsistsOf
Controls what ends up in v-model while cascade is active:
| Value | What is emitted |
|---|---|
"LEAF_PRIORITY" (default) | Only leaf values. A fully-selected branch is implied by its leaves, so no branch value appears in the array. |
"BRANCH_PRIORITY" | The topmost selected ancestor replaces its descendants. Selecting all of Electronics emits ["electronics"] rather than every leaf. |
"ALL" | Every checked node, both fully-selected branches and their leaf descendants. |
"ALL_WITH_INDETERMINATE" | Like ALL but also includes partially-selected (indeterminate) branch values. |
<VPick
v-model="selected"
:options="options"
multiple
value-consists-of="BRANCH_PRIORITY"
/>All four round-trip: feed an emitted value straight back in and the rendered selection is unchanged.
Searching a tree
Combine with searchable to filter. Matching nodes auto-expand their ancestor branches so results are always visible, and expansion reverts when the query is cleared.
A row survives the filter when it matches, or when it sits on the path to something that matches. A branch matching on its own label is treated as the whole category being asked for, so it comes through with everything inside it, matching or not. Branches with nothing matching under them are dropped, whether or not defaultExpandLevel had already opened them.
An empty branch that matches by name shows its noChildrenText placeholder rather than vanishing.
For matching across the ancestor path rather than a single label, see searchNested.
Flattening search results
By default, searching a tree keeps the hierarchy: matching nodes stay nested and their ancestor branches auto-expand so results are visible in context.
flattenSearchResults drops the ancestors and the indent, leaving a flat list:
<VPick :options="options" searchable flatten-search-results />Search both of these for "gaming" to compare. The first walks you down to the matches; the second just lists them.
Searching "gaming" in Electronics > Laptops > Gaming laptop shows just the laptop, rather than all three rows.
What survives the filter is otherwise the same as in nested mode. A branch matching on its own label still brings everything inside it, listed flat and in document order, and an empty branch matching by name still shows its placeholder. Only the ancestor rows and the indentation are dropped.
Useful when the tree is deep and users want to scan results rather than navigate to them. Clearing the query restores the tree, and expansion state is left untouched throughout, since nothing needs expanding to reveal a match.
Loading children on demand
For a tree too large to send up front, fetch each branch's children when it is first opened. Mark those branches with children: null and pass loadChildren, which receives the option and returns a promise of its children, in the same shape as the rest of your options:
<script setup>
const options = [
{ label: "Electronics", value: "electronics", children: null },
{ label: "Gift card", value: "gift-card" },
]
async function loadChildren(option) {
const res = await fetch(`/api/categories/${option.value}`)
return res.json()
}
</script>
<template>
<VPick :options="options" :load-children="loadChildren" multiple />
</template>Open a branch to load it. Laptops is itself loaded on demand, one level down.
[]
- When it loads. When the branch is opened in an open list, whether by the chevron, the arrow keys,
defaultExpandLevel, or search. A control that is never opened sends no requests. Each branch is requested once, and the result is kept for as long as you pass the same option objects. WithfetchOptions, unloaded branches in the results stay closed until someone opens them. - While it loads. The branch shows a spinner and a
loadingChildrenTextrow. If the promise rejects, the row showsloadChildrenErrorTextinstead. Clicking it, or closing and reopening the branch, tries again. - Ticking an unloaded branch. In
multiplemode withcascade, its children load first, and the tick lands once they have, followingvalueConsistsOfas usual. Anything nested inside that is also unloaded loads too. If a load fails, the selection is left as it was. - Values that have not loaded yet. A saved value can name options under a branch nobody has opened. Those values are kept, submitted with the form, and shown as chips carrying the raw value until their branch loads.
- A selected branch that loads later. If the value names a branch but none of its children, the branch stood for all of them. Once they load, the value is rewritten to include them, in the shape
valueConsistsOfasks for, with a singleupdate:modelValue.
Search only matches options that have loaded. Ticking a branch that holds many unloaded branches sends one request for each, so if that is the common path, fetch the tree up front instead.
Styling branch and leaf rows
In tree mode each option row carries a modifier class and its nesting depth, so you can style branches differently from leaves without a slot:
.vpick-option--branch {
font-weight: 600;
}
.vpick-option--leaf {
color: #475569;
}
/* top-level branches only */
.vpick-option--branch[data-depth="0"] {
text-transform: uppercase;
}--branch is set whenever the node has a children array, empty or not. --leaf and data-depth are only applied in tree mode, so flat lists stay untouched.
To style one specific option, every row, flat or tree, also carries its value as data-value. It is set for string, number and boolean values:
/* a "Select all" branch that should not show its chevron */
.vpick-option[data-value="all"] .vpick-option-expand {
visibility: hidden;
}These rules have to be global. .vpick-option and the rest belong to Vue Pick and carry its scope, not yours, so a <style scoped> rule cannot reach them, and a global rule restyles every VPick on the page. To change one instance, set a variable on the component instead: those are forwarded to the panel. See Theming for --vpick-option-branch-weight and the row padding variables.
--vpick-tree-indent (default 1.375rem) is the width of one level of nesting, applied per depth. The default matches the width of the expand chevron and its gap, so a child's chevron lines up under its parent's label.
Props
defaultExpandLevelnumber default undefinedLevels to pre-expand on open. 1 opens top-level branches, 2 opens two levels, and so on.
disableBranchNodesboolean default falseMakes branches unselectable, so only leaves can be picked. Clicking a branch row expands it instead.
cascadeboolean default trueIn multiple tree mode, selecting a branch selects its descendants. false gives independent nodes.
valueConsistsOf"LEAF_PRIORITY" | "ALL" | "BRANCH_PRIORITY" | "ALL_WITH_INDETERMINATE" default "LEAF_PRIORITY"Which nodes end up in v-model while cascade is active.
flattenSearchResultsboolean default falseDrops the ancestor rows and the indent from search results, leaving a flat list.
noChildrenTextstring default "No sub-options"Shown under an expanded branch whose children array is empty.
loadChildren(option: unknown) => Promise<unknown[]> default undefinedFetches a branch's children when it first opens. Mark those branches with children: null.
loadingChildrenTextstring default "Loading..."Shown under a branch while its children load.
loadChildrenErrorTextstring default "Could not load. Click to retry"Shown under a branch whose children failed to load. Clicking it retries.
childrenKeystring default "children"Key for nested children. Any option carrying that array turns tree mode on.