Skip to content

Select

Stable

Allow users to choose one or more options from a dropdown list.

vue
<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:

  1. Default
  2. Sticky top: same as default, but is always sticky at top
  3. Sticky bottom: same as default, but is always sticky at bottom
  4. Group: contains other children options
  5. Submenu handle: contains other children options in a separate submenu.
  6. Action: same as default, but is used to execute the action function, and cannot be selected.
  7. Creating (internal, creatable only): has a key that is unique across the entire options
vue
<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>
ts
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
vue
<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>
ts
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.

vue
<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>
ts
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.

vue
<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>
ts
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

  • Select becomes 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.
vue
<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         <- child

The modelValue will be:

  • all: ['Folder A', 'File 1', 'File 2']
  • parent: ['Folder A']
  • child: ['File 1', 'File 2']
vue
<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>
ts
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.

KeyDescriptionfilterable supported?
ArrowDownWhen focus is on an option, moves focus to the next one.
ArrowUpWhen focus is on an option, moves focus to the previous one.
PageDownWhen focus is on an option, moves focus to the next 10 options.
PageUpWhen focus is on an option, moves focus to the previous 10 options.
HomeMoves focus to the first option.
EndMoves focus to the last option.
EscapeClose the currently opened (sub)menu, and clear the search text.
EnterWhen 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.
SpaceWhen focus is on an option, toggles check on it.
ArrowRightWhen focus is on a non-leaf option: if it's collapsed, expand the option; otherwise, moves focus to the first child.
ArrowLeftWhen 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.

vue
<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.

vue
<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.

vue
<template>
  <HSelect
    :options="options"
    filterable
    filter-include-sticky-options
  />
</template>
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.
vue
<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.

vue
<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>
ts
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.

vue
<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>
ts
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.

vue
<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.

vue
<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.

vue
<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.

vue
<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.

vue
<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.

vue
<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>
ts
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.

vue
<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>
ts
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.

vue
<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.

vue
<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):

vue
<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-verified doesn't have icon, 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-footer customize the entire #option slot 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

NameTypeDescription
options *
readonly SelectOption[]

An array of select options.

Option properties:

  • value [required]
  • label [required]
  • icon
  • disabled
  • class: HTML class
  • style: HTML style
  • children: [Group] list of options (no deep limitation)
  • childrenAsSubmenu: [Group] whether to render the children as a separate submenu
  • childrenSubmenuProps: [Group - Submenu] set submenu
  • initialExpanded: [Group] whether to expand at the first time opening the (sub)menu
  • action: action on click. Interface: (option: SelectOption) => (void | Promise<void>)
  • stickyTop: whether this option should stick to the top of the options panel and does not scroll.
  • stickyBottom: whether this option should stick to the bottom of the options panel and does not scroll.
modelValue 
SelectValue | SelectValue[]

Primitive value: string | number | boolean | null

or Array of primitive values if multiple is enabled.

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 px) between options of 2 continuous level.

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 true, this will always be a multiple select.

NOTE: Due to performance reasons, Select with checkable = true will only work with options having value of type string | number. Other types (boolean, null) will lead to unexpected behavior.

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 true, enable hierarchical and stemming search.

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:

  • false: sticky options are always shown and are not filterable (can still be highlighted)
  • true: only matched sticky options are shown and highlighted
searchText 
string

Search text used for filtering option(s).

searchDebounce 
number
= 200

Debounce time for searching (ms).

searchLoading 
boolean

Whether the onSearch function is loading.

inputId 
string

The id of the input element.

preserveSearchText 
boolean

Whether to preseve search text when blurring, after selecting option, or when pressing Escape while menu isn't opened.

createable 
boolean

If true, new options can be created.

createFn 
CreateOptFn

An array of select options.

Interface: (value: SelectValue) => SelectOptionDefault

createAtTop 
boolean

When true, creating (i.e. unmatched option when searching) & created options will be put at the top of the filterable options list. The order is now reversed: Creating -> Created -> Original.

refreshFn 
RefreshFn

Function to refresh the options.

Interface: () => (void | Promise<void>)

refreshLoading 
boolean

Whether the onRefresh function is loading.

matchAnchorSize 
boolean | "min" | "max"
= true

Set the size of the options menu depending on the size of the select trigger element.

  • true: height/width of options menu = height/width of select trigger element.
  • false: the size of options menu will not depend on the size of select trigger element.
  • 'min': min-height/min-width of options menu = height/width of select trigger element.
  • 'max': max-height/max-width of options menu = height/width of 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 inline mode).

floatingClass 
HTMLAttributeClass

Custom class for the options menu.

maxHeight 
string
= "15.5rem"

Max height of the filterable options for scrolling.

scrollBottomLoading 
boolean

Whether the onScrollBottom function is loading.

preserveFocus 
boolean

Whether to preserve current focused element while the menu is opening and options is changed (e.g. infinite scroll).

optionProps 
((option: SelectOption) => HTMLAttributes)

The getter to bound additional props to the root element of the option. This can be very useful when you want to add some props without having to directly mutate option.props or replicating the entire #option slot.

optionContentProps 
((option: SelectOption) => HTMLAttributes)

The getter to bound additional props to the content element of the option. This can be very useful when you want to add some props without having to directly mutate option.contentProps or replicating the entire #option-content slot.

optionDescriptionProps 
((option: SelectOption) => HTMLAttributes)

The getter to bound additional props to the description element of the option. This can be very useful when you want to add some props without having to directly mutate option.descriptionProps or replicating the entire #option-description slot.

Events

NameParametersDescription
@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 options & createable won't work, the call site must handle options manually.

Interface: (searchText: string) => (void | Promise<void>)

@refresh
DefinePropEmit

Function to refresh the options.

Interface: () => (void | Promise<void>)

@scrollBottom
DefinePropEmit<[menuHolder?: SelectOption]>

Fired when scrolling to bottom of a menu. option = undefined when it's root menu.

Interface: (menuHolder?: SelectOption) => (void | Promise<void>)

Slots

NameScopedDescription
#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

NameTypeDescription
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 assumeChecked).

setCheckedStates
(checkedKeys: Key[] | null | undefined) => void

Calculate, then set correct checked and indeterminate keys based on a list of checked keys.