Skip to content

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.

vue
<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.

vue
<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:

js
{ label: "Archived", value: "archived", children: [] }  // branch, currently empty
{ label: "Archived", value: "archived" }                // leaf

Expanding 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:

ValueWhat 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.
vue
<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:

vue
<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:

vue
<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 without fetchOptions.
  • Loading during a search. loadOnSearch lets 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. With fetchOptions every 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 loadingChildrenText row. If the promise rejects, the row shows loadChildrenErrorText instead. Clicking it, or closing and reopening the branch, tries again.
  • Ticking an unloaded branch. In multiple mode with cascade, its children load first, and the tick lands once they have, following valueConsistsOf as usual. Anything nested inside that is also unloaded loads too. If a load fails, the selection is left as it was. With loadOnSelect: 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 valueConsistsOf asks for, with a single update: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:

css
.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:

css
/* 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:

vue
<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 undefined

Levels to pre-expand on open. 1 opens top-level branches, 2 opens two levels, and so on.

Read more

disableBranchNodes

boolean default false

Makes branches unselectable, so only leaves can be picked. Clicking a branch row expands it instead.

Read more

cascade

boolean default true

In multiple tree mode, selecting a branch selects its descendants. false gives independent nodes.

Read more

valueConsistsOf

"LEAF_PRIORITY" | "ALL" | "BRANCH_PRIORITY" | "ALL_WITH_INDETERMINATE" default "LEAF_PRIORITY"

Which nodes end up in v-model while cascade is active.

Read more

compactChips

boolean default true

With cascade, merges chips into the topmost fully selected branch. false shows one chip per value entry.

Read more

flattenSearchResults

boolean default false

Drops the ancestor rows and the indent from search results, leaving a flat list.

Read more

noChildrenText

string default "No sub-options"

Shown under an expanded branch whose children array is empty.

Read more

loadChildren

(option: unknown) => Promise<unknown[]> default undefined

Fetches a branch's children when it first opens. Mark those branches with children: null.

Read more

loadOnSelect

boolean default true

Ticking an unloaded branch loads its children first. false ticks the branch as its own value with no request.

Read more

loadingChildrenText

string default "Loading..."

Shown under a branch while its children load.

Read more

loadOnSearch

boolean default false

Lets a search open and load unloaded branches that match. Off, they stay closed and match by their own label.

Read more

loadChildrenErrorText

string default "Could not load. Click to retry"

Shown under a branch whose children failed to load. Clicking it retries.

Read more

childrenKey

string default "children"

Key for nested children. Any option carrying that array turns tree mode on.

Read more

Released under the MIT License.