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
单选 + 搜索
多选模式
禁用状态
Installation
pnpm dlx brutx-vue@latest add tree-selectUsage
<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
<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:
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:
<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
| Variant | Description |
|---|---|
single | Default single-select mode, modelValue is string | undefined |
multiple | Multi-select mode (set the multiple prop), modelValue is string[] |
Sizes
| Size | Description |
|---|---|
sm | Small trigger |
default | Default trigger |
lg | Large trigger |
Data Types
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:
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
| Name | Type | Default | Description |
|---|---|---|---|
nodesRequired | TreeNode<unknown>[] | — | Hierarchical options. Each node uses an id and label, may contain children, and can carry states such as disabled. |
DetailsDeclared typeTreeNode<unknown>[] Referenced types TreeNode Type definition — TreeNodeexport 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
Sourcepackages/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. |
DetailsDeclared typestring | string[] | undefined Default value details
Sourcepackages/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. |
DetailsDeclared typeboolean | undefined Literal values
Default value details
Sourcepackages/ui/src/components/tree-select/TreeSelect.vue | |||
multiple | boolean | undefined | false | Allows selecting multiple nodes; modelValue and the select event then use arrays. |
DetailsDeclared typeboolean | undefined Literal values
Default value details
Sourcepackages/ui/src/components/tree-select/TreeSelect.vue | |||
searchable | boolean | undefined | true | Controls whether the panel shows a search box for filtering nodes. |
DetailsDeclared typeboolean | undefined Literal values
Default value details
Sourcepackages/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. |
DetailsDeclared typestring | undefined Default value details
Sourcepackages/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. |
DetailsDeclared typestring | undefined Default value details
Sourcepackages/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. |
DetailsDeclared typestring | undefined Default value details
Sourcepackages/ui/src/components/tree-select/TreeSelect.vue | |||
clearable | boolean | undefined | false | Allows the current selection to be cleared from the trigger. |
DetailsDeclared typeboolean | undefined Literal values
Default value details
Sourcepackages/ui/src/components/tree-select/TreeSelect.vue | |||
disabled | boolean | undefined | false | Disables trigger and panel interaction and prevents the dropdown from opening. |
DetailsDeclared typeboolean | undefined Literal values
Default value details
Sourcepackages/ui/src/components/tree-select/TreeSelect.vue | |||
size | "default" | "lg" | "sm" | undefined | "default" | Sets the trigger size variant. |
DetailsDeclared typeNonNullable<"default" | "lg" | "sm" | null | undefined> | undefined Literal values
Default value details
Sourcepackages/ui/src/components/tree-select/TreeSelect.vue | |||
ariaLabel | string | undefined | undefined | Sets the trigger’s accessible label. |
DetailsDeclared typestring | undefined Default value details
Sourcepackages/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. |
DetailsDeclared typenumber | undefined Default value details
Sourcepackages/ui/src/components/tree-select/TreeSelect.vue | |||
maxHeight | string | undefined | "15rem" | Sets the CSS max-height of the tree dropdown. |
DetailsDeclared typestring | undefined Default value details
Sourcepackages/ui/src/components/tree-select/TreeSelect.vue | |||
dropdownClass | string | undefined | undefined | CSS classes added to the dropdown panel. |
DetailsDeclared typestring | undefined Default value details
Sourcepackages/ui/src/components/tree-select/TreeSelect.vue | |||
class | string | undefined | undefined | CSS classes added to the select trigger root. |
DetailsDeclared typestring | undefined Default value details
Sourcepackages/ui/src/components/tree-select/TreeSelect.vue | |||
iconSize | IconSize | undefined | "md" | Sets the size of the trigger’s main icon. |
DetailsDeclared typeIconSize | undefined Literal values
Referenced types IconSize Type definition — IconSizeexport type IconSize = NonNullable<VariantProps<typeof iconSizeVariants>['size']> Default value details
Sourcepackages/ui/src/components/tree-select/TreeSelect.vue | |||
itemVariant | "default" | "primary" | "secondary" | undefined | "default" | Sets the visual variant used to highlight selected tree nodes. |
DetailsDeclared type"default" | "primary" | "secondary" | undefined Literal values
Default value details
Sourcepackages/ui/src/components/tree-select/TreeSelect.vue | |||
| Name | Signature or type | Description |
|---|---|---|
@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. |
DetailsDeclared type[value: string | string[] | undefined] Sourcepackages/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. |
DetailsDeclared type[value: boolean] Literal values
Sourcepackages/ui/src/components/tree-select/TreeSelect.vue | ||
@open-change | [open: boolean] | Emits the current open state when the dropdown panel opens or closes. |
DetailsDeclared type[open: boolean] Literal values
Sourcepackages/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. |
DetailsDeclared type[node: TreeNode<unknown> | TreeNode<unknown>[] | undefined] Referenced types TreeNode Type definition — TreeNodeexport 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
}Sourcepackages/ui/src/components/tree-select/TreeSelect.vue | ||
| Name | Signature or type | Description |
|---|---|---|
searchQuery | string | The reactive state containing the current search query. |
DetailsDeclared typestring Sourcepackages/ui/src/components/tree-select/TreeSelect.vue | ||
selectedNodes | TreeNode<unknown>[] | The selected node objects resolved from modelValue; this computed state is read-only. |
DetailsDeclared typeTreeNode<unknown>[] Referenced types TreeNode Type definition — TreeNodeexport 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
}Sourcepackages/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. |
DetailsDeclared typeSet<string> Sourcepackages/ui/src/components/tree-select/TreeSelect.vue | ||
focus | () => void | undefined | Moves keyboard focus to the select trigger. |
DetailsDeclared type() => void | undefined Sourcepackages/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. |
DetailsDeclared typeWritableComputedRef<boolean, boolean> Literal values
Sourcepackages/ui/src/components/tree-select/TreeSelect.vue | ||
TreeSelectNode
| Name | Type | Default | Description |
|---|---|---|---|
nodeRequired | TreeNode<unknown> | — | The tree node currently rendered, including its data, hierarchy, and disabled state. |
DetailsDeclared typeTreeNode<unknown> Referenced types TreeNode Type definition — TreeNodeexport 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
Sourcepackages/ui/src/components/tree-select/TreeSelectNode.vue | |||
selectedIdsRequired | Set<string> | — | The set of selected node IDs used to determine whether this node is selected. |
DetailsDeclared typeSet<string> Default value details
Sourcepackages/ui/src/components/tree-select/TreeSelectNode.vue | |||
expandedIdsRequired | Set<string> | — | The set of expanded node IDs used to determine this node’s expanded state. |
DetailsDeclared typeSet<string> Default value details
Sourcepackages/ui/src/components/tree-select/TreeSelectNode.vue | |||
depth | number | undefined | 0 | The node’s depth in the tree, used to calculate indentation. |
DetailsDeclared typenumber | undefined Default value details
Sourcepackages/ui/src/components/tree-select/TreeSelectNode.vue | |||
multiple | boolean | undefined | false | Indicates whether the parent select is in multiple-selection mode. |
DetailsDeclared typeboolean | undefined Literal values
Default value details
Sourcepackages/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. |
DetailsDeclared typestring | undefined Default value details
Sourcepackages/ui/src/components/tree-select/TreeSelectNode.vue | |||
variant | "default" | "primary" | "secondary" | undefined | "default" | Sets the visual variant for the node’s selected state. |
DetailsDeclared type"default" | "primary" | "secondary" | undefined Literal values
Default value details
Sourcepackages/ui/src/components/tree-select/TreeSelectNode.vue | |||
| Name | Signature or type | Description |
|---|---|---|
@select | [node: TreeNode<unknown>] | Emits the node object when a leaf node is selected. |
DetailsDeclared type[node: TreeNode<unknown>] Referenced types TreeNode Type definition — TreeNodeexport 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
}Sourcepackages/ui/src/components/tree-select/TreeSelectNode.vue | ||
@focus | [id: string] | Emits the node ID when this node receives focus. |
DetailsDeclared type[id: string] Sourcepackages/ui/src/components/tree-select/TreeSelectNode.vue | ||
@toggle | [id: string] | Emits the node ID when a non-leaf node is expanded or collapsed. |
DetailsDeclared type[id: string] Sourcepackages/ui/src/components/tree-select/TreeSelectNode.vue | ||
Accessibility
- Keyboard: Nodes support
Arrow Up/Arrow Downnavigation; non-leaf nodes supportArrow Rightto expand /Arrow Leftto collapse;Home/Endkeys jump to the first/last node; the clear button supportsEnter/Spaceactivation - ARIA Attributes: The trigger uses
role="combobox"andaria-expanded; the dropdown list usesrole="tree"androle="treeitem"; multi-select mode addsaria-multiselectable; disabled state usesaria-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'.