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
}

Props

PropTypeDefaultDescription
nodesTreeNode[]— (required)Tree data source
modelValuestring | string[]undefinedSelected value (string for single-select, string[] for multi-select)
openbooleanundefinedControlled expansion state, use with update:open for v-model:open
multiplebooleanfalseWhether to support multi-select
searchablebooleantrueWhether to show the search box
placeholderstringlocale: treeSelect.placeholderPlaceholder text
searchPlaceholderstringlocale: treeSelect.searchPlaceholderSearch box placeholder text
emptyTextstringlocale: treeSelect.emptyTextText shown when no results found
clearablebooleanfalseWhether to show the clear button
disabledbooleanfalseWhether disabled
size'sm' | 'default' | 'lg''default'Trigger size
ariaLabelstringundefinedARIA label
maxDisplaynumber3Maximum number of tags to display in multi-select mode
maxHeightstring'15rem'Maximum height of the dropdown list
dropdownClassstringundefinedCustom CSS class for the dropdown list
iconSizeIconSize'default'Icon size
itemVariant'default' | 'primary' | 'secondary''default'Dropdown item variant (selection highlight color)
classstringundefinedCustom CSS class for the trigger

Events

EventPayloadDescription
update:modelValuestring | string[] | undefinedSelected value change
update:openbooleanDropdown open/close state change, use with open prop for v-model:open
selectTreeNode | TreeNode[] | undefinedSelected node change
open-changebooleanDropdown open/close

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"

Exposed Methods (defineExpose)

Property/MethodTypeDescription
openRef<boolean>Whether the dropdown panel is expanded
searchQueryRef<string>Current search keyword
selectedNodesComputedRef<TreeNode[]>Selected nodes in multi-select mode (read-only)
expandedIdsRef<Set<string>>Currently expanded node IDs
focus() => voidFocus the trigger

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.