Skip to content

Tree Select ​

A neo-brutalist style tree dropdown select component that uses the recursive TreeSelectNode component to render hierarchical structures, combined with Popover for dropdown interaction. Supports single-select, multi-select, and search filtering. Ideal for file selection, category selection, organization charts, and similar scenarios.

Demo ​

Preview

单选 + 搜索

多选模式

禁用状态

Installation ​

pnpm dlx brutx-vue@latest add tree-select

Usage ​

vue
<script setup>
import { ref } from 'vue'
import { TreeSelect } from 'brutx-ui-vue'
import type { TreeNode } from 'brutx-ui-vue'

const nodes: TreeNode[] = [
    {
        id: 'docs',
        label: 'Documentation',
        children: [
            { id: 'guide', label: 'Guide' },
            { id: 'api', label: 'API Reference' },
        ],
    },
    {
        id: 'src',
        label: 'Source',
        children: [
            {
                id: 'components',
                label: 'Components',
                children: [
                    { id: 'button', label: 'Button.vue' },
                    { id: 'input', label: 'Input.vue' },
                ],
            },
            { id: 'utils', label: 'Utilities' },
        ],
    },
]

const selected = ref(undefined)
</script>

<template>
    <TreeSelect
        v-model="selected"
        :nodes="nodes"
        placeholder="Select a file..."
        searchable
        clearable
    />
</template>

Multi-select Mode ​

vue
<script setup>
import { ref } from 'vue'
import { TreeSelect } from 'brutx-ui-vue'

const selected = ref([])
</script>

<template>
    <TreeSelect
        v-model="selected"
        :nodes="nodes"
        multiple
        searchable
        clearable
        placeholder="Select multiple files..."
    />
</template>

Search Filtering ​

When searchable is enabled, user input recursively filters tree nodes. When a match is found, the ancestor path is preserved to maintain visibility.

Disabling Nodes ​

Set disabled: true on a TreeNode to disable selection of that node:

ts
const nodes = [
    { id: '1', label: 'Selectable node' },
    { id: '2', label: 'Disabled node', disabled: true },
]

Controlled Expansion ​

Use the open prop and update:open event to implement controlled management of the dropdown expansion state:

vue
<script setup>
import { ref } from 'vue'
import { TreeSelect } from 'brutx-ui-vue'

const selected = ref(undefined)
const isOpen = ref(false)

function handleClose() {
    isOpen.value = false
}
</script>

<template>
    <TreeSelect
        v-model="selected"
        v-model:open="isOpen"
        :nodes="nodes"
    />
</template>

Arbitrary Depth Support ​

The component uses recursive rendering and supports tree structures of any depth, not limited to two levels.

Variants ​

VariantDescription
singleDefault single-select mode, modelValue is string | undefined
multipleMulti-select mode (set the multiple prop), modelValue is string[]

Sizes ​

SizeDescription
smSmall trigger
defaultDefault trigger
lgLarge trigger

Data Types ​

ts
interface TreeNode {
    id: string              // Unique identifier
    label: string           // Display text
    children?: TreeNode[]   // Child nodes (omit for leaf nodes)
    icon?: string           // Node icon
    disabled?: boolean      // Whether disabled
    data?: unknown          // Custom attached data
}

TreeSelectLocale ​

The component uses the useLocale composable for i18n support. You can customize text via TreeSelectLocale:

ts
interface TreeSelectLocale {
    placeholder: string        // Trigger placeholder text
    searchPlaceholder: string  // Search box placeholder text
    search: string             // Search box ARIA label
    emptyText: string          // No results text
    selectedCount: string      // Count text when multi-select exceeds maxDisplay, supports {count} interpolation
    clear: string              // Clear button ARIA label
}

API Reference ​

TreeSelect

TreeSelect — Props
NameTypeDefaultDescription
nodes
Required
TreeNode<unknown>[]
—
Hierarchical options. Each node uses an id and label, may contain children, and can carry states such as disabled.
Details

Declared type

TreeNode<unknown>[]

Referenced types

TreeNode
Type definition — TreeNode
export interface TreeNode<T = unknown> {
    /** 节点唯一标识 */
    id: string
    /** 节点显示文本 */
    label: string
    /** 节点图标名称或自定义图标类名 */
    icon?: string
    /** 子节点列表 */
    children?: TreeNode<T>[]
    /** 自定义业务数据 */
    data?: T
    /** 是否禁用交互 */
    disabled?: boolean
    /** 是否叶子节点(懒加载时若为 true 则不展示展开图标) */
    isLeaf?: boolean
    /** 懒加载子节点是否已加载 */
    loaded?: boolean
    /** 懒加载是否处于请求中 */
    loading?: boolean
    /** 过滤搜索时是否隐藏 */
    hidden?: boolean
}

Default value details

Not declared
—

Source

TreeSelect.vue:23

packages/ui/src/components/tree-select/TreeSelect.vue

modelValue
string | string[] | undefined
undefined
The selected node IDs, controlled with v-model. A single selection is a string; multiple selections use a string array.
Details

Declared type

string | string[] | undefined

Default value details

Resolved value
undefined

Source

TreeSelect.vue:25

packages/ui/src/components/tree-select/TreeSelect.vue

open
boolean | undefined
undefined
The controlled open state of the dropdown panel; pair it with update:open for v-model:open.
Details

Declared type

boolean | undefined

Literal values

  • false
  • true

Default value details

Resolved value
undefined

Source

TreeSelect.vue:27

packages/ui/src/components/tree-select/TreeSelect.vue

multiple
boolean | undefined
false
Allows selecting multiple nodes; modelValue and the select event then use arrays.
Details

Declared type

boolean | undefined

Literal values

  • false
  • true

Default value details

Resolved value
false

Source

TreeSelect.vue:29

packages/ui/src/components/tree-select/TreeSelect.vue

searchable
boolean | undefined
true
Controls whether the panel shows a search box for filtering nodes.
Details

Declared type

boolean | undefined

Literal values

  • false
  • true

Default value details

Resolved value
true

Source

TreeSelect.vue:31

packages/ui/src/components/tree-select/TreeSelect.vue

placeholder
string | undefined
undefined
Text shown by the trigger when no node is selected; the current locale’s default text is used when omitted.
Details

Declared type

string | undefined

Default value details

Resolved value
undefined

Source

TreeSelect.vue:33

packages/ui/src/components/tree-select/TreeSelect.vue

searchPlaceholder
string | undefined
undefined
Placeholder for the search field; the current locale’s default text is used when omitted.
Details

Declared type

string | undefined

Default value details

Resolved value
undefined

Source

TreeSelect.vue:35

packages/ui/src/components/tree-select/TreeSelect.vue

emptyText
string | undefined
undefined
Message shown when filtering returns no nodes; the current locale’s default text is used when omitted.
Details

Declared type

string | undefined

Default value details

Resolved value
undefined

Source

TreeSelect.vue:37

packages/ui/src/components/tree-select/TreeSelect.vue

clearable
boolean | undefined
false
Allows the current selection to be cleared from the trigger.
Details

Declared type

boolean | undefined

Literal values

  • false
  • true

Default value details

Resolved value
false

Source

TreeSelect.vue:39

packages/ui/src/components/tree-select/TreeSelect.vue

disabled
boolean | undefined
false
Disables trigger and panel interaction and prevents the dropdown from opening.
Details

Declared type

boolean | undefined

Literal values

  • false
  • true

Default value details

Resolved value
false

Source

TreeSelect.vue:41

packages/ui/src/components/tree-select/TreeSelect.vue

size
"default" | "lg" | "sm" | undefined
"default"
Sets the trigger size variant.
Details

Declared type

NonNullable<"default" | "lg" | "sm" | null | undefined> | undefined

Literal values

  • "default"
  • "lg"
  • "sm"

Default value details

Resolved value
"default"

Source

TreeSelect.vue:43

packages/ui/src/components/tree-select/TreeSelect.vue

ariaLabel
string | undefined
undefined
Sets the trigger’s accessible label.
Details

Declared type

string | undefined

Default value details

Resolved value
undefined

Source

TreeSelect.vue:45

packages/ui/src/components/tree-select/TreeSelect.vue

maxDisplay
number | undefined
3
In multiple mode, the maximum number of selected tags shown directly in the trigger; remaining selections are summarized as a count.
Details

Declared type

number | undefined

Default value details

Resolved value
3

Source

TreeSelect.vue:47

packages/ui/src/components/tree-select/TreeSelect.vue

maxHeight
string | undefined
"15rem"
Sets the CSS max-height of the tree dropdown.
Details

Declared type

string | undefined

Default value details

Resolved value
"15rem"

Source

TreeSelect.vue:49

packages/ui/src/components/tree-select/TreeSelect.vue

dropdownClass
string | undefined
undefined
CSS classes added to the dropdown panel.
Details

Declared type

string | undefined

Default value details

Resolved value
undefined

Source

TreeSelect.vue:51

packages/ui/src/components/tree-select/TreeSelect.vue

class
string | undefined
undefined
CSS classes added to the select trigger root.
Details

Declared type

string | undefined

Default value details

Resolved value
undefined

Source

TreeSelect.vue:53

packages/ui/src/components/tree-select/TreeSelect.vue

iconSize
IconSize | undefined
"md"
Sets the size of the trigger’s main icon.
Details

Declared type

IconSize | undefined

Literal values

  • "2xl"
  • "lg"
  • "md"
  • "sm"
  • "xl"
  • "xs"

Referenced types

IconSize
Type definition — IconSize
export type IconSize = NonNullable<VariantProps<typeof iconSizeVariants>['size']>

Default value details

Resolved value
"md"

Source

TreeSelect.vue:55

packages/ui/src/components/tree-select/TreeSelect.vue

itemVariant
"default" | "primary" | "secondary" | undefined
"default"
Sets the visual variant used to highlight selected tree nodes.
Details

Declared type

"default" | "primary" | "secondary" | undefined

Literal values

  • "default"
  • "primary"
  • "secondary"

Default value details

Resolved value
"default"

Source

TreeSelect.vue:57

packages/ui/src/components/tree-select/TreeSelect.vue

TreeSelect — Events
NameSignature or typeDescription
@update:modelValue
[value: string | string[] | undefined]
Emits the updated node ID or IDs when the selection changes; clearing or having no valid selection may emit undefined.
Details

Declared type

[value: string | string[] | undefined]

Source

TreeSelect.vue

packages/ui/src/components/tree-select/TreeSelect.vue

@update:open
[value: boolean]
Emits the new boolean when the dropdown open state changes and can be used with v-model:open.
Details

Declared type

[value: boolean]

Literal values

  • false
  • true

Source

TreeSelect.vue

packages/ui/src/components/tree-select/TreeSelect.vue

@open-change
[open: boolean]
Emits the current open state when the dropdown panel opens or closes.
Details

Declared type

[open: boolean]

Literal values

  • false
  • true

Source

TreeSelect.vue

packages/ui/src/components/tree-select/TreeSelect.vue

@select
[node: TreeNode<unknown> | TreeNode<unknown>[] | undefined]
Emits the selected node when the user selects or clears a node; multiple mode returns an array of nodes.
Details

Declared type

[node: TreeNode<unknown> | TreeNode<unknown>[] | undefined]

Referenced types

TreeNode
Type definition — TreeNode
export interface TreeNode<T = unknown> {
    /** 节点唯一标识 */
    id: string
    /** 节点显示文本 */
    label: string
    /** 节点图标名称或自定义图标类名 */
    icon?: string
    /** 子节点列表 */
    children?: TreeNode<T>[]
    /** 自定义业务数据 */
    data?: T
    /** 是否禁用交互 */
    disabled?: boolean
    /** 是否叶子节点(懒加载时若为 true 则不展示展开图标) */
    isLeaf?: boolean
    /** 懒加载子节点是否已加载 */
    loaded?: boolean
    /** 懒加载是否处于请求中 */
    loading?: boolean
    /** 过滤搜索时是否隐藏 */
    hidden?: boolean
}

Source

TreeSelect.vue

packages/ui/src/components/tree-select/TreeSelect.vue

TreeSelect — Exposes
NameSignature or typeDescription
searchQuery
string
The reactive state containing the current search query.
Details

Declared type

string

Source

TreeSelect.vue:357

packages/ui/src/components/tree-select/TreeSelect.vue

selectedNodes
TreeNode<unknown>[]
The selected node objects resolved from modelValue; this computed state is read-only.
Details

Declared type

TreeNode<unknown>[]

Referenced types

TreeNode
Type definition — TreeNode
export interface TreeNode<T = unknown> {
    /** 节点唯一标识 */
    id: string
    /** 节点显示文本 */
    label: string
    /** 节点图标名称或自定义图标类名 */
    icon?: string
    /** 子节点列表 */
    children?: TreeNode<T>[]
    /** 自定义业务数据 */
    data?: T
    /** 是否禁用交互 */
    disabled?: boolean
    /** 是否叶子节点(懒加载时若为 true 则不展示展开图标) */
    isLeaf?: boolean
    /** 懒加载子节点是否已加载 */
    loaded?: boolean
    /** 懒加载是否处于请求中 */
    loading?: boolean
    /** 过滤搜索时是否隐藏 */
    hidden?: boolean
}

Source

TreeSelect.vue:358

packages/ui/src/components/tree-select/TreeSelect.vue

expandedIds
Set<string>
The set of IDs for currently expanded nodes; it can be read or replaced through the component instance.
Details

Declared type

Set<string>

Source

TreeSelect.vue:359

packages/ui/src/components/tree-select/TreeSelect.vue

focus
() => void | undefined
Moves keyboard focus to the select trigger.
Details

Declared type

() => void | undefined

Source

TreeSelect.vue:360

packages/ui/src/components/tree-select/TreeSelect.vue

open
WritableComputedRef<boolean, boolean>
The writable open state of the dropdown; setting it updates internal or controlled state and emits update:open.
Details

Declared type

WritableComputedRef<boolean, boolean>

Literal values

  • false
  • true

Source

TreeSelect.vue

packages/ui/src/components/tree-select/TreeSelect.vue

TreeSelectNode

TreeSelectNode — Props
NameTypeDefaultDescription
node
Required
TreeNode<unknown>
—
The tree node currently rendered, including its data, hierarchy, and disabled state.
Details

Declared type

TreeNode<unknown>

Referenced types

TreeNode
Type definition — TreeNode
export interface TreeNode<T = unknown> {
    /** 节点唯一标识 */
    id: string
    /** 节点显示文本 */
    label: string
    /** 节点图标名称或自定义图标类名 */
    icon?: string
    /** 子节点列表 */
    children?: TreeNode<T>[]
    /** 自定义业务数据 */
    data?: T
    /** 是否禁用交互 */
    disabled?: boolean
    /** 是否叶子节点(懒加载时若为 true 则不展示展开图标) */
    isLeaf?: boolean
    /** 懒加载子节点是否已加载 */
    loaded?: boolean
    /** 懒加载是否处于请求中 */
    loading?: boolean
    /** 过滤搜索时是否隐藏 */
    hidden?: boolean
}

Default value details

Not declared
—

Source

TreeSelectNode.vue:16

packages/ui/src/components/tree-select/TreeSelectNode.vue

selectedIds
Required
Set<string>
—
The set of selected node IDs used to determine whether this node is selected.
Details

Declared type

Set<string>

Default value details

Not declared
—

Source

TreeSelectNode.vue:17

packages/ui/src/components/tree-select/TreeSelectNode.vue

expandedIds
Required
Set<string>
—
The set of expanded node IDs used to determine this node’s expanded state.
Details

Declared type

Set<string>

Default value details

Not declared
—

Source

TreeSelectNode.vue:18

packages/ui/src/components/tree-select/TreeSelectNode.vue

depth
number | undefined
0
The node’s depth in the tree, used to calculate indentation.
Details

Declared type

number | undefined

Default value details

Resolved value
0

Source

TreeSelectNode.vue:19

packages/ui/src/components/tree-select/TreeSelectNode.vue

multiple
boolean | undefined
false
Indicates whether the parent select is in multiple-selection mode.
Details

Declared type

boolean | undefined

Literal values

  • false
  • true

Default value details

Resolved value
false

Source

TreeSelectNode.vue:20

packages/ui/src/components/tree-select/TreeSelectNode.vue

focusedId
string | undefined
undefined
The ID of the node currently holding keyboard focus, used to render its focus state.
Details

Declared type

string | undefined

Default value details

Resolved value
undefined

Source

TreeSelectNode.vue:21

packages/ui/src/components/tree-select/TreeSelectNode.vue

variant
"default" | "primary" | "secondary" | undefined
"default"
Sets the visual variant for the node’s selected state.
Details

Declared type

"default" | "primary" | "secondary" | undefined

Literal values

  • "default"
  • "primary"
  • "secondary"

Default value details

Resolved value
"default"

Source

TreeSelectNode.vue:22

packages/ui/src/components/tree-select/TreeSelectNode.vue

TreeSelectNode — Events
NameSignature or typeDescription
@select
[node: TreeNode<unknown>]
Emits the node object when a leaf node is selected.
Details

Declared type

[node: TreeNode<unknown>]

Referenced types

TreeNode
Type definition — TreeNode
export interface TreeNode<T = unknown> {
    /** 节点唯一标识 */
    id: string
    /** 节点显示文本 */
    label: string
    /** 节点图标名称或自定义图标类名 */
    icon?: string
    /** 子节点列表 */
    children?: TreeNode<T>[]
    /** 自定义业务数据 */
    data?: T
    /** 是否禁用交互 */
    disabled?: boolean
    /** 是否叶子节点(懒加载时若为 true 则不展示展开图标) */
    isLeaf?: boolean
    /** 懒加载子节点是否已加载 */
    loaded?: boolean
    /** 懒加载是否处于请求中 */
    loading?: boolean
    /** 过滤搜索时是否隐藏 */
    hidden?: boolean
}

Source

TreeSelectNode.vue

packages/ui/src/components/tree-select/TreeSelectNode.vue

@focus
[id: string]
Emits the node ID when this node receives focus.
Details

Declared type

[id: string]

Source

TreeSelectNode.vue

packages/ui/src/components/tree-select/TreeSelectNode.vue

@toggle
[id: string]
Emits the node ID when a non-leaf node is expanded or collapsed.
Details

Declared type

[id: string]

Source

TreeSelectNode.vue

packages/ui/src/components/tree-select/TreeSelectNode.vue

Accessibility ​

  • Keyboard: Nodes support Arrow Up / Arrow Down navigation; non-leaf nodes support Arrow Right to expand / Arrow Left to collapse; Home / End keys jump to the first/last node; the clear button supports Enter / Space activation
  • ARIA Attributes: The trigger uses role="combobox" and aria-expanded; the dropdown list uses role="tree" and role="treeitem"; multi-select mode adds aria-multiselectable; disabled state uses aria-disabled
  • Focus Management: Uses roving tabindex for focus management; disabled nodes have tabindex="-1"

FAQ ​

Q: Why can't some nodes be found during search filtering?

A: Search filtering recursively matches nodes at all levels. When a child node matches, its ancestor path is automatically preserved to ensure visibility. If a node cannot be found, check that its label matches the search keyword. Search is case-insensitive fuzzy matching by default.

Q: The modelValue type differs between single-select and multi-select modes. How do I handle this uniformly?

A: In single-select mode, modelValue is string | undefined; in multi-select mode, it is string[]. In TypeScript projects, determine the type based on whether the multiple attribute is set. If you need to dynamically switch modes, consider using conditional types or defining separate ref variables.

Q: How do I control the display height when the dropdown has many items?

A: Use the maxHeight prop to control the maximum height of the dropdown list (default: '15rem'). When tree nodes exceed the maximum height, the list automatically shows a scrollbar. You can pass any valid CSS height value, such as '200px' or '50vh'.

Brute force builds.