Select
Allow users to choose one or more options from a dropdown list.
<script setup lang="ts">
import { ref } from 'vue'
import { HSelect, type SelectOptionBase } from '@holistics/design-system'
const options = [
{ value: 1, label: 'Option 1' },
{ value: 2, label: 'Option 2' },
{ value: 3, label: 'Option 3', disabled: true },
{ value: 4, label: 'Option 4' },
] as const satisfies SelectOptionBase[]
const value = ref<typeof options[number]['value']>()
</script>
<template>
<HSelect
v-model="value"
:options="options"
class="w-80"
/>
</template>Examples
📦 Types of option
There are 7 types of options:
- Default
- Sticky top: same as default, but is always sticky at top
- Sticky bottom: same as default, but is always sticky at bottom
- Group: contains other
childrenoptions - Submenu handle: contains other
childrenoptions in a separate submenu. - Action: same as default, but is used to execute the
actionfunction, and cannot be selected. - Creating (internal,
creatableonly): has akeythat is unique across the entireoptions
<script setup lang="ts">
import { ref } from 'vue'
import { HSelect, type SelectOptionBase, type SelectValue } from '@holistics/design-system'
const options: SelectOptionBase[] = [
{
stickyTop: true, value: 101, label: 'Top Option 1', tooltip: 'Lorem ipsum donor',
},
{
stickyTop: true,
value: 102,
label: 'Top Option 2',
tooltip: 'Some tips at a group',
children: [
{ stickyTop: true, value: '102.1', label: 'Top Option 2.1' },
{
stickyTop: true, value: '102.2', label: 'Inner-Bell', icon: 'bell', action: () => { console.log('Inner-bell logged!') },
},
],
initialExpanded: true,
},
{
stickyTop: true,
value: 103,
label: 'Top Action Option',
icon: 'function',
action: (option) => { alert(`You clicked an option with value: "${option.value}" and label: "${option.label}"`) },
tooltip: { content: '<em>I can be HTML, too!</em>', html: true },
},
{ value: 1, label: 'Option 1', tooltip: 'I can be anywhere!' },
{ value: 2, label: 'Option 2 (with icon)', icon: 'favorite' },
{ value: 3, label: 'Option 3', description: 'Option with description' },
{
value: 4, label: 'Option 4 has a VERY Very very long label', icon: 'data-set', description: 'Option with description & icon, and a very long long long long long long long long long long long description',
},
{
value: 5, label: 'Option 5 (disabled)', disabled: true, tooltip: 'Disabled option only shows tooltip on hovering (not keyboad)!',
},
{
value: 6,
label: 'Option 6',
children: [
{ label: 'Option a.1', value: '6.1', icon: 'hashtag' },
{ label: 'Option b.2', value: '6.2', icon: 'type/string' },
{
label: 'Option x.3',
value: '6.3',
children: [
{ label: 'Option d.3.1', value: '6.3.1' },
{ label: 'Option e.3.2', value: '6.3.2' },
{
label: 'Option x.3.3',
value: '6.3.3',
children: [
{ label: 'Option g.3.3.1', value: '6.3.3.1' },
{ label: 'Option h.3.3.2', value: '6.3.3.2' },
{ label: 'Option i.3.3.3', value: '6.3.3.3' },
{
label: 'Option x.3.3.4',
value: '6.3.3.4',
children: [
{ label: 'Option k.3.3.4.1', value: '6.3.3.4.1' },
{ label: 'Option l.3.3.4.2', value: '6.3.3.4.2' },
{ label: 'Option 6.3.3.4.3', value: '6.3.3.4.3' },
{ label: 'Option m.3.3.4.4', value: '6.3.3.4.4' },
],
},
{ label: 'Option j.3.3.3', value: '6.3.3.5' },
],
},
{ label: 'Option f.3.4', value: '6.3.4' },
],
initialExpanded: true,
},
{ label: 'Option c.4', value: '6.4' },
],
},
{ value: 7, label: 'Option 7', icon: 'user' },
{ value: 8, label: 'Option 8' },
{ value: 9, label: 'Option 9' },
{ value: 10, label: 'Option 10' },
{ value: null, label: 'Option null' },
{ stickyBottom: true, value: 901, label: 'Bottom Option 1' },
{
stickyBottom: true,
value: 902,
label: 'Bottom Option 2',
children: [
{ stickyBottom: true, value: '901.1', label: 'Bottom Option 2.1' },
{ stickyBottom: true, value: '901.2', label: 'Bottom Option 2.2' },
],
},
{
stickyBottom: true,
value: 903,
label: 'Async Refresh (done after 2s)',
icon: 'refresh',
action: async () => {
await new Promise((res) => { setTimeout(res, 2000) })
alert('Done refreshing!')
},
},
]
const value = ref<SelectValue>()
</script>
<template>
<HSelect
v-model="value"
:options="options"
class="w-80"
/>
</template>import type { SelectOptionBase } from '@holistics/design-system'
export const OPTIONS: SelectOptionBase[] = [
{
stickyTop: true,
value: '101',
label: 'Top Option 1',
tooltip: 'Lorem ipsum donor',
},
{
stickyTop: true,
value: '102',
label: 'Top Option 2',
tooltip: 'Some tips at a group',
initialExpanded: true,
children: [
{
stickyTop: true,
value: '102.1',
label: 'Top Option 2.1',
},
{
stickyTop: true,
value: '102.2',
label: 'Inner-Bell',
icon: 'bell',
action: () => { console.log('Inner-bell logged!') },
},
],
},
{
stickyTop: true,
value: '103',
label: 'Top Action Option',
icon: 'function',
action: (option) => { alert(`You clicked an option with value: "${option.value}" and label: "${option.label}"`) },
tooltip: { content: '<em>I can be HTML, too!</em>', html: true },
},
{
value: '1',
label: 'Option 1',
tooltip: 'I can be anywhere!',
},
{
value: '2',
label: 'Option 2 (with icon)',
icon: 'favorite',
},
{
value: '3',
label: 'Option 3',
description: 'Option with description',
},
{
value: '4',
label: 'Option 4 has a VERY Very very long label',
icon: 'data-set',
description: 'Option with description & icon, and a very long long long long long long long long long long long description',
},
{
value: '5',
label: 'Option 5 (disabled)',
disabled: true,
tooltip: 'Disabled option only shows tooltip on hovering (not keyboad)!',
},
{
value: '6',
label: 'Option 6',
children: [
{ label: 'Option a.1', value: '6.1', icon: 'hashtag' },
{ label: 'Option b.2', value: '6.2', icon: 'type/string' },
{
label: 'Option x.3',
value: '6.3',
initialExpanded: true,
children: [
{ label: 'Option d.3.1', value: '6.3.1' },
{ label: 'Option e.3.2', value: '6.3.2' },
{
label: 'Option x.3.3',
value: '6.3.3',
children: [
{ label: 'Option g.3.3.1', value: '6.3.3.1' },
{ label: 'Option h.3.3.2', value: '6.3.3.2' },
{ label: 'Option i.3.3.3', value: '6.3.3.3' },
{
label: 'Option x.3.3.4',
value: '6.3.3.4',
children: [
{ label: 'Option k.3.3.4.1', value: '6.3.3.4.1' },
{ label: 'Option l.3.3.4.2', value: '6.3.3.4.2' },
{ label: 'Option 6.3.3.4.3', value: '6.3.3.4.3' },
{ label: 'Option m.3.3.4.4', value: '6.3.3.4.4' },
],
},
{ label: 'Option j.3.3.3', value: '6.3.3.5' },
],
},
{ label: 'Option f.3.4', value: '6.3.4' },
],
},
{ label: 'Option c.4', value: '6.4' },
],
},
{
value: '7',
label: 'Option 7',
icon: 'user',
},
{
value: '8',
label: 'Option 8',
},
{
value: '9',
label: 'Option 9',
},
{
value: '10',
label: 'Option 10',
},
{
value: null,
label: 'Option null',
},
{
stickyBottom: true,
value: '901',
label: 'Bottom Option 1',
},
{
stickyBottom: true,
value: '902',
label: 'Bottom Option 2',
children: [
{
stickyBottom: true,
value: '901.1',
label: 'Bottom Option 2.1',
},
{
stickyBottom: true,
value: '901.2',
label: 'Bottom Option 2.2',
},
],
},
{
stickyBottom: true,
value: '903',
label: 'Async Refresh (done after 2s)',
icon: 'refresh',
action: async () => {
await new Promise((res) => { setTimeout(res, 2000) })
alert('Done refreshing!')
},
},
]
export const ADVANCED_SEARCH_OPTIONS = [
{
value: 'users',
label: 'Users',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'countries',
label: 'Countries',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'cities',
label: 'Cities',
initialExpanded: true,
icon: 'data-model',
children: [
{ value: 'beautiful-stories', label: 'Beautiful Stories' },
{ value: 'large-libraries', label: 'Large Libraries' },
],
},
{
value: 'families',
label: 'Families',
initialExpanded: true,
icon: 'data-model',
children: [
{ value: 'with-stories', label: 'With Stories' },
{ value: 'like-parties', label: 'Like Parties' },
],
},
],
},
],
},
{
value: 'A',
label: 'City Dwellers',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'B',
label: 'Urban City',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'C',
label: 'Analyze Data',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'D',
label: 'Analytical Reports',
},
],
},
{
value: 'E',
label: 'Childhood Memories',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'F',
label: "Children's Games",
},
],
},
],
},
{
value: 'G',
label: 'Tooth Brushing',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'H',
label: 'Teeth Cleaning',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'I',
label: 'Mouse Clicks',
},
{
value: 'J',
label: 'Mice Running',
},
],
},
{
value: 'K',
label: 'Foot Prints',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'L',
label: 'Feet Walking',
},
],
},
],
},
],
},
{
value: 'M',
label: 'Person Interview',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'N',
label: 'People Skills',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'O',
label: 'Woman Power',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'P',
label: 'Women Rights',
},
],
},
{
value: 'Q',
label: 'Leaf Fall',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'R',
label: 'Leaves Turning',
},
],
},
],
},
{
value: 'S',
label: 'Knife Sharp',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'T',
label: 'Knives Set',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'U',
label: 'Create Content',
},
{
value: 'V',
label: 'Creative Writing',
},
],
},
{
value: 'W',
label: 'Develop Software',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'X',
label: 'Development Tools',
},
],
},
],
},
],
},
{
value: 'Y',
label: 'Organize Events',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'Z',
label: 'Organizational Skills',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AA',
label: 'Apply Pressure',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AB',
label: 'Application Forms',
},
],
},
{
value: 'AC',
label: 'Multiply Numbers',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AD',
label: 'Multiplication Tables',
},
],
},
],
},
{
value: 'AE',
label: 'Identify Problems',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AF',
label: 'Identification Cards',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AG',
label: 'Rectify Errors',
},
{
value: 'AH',
label: 'Rectification Process',
},
],
},
{
value: 'AI',
label: 'Classify Items',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AJ',
label: 'Classification System',
},
],
},
],
},
],
},
{
value: 'AK',
label: 'Beautiful Flowers',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'AL',
label: 'Beauty Products',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AM',
label: 'Quick Action',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AN',
label: 'Quickly Done',
},
],
},
{
value: 'AO',
label: 'Heavy Lifting',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AP',
label: 'Heavily Loaded',
},
],
},
],
},
{
value: 'AQ',
label: 'Simple Solution',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AR',
label: 'Simply Put',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AS',
label: 'Possible outcome',
},
{
value: 'AT',
label: 'Possibly true',
},
],
},
{
value: 'AU',
label: 'Electric cars',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AV',
label: 'electricity bills',
},
],
},
],
},
],
},
{
value: 'AW',
label: 'Dramatic plays',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'AX',
label: 'dramatically changed',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AY',
label: 'Historic buildings',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AZ',
label: 'historically accurate',
},
],
},
],
},
],
},
] as const satisfies SelectOptionBase[]By default, group options are not selectable. To enable this, set groupSelectable: true. Enabling this changes the following behaviors:
- To select/deselect a group option: Click / Enter
- To expand a group option: Click on chevron icon
>/ Double-click / Shift + Enter
<script setup lang="ts">
import { ref } from 'vue'
import { HSelect, type SelectValue } from '@holistics/design-system'
import { OPTIONS } from './options'
const value = ref<SelectValue>()
</script>
<template>
<HSelect
v-model="value"
:options="OPTIONS"
group-selectable
class="w-80"
/>
</template>import type { SelectOptionBase } from '@holistics/design-system'
export const OPTIONS: SelectOptionBase[] = [
{
stickyTop: true,
value: '101',
label: 'Top Option 1',
tooltip: 'Lorem ipsum donor',
},
{
stickyTop: true,
value: '102',
label: 'Top Option 2',
tooltip: 'Some tips at a group',
initialExpanded: true,
children: [
{
stickyTop: true,
value: '102.1',
label: 'Top Option 2.1',
},
{
stickyTop: true,
value: '102.2',
label: 'Inner-Bell',
icon: 'bell',
action: () => { console.log('Inner-bell logged!') },
},
],
},
{
stickyTop: true,
value: '103',
label: 'Top Action Option',
icon: 'function',
action: (option) => { alert(`You clicked an option with value: "${option.value}" and label: "${option.label}"`) },
tooltip: { content: '<em>I can be HTML, too!</em>', html: true },
},
{
value: '1',
label: 'Option 1',
tooltip: 'I can be anywhere!',
},
{
value: '2',
label: 'Option 2 (with icon)',
icon: 'favorite',
},
{
value: '3',
label: 'Option 3',
description: 'Option with description',
},
{
value: '4',
label: 'Option 4 has a VERY Very very long label',
icon: 'data-set',
description: 'Option with description & icon, and a very long long long long long long long long long long long description',
},
{
value: '5',
label: 'Option 5 (disabled)',
disabled: true,
tooltip: 'Disabled option only shows tooltip on hovering (not keyboad)!',
},
{
value: '6',
label: 'Option 6',
children: [
{ label: 'Option a.1', value: '6.1', icon: 'hashtag' },
{ label: 'Option b.2', value: '6.2', icon: 'type/string' },
{
label: 'Option x.3',
value: '6.3',
initialExpanded: true,
children: [
{ label: 'Option d.3.1', value: '6.3.1' },
{ label: 'Option e.3.2', value: '6.3.2' },
{
label: 'Option x.3.3',
value: '6.3.3',
children: [
{ label: 'Option g.3.3.1', value: '6.3.3.1' },
{ label: 'Option h.3.3.2', value: '6.3.3.2' },
{ label: 'Option i.3.3.3', value: '6.3.3.3' },
{
label: 'Option x.3.3.4',
value: '6.3.3.4',
children: [
{ label: 'Option k.3.3.4.1', value: '6.3.3.4.1' },
{ label: 'Option l.3.3.4.2', value: '6.3.3.4.2' },
{ label: 'Option 6.3.3.4.3', value: '6.3.3.4.3' },
{ label: 'Option m.3.3.4.4', value: '6.3.3.4.4' },
],
},
{ label: 'Option j.3.3.3', value: '6.3.3.5' },
],
},
{ label: 'Option f.3.4', value: '6.3.4' },
],
},
{ label: 'Option c.4', value: '6.4' },
],
},
{
value: '7',
label: 'Option 7',
icon: 'user',
},
{
value: '8',
label: 'Option 8',
},
{
value: '9',
label: 'Option 9',
},
{
value: '10',
label: 'Option 10',
},
{
value: null,
label: 'Option null',
},
{
stickyBottom: true,
value: '901',
label: 'Bottom Option 1',
},
{
stickyBottom: true,
value: '902',
label: 'Bottom Option 2',
children: [
{
stickyBottom: true,
value: '901.1',
label: 'Bottom Option 2.1',
},
{
stickyBottom: true,
value: '901.2',
label: 'Bottom Option 2.2',
},
],
},
{
stickyBottom: true,
value: '903',
label: 'Async Refresh (done after 2s)',
icon: 'refresh',
action: async () => {
await new Promise((res) => { setTimeout(res, 2000) })
alert('Done refreshing!')
},
},
]
export const ADVANCED_SEARCH_OPTIONS = [
{
value: 'users',
label: 'Users',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'countries',
label: 'Countries',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'cities',
label: 'Cities',
initialExpanded: true,
icon: 'data-model',
children: [
{ value: 'beautiful-stories', label: 'Beautiful Stories' },
{ value: 'large-libraries', label: 'Large Libraries' },
],
},
{
value: 'families',
label: 'Families',
initialExpanded: true,
icon: 'data-model',
children: [
{ value: 'with-stories', label: 'With Stories' },
{ value: 'like-parties', label: 'Like Parties' },
],
},
],
},
],
},
{
value: 'A',
label: 'City Dwellers',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'B',
label: 'Urban City',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'C',
label: 'Analyze Data',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'D',
label: 'Analytical Reports',
},
],
},
{
value: 'E',
label: 'Childhood Memories',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'F',
label: "Children's Games",
},
],
},
],
},
{
value: 'G',
label: 'Tooth Brushing',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'H',
label: 'Teeth Cleaning',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'I',
label: 'Mouse Clicks',
},
{
value: 'J',
label: 'Mice Running',
},
],
},
{
value: 'K',
label: 'Foot Prints',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'L',
label: 'Feet Walking',
},
],
},
],
},
],
},
{
value: 'M',
label: 'Person Interview',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'N',
label: 'People Skills',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'O',
label: 'Woman Power',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'P',
label: 'Women Rights',
},
],
},
{
value: 'Q',
label: 'Leaf Fall',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'R',
label: 'Leaves Turning',
},
],
},
],
},
{
value: 'S',
label: 'Knife Sharp',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'T',
label: 'Knives Set',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'U',
label: 'Create Content',
},
{
value: 'V',
label: 'Creative Writing',
},
],
},
{
value: 'W',
label: 'Develop Software',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'X',
label: 'Development Tools',
},
],
},
],
},
],
},
{
value: 'Y',
label: 'Organize Events',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'Z',
label: 'Organizational Skills',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AA',
label: 'Apply Pressure',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AB',
label: 'Application Forms',
},
],
},
{
value: 'AC',
label: 'Multiply Numbers',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AD',
label: 'Multiplication Tables',
},
],
},
],
},
{
value: 'AE',
label: 'Identify Problems',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AF',
label: 'Identification Cards',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AG',
label: 'Rectify Errors',
},
{
value: 'AH',
label: 'Rectification Process',
},
],
},
{
value: 'AI',
label: 'Classify Items',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AJ',
label: 'Classification System',
},
],
},
],
},
],
},
{
value: 'AK',
label: 'Beautiful Flowers',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'AL',
label: 'Beauty Products',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AM',
label: 'Quick Action',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AN',
label: 'Quickly Done',
},
],
},
{
value: 'AO',
label: 'Heavy Lifting',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AP',
label: 'Heavily Loaded',
},
],
},
],
},
{
value: 'AQ',
label: 'Simple Solution',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AR',
label: 'Simply Put',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AS',
label: 'Possible outcome',
},
{
value: 'AT',
label: 'Possibly true',
},
],
},
{
value: 'AU',
label: 'Electric cars',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AV',
label: 'electricity bills',
},
],
},
],
},
],
},
{
value: 'AW',
label: 'Dramatic plays',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'AX',
label: 'dramatically changed',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AY',
label: 'Historic buildings',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AZ',
label: 'historically accurate',
},
],
},
],
},
],
},
] as const satisfies SelectOptionBase[]📋 Multiple Select
Allow users to select more than one option.
<script setup lang="ts">
import { ref } from 'vue'
import { HSelect } from '@holistics/design-system'
import { options } from './options'
const value = ref<typeof options[number]['value']>()
</script>
<template>
<HSelect
v-model="value"
:options="options"
multiple
placeholder="Multiple..."
class="w-80"
/>
</template>import type { SelectOptionBase } from '@holistics/design-system'
export const options: SelectOptionBase[] = [
{ value: 1, label: 'Option 1' },
{ value: 2, label: 'Option 2' },
{ value: 3, label: 'Option 3', disabled: true },
{ value: 4, label: 'Option 4 (longer)' },
{ value: 5, label: 'Option 5 (even longgggeeerrr)' },
{
value: 6,
label: 'Option 6',
children: [
{ value: '6.1', label: 'Child of Option 6' },
{ value: '6.2', label: 'Another child of Option 6', disabled: true },
{ value: '6.3', label: 'Last child of Option 6' },
],
},
{ value: 7, label: 'Option 7' },
{ value: 8, label: '8' },
{ value: 9, label: 'Option 9', description: 'Some explanation for Option 9...' },
{ value: null, label: 'Option with null value' },
]With counter
By default, Select always adjusts its height to fit all selected options. If you want to force a consistent height, set multiple: 'counter'. This will display a counter of remaining selected options if the list is too long and can't fit in 1 row (similar to a TagList).
🔍 multiple: 'counter' won't work with filterable: true
This is because filterable requires an <input> placed after the selected options. But multiple: 'counter' forces the Select to have a consistent height, so the <input> might not have the required space to type any text.
<script setup lang="ts">
import { ref } from 'vue'
import { HSelect } from '@holistics/design-system'
import { options } from './options'
const value = ref<typeof options[number]['value']>()
</script>
<template>
<HSelect
v-model="value"
:options="options"
multiple="counter"
placeholder="Multiple (with counter)..."
class="w-80 overflow-hidden"
/>
</template>import type { SelectOptionBase } from '@holistics/design-system'
export const options: SelectOptionBase[] = [
{ value: 1, label: 'Option 1' },
{ value: 2, label: 'Option 2' },
{ value: 3, label: 'Option 3', disabled: true },
{ value: 4, label: 'Option 4 (longer)' },
{ value: 5, label: 'Option 5 (even longgggeeerrr)' },
{
value: 6,
label: 'Option 6',
children: [
{ value: '6.1', label: 'Child of Option 6' },
{ value: '6.2', label: 'Another child of Option 6', disabled: true },
{ value: '6.3', label: 'Last child of Option 6' },
],
},
{ value: 7, label: 'Option 7' },
{ value: 8, label: '8' },
{ value: 9, label: 'Option 9', description: 'Some explanation for Option 9...' },
{ value: null, label: 'Option with null value' },
]✅ Checkbox
Enable checkboxes in your select for use cases like file selection, permission management, or multi-item actions by setting checkable: true. The checked states are managed through two v-model bindings: the default modelValue and indeterminateKeys.
Implicit behaviors when checkbox is enabled
Selectbecomes multiple select.- Group options and Submenu handles are selectable now, but the actual selected options can be control via
checkStrategy(see below). - Action options cannot be checked.
<template>
<HSelect
v-model="selectedKeys"
v-model:indeterminate-keys="indeterminateKeys"
:options="options"
checkable
/>
</template>Cascading behavior
To enable hierarchical cascading, set checkCascade: true. With this enabled:
- A parent option is checked only when all of its children are checked.
- A parent option is indeterminate when only some of its children are checked.
- A parent option is unchecked when none of its children are checked.
Check strategies
When cascading is enabled, Select provides 3 check strategies that control which options appear in modelValue:
all: Collects all checked options.parent: Collects only the highest-level checked options.child: Collects only the lowest-level checked options (leaf options).
Example
Given a select where Folder A contains File 1 (checked) and File 2 (checked):
☑ Folder A <- parent is auto-checked because all children are checked
☑ File 1 <- child
☑ File 2 <- childThe modelValue will be:
all:['Folder A', 'File 1', 'File 2']parent:['Folder A']child:['File 1', 'File 2']
<script setup lang="ts">
import { ref, useTemplateRef } from 'vue'
import type { CheckStrategy, Key } from 'treemate'
import {
HSelect,
HRadio,
HRadioGroup,
HButton,
HCheckbox,
} from '@holistics/design-system'
import { OPTIONS } from './options'
const select = useTemplateRef('selectRef')
const selectedKeys = ref<Key[]>([])
const indeterminateKeys = ref<Key[]>([])
const checkCascade = ref(true)
const CHECK_STRATEGIES = ['all', 'child', 'parent'] as const satisfies CheckStrategy[]
const checkStrategy = ref<CheckStrategy>('all')
function manualCheck () {
select.value?.setCheckedStates(['102', '902', '6.2', '6.3.2'])
}
function clear () {
select.value?.setCheckedStates([])
}
</script>
<template>
<div class="space-y-4 text-xs">
<div class="flex flex-wrap gap-x-8 gap-y-4">
<HCheckbox
v-model="checkCascade"
label="Cascade"
/>
<HRadioGroup
v-model="checkStrategy"
class="flex gap-1"
>
<div>
Strategy
</div>
<HRadio
v-for="strategy in CHECK_STRATEGIES"
:key="strategy"
:value="strategy"
>
{{ strategy }}
</HRadio>
</HRadioGroup>
</div>
<div class="flex gap-1">
<HButton
type="primary-highlight"
label="Manual check"
@click="manualCheck"
/>
<HButton
type="primary-danger"
label="Clear"
@click="clear"
/>
</div>
<div class="flex flex-col items-center justify-center gap-x-4 gap-y-2 md:flex-row lg:flex-col 2xl:flex-row">
<HSelect
ref="selectRef"
v-model="selectedKeys"
v-model:indeterminate-keys="indeterminateKeys"
:options="OPTIONS"
checkable
:check-cascade
:check-strategy
class="w-80"
/>
</div>
<div class="flex gap-1">
<div class="flex-shrink-0">
Selected Keys
</div>
<code class="text-blue-700">{{ selectedKeys }}</code>
</div>
<div class="flex gap-1">
<div class="flex-shrink-0">
Indeterminate Keys
</div>
<code class="text-blue-700">{{ indeterminateKeys }}</code>
</div>
</div>
</template>import type { SelectOptionBase } from '@holistics/design-system'
export const OPTIONS: SelectOptionBase[] = [
{
stickyTop: true,
value: '101',
label: 'Top Option 1',
tooltip: 'Lorem ipsum donor',
},
{
stickyTop: true,
value: '102',
label: 'Top Option 2',
tooltip: 'Some tips at a group',
initialExpanded: true,
children: [
{
stickyTop: true,
value: '102.1',
label: 'Top Option 2.1',
},
{
stickyTop: true,
value: '102.2',
label: 'Inner-Bell',
icon: 'bell',
action: () => { console.log('Inner-bell logged!') },
},
],
},
{
stickyTop: true,
value: '103',
label: 'Top Action Option',
icon: 'function',
action: (option) => { alert(`You clicked an option with value: "${option.value}" and label: "${option.label}"`) },
tooltip: { content: '<em>I can be HTML, too!</em>', html: true },
},
{
value: '1',
label: 'Option 1',
tooltip: 'I can be anywhere!',
},
{
value: '2',
label: 'Option 2 (with icon)',
icon: 'favorite',
},
{
value: '3',
label: 'Option 3',
description: 'Option with description',
},
{
value: '4',
label: 'Option 4 has a VERY Very very long label',
icon: 'data-set',
description: 'Option with description & icon, and a very long long long long long long long long long long long description',
},
{
value: '5',
label: 'Option 5 (disabled)',
disabled: true,
tooltip: 'Disabled option only shows tooltip on hovering (not keyboad)!',
},
{
value: '6',
label: 'Option 6',
children: [
{ label: 'Option a.1', value: '6.1', icon: 'hashtag' },
{ label: 'Option b.2', value: '6.2', icon: 'type/string' },
{
label: 'Option x.3',
value: '6.3',
initialExpanded: true,
children: [
{ label: 'Option d.3.1', value: '6.3.1' },
{ label: 'Option e.3.2', value: '6.3.2' },
{
label: 'Option x.3.3',
value: '6.3.3',
children: [
{ label: 'Option g.3.3.1', value: '6.3.3.1' },
{ label: 'Option h.3.3.2', value: '6.3.3.2' },
{ label: 'Option i.3.3.3', value: '6.3.3.3' },
{
label: 'Option x.3.3.4',
value: '6.3.3.4',
children: [
{ label: 'Option k.3.3.4.1', value: '6.3.3.4.1' },
{ label: 'Option l.3.3.4.2', value: '6.3.3.4.2' },
{ label: 'Option 6.3.3.4.3', value: '6.3.3.4.3' },
{ label: 'Option m.3.3.4.4', value: '6.3.3.4.4' },
],
},
{ label: 'Option j.3.3.3', value: '6.3.3.5' },
],
},
{ label: 'Option f.3.4', value: '6.3.4' },
],
},
{ label: 'Option c.4', value: '6.4' },
],
},
{
value: '7',
label: 'Option 7',
icon: 'user',
},
{
value: '8',
label: 'Option 8',
},
{
value: '9',
label: 'Option 9',
},
{
value: '10',
label: 'Option 10',
},
{
value: null,
label: 'Option null',
},
{
stickyBottom: true,
value: '901',
label: 'Bottom Option 1',
},
{
stickyBottom: true,
value: '902',
label: 'Bottom Option 2',
children: [
{
stickyBottom: true,
value: '901.1',
label: 'Bottom Option 2.1',
},
{
stickyBottom: true,
value: '901.2',
label: 'Bottom Option 2.2',
},
],
},
{
stickyBottom: true,
value: '903',
label: 'Async Refresh (done after 2s)',
icon: 'refresh',
action: async () => {
await new Promise((res) => { setTimeout(res, 2000) })
alert('Done refreshing!')
},
},
]
export const ADVANCED_SEARCH_OPTIONS = [
{
value: 'users',
label: 'Users',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'countries',
label: 'Countries',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'cities',
label: 'Cities',
initialExpanded: true,
icon: 'data-model',
children: [
{ value: 'beautiful-stories', label: 'Beautiful Stories' },
{ value: 'large-libraries', label: 'Large Libraries' },
],
},
{
value: 'families',
label: 'Families',
initialExpanded: true,
icon: 'data-model',
children: [
{ value: 'with-stories', label: 'With Stories' },
{ value: 'like-parties', label: 'Like Parties' },
],
},
],
},
],
},
{
value: 'A',
label: 'City Dwellers',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'B',
label: 'Urban City',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'C',
label: 'Analyze Data',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'D',
label: 'Analytical Reports',
},
],
},
{
value: 'E',
label: 'Childhood Memories',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'F',
label: "Children's Games",
},
],
},
],
},
{
value: 'G',
label: 'Tooth Brushing',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'H',
label: 'Teeth Cleaning',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'I',
label: 'Mouse Clicks',
},
{
value: 'J',
label: 'Mice Running',
},
],
},
{
value: 'K',
label: 'Foot Prints',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'L',
label: 'Feet Walking',
},
],
},
],
},
],
},
{
value: 'M',
label: 'Person Interview',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'N',
label: 'People Skills',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'O',
label: 'Woman Power',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'P',
label: 'Women Rights',
},
],
},
{
value: 'Q',
label: 'Leaf Fall',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'R',
label: 'Leaves Turning',
},
],
},
],
},
{
value: 'S',
label: 'Knife Sharp',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'T',
label: 'Knives Set',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'U',
label: 'Create Content',
},
{
value: 'V',
label: 'Creative Writing',
},
],
},
{
value: 'W',
label: 'Develop Software',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'X',
label: 'Development Tools',
},
],
},
],
},
],
},
{
value: 'Y',
label: 'Organize Events',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'Z',
label: 'Organizational Skills',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AA',
label: 'Apply Pressure',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AB',
label: 'Application Forms',
},
],
},
{
value: 'AC',
label: 'Multiply Numbers',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AD',
label: 'Multiplication Tables',
},
],
},
],
},
{
value: 'AE',
label: 'Identify Problems',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AF',
label: 'Identification Cards',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AG',
label: 'Rectify Errors',
},
{
value: 'AH',
label: 'Rectification Process',
},
],
},
{
value: 'AI',
label: 'Classify Items',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AJ',
label: 'Classification System',
},
],
},
],
},
],
},
{
value: 'AK',
label: 'Beautiful Flowers',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'AL',
label: 'Beauty Products',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AM',
label: 'Quick Action',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AN',
label: 'Quickly Done',
},
],
},
{
value: 'AO',
label: 'Heavy Lifting',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AP',
label: 'Heavily Loaded',
},
],
},
],
},
{
value: 'AQ',
label: 'Simple Solution',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AR',
label: 'Simply Put',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AS',
label: 'Possible outcome',
},
{
value: 'AT',
label: 'Possibly true',
},
],
},
{
value: 'AU',
label: 'Electric cars',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AV',
label: 'electricity bills',
},
],
},
],
},
],
},
{
value: 'AW',
label: 'Dramatic plays',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'AX',
label: 'dramatically changed',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AY',
label: 'Historic buildings',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AZ',
label: 'historically accurate',
},
],
},
],
},
],
},
] as const satisfies SelectOptionBase[]❌ Select with checkbox doesn't support options with null or boolean value
This is because the algorithm used to sync checked states doesn't support null/boolean values. If your use-case absolutely needs null/boolean values, try converting them to string 'null' | 'true' | 'false' first, then deserializing it at @update:modelValue. You will encounter some rough cases, but it's the current limitation...
⌨️ Keyboard Interactions
Select automatically handles keys in the following table when it receives focus.
| Key | Description | filterable supported? |
|---|---|---|
| ArrowDown | When focus is on an option, moves focus to the next one. | ✅ |
| ArrowUp | When focus is on an option, moves focus to the previous one. | ✅ |
| PageDown | When focus is on an option, moves focus to the next 10 options. | ✅ |
| PageUp | When focus is on an option, moves focus to the previous 10 options. | ✅ |
| Home | Moves focus to the first option. | ❌ |
| End | Moves focus to the last option. | ❌ |
| Escape | Close the currently opened (sub)menu, and clear the search text. | ✅ |
| Enter | When focus is on a selectable option, select it. Otherwise, if the option is non-leaf, expand it. If the option has action set, execute that function. | ✅ |
| Space | When focus is on an option, toggles check on it. | ❌ |
| ArrowRight | When focus is on a non-leaf option: if it's collapsed, expand the option; otherwise, moves focus to the first child. | ❌ |
| ArrowLeft | When focus is on a non-leaf option & it's expanded, collapse the option. Otherwise, moves focus to its parent. | ❌ |
🔍 Filterable
Select provides powerful search-and-filter capabilities that automatically expand matching options, scroll to the first match, and focus it for easy keyboard interactions.
Simply set filterable: true to enable substring matching. By default, Select searches against each option's label.
<template>
<HSelect
:options="options"
filterable
/>
</template>Matched groups showing their direct children
By default, Select only shows matched options. If a group option is matched but none of its descendants are, none of the descendants are shown. To show all direct children of the matched group option, set filterIncludeDirectChildren: true. Note that children at deeper levels still won't be shown, hence the term: direct.
<template>
<HSelect
:options="options"
filterable
filter-include-direct-children
/>
</template>Sticky options
Sticky options are always visible regardless of the search text. If you need to filter them based on the search text as well, set filterIncludeStickyOptions: true.
<template>
<HSelect
:options="options"
filterable
filter-include-sticky-options
/>
</template>Advanced: Stemming with hierarchical search
What is stemming?
Stemming reduces words to their root form, allowing fuzzy matches. For example:
- Searching "run" matches "running", "runner", "runs"
- Searching "develop" matches "developer", "development", "developing"
This creates a more forgiving search experience, especially useful for selects with many options where users might not know the exact terminology.
When stemming is enabled, the search also becomes "hierarchical":
- The search text is split into tokens.
- An option is matched only when it matches at least 1 search token.
- Remaining tokens are matched against the option's ancestors to narrow down results.
<template>
<HSelect
:options="options"
filterable
filter-advanced-search
/>
</template>Select will automatically generate a stem index from the passed options to improve search performance.
What is a stem index?
A stem index is an inverted index that maps word stems to their matching options. It improves search performance for large selects by avoiding the need to tokenize and stem all options on every search.
The index usually stores metadata about each match (like the original word forms and their positions) so that search results can be highlighted in the UI with the exact terms users searched for, not just the stems.
<script setup lang="ts">
import { ref } from 'vue'
import type { CheckStrategy, Key } from 'treemate'
import {
HSelect,
HCheckbox,
HRadio,
HRadioGroup,
} from '@holistics/design-system'
import { OPTIONS } from './options'
const selectedKeys = ref<Key[]>([])
const indeterminateKeys = ref<Key[]>([])
const checkable = ref(false)
const checkCascade = ref(true)
const CHECK_STRATEGIES = ['all', 'child', 'parent'] as const satisfies CheckStrategy[]
const checkStrategy = ref<CheckStrategy>('all')
const filterIncludeDirectChildren = ref(false)
const filterIncludeStickyOptions = ref(false)
const filterAdvancedSearch = ref(false)
</script>
<template>
<div class="space-y-4 text-xs">
<div class="flex flex-wrap gap-x-8 gap-y-4">
<HCheckbox
v-model="filterIncludeDirectChildren"
label="Include direct children"
/>
<HCheckbox
v-model="filterIncludeStickyOptions"
label="Include sticky options"
/>
<HCheckbox
v-model="filterAdvancedSearch"
label="Stemming with Hierarchical"
/>
</div>
<div class="flex flex-wrap gap-x-8 gap-y-4">
<HCheckbox
v-model="checkable"
label="Checkable"
/>
<HCheckbox
v-model="checkCascade"
label="Check Cascade"
:disabled="!checkable"
/>
<HRadioGroup
v-model="checkStrategy"
class="flex gap-1"
>
<div>
Check Strategy
</div>
<HRadio
v-for="strategy in CHECK_STRATEGIES"
:key="strategy"
:value="strategy"
:disabled="!checkable"
>
{{ strategy }}
</HRadio>
</HRadioGroup>
</div>
<div class="flex flex-col items-center justify-center gap-x-4 gap-y-2 md:flex-row lg:flex-col 2xl:flex-row">
<HSelect
v-model="selectedKeys"
v-model:indeterminate-keys="indeterminateKeys"
:options="OPTIONS"
:checkable
:check-cascade
:check-strategy
filterable
:filter-include-direct-children
:filter-include-sticky-options
:filter-advanced-search
class="w-80"
/>
<HSelect
v-model="selectedKeys"
v-model:indeterminate-keys="indeterminateKeys"
:options="OPTIONS"
multiple
:checkable
:check-cascade
:check-strategy
filterable
:filter-include-direct-children
:filter-include-sticky-options
:filter-advanced-search
placeholder="Select multiple..."
class="w-80"
/>
</div>
<div class="flex gap-1">
<div class="flex-shrink-0">
Selected Keys
</div>
<code class="text-blue-700">{{ selectedKeys }}</code>
</div>
<div class="flex gap-1">
<div class="flex-shrink-0">
Indeterminate Keys
</div>
<code class="text-blue-700">{{ indeterminateKeys }}</code>
</div>
</div>
</template>import type { SelectOptionBase } from '@holistics/design-system'
export const OPTIONS: SelectOptionBase[] = [
{
stickyTop: true,
value: '101',
label: 'Top Option 1',
tooltip: 'Lorem ipsum donor',
},
{
stickyTop: true,
value: '102',
label: 'Top Option 2',
tooltip: 'Some tips at a group',
initialExpanded: true,
children: [
{
stickyTop: true,
value: '102.1',
label: 'Top Option 2.1',
},
{
stickyTop: true,
value: '102.2',
label: 'Inner-Bell',
icon: 'bell',
action: () => { console.log('Inner-bell logged!') },
},
],
},
{
stickyTop: true,
value: '103',
label: 'Top Action Option',
icon: 'function',
action: (option) => { alert(`You clicked an option with value: "${option.value}" and label: "${option.label}"`) },
tooltip: { content: '<em>I can be HTML, too!</em>', html: true },
},
{
value: '1',
label: 'Option 1',
tooltip: 'I can be anywhere!',
},
{
value: '2',
label: 'Option 2 (with icon)',
icon: 'favorite',
},
{
value: '3',
label: 'Option 3',
description: 'Option with description',
},
{
value: '4',
label: 'Option 4 has a VERY Very very long label',
icon: 'data-set',
description: 'Option with description & icon, and a very long long long long long long long long long long long description',
},
{
value: '5',
label: 'Option 5 (disabled)',
disabled: true,
tooltip: 'Disabled option only shows tooltip on hovering (not keyboad)!',
},
{
value: '6',
label: 'Option 6',
children: [
{ label: 'Option a.1', value: '6.1', icon: 'hashtag' },
{ label: 'Option b.2', value: '6.2', icon: 'type/string' },
{
label: 'Option x.3',
value: '6.3',
initialExpanded: true,
children: [
{ label: 'Option d.3.1', value: '6.3.1' },
{ label: 'Option e.3.2', value: '6.3.2' },
{
label: 'Option x.3.3',
value: '6.3.3',
children: [
{ label: 'Option g.3.3.1', value: '6.3.3.1' },
{ label: 'Option h.3.3.2', value: '6.3.3.2' },
{ label: 'Option i.3.3.3', value: '6.3.3.3' },
{
label: 'Option x.3.3.4',
value: '6.3.3.4',
children: [
{ label: 'Option k.3.3.4.1', value: '6.3.3.4.1' },
{ label: 'Option l.3.3.4.2', value: '6.3.3.4.2' },
{ label: 'Option 6.3.3.4.3', value: '6.3.3.4.3' },
{ label: 'Option m.3.3.4.4', value: '6.3.3.4.4' },
],
},
{ label: 'Option j.3.3.3', value: '6.3.3.5' },
],
},
{ label: 'Option f.3.4', value: '6.3.4' },
],
},
{ label: 'Option c.4', value: '6.4' },
],
},
{
value: '7',
label: 'Option 7',
icon: 'user',
},
{
value: '8',
label: 'Option 8',
},
{
value: '9',
label: 'Option 9',
},
{
value: '10',
label: 'Option 10',
},
{
value: null,
label: 'Option null',
},
{
stickyBottom: true,
value: '901',
label: 'Bottom Option 1',
},
{
stickyBottom: true,
value: '902',
label: 'Bottom Option 2',
children: [
{
stickyBottom: true,
value: '901.1',
label: 'Bottom Option 2.1',
},
{
stickyBottom: true,
value: '901.2',
label: 'Bottom Option 2.2',
},
],
},
{
stickyBottom: true,
value: '903',
label: 'Async Refresh (done after 2s)',
icon: 'refresh',
action: async () => {
await new Promise((res) => { setTimeout(res, 2000) })
alert('Done refreshing!')
},
},
]
export const ADVANCED_SEARCH_OPTIONS = [
{
value: 'users',
label: 'Users',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'countries',
label: 'Countries',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'cities',
label: 'Cities',
initialExpanded: true,
icon: 'data-model',
children: [
{ value: 'beautiful-stories', label: 'Beautiful Stories' },
{ value: 'large-libraries', label: 'Large Libraries' },
],
},
{
value: 'families',
label: 'Families',
initialExpanded: true,
icon: 'data-model',
children: [
{ value: 'with-stories', label: 'With Stories' },
{ value: 'like-parties', label: 'Like Parties' },
],
},
],
},
],
},
{
value: 'A',
label: 'City Dwellers',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'B',
label: 'Urban City',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'C',
label: 'Analyze Data',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'D',
label: 'Analytical Reports',
},
],
},
{
value: 'E',
label: 'Childhood Memories',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'F',
label: "Children's Games",
},
],
},
],
},
{
value: 'G',
label: 'Tooth Brushing',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'H',
label: 'Teeth Cleaning',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'I',
label: 'Mouse Clicks',
},
{
value: 'J',
label: 'Mice Running',
},
],
},
{
value: 'K',
label: 'Foot Prints',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'L',
label: 'Feet Walking',
},
],
},
],
},
],
},
{
value: 'M',
label: 'Person Interview',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'N',
label: 'People Skills',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'O',
label: 'Woman Power',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'P',
label: 'Women Rights',
},
],
},
{
value: 'Q',
label: 'Leaf Fall',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'R',
label: 'Leaves Turning',
},
],
},
],
},
{
value: 'S',
label: 'Knife Sharp',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'T',
label: 'Knives Set',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'U',
label: 'Create Content',
},
{
value: 'V',
label: 'Creative Writing',
},
],
},
{
value: 'W',
label: 'Develop Software',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'X',
label: 'Development Tools',
},
],
},
],
},
],
},
{
value: 'Y',
label: 'Organize Events',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'Z',
label: 'Organizational Skills',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AA',
label: 'Apply Pressure',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AB',
label: 'Application Forms',
},
],
},
{
value: 'AC',
label: 'Multiply Numbers',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AD',
label: 'Multiplication Tables',
},
],
},
],
},
{
value: 'AE',
label: 'Identify Problems',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AF',
label: 'Identification Cards',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AG',
label: 'Rectify Errors',
},
{
value: 'AH',
label: 'Rectification Process',
},
],
},
{
value: 'AI',
label: 'Classify Items',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AJ',
label: 'Classification System',
},
],
},
],
},
],
},
{
value: 'AK',
label: 'Beautiful Flowers',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'AL',
label: 'Beauty Products',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AM',
label: 'Quick Action',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AN',
label: 'Quickly Done',
},
],
},
{
value: 'AO',
label: 'Heavy Lifting',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AP',
label: 'Heavily Loaded',
},
],
},
],
},
{
value: 'AQ',
label: 'Simple Solution',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AR',
label: 'Simply Put',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AS',
label: 'Possible outcome',
},
{
value: 'AT',
label: 'Possibly true',
},
],
},
{
value: 'AU',
label: 'Electric cars',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AV',
label: 'electricity bills',
},
],
},
],
},
],
},
{
value: 'AW',
label: 'Dramatic plays',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'AX',
label: 'dramatically changed',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AY',
label: 'Historic buildings',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AZ',
label: 'historically accurate',
},
],
},
],
},
],
},
] as const satisfies SelectOptionBase[]🌳 Relationship with Tree
Unlike Tree, Select:
- Only supports filtering (hiding unmatched options).
- Automatically re-focuses on the first option whenever the search text changes.
- Automatically clears search text after selecting.
- Does not support custom stem index or search algorithm.
These differences come from the fact that Select is typically used for use cases where options are passed once and the menu displays only a few options at a time. In contrast, Tree usually involves more actions (adding, moving, deleting, etc.) and is used in larger vertical layouts with deeper hierarchies.
🔍 Filterable (Async)
You can listen to the search text via the @search event and control which options are rendered.
WARNING
When using @search, the built-in search behavior from filterable* and creatable props is disabled. You must implement your own search logic.
<script setup lang="ts">
import { type Ref, ref } from 'vue'
import { HSelect, type SelectValue, type SelectOptionBase } from '@holistics/design-system'
import { OPTIONS as baseOptions } from './options'
// @ts-expect-error Type instantiation is excessively deep and possibly infinite. Vue's `ref()` is doing too much work here...
const options = ref(baseOptions) as Ref<SelectOptionBase[]>
const value = ref<SelectValue | SelectValue[]>()
async function onSearch (text: string) {
if (!text) {
options.value = baseOptions
return
}
await new Promise((res) => { setTimeout(res, 3000) })
options.value = baseOptions.filter((o) => o.label.includes(text)) // [!code warning] // 🗃️️ Should be a list returned from the server
}
</script>
<template>
<div class="space-y-4">
<HSelect
v-model="value"
:options="options"
filterable
class="w-80"
@search="onSearch"
/>
<HSelect
v-model="value"
:options="options"
mutliple
filterable
placeholder="Select multiple..."
class="w-80"
@search="onSearch"
/>
</div>
</template>import type { SelectOptionBase } from '@holistics/design-system'
export const OPTIONS: SelectOptionBase[] = [
{
stickyTop: true,
value: '101',
label: 'Top Option 1',
tooltip: 'Lorem ipsum donor',
},
{
stickyTop: true,
value: '102',
label: 'Top Option 2',
tooltip: 'Some tips at a group',
initialExpanded: true,
children: [
{
stickyTop: true,
value: '102.1',
label: 'Top Option 2.1',
},
{
stickyTop: true,
value: '102.2',
label: 'Inner-Bell',
icon: 'bell',
action: () => { console.log('Inner-bell logged!') },
},
],
},
{
stickyTop: true,
value: '103',
label: 'Top Action Option',
icon: 'function',
action: (option) => { alert(`You clicked an option with value: "${option.value}" and label: "${option.label}"`) },
tooltip: { content: '<em>I can be HTML, too!</em>', html: true },
},
{
value: '1',
label: 'Option 1',
tooltip: 'I can be anywhere!',
},
{
value: '2',
label: 'Option 2 (with icon)',
icon: 'favorite',
},
{
value: '3',
label: 'Option 3',
description: 'Option with description',
},
{
value: '4',
label: 'Option 4 has a VERY Very very long label',
icon: 'data-set',
description: 'Option with description & icon, and a very long long long long long long long long long long long description',
},
{
value: '5',
label: 'Option 5 (disabled)',
disabled: true,
tooltip: 'Disabled option only shows tooltip on hovering (not keyboad)!',
},
{
value: '6',
label: 'Option 6',
children: [
{ label: 'Option a.1', value: '6.1', icon: 'hashtag' },
{ label: 'Option b.2', value: '6.2', icon: 'type/string' },
{
label: 'Option x.3',
value: '6.3',
initialExpanded: true,
children: [
{ label: 'Option d.3.1', value: '6.3.1' },
{ label: 'Option e.3.2', value: '6.3.2' },
{
label: 'Option x.3.3',
value: '6.3.3',
children: [
{ label: 'Option g.3.3.1', value: '6.3.3.1' },
{ label: 'Option h.3.3.2', value: '6.3.3.2' },
{ label: 'Option i.3.3.3', value: '6.3.3.3' },
{
label: 'Option x.3.3.4',
value: '6.3.3.4',
children: [
{ label: 'Option k.3.3.4.1', value: '6.3.3.4.1' },
{ label: 'Option l.3.3.4.2', value: '6.3.3.4.2' },
{ label: 'Option 6.3.3.4.3', value: '6.3.3.4.3' },
{ label: 'Option m.3.3.4.4', value: '6.3.3.4.4' },
],
},
{ label: 'Option j.3.3.3', value: '6.3.3.5' },
],
},
{ label: 'Option f.3.4', value: '6.3.4' },
],
},
{ label: 'Option c.4', value: '6.4' },
],
},
{
value: '7',
label: 'Option 7',
icon: 'user',
},
{
value: '8',
label: 'Option 8',
},
{
value: '9',
label: 'Option 9',
},
{
value: '10',
label: 'Option 10',
},
{
value: null,
label: 'Option null',
},
{
stickyBottom: true,
value: '901',
label: 'Bottom Option 1',
},
{
stickyBottom: true,
value: '902',
label: 'Bottom Option 2',
children: [
{
stickyBottom: true,
value: '901.1',
label: 'Bottom Option 2.1',
},
{
stickyBottom: true,
value: '901.2',
label: 'Bottom Option 2.2',
},
],
},
{
stickyBottom: true,
value: '903',
label: 'Async Refresh (done after 2s)',
icon: 'refresh',
action: async () => {
await new Promise((res) => { setTimeout(res, 2000) })
alert('Done refreshing!')
},
},
]
export const ADVANCED_SEARCH_OPTIONS = [
{
value: 'users',
label: 'Users',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'countries',
label: 'Countries',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'cities',
label: 'Cities',
initialExpanded: true,
icon: 'data-model',
children: [
{ value: 'beautiful-stories', label: 'Beautiful Stories' },
{ value: 'large-libraries', label: 'Large Libraries' },
],
},
{
value: 'families',
label: 'Families',
initialExpanded: true,
icon: 'data-model',
children: [
{ value: 'with-stories', label: 'With Stories' },
{ value: 'like-parties', label: 'Like Parties' },
],
},
],
},
],
},
{
value: 'A',
label: 'City Dwellers',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'B',
label: 'Urban City',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'C',
label: 'Analyze Data',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'D',
label: 'Analytical Reports',
},
],
},
{
value: 'E',
label: 'Childhood Memories',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'F',
label: "Children's Games",
},
],
},
],
},
{
value: 'G',
label: 'Tooth Brushing',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'H',
label: 'Teeth Cleaning',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'I',
label: 'Mouse Clicks',
},
{
value: 'J',
label: 'Mice Running',
},
],
},
{
value: 'K',
label: 'Foot Prints',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'L',
label: 'Feet Walking',
},
],
},
],
},
],
},
{
value: 'M',
label: 'Person Interview',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'N',
label: 'People Skills',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'O',
label: 'Woman Power',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'P',
label: 'Women Rights',
},
],
},
{
value: 'Q',
label: 'Leaf Fall',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'R',
label: 'Leaves Turning',
},
],
},
],
},
{
value: 'S',
label: 'Knife Sharp',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'T',
label: 'Knives Set',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'U',
label: 'Create Content',
},
{
value: 'V',
label: 'Creative Writing',
},
],
},
{
value: 'W',
label: 'Develop Software',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'X',
label: 'Development Tools',
},
],
},
],
},
],
},
{
value: 'Y',
label: 'Organize Events',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'Z',
label: 'Organizational Skills',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AA',
label: 'Apply Pressure',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AB',
label: 'Application Forms',
},
],
},
{
value: 'AC',
label: 'Multiply Numbers',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AD',
label: 'Multiplication Tables',
},
],
},
],
},
{
value: 'AE',
label: 'Identify Problems',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AF',
label: 'Identification Cards',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AG',
label: 'Rectify Errors',
},
{
value: 'AH',
label: 'Rectification Process',
},
],
},
{
value: 'AI',
label: 'Classify Items',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AJ',
label: 'Classification System',
},
],
},
],
},
],
},
{
value: 'AK',
label: 'Beautiful Flowers',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'AL',
label: 'Beauty Products',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AM',
label: 'Quick Action',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AN',
label: 'Quickly Done',
},
],
},
{
value: 'AO',
label: 'Heavy Lifting',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AP',
label: 'Heavily Loaded',
},
],
},
],
},
{
value: 'AQ',
label: 'Simple Solution',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AR',
label: 'Simply Put',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AS',
label: 'Possible outcome',
},
{
value: 'AT',
label: 'Possibly true',
},
],
},
{
value: 'AU',
label: 'Electric cars',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AV',
label: 'electricity bills',
},
],
},
],
},
],
},
{
value: 'AW',
label: 'Dramatic plays',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'AX',
label: 'dramatically changed',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AY',
label: 'Historic buildings',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AZ',
label: 'historically accurate',
},
],
},
],
},
],
},
] as const satisfies SelectOptionBase[]➕ Create Options Dynamically
Allow users to create new options on the fly by typing a value that doesn't exist in the list.
By default, newly created options are root-level and are put at the bottom of the filterable list (i.e. before sticky bottom options). To put them at top, set createAtTop: true.
<script setup lang="ts">
import { ref } from 'vue'
import { HSelect, type SelectOptionBase } from '@holistics/design-system'
const options = [
{ value: 1, label: 'Option 1' },
{ value: 2, label: 'Option 2' },
{ value: 3, label: 'Option 3', disabled: true },
{ value: 4, label: 'Option 4' },
] as const satisfies SelectOptionBase[]
const value = ref<typeof options[number]['value']>()
</script>
<template>
<div class="space-y-4">
<HSelect
v-model="value"
:options="options"
filterable
createable
class="w-80"
/>
<HSelect
v-model="value"
:options="options"
multiple
filterable
createable
placeholder="Select multiple..."
class="w-80"
/>
</div>
</template>🧹 Clearable
Show a clear button at the right side of the trigger that allows users to reset the selection. Works for both single and multiple select modes.
<script setup lang="ts">
import { ref } from 'vue'
import { HSelect, type SelectOptionBase } from '@holistics/design-system'
const options = [
{ value: 1, label: 'Option 1' },
{ value: 2, label: 'Option 2' },
{ value: 3, label: 'Option 3', disabled: true },
{ value: 4, label: 'Option 4' },
] as const satisfies SelectOptionBase[]
const value = ref<typeof options[number]['value']>()
</script>
<template>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-1 2xl:grid-cols-2">
<HSelect
v-model="value"
:options="options"
clearable
class="w-80"
/>
<HSelect
v-model="value"
:options="options"
filterable
clearable
class="w-80"
/>
<HSelect
v-model="value"
:options="options"
multiple
clearable
placeholder="Select multiple..."
class="w-80"
/>
<HSelect
v-model="value"
:options="options"
multiple
filterable
clearable
placeholder="Select multiple..."
class="w-80"
/>
</div>
</template>🔄 With Refresh Button
Add a refresh button to reload the options, useful when options are fetched from an API.
<script setup lang="ts">
import { ref } from 'vue'
import { HSelect, type SelectOptionBase } from '@holistics/design-system'
const options = [
{ value: 1, label: 'Option 1' },
{ value: 2, label: 'Option 2' },
{ value: 3, label: 'Option 3', disabled: true },
{ value: 4, label: 'Option 4' },
] as const satisfies SelectOptionBase[]
const value = ref<typeof options[number]['value']>()
async function onRefresh () {
await new Promise((res) => { setTimeout(res, 2000) })
}
</script>
<template>
<HSelect
v-model="value"
:options="options"
class="w-80"
@refresh="onRefresh"
/>
</template>♾️ Infinite Scroll
Load more options as the user scrolls to the bottom of the list, ideal for large datasets.
<script setup lang="ts">
import { type Ref, ref } from 'vue'
import { HSelect, type SelectOptionBase, type SelectValue } from '@holistics/design-system'
const options = ref([
{ value: 1, label: 'Option 1' },
{ value: 2, label: 'Option 2' },
{ value: 3, label: 'Option 3', disabled: true },
{ value: 4, label: 'Option 4' },
{ value: 5, label: 'Option 5' },
{ value: 6, label: 'Option 6' },
{ value: 7, label: 'Option 7' },
{ value: 8, label: 'Option 8' },
]) as Ref<SelectOptionBase[]>
const value = ref<SelectValue>()
const page = ref(0)
async function onScrollBottom () {
await new Promise((res) => { setTimeout(res, 3000) })
const temp = options.value.slice()
// [!code warning] // 🗃️ Should be a list returned from the server
temp.push(...Array.from({ length: 5 }, (_, i) => {
const num = page.value * 5 + (i + 1)
return {
value: `appended-${num}`,
label: `Appended Option ${num}`,
}
}))
options.value = temp
page.value += 1
}
</script>
<template>
<HSelect
v-model="value"
:options="options"
class="w-80"
preserve-focus
@scroll-bottom="onScrollBottom"
/>
</template>⚡ Virtual List
Virtual List is enabled by default and cannot be opted out at the moment.
<script setup lang="ts">
import { ref } from 'vue'
import { HSelect, type SelectOptionBase, type SelectValue } from '@holistics/design-system'
const options = Array.from(
{ length: 1000 },
(_, i) => ({ value: `opt-${i + 1}`, label: `Option ${i + 1}` }),
) satisfies SelectOptionBase[]
const value = ref<SelectValue>()
</script>
<template>
<HSelect
v-model="value"
:options="options"
class="w-80"
/>
</template>📂 Submenu
Display nested options in a separate flyout menu, useful for organizing related actions or hierarchical data.
<script setup lang="ts">
import { ref } from 'vue'
import { HSelect, type SelectValue } from '@holistics/design-system'
import { options } from './optionsSubmenu'
const value = ref<SelectValue>()
</script>
<template>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-1 2xl:grid-cols-2">
<HSelect
v-model="value"
:options="options"
class="w-80"
/>
<HSelect
v-model="value"
:options="options"
multiple
placeholder="Select multiple..."
class="w-80"
/>
<HSelect
v-model="value"
:options="options"
filterable
class="w-80"
/>
<HSelect
v-model="value"
:options="options"
multiple
filterable
placeholder="Select multiple..."
class="w-80"
/>
<HSelect
v-model="value"
:options="options"
filterable
filter-include-direct-children
placeholder="Select (include direct children)..."
class="w-80"
/>
<HSelect
v-model="value"
:options="options"
multiple
filterable
filter-include-direct-children
placeholder="Select multiple (include direct children)..."
class="w-80"
/>
</div>
</template>import type { SelectOptionBase } from '@holistics/design-system'
export const options: SelectOptionBase[] = [
{ value: 1, label: 'Option 1' },
{ value: 2, label: 'Option 2' },
{
value: 'more-options',
label: 'More Options',
childrenAsSubmenu: true,
children: [
{ value: '3.1', label: 'Option 3.1' },
{ value: '3.2', label: 'Option 3.2' },
{
value: '3.3-group',
label: 'Option 3.3 (Group)',
children: [
{ value: '3.x.1', label: 'Option 3.x.1' },
{ value: '3.x.2', label: 'Option 3.x.2' },
{ value: '3.x.3', label: 'Option 3.x.3' },
{
value: '3.3.4-more-options',
label: 'More Options',
childrenAsSubmenu: true,
children: [
{ value: '3.3.4.1', label: 'Option 3.3.4.1' },
{
value: '3.3.4.2-more-options',
label: 'More Options',
childrenAsSubmenu: true,
children: [
{ value: '3.3.4.2.1', label: 'Option 3.3.4.2.1' },
{ value: '3.3.4.2.2', label: 'Option 3.3.4.2.2' },
],
},
{
value: '3.3.4.3',
label: 'Option 3.3.4.3 (Group)',
children: [
{ value: '3.3.4.x.1', label: 'Option 3.3.4.x.1' },
{ value: '3.3.4.x.2', label: 'Option 3.3.4.x.2' },
{ value: '3.3.4.x.3', label: 'Option 3.3.4.x.3' },
{ value: '3.3.4.x.4', label: 'Option 3.3.4.x.4' },
],
},
],
},
{
value: '3.3.5',
label: 'Option 3.3.5 (Group)',
children: [
{ value: '3.3.x.1', label: 'Option 3.3.x.1' },
{ value: '3.3.x.2', label: 'Option 3.3.x.2' },
{
value: '3.3.5.3 (Group)',
label: 'Option 3.3.5.3',
children: [
{ value: '3.3.5.x.1', label: 'Option 3.3.5.x.1' },
{ value: '3.3.5.x.2', label: 'Option 3.3.5.x.2' },
{ value: '3.3.5.x.3', label: 'Option 3.3.5.x.3' },
],
},
],
},
],
},
],
},
{ value: 4, label: 'Option 4' },
]Unsupported features for submenus
- Create options dynamically: New options can only be created in the root menu, not in the currently focused submenu.
- Sticky options when filtering: Unlike the root menu, sticky options within submenus will be filtered out if they aren't matched.
📐 Inlined
Display the select menu inline without a popover menu, useful for embedding directly in forms or panels.
<script setup lang="ts">
import { ref } from 'vue'
import { HSelect, type SelectValue } from '@holistics/design-system'
import { OPTIONS } from './options'
const value = ref<SelectValue>()
</script>
<template>
<HSelect
v-model="value"
:options="OPTIONS"
inline
filterable
class="w-80"
/>
</template>import type { SelectOptionBase } from '@holistics/design-system'
export const OPTIONS: SelectOptionBase[] = [
{
stickyTop: true,
value: '101',
label: 'Top Option 1',
tooltip: 'Lorem ipsum donor',
},
{
stickyTop: true,
value: '102',
label: 'Top Option 2',
tooltip: 'Some tips at a group',
initialExpanded: true,
children: [
{
stickyTop: true,
value: '102.1',
label: 'Top Option 2.1',
},
{
stickyTop: true,
value: '102.2',
label: 'Inner-Bell',
icon: 'bell',
action: () => { console.log('Inner-bell logged!') },
},
],
},
{
stickyTop: true,
value: '103',
label: 'Top Action Option',
icon: 'function',
action: (option) => { alert(`You clicked an option with value: "${option.value}" and label: "${option.label}"`) },
tooltip: { content: '<em>I can be HTML, too!</em>', html: true },
},
{
value: '1',
label: 'Option 1',
tooltip: 'I can be anywhere!',
},
{
value: '2',
label: 'Option 2 (with icon)',
icon: 'favorite',
},
{
value: '3',
label: 'Option 3',
description: 'Option with description',
},
{
value: '4',
label: 'Option 4 has a VERY Very very long label',
icon: 'data-set',
description: 'Option with description & icon, and a very long long long long long long long long long long long description',
},
{
value: '5',
label: 'Option 5 (disabled)',
disabled: true,
tooltip: 'Disabled option only shows tooltip on hovering (not keyboad)!',
},
{
value: '6',
label: 'Option 6',
children: [
{ label: 'Option a.1', value: '6.1', icon: 'hashtag' },
{ label: 'Option b.2', value: '6.2', icon: 'type/string' },
{
label: 'Option x.3',
value: '6.3',
initialExpanded: true,
children: [
{ label: 'Option d.3.1', value: '6.3.1' },
{ label: 'Option e.3.2', value: '6.3.2' },
{
label: 'Option x.3.3',
value: '6.3.3',
children: [
{ label: 'Option g.3.3.1', value: '6.3.3.1' },
{ label: 'Option h.3.3.2', value: '6.3.3.2' },
{ label: 'Option i.3.3.3', value: '6.3.3.3' },
{
label: 'Option x.3.3.4',
value: '6.3.3.4',
children: [
{ label: 'Option k.3.3.4.1', value: '6.3.3.4.1' },
{ label: 'Option l.3.3.4.2', value: '6.3.3.4.2' },
{ label: 'Option 6.3.3.4.3', value: '6.3.3.4.3' },
{ label: 'Option m.3.3.4.4', value: '6.3.3.4.4' },
],
},
{ label: 'Option j.3.3.3', value: '6.3.3.5' },
],
},
{ label: 'Option f.3.4', value: '6.3.4' },
],
},
{ label: 'Option c.4', value: '6.4' },
],
},
{
value: '7',
label: 'Option 7',
icon: 'user',
},
{
value: '8',
label: 'Option 8',
},
{
value: '9',
label: 'Option 9',
},
{
value: '10',
label: 'Option 10',
},
{
value: null,
label: 'Option null',
},
{
stickyBottom: true,
value: '901',
label: 'Bottom Option 1',
},
{
stickyBottom: true,
value: '902',
label: 'Bottom Option 2',
children: [
{
stickyBottom: true,
value: '901.1',
label: 'Bottom Option 2.1',
},
{
stickyBottom: true,
value: '901.2',
label: 'Bottom Option 2.2',
},
],
},
{
stickyBottom: true,
value: '903',
label: 'Async Refresh (done after 2s)',
icon: 'refresh',
action: async () => {
await new Promise((res) => { setTimeout(res, 2000) })
alert('Done refreshing!')
},
},
]
export const ADVANCED_SEARCH_OPTIONS = [
{
value: 'users',
label: 'Users',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'countries',
label: 'Countries',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'cities',
label: 'Cities',
initialExpanded: true,
icon: 'data-model',
children: [
{ value: 'beautiful-stories', label: 'Beautiful Stories' },
{ value: 'large-libraries', label: 'Large Libraries' },
],
},
{
value: 'families',
label: 'Families',
initialExpanded: true,
icon: 'data-model',
children: [
{ value: 'with-stories', label: 'With Stories' },
{ value: 'like-parties', label: 'Like Parties' },
],
},
],
},
],
},
{
value: 'A',
label: 'City Dwellers',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'B',
label: 'Urban City',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'C',
label: 'Analyze Data',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'D',
label: 'Analytical Reports',
},
],
},
{
value: 'E',
label: 'Childhood Memories',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'F',
label: "Children's Games",
},
],
},
],
},
{
value: 'G',
label: 'Tooth Brushing',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'H',
label: 'Teeth Cleaning',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'I',
label: 'Mouse Clicks',
},
{
value: 'J',
label: 'Mice Running',
},
],
},
{
value: 'K',
label: 'Foot Prints',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'L',
label: 'Feet Walking',
},
],
},
],
},
],
},
{
value: 'M',
label: 'Person Interview',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'N',
label: 'People Skills',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'O',
label: 'Woman Power',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'P',
label: 'Women Rights',
},
],
},
{
value: 'Q',
label: 'Leaf Fall',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'R',
label: 'Leaves Turning',
},
],
},
],
},
{
value: 'S',
label: 'Knife Sharp',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'T',
label: 'Knives Set',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'U',
label: 'Create Content',
},
{
value: 'V',
label: 'Creative Writing',
},
],
},
{
value: 'W',
label: 'Develop Software',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'X',
label: 'Development Tools',
},
],
},
],
},
],
},
{
value: 'Y',
label: 'Organize Events',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'Z',
label: 'Organizational Skills',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AA',
label: 'Apply Pressure',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AB',
label: 'Application Forms',
},
],
},
{
value: 'AC',
label: 'Multiply Numbers',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AD',
label: 'Multiplication Tables',
},
],
},
],
},
{
value: 'AE',
label: 'Identify Problems',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AF',
label: 'Identification Cards',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AG',
label: 'Rectify Errors',
},
{
value: 'AH',
label: 'Rectification Process',
},
],
},
{
value: 'AI',
label: 'Classify Items',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AJ',
label: 'Classification System',
},
],
},
],
},
],
},
{
value: 'AK',
label: 'Beautiful Flowers',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'AL',
label: 'Beauty Products',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AM',
label: 'Quick Action',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AN',
label: 'Quickly Done',
},
],
},
{
value: 'AO',
label: 'Heavy Lifting',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AP',
label: 'Heavily Loaded',
},
],
},
],
},
{
value: 'AQ',
label: 'Simple Solution',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AR',
label: 'Simply Put',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AS',
label: 'Possible outcome',
},
{
value: 'AT',
label: 'Possibly true',
},
],
},
{
value: 'AU',
label: 'Electric cars',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AV',
label: 'electricity bills',
},
],
},
],
},
],
},
{
value: 'AW',
label: 'Dramatic plays',
initialExpanded: true,
icon: 'canvas-light',
children: [
{
value: 'AX',
label: 'dramatically changed',
initialExpanded: true,
icon: 'data-set',
children: [
{
value: 'AY',
label: 'Historic buildings',
initialExpanded: true,
icon: 'data-model',
children: [
{
value: 'AZ',
label: 'historically accurate',
},
],
},
],
},
],
},
] as const satisfies SelectOptionBase[]🎨 Themes
Apply different visual themes to the select component.
<script setup lang="ts">
import { type Ref, ref } from 'vue'
import { HSelect, type SelectOptionBase, type SelectValue } from '@holistics/design-system'
const options = ref([
{ value: 1, label: 'Option 1' },
{ value: 2, label: 'Option 2' },
{ value: 3, label: 'Option 3', disabled: true },
{ value: 4, label: 'Option 4' },
]) as Ref<SelectOptionBase[]>
const value = ref<SelectValue>()
</script>
<template>
<div class="flex flex-col items-center gap-4 md:flex-row lg:flex-col 2xl:flex-row">
<HSelect
v-model="value"
:options="options"
placeholder="Default..."
class="w-80"
/>
<HSelect
v-model="value"
:options="options"
placeholder="Underline..."
theme="underline"
class="w-80"
/>
</div>
</template>🖌️ Custom CSS & Styles for Options
Apply custom CSS classes and inline styles to options for visual customization.
<script setup lang="ts">
import { ref } from 'vue'
import { HSelect, type SelectOptionBase, type SelectValue } from '@holistics/design-system'
const options: SelectOptionBase[] = [
{
stickyTop: true,
value: '1',
label: 'A big padding option!',
style: { padding: '1rem 1.5rem' },
},
{
value: '2',
label: 'Open me',
children: [
{
value: '2-1',
label: 'A little bit italic',
class: 'italic',
},
{
value: '2-2',
label: 'And super large text!',
style: { 'font-size': '2rem', 'line-height': '1.5' },
},
],
},
]
const value = ref<SelectValue>()
</script>
<template>
<HSelect
v-model="value"
:options="options"
class="w-80"
/>
</template>🧩 Slots
You have many options for slot customizing, including global slots (i.e. applying for all options) & individual slots. Please refer to the Slots API for reference.
Following is an example of using both global slot (#option-content) and individual slots (#option-content-verified and #option-footer):
<script setup lang="ts">
import { ref } from 'vue'
import {
HSelect,
HTextHighlight,
HIcon,
type SelectOptionBase,
type SelectValue,
} from '@holistics/design-system'
const options: SelectOptionBase[] = [
{
value: 'Default Option',
label: 'Default Option',
tooltip: 'This option has the exactly same default implementation of `#option-content`!',
},
{
value: 'Verified Option',
label: 'Verified Option',
slotContent: 'option-content-verified',
tooltip: 'This option stripped down unnecessary default parts of `#option-content`.',
},
{
stickyBottom: true,
value: 'Footer Option',
slot: 'option-footer',
label: 'A footer that has a link!', // This will be used for `filterable`
disabled: true, // To avoid keyboard interactions
},
]
const value = ref<SelectValue>()
</script>
<template>
<HSelect
v-model="value"
:options="options"
filterable
class="w-80"
>
<template #option-content="{ option, searchText, highlights }">
<div
class="flex min-w-0 flex-1 items-start"
v-bind="option.contentProps"
>
<HIcon
v-if="option.creating"
name="add"
class="mr-1"
/>
<HIcon
v-if="option.icon"
:name="option.icon"
class="mr-1"
/>
<div
class="min-w-0 flex-1"
:class="option.children && !option.childrenAsSubmenu && 'font-medium'"
>
<span v-if="option.creating || !searchText">{{ option.label }}</span>
<HTextHighlight
v-else
:text="option.label"
:highlights="highlights"
/>
</div>
</div>
</template>
<template #option-content-verified="{ option, selected, searchText, highlights, }">
<div
class="flex min-w-0 flex-1 items-start"
:class="!selected && 'text-green-600'"
v-bind="option.contentProps"
>
<div>
<HTextHighlight
v-if="searchText"
:text="option.label"
:highlights="highlights"
/>
<span v-else>{{ option.label }}</span>
</div>
<HIcon
name="verified"
class="ml-1"
/>
</div>
</template>
<template #option-footer>
<div class="p-2">
A footer that has a <a
href="https://holistics.io"
target="_blank"
class="text-blue-600 underline"
>link</a>!
</div>
</template>
</HSelect>
</template>💡 Default implementation of #option-content
The above code example has the exact same default implementation of #option-content, and will be used as a reference in all version of Select. Therefore, whenever you need to customize the options, try copying that code first, then adjust it to your needs.
For example:
#option-content-verifieddoesn't haveicon,children, can never be a Creating Option, and it needs the "verified" icon to be placed after the text; hence we have the simplified code.#option-footercustomize the entire#optionslot which is far more complicated than#option-content. But since it's intended to to render a footer UI without any (default) interactions, we can just put simple markup there.
🏷️ Generics
Select supports generic types by looking at the options prop so that all the emitted events and other props referencing options will have the passed type.
<script setup lang="ts">
import { ref, type Ref } from 'vue'
import { HSelect, type SelectOption } from '@holistics/design-system'
interface MySelectOption extends SelectOption<MySelectOption> {
// ^ Your option ^ `children` of your option
extraProperty?: boolean
}
const options = ref([]) as Ref<MySelectOption[]>
</script>
<template>
<HSelect
v-model:options="options"
:option-props="(option) => {
// `option` will have type `MySelectOption`
}"
@select="(value, option) => {
// `option` will have type `MySelectOption`
if (option.extraProperty) {
// ...
}
}"
/>
</template>Fixed depth via recursive generic type
You can describe your options structure via recursive generic type to get the best type-safety and convenience.
Example
// A Dataset can only contain Data Models & is always be a parent option
interface DatasetOption extends SelectOption<DataModelOption> {
type: 'dataset'
icon: 'data-set'
children: DataModelOption[]
}
// A Data Model can only contain Fields & is always be a parent option
interface DataModelOption extends SelectOption<FieldOption> {
type: 'data-model'
icon: 'data-model'
children: FieldOption[]
}
// A Field is always a leaf option
interface FieldOption extends SelectOption<never> {
type: 'field'
icon: 'type/string' | 'type/number' | 'type/boolean' | 'type/date' | 'type/timestamp' | 'type/unknown'
children?: never
}Forcing generic type
Since Vue components are just functions, you can force a generic type when using Select.
Example
<script setup lang="ts">
import { _HSelect, type SelectOption } from '@holistics/design-system'
interface ForcedSelectOption extends SelectOption<ForcedSelectOption> {
someKey?: boolean
}
const HSelect = _HSelect<ForcedSelectOption>
</script>
<template>
<HSelect
@select="(option) => {
// `option` will have type `ForcedSelectOption` even though `options` prop is not specified
}"
/>
</template>API
Pass-through: <HPopper>
What does this mean?
All props, events, and attrs that are not specified in the tables below will be passed to the element/component described above.
Props
| Name | Type | Description |
|---|---|---|
options * | readonly SelectOption<any>[] | List of options. |
modelValue | SelectValue | SelectValue[] | Selected/checked value(s):
|
disabled | boolean | When |
inputId | string | The |
theme | "normal" | "underline"= "normal" | The theme of the select. |
icon | "function" | "add-block" | "add-filter" | "add-tag" | "add" | "address-card" | "adhoc-query" | "ai/file" | "ai/generate" | "ai/mascot" | "ai/openai-black-monoblossom" | "ai/sparkle-gradient" | ... 431 more ... | Name of the icon to display at the prepend of the trigger. |
iconSpin | boolean | Whether the trigger icon should spin. |
placeholder | string= "Select..." | A (muted) text to show when no option is being selected. |
clearable | boolean | When |
refreshFn | (() => void | Promise<void>) | Custom function to refresh the |
refreshLoading | boolean | When |
groupSelectable | boolean | By default, group options can only be expanded/collapsed. To allow selecting them, set this to |
showCheckmark | boolean | When |
multiple | SelectMultiple | When |
indeterminateKeys | Key[] | List of option values that have some children checked (when |
checkable | boolean | Whether to display the selection checkbox. When NOTE: Due to performance reasons, |
checkCascade | boolean | Whether to cascade checkboxes. |
checkStrategy | CheckStrategy= "all" | |
filterable | boolean | When |
searchText | string | The search text used for filtering options. |
preserveSearchText | boolean | By default, the search text will be automatically reset when |
searchDebounce | number= 200 | Debounce time (in |
filterIncludeDirectChildren | boolean | By default, |
filterIncludeStickyOptions | boolean | By default, sticky options are always visible regardless of the search text (can still be auto-expaned,
auto-focused, highlighted,...). To also filter them based on the search text, set this to |
filterAdvancedSearch | boolean | SelectSearchStemIndex | Whether to enable stemming with hierarchical search for filtering. This is achieved by generating a stem index which is essentially an inverted index map from a stem to option values, then for each value map to its list of original tokens.
|
searchLoading | boolean | When |
createable | boolean | When While inputting the text, if no existing option has |
createFn | SelectCreateOptionFn<SelectOption<any>> | Custom low-level function used to create new options based on the search text. |
createAtTop | boolean | When |
maxHeight | string= "15.5rem" | Max height of the container of filterable options (not the menu). |
scrollBottomLoading | boolean | When |
preserveFocus | boolean | In general cases, whenever |
inline | boolean | When |
matchAnchorSize | boolean | "min" | "max"= true | Config to match the Anchor "size". The "size" here is detected by the final side of the Floating element:
|
floatingClass | HTMLAttributeClass | Custom class for the root options menu. |
optionProps | ((option: SelectOption<any>) => HTMLAttributes) | The getter to bind additional props to the root element of the |
optionContentProps | ((option: SelectOption<any>) => HTMLAttributes) | The getter to bind additional props to the content element of the |
optionDescriptionProps | ((option: SelectOption<any>) => HTMLAttributes) | The getter to bind additional props to the description element of the |
optionIndent | number= 20 | [Nested] The distance (in |
optionEmptyParentText | string | null= "(empty)" | Text to display right after the label of options having no children. The text will be hidden when searching (as the
children could have been filtered out). To completely hide it in all circumstances, set this to |
Events
| Name | Parameters | Description |
|---|---|---|
@update:modelValue | [value: SelectValue | SelectValue[] | undefined] | |
@select | [value: SelectValue, option: SelectOption<any>] | |
@update:refreshLoading | [loading: boolean] | |
@deselect | [value: SelectValue, option: SelectOption<any>] | |
@update:indeterminateKeys | [keys: Key[]] | |
@update:searchText | [value: string] | |
@update:searchLoading | [loading: boolean] | |
@update:scrollBottomLoading | [loading: boolean] | |
@focus | [event: FocusEvent] | |
@blur | [event: FocusEvent] | |
@focusOption | [option?: SelectOption<any> | undefined] |
Slots
| Name | Scoped | Description |
|---|---|---|
#placeholder | { placeholder: string; } | |
#trigger-selected-options | SelectSlotTriggerSelectedOptionsProps<SelectOption<any>> | |
#option | SelectSlotOptionProps<SelectOption<any>> | |
#option-toggle | SelectSlotOptionContentProps<SelectOption<any>> | |
#option-prepend | SelectSlotOptionContentProps<SelectOption<any>> | |
#option-content | SelectSlotOptionContentProps<SelectOption<any>> | |
#option-append | SelectSlotOptionContentProps<SelectOption<any>> | |
#option-description | SelectSlotOptionContentProps<SelectOption<any>> | |
#options-empty | any | |
#options-filter-empty | any |