Select
Allow users to choose one or more options from a dropdown list.
<script setup lang="ts">
import { ref } from 'vue'
import { HSelect, type SelectOption } 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 SelectOption[]
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 SelectOption, type SelectValue } from '@holistics/design-system'
const options = [
{
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!')
},
},
] as const satisfies SelectOption[]
const value = ref<SelectValue>()
</script>
<template>
<HSelect
v-model="value"
:options="options"
class="w-80"
/>
</template>import type { SelectOption } from '@holistics/design-system'
export const OPTIONS = [
{
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!')
},
},
] as const satisfies SelectOption[]
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 SelectOption[]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 { SelectOption } from '@holistics/design-system'
export const OPTIONS = [
{
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!')
},
},
] as const satisfies SelectOption[]
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 SelectOption[]📋 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 { SelectOption } from '@holistics/design-system'
export const options = [
{ 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' },
] as const satisfies SelectOption[]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 { SelectOption } from '@holistics/design-system'
export const options = [
{ 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' },
] as const satisfies SelectOption[]✅ 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 { SelectOption } from '@holistics/design-system'
export const OPTIONS = [
{
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!')
},
},
] as const satisfies SelectOption[]
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 SelectOption[]❌ 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 { SelectOption } from '@holistics/design-system'
export const OPTIONS = [
{
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!')
},
},
] as const satisfies SelectOption[]
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 SelectOption[]🌳 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 SelectOption } from '@holistics/design-system'
import { OPTIONS as baseOptions } from './options'
const options = ref(baseOptions) as Ref<SelectOption[]>
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 { SelectOption } from '@holistics/design-system'
export const OPTIONS = [
{
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!')
},
},
] as const satisfies SelectOption[]
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 SelectOption[]➕ 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 SelectOption } 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 SelectOption[]
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 SelectOption } 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 SelectOption[]
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 SelectOption } 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 SelectOption[]
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 SelectOption, 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<SelectOption[]>
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 SelectOption, type SelectValue } from '@holistics/design-system'
const options = Array.from(
{ length: 1000 },
(_, i) => ({ value: `opt-${i + 1}`, label: `Option ${i + 1}` }),
) satisfies SelectOption[]
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 { SelectOption } from '@holistics/design-system'
export const options = [
{ 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' },
] as const satisfies SelectOption[]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 { SelectOption } from '@holistics/design-system'
export const OPTIONS = [
{
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!')
},
},
] as const satisfies SelectOption[]
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 SelectOption[]🎨 Themes
Apply different visual themes to the select component.
<script setup lang="ts">
import { type Ref, ref } from 'vue'
import { HSelect, type SelectOption, 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<SelectOption[]>
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 SelectOption, type SelectValue } from '@holistics/design-system'
const options = [
{
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' },
},
],
},
] as const satisfies SelectOption[]
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 SelectOption,
type SelectValue,
} from '@holistics/design-system'
const options = [
{
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
},
] as const satisfies SelectOption[]
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.
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[] | An array of select options. Option properties:
|
modelValue | SelectValue | SelectValue[] | Primitive value:
or Array of primitive values if |
icon | "function" | "cancel" | "copy" | "cut" | "error" | "pause" | "play" | "add-block" | "add-circle" | "add-filter" | "add-tag" | "add" | "address-card" | "adhoc-query" | "ai/file" | ... 448 more ... | Icon to display on the left side of the select. |
iconSpin | boolean | Whether the icon should spin. |
placeholder | string= "Select..." | Placeholder text. |
optionIndent | number= 20 | [Tree] The distance (in |
disabled | boolean | |
clearable | boolean | If true, select can be cleared. |
multiple | SelectMultiple | If true, multiple values can be selected. |
indeterminateKeys | Key[] | |
checkable | boolean | Whether to display the selection checkbox. When NOTE: Due to performance reasons, |
checkCascade | boolean | Whether to cascade checkboxes. |
checkStrategy | CheckStrategy= "all" | |
showCheckmark | boolean | Whether to show a check mark at the selected options. |
filterable | boolean | If true, options are filterable/searchable. |
filterAdvancedSearch | boolean | When |
filterIncludeDirectChildren | boolean | Whether to also include all direct (first-level) children of matched groups when filtering/searching. |
filterIncludeStickyOptions | boolean | Whether to include Sticky Options when filtering/searching:
|
searchText | string | Search text used for filtering option(s). |
searchDebounce | number= 200 | Debounce time for searching (ms). |
searchLoading | boolean | Whether the |
inputId | string | The id of the input element. |
preserveSearchText | boolean | Whether to preseve search text when blurring, after selecting option, or when pressing |
createable | boolean | If true, new options can be created. |
createFn | CreateOptFn | An array of select options. Interface: |
createAtTop | boolean | When |
refreshFn | RefreshFn | Function to refresh the options. Interface: |
refreshLoading | boolean | Whether the |
matchAnchorSize | boolean | "min" | "max"= true | Set the size of the options menu depending on the size of the select trigger element.
|
theme | "normal" | "underline"= "normal" | The theme of the select component. |
inline | boolean | If true, display the options menu as inlined instead of floating. |
groupSelectable | boolean | Whether to allow selecting group option (best when using with |
floatingClass | HTMLAttributeClass | Custom class for the options menu. |
maxHeight | string= "15.5rem" | Max height of the filterable options for scrolling. |
scrollBottomLoading | boolean | Whether the |
preserveFocus | boolean | Whether to preserve current focused element while the menu is opening and |
optionProps | ((option: SelectOption) => HTMLAttributes) | The getter to bound additional props to the root element of the |
optionContentProps | ((option: SelectOption) => HTMLAttributes) | The getter to bound additional props to the content element of the |
optionDescriptionProps | ((option: SelectOption) => HTMLAttributes) | The getter to bound additional props to the description element of the |
Events
| Name | Parameters | Description |
|---|---|---|
@blur | [event: FocusEvent] | |
@focus | [event: FocusEvent] | |
@select | [value: SelectValue, option: SelectOption] | |
@update:modelValue | [value: SelectValue | SelectValue[] | undefined] | |
@update:indeterminateKeys | [keys: Key[]] | |
@update:searchText | [value: string] | |
@update:searchLoading | [loading: boolean] | |
@update:refreshLoading | [loading: boolean] | |
@update:scrollBottomLoading | [loading: boolean] | |
@deselect | [value: SelectValue, option: SelectOption] | |
@focusOption | [option?: SelectOption | undefined] | |
@search | SearchFn | Function triggered when the search text is changed. If this is specified, searching from Interface: |
@refresh | DefinePropEmit | Function to refresh the options. Interface: |
@scrollBottom | DefinePropEmit<[menuHolder?: SelectOption]> | Fired when scrolling to bottom of a menu. Interface: |
Slots
| Name | Scoped | Description |
|---|---|---|
#placeholder | { placeholder: string; } | |
#trigger-selected-options | SelectSlotTriggerSelectedOptionsProps | |
#option | SelectSlotOptionProps | |
#option-toggle | SelectSlotOptionContentProps | |
#option-prepend | SelectSlotOptionContentProps | |
#option-content | SelectSlotOptionContentProps | |
#option-append | SelectSlotOptionContentProps | |
#option-description | SelectSlotOptionContentProps | |
#options-empty | any | |
#options-filter-empty | any |
Exposed
| Name | Type | Description |
|---|---|---|
focusAndOpen | () => void | |
close | (keepFocus?: boolean | undefined) => void | |
updatePositions | () => void | |
handleCheck | (option: SelectOption, assumeChecked?: boolean | undefined) => void | Check/uncheck a single option based on its current checked state (can be overridden by |
setCheckedStates | (checkedKeys: Key[] | null | undefined) => void | Calculate, then set correct checked and indeterminate keys based on a list of checked keys. |