Skip to content

UITreeDropdown

UITreeDropdown

A versatile tree dropdown component that allows users to navigate and select items from hierarchical data structures with support for multiple selection, filtering, and customization.

<template>
<UITreeDropdown
v-model="selectedItems"
:options="treeData"
:multiple="true"
:filterable="true"
placeholder="Select categories"
@change="handleSelectionChange"
>
<template #trigger="{ selectedLabels }">
<UIButton type="default">
{{ selectedLabels.length ? selectedLabels.join(', ') : 'Select' }}
<ChevronDownIcon class="w-5 h-5 ml-2" />
</UIButton>
</template>
</UITreeDropdown>
</template>
<script setup lang="ts">
interface TreeNode {
id: string
label: string
children?: TreeNode[]
disabled?: boolean
}
const selectedItems = ref<string[]>([])
const treeData: TreeNode[] = [
{
id: '1',
label: 'Electronics',
children: [
{
id: '1-1',
label: 'Phones',
children: [
{ id: '1-1-1', label: 'iPhone' },
{ id: '1-1-2', label: 'Android' }
]
},
{
id: '1-2',
label: 'Laptops'
}
]
},
{
id: '2',
label: 'Clothing',
children: [
{ id: '2-1', label: 'Men' },
{ id: '2-2', label: 'Women' }
]
}
]
const handleSelectionChange = (values: string[]) => {
console.log('Selected:', values)
}
</script>

Props

  • modelValue (string | string[]): Selected value(s)
  • options (array): Tree data structure
  • multiple (boolean): Enable multiple selection
  • filterable (boolean): Enable search/filter
  • placeholder (string): Input placeholder
  • disabled (boolean): Disable component
  • loading (boolean): Loading state
  • checkStrictly (boolean): Parent/child selections are independent
  • expandOnSearch (boolean): Auto-expand on search
  • defaultExpandAll (boolean): Expand all nodes by default
  • defaultExpandedKeys (array): Initially expanded node keys
  • emptyText (string): Text when no data
  • maxTagCount (number): Max visible selected tags
  • size (‘sm’ | ‘md’ | ‘lg’): Component size
  • placement (string): Dropdown placement

Events

  • update:modelValue: Selection changed
  • change: Selection changed
  • expand: Node expanded/collapsed
  • search: Search query changed
  • clear: Selection cleared
  • focus: Component focused
  • blur: Component blurred

Slots

  • trigger: Custom trigger content
  • empty: Empty state content
  • prefix: Node prefix content
  • suffix: Node suffix content
  • loading: Loading state content
  • tag: Selected tag content

Usage Examples

  1. Basic Tree Dropdown:
<template>
<UITreeDropdown
v-model="selected"
:options="categories"
placeholder="Select category"
/>
</template>
<script setup>
const selected = ref('')
const categories = [
{
id: '1',
label: 'Category 1',
children: [
{ id: '1-1', label: 'Subcategory 1' },
{ id: '1-2', label: 'Subcategory 2' }
]
}
]
</script>
  1. Advanced Multi-select Tree:
<template>
<UITreeDropdown
v-model="selectedDepartments"
:options="departments"
:multiple="true"
:filterable="true"
:checkStrictly="false"
:maxTagCount="3"
placeholder="Select departments"
class="w-80"
>
<template #trigger="{ selectedLabels, selectedCount }">
<UIButton
type="default"
class="w-full justify-between"
>
<span class="truncate">
{{ selectedCount ? `${selectedCount} selected` : 'Select departments' }}
</span>
<ChevronDownIcon class="w-5 h-5 ml-2 flex-shrink-0" />
</UIButton>
</template>
<template #tag="{ label, onClose }">
<UITag
:closable="true"
@close="onClose"
>
{{ label }}
</UITag>
</template>
<template #prefix="{ node, expanded }">
<FolderIcon
v-if="node.children"
class="w-5 h-5 mr-2"
:class="{ 'text-primary-500': expanded }"
/>
<FileIcon
v-else
class="w-5 h-5 mr-2"
/>
</template>
</UITreeDropdown>
</template>
<script setup>
const selectedDepartments = ref([])
const departments = [
{
id: 'eng',
label: 'Engineering',
children: [
{
id: 'frontend',
label: 'Frontend',
children: [
{ id: 'react', label: 'React Team' },
{ id: 'vue', label: 'Vue Team' }
]
},
{
id: 'backend',
label: 'Backend',
children: [
{ id: 'api', label: 'API Team' },
{ id: 'db', label: 'Database Team' }
]
}
]
},
{
id: 'design',
label: 'Design',
children: [
{ id: 'ui', label: 'UI Team' },
{ id: 'ux', label: 'UX Team' }
]
}
]
</script>
  1. Async Loading Tree:
<template>
<UITreeDropdown
v-model="selectedLocation"
:options="locations"
:loading="loading"
filterable
placeholder="Select location"
>
<template #prefix="{ node, loading }">
<UISpinner
v-if="loading"
size="sm"
class="mr-2"
/>
<template v-else>
<FolderIcon
v-if="node.children"
class="w-5 h-5 mr-2"
/>
<MapPinIcon
v-else
class="w-5 h-5 mr-2"
/>
</template>
</template>
</UITreeDropdown>
</template>
<script setup>
const selectedLocation = ref('')
const locations = ref([])
const loading = ref(false)
const loadLocations = async (parentId?: string) => {
loading.value = true
try {
const response = await fetchLocations(parentId)
if (!parentId) {
locations.value = response
} else {
// Update nested children
updateTreeNodes(locations.value, parentId, response)
}
} finally {
loading.value = false
}
}
onMounted(() => {
loadLocations()
})
</script>

Best Practices

  1. User Experience:

    • Clear hierarchy
    • Intuitive navigation
    • Search functionality
    • Loading states
  2. Performance:

    • Lazy loading
    • Virtual scrolling
    • Efficient updates
    • Memory management
  3. Accessibility:

    • Keyboard navigation
    • ARIA attributes
    • Focus management
    • Screen reader support
  4. Mobile:

    • Touch targets
    • Responsive design
    • Clear feedback
    • Simplified interaction

Common Use Cases

  1. Category Selection:
<template>
<div class="space-y-2">
<label class="text-sm font-medium">
Product Categories
</label>
<UITreeDropdown
v-model="selectedCategories"
:options="categories"
:multiple="true"
:filterable="true"
placeholder="Select categories"
class="w-full"
>
<template #empty>
<div class="p-4 text-center text-gray-500">
No categories found
</div>
</template>
</UITreeDropdown>
<div
v-if="selectedCategories.length"
class="flex flex-wrap gap-2"
>
<UITag
v-for="category in selectedCategoryLabels"
:key="category"
:closable="true"
@close="removeCategory(category)"
>
{{ category }}
</UITag>
</div>
</div>
</template>
  1. Organization Structure:
<template>
<UITreeDropdown
v-model="selectedTeam"
:options="organization"
:filterable="true"
class="w-72"
>
<template #prefix="{ node }">
<div
class="w-6 h-6 rounded-full mr-2 flex items-center justify-center"
:class="{
'bg-primary-100 text-primary-600': node.type === 'department',
'bg-success-100 text-success-600': node.type === 'team'
}"
>
<BuildingIcon
v-if="node.type === 'department'"
class="w-4 h-4"
/>
<UsersIcon
v-else-if="node.type === 'team'"
class="w-4 h-4"
/>
</div>
</template>
<template #suffix="{ node }">
<UIBadge
v-if="node.memberCount"
size="sm"
variant="gray"
>
{{ node.memberCount }}
</UIBadge>
</template>
</UITreeDropdown>
</template>
  1. File Browser:
<template>
<UITreeDropdown
v-model="selectedPath"
:options="fileSystem"
:defaultExpandAll="true"
:filterable="true"
placeholder="Select file location"
>
<template #prefix="{ node }">
<component
:is="getFileIcon(node.type)"
class="w-5 h-5 mr-2"
:class="getFileIconColor(node.type)"
/>
</template>
<template #suffix="{ node }">
<span
v-if="node.size"
class="text-xs text-gray-500"
>
{{ formatFileSize(node.size) }}
</span>
</template>
</UITreeDropdown>
</template>
<script setup>
const getFileIcon = (type: string) => {
switch (type) {
case 'folder': return FolderIcon
case 'image': return ImageIcon
case 'document': return DocumentIcon
default: return FileIcon
}
}
const getFileIconColor = (type: string) => {
switch (type) {
case 'folder': return 'text-primary-500'
case 'image': return 'text-success-500'
case 'document': return 'text-info-500'
default: return 'text-gray-500'
}
}
</script>

Component Composition

The UITreeDropdown component works well with:

  • UIButton for triggers
  • UITag for selected items
  • UIBadge for counts/status
  • UISpinner for loading states
  • UITooltip for additional information
  • UIIcon for visual indicators
  • UIInput for search functionality