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.
Chips
Chips are compact by default: a fully selected branch shows one chip instead of one per descendant, whatever valueConsistsOf emits. Set compactChips: false to show one chip per value entry instead, in sortValueBy order. With ALL_WITH_INDETERMINATE that includes partially selected branches. Removing a branch's chip unticks everything under it.
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, or
defaultExpandLevel. 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. Search does not open unloaded branches: they match by their own label and stay closed until someone opens them, with or withoutfetchOptions. - Loading during a search.
loadOnSearchlets a search open and load the unloaded branches it matches, so their children show up under them. Use it with care: every keystroke can send one request per matching branch, and a short query matches many. WithfetchOptionsevery unloaded branch in the results is opened, so each one is requested on every search. Leave it off for large trees. - 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. WithloadOnSelect: false, nothing loads: the branch is ticked as its own value, and is replaced by its children when it is opened later, as described below. - 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;
}Adding your own attributes
optionAttrs adds your own attributes to each option row. It receives the option and returns an object, for example a test id:
<VPick :options="options" :option-attrs="(o) => ({ 'data-testid': o.value })" />class and style are merged with the row's own. Attributes Vue Pick relies on are ignored: id, role, tabindex, data-value, data-depth, any aria-* and any on* handler.
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
defaultExpandLevel
number default undefinedLevels to pre-expand on open. 1 opens top-level branches, 2 opens two levels, and so on.
disableBranchNodes
boolean default falseMakes branches unselectable, so only leaves can be picked. Clicking a branch row expands it instead.
cascade
boolean 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.
compactChips
boolean default trueWith cascade, merges chips into the topmost fully selected branch. false shows one chip per value entry.
flattenSearchResults
boolean default falseDrops the ancestor rows and the indent from search results, leaving a flat list.
noChildrenText
string 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.
loadOnSelect
boolean default trueTicking an unloaded branch loads its children first. false ticks the branch as its own value with no request.
loadingChildrenText
string default "Loading..."Shown under a branch while its children load.
loadOnSearch
boolean default falseLets a search open and load unloaded branches that match. Off, they stay closed and match by their own label.
loadChildrenErrorText
string default "Could not load. Click to retry"Shown under a branch whose children failed to load. Clicking it retries.
childrenKey
string default "children"Key for nested children. Any option carrying that array turns tree mode on.