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 SelectOptionBase } from '@holistics/design-system'

const options = [
  { value: 1, label: 'Option 1' },
  { value: 2, label: 'Option 2' },
  { value: 3, label: 'Option 3', disabled: true },
  { value: 4, label: 'Option 4' },
] as const satisfies SelectOptionBase[]

const value = ref<typeof options[number]['value']>()
</script>

<template>
  <HSelect
    v-model="value"
    :options="options"
    class="w-80"
  />
</template>

Examples

📦 Types of option

There are 7 types of options:

  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 SelectOptionBase, type SelectValue } from '@holistics/design-system'

const options: SelectOptionBase[] = [
  {
    stickyTop: true, value: 101, label: 'Top Option 1', tooltip: 'Lorem ipsum donor',
  },
  {
    stickyTop: true,
    value: 102,
    label: 'Top Option 2',
    tooltip: 'Some tips at a group',
    children: [
      { stickyTop: true, value: '102.1', label: 'Top Option 2.1' },
      {
        stickyTop: true, value: '102.2', label: 'Inner-Bell', icon: 'bell', action: () => { console.log('Inner-bell logged!') },
      },
    ],
    initialExpanded: true,
  },
  {
    stickyTop: true,
    value: 103,
    label: 'Top Action Option',
    icon: 'function',
    action: (option) => { alert(`You clicked an option with value: "${option.value}" and label: "${option.label}"`) },
    tooltip: { content: '<em>I can be HTML, too!</em>', html: true },
  },
  { value: 1, label: 'Option 1', tooltip: 'I can be anywhere!' },
  { value: 2, label: 'Option 2 (with icon)', icon: 'favorite' },
  { value: 3, label: 'Option 3', description: 'Option with description' },
  {
    value: 4, label: 'Option 4 has a VERY Very very long label', icon: 'data-set', description: 'Option with description & icon, and a very long long long long long long long long long long long description',
  },
  {
    value: 5, label: 'Option 5 (disabled)', disabled: true, tooltip: 'Disabled option only shows tooltip on hovering (not keyboad)!',
  },
  {
    value: 6,
    label: 'Option 6',
    children: [
      { label: 'Option a.1', value: '6.1', icon: 'hashtag' },
      { label: 'Option b.2', value: '6.2', icon: 'type/string' },
      {
        label: 'Option x.3',
        value: '6.3',
        children: [
          { label: 'Option d.3.1', value: '6.3.1' },
          { label: 'Option e.3.2', value: '6.3.2' },
          {
            label: 'Option x.3.3',
            value: '6.3.3',
            children: [
              { label: 'Option g.3.3.1', value: '6.3.3.1' },
              { label: 'Option h.3.3.2', value: '6.3.3.2' },
              { label: 'Option i.3.3.3', value: '6.3.3.3' },
              {
                label: 'Option x.3.3.4',
                value: '6.3.3.4',
                children: [
                  { label: 'Option k.3.3.4.1', value: '6.3.3.4.1' },
                  { label: 'Option l.3.3.4.2', value: '6.3.3.4.2' },
                  { label: 'Option 6.3.3.4.3', value: '6.3.3.4.3' },
                  { label: 'Option m.3.3.4.4', value: '6.3.3.4.4' },
                ],
              },
              { label: 'Option j.3.3.3', value: '6.3.3.5' },
            ],
          },
          { label: 'Option f.3.4', value: '6.3.4' },
        ],
        initialExpanded: true,
      },
      { label: 'Option c.4', value: '6.4' },
    ],
  },
  { value: 7, label: 'Option 7', icon: 'user' },
  { value: 8, label: 'Option 8' },
  { value: 9, label: 'Option 9' },
  { value: 10, label: 'Option 10' },
  { value: null, label: 'Option null' },
  { stickyBottom: true, value: 901, label: 'Bottom Option 1' },
  {
    stickyBottom: true,
    value: 902,
    label: 'Bottom Option 2',
    children: [
      { stickyBottom: true, value: '901.1', label: 'Bottom Option 2.1' },
      { stickyBottom: true, value: '901.2', label: 'Bottom Option 2.2' },
    ],
  },
  {
    stickyBottom: true,
    value: 903,
    label: 'Async Refresh (done after 2s)',
    icon: 'refresh',
    action: async () => {
      await new Promise((res) => { setTimeout(res, 2000) })
      alert('Done refreshing!')
    },
  },
]

const value = ref<SelectValue>()
</script>

<template>
  <HSelect
    v-model="value"
    :options="options"
    class="w-80"
  />
</template>
ts
import type { SelectOptionBase } from '@holistics/design-system'

export const OPTIONS: SelectOptionBase[] = [
  {
    stickyTop: true,
    value: '101',
    label: 'Top Option 1',
    tooltip: 'Lorem ipsum donor',
  },
  {
    stickyTop: true,
    value: '102',
    label: 'Top Option 2',
    tooltip: 'Some tips at a group',
    initialExpanded: true,
    children: [
      {
        stickyTop: true,
        value: '102.1',
        label: 'Top Option 2.1',
      },
      {
        stickyTop: true,
        value: '102.2',
        label: 'Inner-Bell',
        icon: 'bell',
        action: () => { console.log('Inner-bell logged!') },
      },
    ],
  },
  {
    stickyTop: true,
    value: '103',
    label: 'Top Action Option',
    icon: 'function',
    action: (option) => { alert(`You clicked an option with value: "${option.value}" and label: "${option.label}"`) },
    tooltip: { content: '<em>I can be HTML, too!</em>', html: true },
  },
  {
    value: '1',
    label: 'Option 1',
    tooltip: 'I can be anywhere!',
  },
  {
    value: '2',
    label: 'Option 2 (with icon)',
    icon: 'favorite',
  },
  {
    value: '3',
    label: 'Option 3',
    description: 'Option with description',
  },
  {
    value: '4',
    label: 'Option 4 has a VERY Very very long label',
    icon: 'data-set',
    description: 'Option with description & icon, and a very long long long long long long long long long long long description',
  },
  {
    value: '5',
    label: 'Option 5 (disabled)',
    disabled: true,
    tooltip: 'Disabled option only shows tooltip on hovering (not keyboad)!',
  },
  {
    value: '6',
    label: 'Option 6',
    children: [
      { label: 'Option a.1', value: '6.1', icon: 'hashtag' },
      { label: 'Option b.2', value: '6.2', icon: 'type/string' },
      {
        label: 'Option x.3',
        value: '6.3',
        initialExpanded: true,
        children: [
          { label: 'Option d.3.1', value: '6.3.1' },
          { label: 'Option e.3.2', value: '6.3.2' },
          {
            label: 'Option x.3.3',
            value: '6.3.3',
            children: [
              { label: 'Option g.3.3.1', value: '6.3.3.1' },
              { label: 'Option h.3.3.2', value: '6.3.3.2' },
              { label: 'Option i.3.3.3', value: '6.3.3.3' },
              {
                label: 'Option x.3.3.4',
                value: '6.3.3.4',
                children: [
                  { label: 'Option k.3.3.4.1', value: '6.3.3.4.1' },
                  { label: 'Option l.3.3.4.2', value: '6.3.3.4.2' },
                  { label: 'Option 6.3.3.4.3', value: '6.3.3.4.3' },
                  { label: 'Option m.3.3.4.4', value: '6.3.3.4.4' },
                ],
              },
              { label: 'Option j.3.3.3', value: '6.3.3.5' },
            ],
          },
          { label: 'Option f.3.4', value: '6.3.4' },
        ],
      },
      { label: 'Option c.4', value: '6.4' },
    ],
  },
  {
    value: '7',
    label: 'Option 7',
    icon: 'user',
  },
  {
    value: '8',
    label: 'Option 8',
  },
  {
    value: '9',
    label: 'Option 9',
  },
  {
    value: '10',
    label: 'Option 10',
  },
  {
    value: null,
    label: 'Option null',
  },
  {
    stickyBottom: true,
    value: '901',
    label: 'Bottom Option 1',
  },
  {
    stickyBottom: true,
    value: '902',
    label: 'Bottom Option 2',
    children: [
      {
        stickyBottom: true,
        value: '901.1',
        label: 'Bottom Option 2.1',
      },
      {
        stickyBottom: true,
        value: '901.2',
        label: 'Bottom Option 2.2',
      },
    ],
  },
  {
    stickyBottom: true,
    value: '903',
    label: 'Async Refresh (done after 2s)',
    icon: 'refresh',
    action: async () => {
      await new Promise((res) => { setTimeout(res, 2000) })
      alert('Done refreshing!')
    },
  },
]

export const ADVANCED_SEARCH_OPTIONS = [
  {
    value: 'users',
    label: 'Users',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'countries',
        label: 'Countries',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'cities',
            label: 'Cities',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              { value: 'beautiful-stories', label: 'Beautiful Stories' },
              { value: 'large-libraries', label: 'Large Libraries' },
            ],
          },
          {
            value: 'families',
            label: 'Families',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              { value: 'with-stories', label: 'With Stories' },
              { value: 'like-parties', label: 'Like Parties' },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'A',
    label: 'City Dwellers',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'B',
        label: 'Urban City',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'C',
            label: 'Analyze Data',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'D',
                label: 'Analytical Reports',
              },
            ],
          },
          {
            value: 'E',
            label: 'Childhood Memories',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'F',
                label: "Children's Games",
              },
            ],
          },
        ],
      },
      {
        value: 'G',
        label: 'Tooth Brushing',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'H',
            label: 'Teeth Cleaning',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'I',
                label: 'Mouse Clicks',
              },
              {
                value: 'J',
                label: 'Mice Running',
              },
            ],
          },
          {
            value: 'K',
            label: 'Foot Prints',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'L',
                label: 'Feet Walking',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'M',
    label: 'Person Interview',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'N',
        label: 'People Skills',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'O',
            label: 'Woman Power',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'P',
                label: 'Women Rights',
              },
            ],
          },
          {
            value: 'Q',
            label: 'Leaf Fall',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'R',
                label: 'Leaves Turning',
              },
            ],
          },
        ],
      },
      {
        value: 'S',
        label: 'Knife Sharp',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'T',
            label: 'Knives Set',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'U',
                label: 'Create Content',
              },
              {
                value: 'V',
                label: 'Creative Writing',
              },
            ],
          },
          {
            value: 'W',
            label: 'Develop Software',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'X',
                label: 'Development Tools',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'Y',
    label: 'Organize Events',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'Z',
        label: 'Organizational Skills',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AA',
            label: 'Apply Pressure',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AB',
                label: 'Application Forms',
              },
            ],
          },
          {
            value: 'AC',
            label: 'Multiply Numbers',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AD',
                label: 'Multiplication Tables',
              },
            ],
          },
        ],
      },
      {
        value: 'AE',
        label: 'Identify Problems',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AF',
            label: 'Identification Cards',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AG',
                label: 'Rectify Errors',
              },
              {
                value: 'AH',
                label: 'Rectification Process',
              },
            ],
          },
          {
            value: 'AI',
            label: 'Classify Items',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AJ',
                label: 'Classification System',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'AK',
    label: 'Beautiful Flowers',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'AL',
        label: 'Beauty Products',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AM',
            label: 'Quick Action',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AN',
                label: 'Quickly Done',
              },
            ],
          },
          {
            value: 'AO',
            label: 'Heavy Lifting',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AP',
                label: 'Heavily Loaded',
              },
            ],
          },
        ],
      },
      {
        value: 'AQ',
        label: 'Simple Solution',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AR',
            label: 'Simply Put',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AS',
                label: 'Possible outcome',
              },
              {
                value: 'AT',
                label: 'Possibly true',
              },
            ],
          },
          {
            value: 'AU',
            label: 'Electric cars',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AV',
                label: 'electricity bills',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'AW',
    label: 'Dramatic plays',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'AX',
        label: 'dramatically changed',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AY',
            label: 'Historic buildings',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AZ',
                label: 'historically accurate',
              },
            ],
          },
        ],
      },
    ],
  },
] as const satisfies SelectOptionBase[]

By default, group options are not selectable. To enable this, set groupSelectable: true. Enabling this changes the following behaviors:

  • To select/deselect a group option: Click / Enter
  • To expand a group option: Click on chevron icon > / Double-click / Shift + Enter
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 { SelectOptionBase } from '@holistics/design-system'

export const OPTIONS: SelectOptionBase[] = [
  {
    stickyTop: true,
    value: '101',
    label: 'Top Option 1',
    tooltip: 'Lorem ipsum donor',
  },
  {
    stickyTop: true,
    value: '102',
    label: 'Top Option 2',
    tooltip: 'Some tips at a group',
    initialExpanded: true,
    children: [
      {
        stickyTop: true,
        value: '102.1',
        label: 'Top Option 2.1',
      },
      {
        stickyTop: true,
        value: '102.2',
        label: 'Inner-Bell',
        icon: 'bell',
        action: () => { console.log('Inner-bell logged!') },
      },
    ],
  },
  {
    stickyTop: true,
    value: '103',
    label: 'Top Action Option',
    icon: 'function',
    action: (option) => { alert(`You clicked an option with value: "${option.value}" and label: "${option.label}"`) },
    tooltip: { content: '<em>I can be HTML, too!</em>', html: true },
  },
  {
    value: '1',
    label: 'Option 1',
    tooltip: 'I can be anywhere!',
  },
  {
    value: '2',
    label: 'Option 2 (with icon)',
    icon: 'favorite',
  },
  {
    value: '3',
    label: 'Option 3',
    description: 'Option with description',
  },
  {
    value: '4',
    label: 'Option 4 has a VERY Very very long label',
    icon: 'data-set',
    description: 'Option with description & icon, and a very long long long long long long long long long long long description',
  },
  {
    value: '5',
    label: 'Option 5 (disabled)',
    disabled: true,
    tooltip: 'Disabled option only shows tooltip on hovering (not keyboad)!',
  },
  {
    value: '6',
    label: 'Option 6',
    children: [
      { label: 'Option a.1', value: '6.1', icon: 'hashtag' },
      { label: 'Option b.2', value: '6.2', icon: 'type/string' },
      {
        label: 'Option x.3',
        value: '6.3',
        initialExpanded: true,
        children: [
          { label: 'Option d.3.1', value: '6.3.1' },
          { label: 'Option e.3.2', value: '6.3.2' },
          {
            label: 'Option x.3.3',
            value: '6.3.3',
            children: [
              { label: 'Option g.3.3.1', value: '6.3.3.1' },
              { label: 'Option h.3.3.2', value: '6.3.3.2' },
              { label: 'Option i.3.3.3', value: '6.3.3.3' },
              {
                label: 'Option x.3.3.4',
                value: '6.3.3.4',
                children: [
                  { label: 'Option k.3.3.4.1', value: '6.3.3.4.1' },
                  { label: 'Option l.3.3.4.2', value: '6.3.3.4.2' },
                  { label: 'Option 6.3.3.4.3', value: '6.3.3.4.3' },
                  { label: 'Option m.3.3.4.4', value: '6.3.3.4.4' },
                ],
              },
              { label: 'Option j.3.3.3', value: '6.3.3.5' },
            ],
          },
          { label: 'Option f.3.4', value: '6.3.4' },
        ],
      },
      { label: 'Option c.4', value: '6.4' },
    ],
  },
  {
    value: '7',
    label: 'Option 7',
    icon: 'user',
  },
  {
    value: '8',
    label: 'Option 8',
  },
  {
    value: '9',
    label: 'Option 9',
  },
  {
    value: '10',
    label: 'Option 10',
  },
  {
    value: null,
    label: 'Option null',
  },
  {
    stickyBottom: true,
    value: '901',
    label: 'Bottom Option 1',
  },
  {
    stickyBottom: true,
    value: '902',
    label: 'Bottom Option 2',
    children: [
      {
        stickyBottom: true,
        value: '901.1',
        label: 'Bottom Option 2.1',
      },
      {
        stickyBottom: true,
        value: '901.2',
        label: 'Bottom Option 2.2',
      },
    ],
  },
  {
    stickyBottom: true,
    value: '903',
    label: 'Async Refresh (done after 2s)',
    icon: 'refresh',
    action: async () => {
      await new Promise((res) => { setTimeout(res, 2000) })
      alert('Done refreshing!')
    },
  },
]

export const ADVANCED_SEARCH_OPTIONS = [
  {
    value: 'users',
    label: 'Users',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'countries',
        label: 'Countries',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'cities',
            label: 'Cities',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              { value: 'beautiful-stories', label: 'Beautiful Stories' },
              { value: 'large-libraries', label: 'Large Libraries' },
            ],
          },
          {
            value: 'families',
            label: 'Families',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              { value: 'with-stories', label: 'With Stories' },
              { value: 'like-parties', label: 'Like Parties' },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'A',
    label: 'City Dwellers',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'B',
        label: 'Urban City',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'C',
            label: 'Analyze Data',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'D',
                label: 'Analytical Reports',
              },
            ],
          },
          {
            value: 'E',
            label: 'Childhood Memories',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'F',
                label: "Children's Games",
              },
            ],
          },
        ],
      },
      {
        value: 'G',
        label: 'Tooth Brushing',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'H',
            label: 'Teeth Cleaning',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'I',
                label: 'Mouse Clicks',
              },
              {
                value: 'J',
                label: 'Mice Running',
              },
            ],
          },
          {
            value: 'K',
            label: 'Foot Prints',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'L',
                label: 'Feet Walking',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'M',
    label: 'Person Interview',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'N',
        label: 'People Skills',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'O',
            label: 'Woman Power',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'P',
                label: 'Women Rights',
              },
            ],
          },
          {
            value: 'Q',
            label: 'Leaf Fall',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'R',
                label: 'Leaves Turning',
              },
            ],
          },
        ],
      },
      {
        value: 'S',
        label: 'Knife Sharp',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'T',
            label: 'Knives Set',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'U',
                label: 'Create Content',
              },
              {
                value: 'V',
                label: 'Creative Writing',
              },
            ],
          },
          {
            value: 'W',
            label: 'Develop Software',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'X',
                label: 'Development Tools',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'Y',
    label: 'Organize Events',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'Z',
        label: 'Organizational Skills',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AA',
            label: 'Apply Pressure',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AB',
                label: 'Application Forms',
              },
            ],
          },
          {
            value: 'AC',
            label: 'Multiply Numbers',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AD',
                label: 'Multiplication Tables',
              },
            ],
          },
        ],
      },
      {
        value: 'AE',
        label: 'Identify Problems',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AF',
            label: 'Identification Cards',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AG',
                label: 'Rectify Errors',
              },
              {
                value: 'AH',
                label: 'Rectification Process',
              },
            ],
          },
          {
            value: 'AI',
            label: 'Classify Items',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AJ',
                label: 'Classification System',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'AK',
    label: 'Beautiful Flowers',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'AL',
        label: 'Beauty Products',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AM',
            label: 'Quick Action',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AN',
                label: 'Quickly Done',
              },
            ],
          },
          {
            value: 'AO',
            label: 'Heavy Lifting',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AP',
                label: 'Heavily Loaded',
              },
            ],
          },
        ],
      },
      {
        value: 'AQ',
        label: 'Simple Solution',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AR',
            label: 'Simply Put',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AS',
                label: 'Possible outcome',
              },
              {
                value: 'AT',
                label: 'Possibly true',
              },
            ],
          },
          {
            value: 'AU',
            label: 'Electric cars',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AV',
                label: 'electricity bills',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'AW',
    label: 'Dramatic plays',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'AX',
        label: 'dramatically changed',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AY',
            label: 'Historic buildings',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AZ',
                label: 'historically accurate',
              },
            ],
          },
        ],
      },
    ],
  },
] as const satisfies SelectOptionBase[]

📋 Multiple Select

Allow users to select more than one option.

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 { SelectOptionBase } from '@holistics/design-system'

export const options: SelectOptionBase[] = [
  { value: 1, label: 'Option 1' },
  { value: 2, label: 'Option 2' },
  { value: 3, label: 'Option 3', disabled: true },
  { value: 4, label: 'Option 4 (longer)' },
  { value: 5, label: 'Option 5 (even longgggeeerrr)' },
  {
    value: 6,
    label: 'Option 6',
    children: [
      { value: '6.1', label: 'Child of Option 6' },
      { value: '6.2', label: 'Another child of Option 6', disabled: true },
      { value: '6.3', label: 'Last child of Option 6' },
    ],
  },
  { value: 7, label: 'Option 7' },
  { value: 8, label: '8' },
  { value: 9, label: 'Option 9', description: 'Some explanation for Option 9...' },
  { value: null, label: 'Option with null value' },
]

With counter

By default, Select always adjusts its height to fit all selected options. If you want to force a consistent height, set multiple: 'counter'. This will display a counter of remaining selected options if the list is too long and can't fit in 1 row (similar to a TagList).

🔍 multiple: 'counter' won't work with filterable: true

This is because filterable requires an <input> placed after the selected options. But multiple: 'counter' forces the Select to have a consistent height, so the <input> might not have the required space to type any text.

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 { SelectOptionBase } from '@holistics/design-system'

export const options: SelectOptionBase[] = [
  { value: 1, label: 'Option 1' },
  { value: 2, label: 'Option 2' },
  { value: 3, label: 'Option 3', disabled: true },
  { value: 4, label: 'Option 4 (longer)' },
  { value: 5, label: 'Option 5 (even longgggeeerrr)' },
  {
    value: 6,
    label: 'Option 6',
    children: [
      { value: '6.1', label: 'Child of Option 6' },
      { value: '6.2', label: 'Another child of Option 6', disabled: true },
      { value: '6.3', label: 'Last child of Option 6' },
    ],
  },
  { value: 7, label: 'Option 7' },
  { value: 8, label: '8' },
  { value: 9, label: 'Option 9', description: 'Some explanation for Option 9...' },
  { value: null, label: 'Option with null value' },
]

✅ Checkbox

Enable checkboxes in your select for use cases like file selection, permission management, or multi-item actions by setting checkable: true. The checked states are managed through two v-model bindings: the default modelValue and indeterminateKeys.

Implicit behaviors when checkbox is enabled

  • 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 { SelectOptionBase } from '@holistics/design-system'

export const OPTIONS: SelectOptionBase[] = [
  {
    stickyTop: true,
    value: '101',
    label: 'Top Option 1',
    tooltip: 'Lorem ipsum donor',
  },
  {
    stickyTop: true,
    value: '102',
    label: 'Top Option 2',
    tooltip: 'Some tips at a group',
    initialExpanded: true,
    children: [
      {
        stickyTop: true,
        value: '102.1',
        label: 'Top Option 2.1',
      },
      {
        stickyTop: true,
        value: '102.2',
        label: 'Inner-Bell',
        icon: 'bell',
        action: () => { console.log('Inner-bell logged!') },
      },
    ],
  },
  {
    stickyTop: true,
    value: '103',
    label: 'Top Action Option',
    icon: 'function',
    action: (option) => { alert(`You clicked an option with value: "${option.value}" and label: "${option.label}"`) },
    tooltip: { content: '<em>I can be HTML, too!</em>', html: true },
  },
  {
    value: '1',
    label: 'Option 1',
    tooltip: 'I can be anywhere!',
  },
  {
    value: '2',
    label: 'Option 2 (with icon)',
    icon: 'favorite',
  },
  {
    value: '3',
    label: 'Option 3',
    description: 'Option with description',
  },
  {
    value: '4',
    label: 'Option 4 has a VERY Very very long label',
    icon: 'data-set',
    description: 'Option with description & icon, and a very long long long long long long long long long long long description',
  },
  {
    value: '5',
    label: 'Option 5 (disabled)',
    disabled: true,
    tooltip: 'Disabled option only shows tooltip on hovering (not keyboad)!',
  },
  {
    value: '6',
    label: 'Option 6',
    children: [
      { label: 'Option a.1', value: '6.1', icon: 'hashtag' },
      { label: 'Option b.2', value: '6.2', icon: 'type/string' },
      {
        label: 'Option x.3',
        value: '6.3',
        initialExpanded: true,
        children: [
          { label: 'Option d.3.1', value: '6.3.1' },
          { label: 'Option e.3.2', value: '6.3.2' },
          {
            label: 'Option x.3.3',
            value: '6.3.3',
            children: [
              { label: 'Option g.3.3.1', value: '6.3.3.1' },
              { label: 'Option h.3.3.2', value: '6.3.3.2' },
              { label: 'Option i.3.3.3', value: '6.3.3.3' },
              {
                label: 'Option x.3.3.4',
                value: '6.3.3.4',
                children: [
                  { label: 'Option k.3.3.4.1', value: '6.3.3.4.1' },
                  { label: 'Option l.3.3.4.2', value: '6.3.3.4.2' },
                  { label: 'Option 6.3.3.4.3', value: '6.3.3.4.3' },
                  { label: 'Option m.3.3.4.4', value: '6.3.3.4.4' },
                ],
              },
              { label: 'Option j.3.3.3', value: '6.3.3.5' },
            ],
          },
          { label: 'Option f.3.4', value: '6.3.4' },
        ],
      },
      { label: 'Option c.4', value: '6.4' },
    ],
  },
  {
    value: '7',
    label: 'Option 7',
    icon: 'user',
  },
  {
    value: '8',
    label: 'Option 8',
  },
  {
    value: '9',
    label: 'Option 9',
  },
  {
    value: '10',
    label: 'Option 10',
  },
  {
    value: null,
    label: 'Option null',
  },
  {
    stickyBottom: true,
    value: '901',
    label: 'Bottom Option 1',
  },
  {
    stickyBottom: true,
    value: '902',
    label: 'Bottom Option 2',
    children: [
      {
        stickyBottom: true,
        value: '901.1',
        label: 'Bottom Option 2.1',
      },
      {
        stickyBottom: true,
        value: '901.2',
        label: 'Bottom Option 2.2',
      },
    ],
  },
  {
    stickyBottom: true,
    value: '903',
    label: 'Async Refresh (done after 2s)',
    icon: 'refresh',
    action: async () => {
      await new Promise((res) => { setTimeout(res, 2000) })
      alert('Done refreshing!')
    },
  },
]

export const ADVANCED_SEARCH_OPTIONS = [
  {
    value: 'users',
    label: 'Users',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'countries',
        label: 'Countries',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'cities',
            label: 'Cities',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              { value: 'beautiful-stories', label: 'Beautiful Stories' },
              { value: 'large-libraries', label: 'Large Libraries' },
            ],
          },
          {
            value: 'families',
            label: 'Families',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              { value: 'with-stories', label: 'With Stories' },
              { value: 'like-parties', label: 'Like Parties' },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'A',
    label: 'City Dwellers',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'B',
        label: 'Urban City',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'C',
            label: 'Analyze Data',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'D',
                label: 'Analytical Reports',
              },
            ],
          },
          {
            value: 'E',
            label: 'Childhood Memories',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'F',
                label: "Children's Games",
              },
            ],
          },
        ],
      },
      {
        value: 'G',
        label: 'Tooth Brushing',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'H',
            label: 'Teeth Cleaning',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'I',
                label: 'Mouse Clicks',
              },
              {
                value: 'J',
                label: 'Mice Running',
              },
            ],
          },
          {
            value: 'K',
            label: 'Foot Prints',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'L',
                label: 'Feet Walking',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'M',
    label: 'Person Interview',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'N',
        label: 'People Skills',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'O',
            label: 'Woman Power',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'P',
                label: 'Women Rights',
              },
            ],
          },
          {
            value: 'Q',
            label: 'Leaf Fall',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'R',
                label: 'Leaves Turning',
              },
            ],
          },
        ],
      },
      {
        value: 'S',
        label: 'Knife Sharp',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'T',
            label: 'Knives Set',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'U',
                label: 'Create Content',
              },
              {
                value: 'V',
                label: 'Creative Writing',
              },
            ],
          },
          {
            value: 'W',
            label: 'Develop Software',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'X',
                label: 'Development Tools',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'Y',
    label: 'Organize Events',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'Z',
        label: 'Organizational Skills',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AA',
            label: 'Apply Pressure',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AB',
                label: 'Application Forms',
              },
            ],
          },
          {
            value: 'AC',
            label: 'Multiply Numbers',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AD',
                label: 'Multiplication Tables',
              },
            ],
          },
        ],
      },
      {
        value: 'AE',
        label: 'Identify Problems',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AF',
            label: 'Identification Cards',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AG',
                label: 'Rectify Errors',
              },
              {
                value: 'AH',
                label: 'Rectification Process',
              },
            ],
          },
          {
            value: 'AI',
            label: 'Classify Items',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AJ',
                label: 'Classification System',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'AK',
    label: 'Beautiful Flowers',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'AL',
        label: 'Beauty Products',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AM',
            label: 'Quick Action',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AN',
                label: 'Quickly Done',
              },
            ],
          },
          {
            value: 'AO',
            label: 'Heavy Lifting',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AP',
                label: 'Heavily Loaded',
              },
            ],
          },
        ],
      },
      {
        value: 'AQ',
        label: 'Simple Solution',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AR',
            label: 'Simply Put',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AS',
                label: 'Possible outcome',
              },
              {
                value: 'AT',
                label: 'Possibly true',
              },
            ],
          },
          {
            value: 'AU',
            label: 'Electric cars',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AV',
                label: 'electricity bills',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'AW',
    label: 'Dramatic plays',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'AX',
        label: 'dramatically changed',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AY',
            label: 'Historic buildings',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AZ',
                label: 'historically accurate',
              },
            ],
          },
        ],
      },
    ],
  },
] as const satisfies SelectOptionBase[]

Select with checkbox doesn't support options with null or boolean value

This is because the algorithm used to sync checked states doesn't support null/boolean values. If your use-case absolutely needs null/boolean values, try converting them to string 'null' | 'true' | 'false' first, then deserializing it at @update:modelValue. You will encounter some rough cases, but it's the current limitation...

⌨️ Keyboard Interactions

Select automatically handles keys in the following table when it receives focus.

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 { SelectOptionBase } from '@holistics/design-system'

export const OPTIONS: SelectOptionBase[] = [
  {
    stickyTop: true,
    value: '101',
    label: 'Top Option 1',
    tooltip: 'Lorem ipsum donor',
  },
  {
    stickyTop: true,
    value: '102',
    label: 'Top Option 2',
    tooltip: 'Some tips at a group',
    initialExpanded: true,
    children: [
      {
        stickyTop: true,
        value: '102.1',
        label: 'Top Option 2.1',
      },
      {
        stickyTop: true,
        value: '102.2',
        label: 'Inner-Bell',
        icon: 'bell',
        action: () => { console.log('Inner-bell logged!') },
      },
    ],
  },
  {
    stickyTop: true,
    value: '103',
    label: 'Top Action Option',
    icon: 'function',
    action: (option) => { alert(`You clicked an option with value: "${option.value}" and label: "${option.label}"`) },
    tooltip: { content: '<em>I can be HTML, too!</em>', html: true },
  },
  {
    value: '1',
    label: 'Option 1',
    tooltip: 'I can be anywhere!',
  },
  {
    value: '2',
    label: 'Option 2 (with icon)',
    icon: 'favorite',
  },
  {
    value: '3',
    label: 'Option 3',
    description: 'Option with description',
  },
  {
    value: '4',
    label: 'Option 4 has a VERY Very very long label',
    icon: 'data-set',
    description: 'Option with description & icon, and a very long long long long long long long long long long long description',
  },
  {
    value: '5',
    label: 'Option 5 (disabled)',
    disabled: true,
    tooltip: 'Disabled option only shows tooltip on hovering (not keyboad)!',
  },
  {
    value: '6',
    label: 'Option 6',
    children: [
      { label: 'Option a.1', value: '6.1', icon: 'hashtag' },
      { label: 'Option b.2', value: '6.2', icon: 'type/string' },
      {
        label: 'Option x.3',
        value: '6.3',
        initialExpanded: true,
        children: [
          { label: 'Option d.3.1', value: '6.3.1' },
          { label: 'Option e.3.2', value: '6.3.2' },
          {
            label: 'Option x.3.3',
            value: '6.3.3',
            children: [
              { label: 'Option g.3.3.1', value: '6.3.3.1' },
              { label: 'Option h.3.3.2', value: '6.3.3.2' },
              { label: 'Option i.3.3.3', value: '6.3.3.3' },
              {
                label: 'Option x.3.3.4',
                value: '6.3.3.4',
                children: [
                  { label: 'Option k.3.3.4.1', value: '6.3.3.4.1' },
                  { label: 'Option l.3.3.4.2', value: '6.3.3.4.2' },
                  { label: 'Option 6.3.3.4.3', value: '6.3.3.4.3' },
                  { label: 'Option m.3.3.4.4', value: '6.3.3.4.4' },
                ],
              },
              { label: 'Option j.3.3.3', value: '6.3.3.5' },
            ],
          },
          { label: 'Option f.3.4', value: '6.3.4' },
        ],
      },
      { label: 'Option c.4', value: '6.4' },
    ],
  },
  {
    value: '7',
    label: 'Option 7',
    icon: 'user',
  },
  {
    value: '8',
    label: 'Option 8',
  },
  {
    value: '9',
    label: 'Option 9',
  },
  {
    value: '10',
    label: 'Option 10',
  },
  {
    value: null,
    label: 'Option null',
  },
  {
    stickyBottom: true,
    value: '901',
    label: 'Bottom Option 1',
  },
  {
    stickyBottom: true,
    value: '902',
    label: 'Bottom Option 2',
    children: [
      {
        stickyBottom: true,
        value: '901.1',
        label: 'Bottom Option 2.1',
      },
      {
        stickyBottom: true,
        value: '901.2',
        label: 'Bottom Option 2.2',
      },
    ],
  },
  {
    stickyBottom: true,
    value: '903',
    label: 'Async Refresh (done after 2s)',
    icon: 'refresh',
    action: async () => {
      await new Promise((res) => { setTimeout(res, 2000) })
      alert('Done refreshing!')
    },
  },
]

export const ADVANCED_SEARCH_OPTIONS = [
  {
    value: 'users',
    label: 'Users',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'countries',
        label: 'Countries',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'cities',
            label: 'Cities',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              { value: 'beautiful-stories', label: 'Beautiful Stories' },
              { value: 'large-libraries', label: 'Large Libraries' },
            ],
          },
          {
            value: 'families',
            label: 'Families',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              { value: 'with-stories', label: 'With Stories' },
              { value: 'like-parties', label: 'Like Parties' },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'A',
    label: 'City Dwellers',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'B',
        label: 'Urban City',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'C',
            label: 'Analyze Data',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'D',
                label: 'Analytical Reports',
              },
            ],
          },
          {
            value: 'E',
            label: 'Childhood Memories',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'F',
                label: "Children's Games",
              },
            ],
          },
        ],
      },
      {
        value: 'G',
        label: 'Tooth Brushing',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'H',
            label: 'Teeth Cleaning',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'I',
                label: 'Mouse Clicks',
              },
              {
                value: 'J',
                label: 'Mice Running',
              },
            ],
          },
          {
            value: 'K',
            label: 'Foot Prints',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'L',
                label: 'Feet Walking',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'M',
    label: 'Person Interview',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'N',
        label: 'People Skills',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'O',
            label: 'Woman Power',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'P',
                label: 'Women Rights',
              },
            ],
          },
          {
            value: 'Q',
            label: 'Leaf Fall',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'R',
                label: 'Leaves Turning',
              },
            ],
          },
        ],
      },
      {
        value: 'S',
        label: 'Knife Sharp',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'T',
            label: 'Knives Set',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'U',
                label: 'Create Content',
              },
              {
                value: 'V',
                label: 'Creative Writing',
              },
            ],
          },
          {
            value: 'W',
            label: 'Develop Software',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'X',
                label: 'Development Tools',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'Y',
    label: 'Organize Events',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'Z',
        label: 'Organizational Skills',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AA',
            label: 'Apply Pressure',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AB',
                label: 'Application Forms',
              },
            ],
          },
          {
            value: 'AC',
            label: 'Multiply Numbers',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AD',
                label: 'Multiplication Tables',
              },
            ],
          },
        ],
      },
      {
        value: 'AE',
        label: 'Identify Problems',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AF',
            label: 'Identification Cards',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AG',
                label: 'Rectify Errors',
              },
              {
                value: 'AH',
                label: 'Rectification Process',
              },
            ],
          },
          {
            value: 'AI',
            label: 'Classify Items',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AJ',
                label: 'Classification System',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'AK',
    label: 'Beautiful Flowers',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'AL',
        label: 'Beauty Products',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AM',
            label: 'Quick Action',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AN',
                label: 'Quickly Done',
              },
            ],
          },
          {
            value: 'AO',
            label: 'Heavy Lifting',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AP',
                label: 'Heavily Loaded',
              },
            ],
          },
        ],
      },
      {
        value: 'AQ',
        label: 'Simple Solution',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AR',
            label: 'Simply Put',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AS',
                label: 'Possible outcome',
              },
              {
                value: 'AT',
                label: 'Possibly true',
              },
            ],
          },
          {
            value: 'AU',
            label: 'Electric cars',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AV',
                label: 'electricity bills',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'AW',
    label: 'Dramatic plays',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'AX',
        label: 'dramatically changed',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AY',
            label: 'Historic buildings',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AZ',
                label: 'historically accurate',
              },
            ],
          },
        ],
      },
    ],
  },
] as const satisfies SelectOptionBase[]

🌳 Relationship with Tree

Unlike Tree, Select:

  • Only supports filtering (hiding unmatched options).
  • Automatically re-focuses on the first option whenever the search text changes.
  • Automatically clears search text after selecting.
  • Does not support custom stem index or search algorithm.

These differences come from the fact that Select is typically used for use cases where options are passed once and the menu displays only a few options at a time. In contrast, Tree usually involves more actions (adding, moving, deleting, etc.) and is used in larger vertical layouts with deeper hierarchies.

🔍 Filterable (Async)

You can listen to the search text via the @search event and control which options are rendered.

WARNING

When using @search, the built-in search behavior from filterable* and creatable props is disabled. You must implement your own search logic.

vue
<script setup lang="ts">
import { type Ref, ref } from 'vue'
import { HSelect, type SelectValue, type SelectOptionBase } from '@holistics/design-system'
import { OPTIONS as baseOptions } from './options'

// @ts-expect-error Type instantiation is excessively deep and possibly infinite. Vue's `ref()` is doing too much work here...
const options = ref(baseOptions) as Ref<SelectOptionBase[]>

const value = ref<SelectValue | SelectValue[]>()

async function onSearch (text: string) {
  if (!text) {
    options.value = baseOptions
    return
  }

  await new Promise((res) => { setTimeout(res, 3000) })
  options.value = baseOptions.filter((o) => o.label.includes(text)) // [!code warning] // 🗃️️ Should be a list returned from the server
}
</script>

<template>
  <div class="space-y-4">
    <HSelect
      v-model="value"
      :options="options"
      filterable
      class="w-80"
      @search="onSearch"
    />

    <HSelect
      v-model="value"
      :options="options"
      mutliple
      filterable
      placeholder="Select multiple..."
      class="w-80"
      @search="onSearch"
    />
  </div>
</template>
ts
import type { SelectOptionBase } from '@holistics/design-system'

export const OPTIONS: SelectOptionBase[] = [
  {
    stickyTop: true,
    value: '101',
    label: 'Top Option 1',
    tooltip: 'Lorem ipsum donor',
  },
  {
    stickyTop: true,
    value: '102',
    label: 'Top Option 2',
    tooltip: 'Some tips at a group',
    initialExpanded: true,
    children: [
      {
        stickyTop: true,
        value: '102.1',
        label: 'Top Option 2.1',
      },
      {
        stickyTop: true,
        value: '102.2',
        label: 'Inner-Bell',
        icon: 'bell',
        action: () => { console.log('Inner-bell logged!') },
      },
    ],
  },
  {
    stickyTop: true,
    value: '103',
    label: 'Top Action Option',
    icon: 'function',
    action: (option) => { alert(`You clicked an option with value: "${option.value}" and label: "${option.label}"`) },
    tooltip: { content: '<em>I can be HTML, too!</em>', html: true },
  },
  {
    value: '1',
    label: 'Option 1',
    tooltip: 'I can be anywhere!',
  },
  {
    value: '2',
    label: 'Option 2 (with icon)',
    icon: 'favorite',
  },
  {
    value: '3',
    label: 'Option 3',
    description: 'Option with description',
  },
  {
    value: '4',
    label: 'Option 4 has a VERY Very very long label',
    icon: 'data-set',
    description: 'Option with description & icon, and a very long long long long long long long long long long long description',
  },
  {
    value: '5',
    label: 'Option 5 (disabled)',
    disabled: true,
    tooltip: 'Disabled option only shows tooltip on hovering (not keyboad)!',
  },
  {
    value: '6',
    label: 'Option 6',
    children: [
      { label: 'Option a.1', value: '6.1', icon: 'hashtag' },
      { label: 'Option b.2', value: '6.2', icon: 'type/string' },
      {
        label: 'Option x.3',
        value: '6.3',
        initialExpanded: true,
        children: [
          { label: 'Option d.3.1', value: '6.3.1' },
          { label: 'Option e.3.2', value: '6.3.2' },
          {
            label: 'Option x.3.3',
            value: '6.3.3',
            children: [
              { label: 'Option g.3.3.1', value: '6.3.3.1' },
              { label: 'Option h.3.3.2', value: '6.3.3.2' },
              { label: 'Option i.3.3.3', value: '6.3.3.3' },
              {
                label: 'Option x.3.3.4',
                value: '6.3.3.4',
                children: [
                  { label: 'Option k.3.3.4.1', value: '6.3.3.4.1' },
                  { label: 'Option l.3.3.4.2', value: '6.3.3.4.2' },
                  { label: 'Option 6.3.3.4.3', value: '6.3.3.4.3' },
                  { label: 'Option m.3.3.4.4', value: '6.3.3.4.4' },
                ],
              },
              { label: 'Option j.3.3.3', value: '6.3.3.5' },
            ],
          },
          { label: 'Option f.3.4', value: '6.3.4' },
        ],
      },
      { label: 'Option c.4', value: '6.4' },
    ],
  },
  {
    value: '7',
    label: 'Option 7',
    icon: 'user',
  },
  {
    value: '8',
    label: 'Option 8',
  },
  {
    value: '9',
    label: 'Option 9',
  },
  {
    value: '10',
    label: 'Option 10',
  },
  {
    value: null,
    label: 'Option null',
  },
  {
    stickyBottom: true,
    value: '901',
    label: 'Bottom Option 1',
  },
  {
    stickyBottom: true,
    value: '902',
    label: 'Bottom Option 2',
    children: [
      {
        stickyBottom: true,
        value: '901.1',
        label: 'Bottom Option 2.1',
      },
      {
        stickyBottom: true,
        value: '901.2',
        label: 'Bottom Option 2.2',
      },
    ],
  },
  {
    stickyBottom: true,
    value: '903',
    label: 'Async Refresh (done after 2s)',
    icon: 'refresh',
    action: async () => {
      await new Promise((res) => { setTimeout(res, 2000) })
      alert('Done refreshing!')
    },
  },
]

export const ADVANCED_SEARCH_OPTIONS = [
  {
    value: 'users',
    label: 'Users',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'countries',
        label: 'Countries',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'cities',
            label: 'Cities',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              { value: 'beautiful-stories', label: 'Beautiful Stories' },
              { value: 'large-libraries', label: 'Large Libraries' },
            ],
          },
          {
            value: 'families',
            label: 'Families',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              { value: 'with-stories', label: 'With Stories' },
              { value: 'like-parties', label: 'Like Parties' },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'A',
    label: 'City Dwellers',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'B',
        label: 'Urban City',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'C',
            label: 'Analyze Data',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'D',
                label: 'Analytical Reports',
              },
            ],
          },
          {
            value: 'E',
            label: 'Childhood Memories',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'F',
                label: "Children's Games",
              },
            ],
          },
        ],
      },
      {
        value: 'G',
        label: 'Tooth Brushing',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'H',
            label: 'Teeth Cleaning',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'I',
                label: 'Mouse Clicks',
              },
              {
                value: 'J',
                label: 'Mice Running',
              },
            ],
          },
          {
            value: 'K',
            label: 'Foot Prints',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'L',
                label: 'Feet Walking',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'M',
    label: 'Person Interview',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'N',
        label: 'People Skills',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'O',
            label: 'Woman Power',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'P',
                label: 'Women Rights',
              },
            ],
          },
          {
            value: 'Q',
            label: 'Leaf Fall',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'R',
                label: 'Leaves Turning',
              },
            ],
          },
        ],
      },
      {
        value: 'S',
        label: 'Knife Sharp',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'T',
            label: 'Knives Set',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'U',
                label: 'Create Content',
              },
              {
                value: 'V',
                label: 'Creative Writing',
              },
            ],
          },
          {
            value: 'W',
            label: 'Develop Software',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'X',
                label: 'Development Tools',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'Y',
    label: 'Organize Events',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'Z',
        label: 'Organizational Skills',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AA',
            label: 'Apply Pressure',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AB',
                label: 'Application Forms',
              },
            ],
          },
          {
            value: 'AC',
            label: 'Multiply Numbers',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AD',
                label: 'Multiplication Tables',
              },
            ],
          },
        ],
      },
      {
        value: 'AE',
        label: 'Identify Problems',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AF',
            label: 'Identification Cards',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AG',
                label: 'Rectify Errors',
              },
              {
                value: 'AH',
                label: 'Rectification Process',
              },
            ],
          },
          {
            value: 'AI',
            label: 'Classify Items',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AJ',
                label: 'Classification System',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'AK',
    label: 'Beautiful Flowers',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'AL',
        label: 'Beauty Products',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AM',
            label: 'Quick Action',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AN',
                label: 'Quickly Done',
              },
            ],
          },
          {
            value: 'AO',
            label: 'Heavy Lifting',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AP',
                label: 'Heavily Loaded',
              },
            ],
          },
        ],
      },
      {
        value: 'AQ',
        label: 'Simple Solution',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AR',
            label: 'Simply Put',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AS',
                label: 'Possible outcome',
              },
              {
                value: 'AT',
                label: 'Possibly true',
              },
            ],
          },
          {
            value: 'AU',
            label: 'Electric cars',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AV',
                label: 'electricity bills',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'AW',
    label: 'Dramatic plays',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'AX',
        label: 'dramatically changed',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AY',
            label: 'Historic buildings',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AZ',
                label: 'historically accurate',
              },
            ],
          },
        ],
      },
    ],
  },
] as const satisfies SelectOptionBase[]

➕ Create Options Dynamically

Allow users to create new options on the fly by typing a value that doesn't exist in the list.

By default, newly created options are root-level and are put at the bottom of the filterable list (i.e. before sticky bottom options). To put them at top, set createAtTop: true.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { HSelect, type SelectOptionBase } from '@holistics/design-system'

const options = [
  { value: 1, label: 'Option 1' },
  { value: 2, label: 'Option 2' },
  { value: 3, label: 'Option 3', disabled: true },
  { value: 4, label: 'Option 4' },
] as const satisfies SelectOptionBase[]

const value = ref<typeof options[number]['value']>()
</script>

<template>
  <div class="space-y-4">
    <HSelect
      v-model="value"
      :options="options"
      filterable
      createable
      class="w-80"
    />

    <HSelect
      v-model="value"
      :options="options"
      multiple
      filterable
      createable
      placeholder="Select multiple..."
      class="w-80"
    />
  </div>
</template>

🧹 Clearable

Show a clear button at the right side of the trigger that allows users to reset the selection. Works for both single and multiple select modes.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { HSelect, type SelectOptionBase } from '@holistics/design-system'

const options = [
  { value: 1, label: 'Option 1' },
  { value: 2, label: 'Option 2' },
  { value: 3, label: 'Option 3', disabled: true },
  { value: 4, label: 'Option 4' },
] as const satisfies SelectOptionBase[]

const value = ref<typeof options[number]['value']>()
</script>

<template>
  <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-1 2xl:grid-cols-2">
    <HSelect
      v-model="value"
      :options="options"
      clearable
      class="w-80"
    />

    <HSelect
      v-model="value"
      :options="options"
      filterable
      clearable
      class="w-80"
    />

    <HSelect
      v-model="value"
      :options="options"
      multiple
      clearable
      placeholder="Select multiple..."
      class="w-80"
    />

    <HSelect
      v-model="value"
      :options="options"
      multiple
      filterable
      clearable
      placeholder="Select multiple..."
      class="w-80"
    />
  </div>
</template>

🔄 With Refresh Button

Add a refresh button to reload the options, useful when options are fetched from an API.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { HSelect, type SelectOptionBase } from '@holistics/design-system'

const options = [
  { value: 1, label: 'Option 1' },
  { value: 2, label: 'Option 2' },
  { value: 3, label: 'Option 3', disabled: true },
  { value: 4, label: 'Option 4' },
] as const satisfies SelectOptionBase[]

const value = ref<typeof options[number]['value']>()

async function onRefresh () {
  await new Promise((res) => { setTimeout(res, 2000) })
}
</script>

<template>
  <HSelect
    v-model="value"
    :options="options"
    class="w-80"
    @refresh="onRefresh"
  />
</template>

♾️ Infinite Scroll

Load more options as the user scrolls to the bottom of the list, ideal for large datasets.

vue
<script setup lang="ts">
import { type Ref, ref } from 'vue'
import { HSelect, type SelectOptionBase, type SelectValue } from '@holistics/design-system'

const options = ref([
  { value: 1, label: 'Option 1' },
  { value: 2, label: 'Option 2' },
  { value: 3, label: 'Option 3', disabled: true },
  { value: 4, label: 'Option 4' },
  { value: 5, label: 'Option 5' },
  { value: 6, label: 'Option 6' },
  { value: 7, label: 'Option 7' },
  { value: 8, label: 'Option 8' },
]) as Ref<SelectOptionBase[]>

const value = ref<SelectValue>()

const page = ref(0)
async function onScrollBottom () {
  await new Promise((res) => { setTimeout(res, 3000) })

  const temp = options.value.slice()
  // [!code warning] // 🗃️ Should be a list returned from the server
  temp.push(...Array.from({ length: 5 }, (_, i) => {
    const num = page.value * 5 + (i + 1)
    return {
      value: `appended-${num}`,
      label: `Appended Option ${num}`,
    }
  }))
  options.value = temp

  page.value += 1
}
</script>

<template>
  <HSelect
    v-model="value"
    :options="options"
    class="w-80"
    preserve-focus
    @scroll-bottom="onScrollBottom"
  />
</template>

⚡ Virtual List

Virtual List is enabled by default and cannot be opted out at the moment.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { HSelect, type SelectOptionBase, type SelectValue } from '@holistics/design-system'

const options = Array.from(
  { length: 1000 },
  (_, i) => ({ value: `opt-${i + 1}`, label: `Option ${i + 1}` }),
) satisfies SelectOptionBase[]

const value = ref<SelectValue>()
</script>

<template>
  <HSelect
    v-model="value"
    :options="options"
    class="w-80"
  />
</template>

📂 Submenu

Display nested options in a separate flyout menu, useful for organizing related actions or hierarchical data.

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 { SelectOptionBase } from '@holistics/design-system'

export const options: SelectOptionBase[] = [
  { value: 1, label: 'Option 1' },
  { value: 2, label: 'Option 2' },
  {
    value: 'more-options',
    label: 'More Options',
    childrenAsSubmenu: true,
    children: [
      { value: '3.1', label: 'Option 3.1' },
      { value: '3.2', label: 'Option 3.2' },
      {
        value: '3.3-group',
        label: 'Option 3.3 (Group)',
        children: [
          { value: '3.x.1', label: 'Option 3.x.1' },
          { value: '3.x.2', label: 'Option 3.x.2' },
          { value: '3.x.3', label: 'Option 3.x.3' },
          {
            value: '3.3.4-more-options',
            label: 'More Options',
            childrenAsSubmenu: true,
            children: [
              { value: '3.3.4.1', label: 'Option 3.3.4.1' },
              {
                value: '3.3.4.2-more-options',
                label: 'More Options',
                childrenAsSubmenu: true,
                children: [
                  { value: '3.3.4.2.1', label: 'Option 3.3.4.2.1' },
                  { value: '3.3.4.2.2', label: 'Option 3.3.4.2.2' },
                ],
              },
              {
                value: '3.3.4.3',
                label: 'Option 3.3.4.3 (Group)',
                children: [
                  { value: '3.3.4.x.1', label: 'Option 3.3.4.x.1' },
                  { value: '3.3.4.x.2', label: 'Option 3.3.4.x.2' },
                  { value: '3.3.4.x.3', label: 'Option 3.3.4.x.3' },
                  { value: '3.3.4.x.4', label: 'Option 3.3.4.x.4' },
                ],
              },
            ],
          },
          {
            value: '3.3.5',
            label: 'Option 3.3.5 (Group)',
            children: [
              { value: '3.3.x.1', label: 'Option 3.3.x.1' },
              { value: '3.3.x.2', label: 'Option 3.3.x.2' },
              {
                value: '3.3.5.3 (Group)',
                label: 'Option 3.3.5.3',
                children: [
                  { value: '3.3.5.x.1', label: 'Option 3.3.5.x.1' },
                  { value: '3.3.5.x.2', label: 'Option 3.3.5.x.2' },
                  { value: '3.3.5.x.3', label: 'Option 3.3.5.x.3' },
                ],
              },
            ],
          },
        ],
      },
    ],
  },
  { value: 4, label: 'Option 4' },
]

Unsupported features for submenus

  • Create options dynamically: New options can only be created in the root menu, not in the currently focused submenu.
  • Sticky options when filtering: Unlike the root menu, sticky options within submenus will be filtered out if they aren't matched.

📐 Inlined

Display the select menu inline without a popover menu, useful for embedding directly in forms or panels.

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 { SelectOptionBase } from '@holistics/design-system'

export const OPTIONS: SelectOptionBase[] = [
  {
    stickyTop: true,
    value: '101',
    label: 'Top Option 1',
    tooltip: 'Lorem ipsum donor',
  },
  {
    stickyTop: true,
    value: '102',
    label: 'Top Option 2',
    tooltip: 'Some tips at a group',
    initialExpanded: true,
    children: [
      {
        stickyTop: true,
        value: '102.1',
        label: 'Top Option 2.1',
      },
      {
        stickyTop: true,
        value: '102.2',
        label: 'Inner-Bell',
        icon: 'bell',
        action: () => { console.log('Inner-bell logged!') },
      },
    ],
  },
  {
    stickyTop: true,
    value: '103',
    label: 'Top Action Option',
    icon: 'function',
    action: (option) => { alert(`You clicked an option with value: "${option.value}" and label: "${option.label}"`) },
    tooltip: { content: '<em>I can be HTML, too!</em>', html: true },
  },
  {
    value: '1',
    label: 'Option 1',
    tooltip: 'I can be anywhere!',
  },
  {
    value: '2',
    label: 'Option 2 (with icon)',
    icon: 'favorite',
  },
  {
    value: '3',
    label: 'Option 3',
    description: 'Option with description',
  },
  {
    value: '4',
    label: 'Option 4 has a VERY Very very long label',
    icon: 'data-set',
    description: 'Option with description & icon, and a very long long long long long long long long long long long description',
  },
  {
    value: '5',
    label: 'Option 5 (disabled)',
    disabled: true,
    tooltip: 'Disabled option only shows tooltip on hovering (not keyboad)!',
  },
  {
    value: '6',
    label: 'Option 6',
    children: [
      { label: 'Option a.1', value: '6.1', icon: 'hashtag' },
      { label: 'Option b.2', value: '6.2', icon: 'type/string' },
      {
        label: 'Option x.3',
        value: '6.3',
        initialExpanded: true,
        children: [
          { label: 'Option d.3.1', value: '6.3.1' },
          { label: 'Option e.3.2', value: '6.3.2' },
          {
            label: 'Option x.3.3',
            value: '6.3.3',
            children: [
              { label: 'Option g.3.3.1', value: '6.3.3.1' },
              { label: 'Option h.3.3.2', value: '6.3.3.2' },
              { label: 'Option i.3.3.3', value: '6.3.3.3' },
              {
                label: 'Option x.3.3.4',
                value: '6.3.3.4',
                children: [
                  { label: 'Option k.3.3.4.1', value: '6.3.3.4.1' },
                  { label: 'Option l.3.3.4.2', value: '6.3.3.4.2' },
                  { label: 'Option 6.3.3.4.3', value: '6.3.3.4.3' },
                  { label: 'Option m.3.3.4.4', value: '6.3.3.4.4' },
                ],
              },
              { label: 'Option j.3.3.3', value: '6.3.3.5' },
            ],
          },
          { label: 'Option f.3.4', value: '6.3.4' },
        ],
      },
      { label: 'Option c.4', value: '6.4' },
    ],
  },
  {
    value: '7',
    label: 'Option 7',
    icon: 'user',
  },
  {
    value: '8',
    label: 'Option 8',
  },
  {
    value: '9',
    label: 'Option 9',
  },
  {
    value: '10',
    label: 'Option 10',
  },
  {
    value: null,
    label: 'Option null',
  },
  {
    stickyBottom: true,
    value: '901',
    label: 'Bottom Option 1',
  },
  {
    stickyBottom: true,
    value: '902',
    label: 'Bottom Option 2',
    children: [
      {
        stickyBottom: true,
        value: '901.1',
        label: 'Bottom Option 2.1',
      },
      {
        stickyBottom: true,
        value: '901.2',
        label: 'Bottom Option 2.2',
      },
    ],
  },
  {
    stickyBottom: true,
    value: '903',
    label: 'Async Refresh (done after 2s)',
    icon: 'refresh',
    action: async () => {
      await new Promise((res) => { setTimeout(res, 2000) })
      alert('Done refreshing!')
    },
  },
]

export const ADVANCED_SEARCH_OPTIONS = [
  {
    value: 'users',
    label: 'Users',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'countries',
        label: 'Countries',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'cities',
            label: 'Cities',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              { value: 'beautiful-stories', label: 'Beautiful Stories' },
              { value: 'large-libraries', label: 'Large Libraries' },
            ],
          },
          {
            value: 'families',
            label: 'Families',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              { value: 'with-stories', label: 'With Stories' },
              { value: 'like-parties', label: 'Like Parties' },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'A',
    label: 'City Dwellers',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'B',
        label: 'Urban City',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'C',
            label: 'Analyze Data',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'D',
                label: 'Analytical Reports',
              },
            ],
          },
          {
            value: 'E',
            label: 'Childhood Memories',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'F',
                label: "Children's Games",
              },
            ],
          },
        ],
      },
      {
        value: 'G',
        label: 'Tooth Brushing',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'H',
            label: 'Teeth Cleaning',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'I',
                label: 'Mouse Clicks',
              },
              {
                value: 'J',
                label: 'Mice Running',
              },
            ],
          },
          {
            value: 'K',
            label: 'Foot Prints',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'L',
                label: 'Feet Walking',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'M',
    label: 'Person Interview',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'N',
        label: 'People Skills',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'O',
            label: 'Woman Power',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'P',
                label: 'Women Rights',
              },
            ],
          },
          {
            value: 'Q',
            label: 'Leaf Fall',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'R',
                label: 'Leaves Turning',
              },
            ],
          },
        ],
      },
      {
        value: 'S',
        label: 'Knife Sharp',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'T',
            label: 'Knives Set',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'U',
                label: 'Create Content',
              },
              {
                value: 'V',
                label: 'Creative Writing',
              },
            ],
          },
          {
            value: 'W',
            label: 'Develop Software',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'X',
                label: 'Development Tools',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'Y',
    label: 'Organize Events',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'Z',
        label: 'Organizational Skills',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AA',
            label: 'Apply Pressure',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AB',
                label: 'Application Forms',
              },
            ],
          },
          {
            value: 'AC',
            label: 'Multiply Numbers',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AD',
                label: 'Multiplication Tables',
              },
            ],
          },
        ],
      },
      {
        value: 'AE',
        label: 'Identify Problems',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AF',
            label: 'Identification Cards',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AG',
                label: 'Rectify Errors',
              },
              {
                value: 'AH',
                label: 'Rectification Process',
              },
            ],
          },
          {
            value: 'AI',
            label: 'Classify Items',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AJ',
                label: 'Classification System',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'AK',
    label: 'Beautiful Flowers',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'AL',
        label: 'Beauty Products',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AM',
            label: 'Quick Action',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AN',
                label: 'Quickly Done',
              },
            ],
          },
          {
            value: 'AO',
            label: 'Heavy Lifting',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AP',
                label: 'Heavily Loaded',
              },
            ],
          },
        ],
      },
      {
        value: 'AQ',
        label: 'Simple Solution',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AR',
            label: 'Simply Put',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AS',
                label: 'Possible outcome',
              },
              {
                value: 'AT',
                label: 'Possibly true',
              },
            ],
          },
          {
            value: 'AU',
            label: 'Electric cars',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AV',
                label: 'electricity bills',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    value: 'AW',
    label: 'Dramatic plays',
    initialExpanded: true,
    icon: 'canvas-light',
    children: [
      {
        value: 'AX',
        label: 'dramatically changed',
        initialExpanded: true,
        icon: 'data-set',
        children: [
          {
            value: 'AY',
            label: 'Historic buildings',
            initialExpanded: true,
            icon: 'data-model',
            children: [
              {
                value: 'AZ',
                label: 'historically accurate',
              },
            ],
          },
        ],
      },
    ],
  },
] as const satisfies SelectOptionBase[]

🎨 Themes

Apply different visual themes to the select component.

vue
<script setup lang="ts">
import { type Ref, ref } from 'vue'
import { HSelect, type SelectOptionBase, type SelectValue } from '@holistics/design-system'

const options = ref([
  { value: 1, label: 'Option 1' },
  { value: 2, label: 'Option 2' },
  { value: 3, label: 'Option 3', disabled: true },
  { value: 4, label: 'Option 4' },
]) as Ref<SelectOptionBase[]>

const value = ref<SelectValue>()
</script>

<template>
  <div class="flex flex-col items-center gap-4 md:flex-row lg:flex-col 2xl:flex-row">
    <HSelect
      v-model="value"
      :options="options"
      placeholder="Default..."
      class="w-80"
    />

    <HSelect
      v-model="value"
      :options="options"
      placeholder="Underline..."
      theme="underline"
      class="w-80"
    />
  </div>
</template>

🖌️ Custom CSS & Styles for Options

Apply custom CSS classes and inline styles to options for visual customization.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { HSelect, type SelectOptionBase, type SelectValue } from '@holistics/design-system'

const options: SelectOptionBase[] = [
  {
    stickyTop: true,
    value: '1',
    label: 'A big padding option!',
    style: { padding: '1rem 1.5rem' },
  },
  {
    value: '2',
    label: 'Open me',
    children: [
      {
        value: '2-1',
        label: 'A little bit italic',
        class: 'italic',
      },
      {
        value: '2-2',
        label: 'And super large text!',
        style: { 'font-size': '2rem', 'line-height': '1.5' },
      },
    ],
  },
]

const value = ref<SelectValue>()
</script>

<template>
  <HSelect
    v-model="value"
    :options="options"
    class="w-80"
  />
</template>

🧩 Slots

You have many options for slot customizing, including global slots (i.e. applying for all options) & individual slots. Please refer to the Slots API for reference.

Following is an example of using both global slot (#option-content) and individual slots (#option-content-verified and #option-footer):

vue
<script setup lang="ts">
import { ref } from 'vue'
import {
  HSelect,
  HTextHighlight,
  HIcon,
  type SelectOptionBase,
  type SelectValue,
} from '@holistics/design-system'

const options: SelectOptionBase[] = [
  {
    value: 'Default Option',
    label: 'Default Option',
    tooltip: 'This option has the exactly same default implementation of `#option-content`!',
  },
  {
    value: 'Verified Option',
    label: 'Verified Option',
    slotContent: 'option-content-verified',
    tooltip: 'This option stripped down unnecessary default parts of `#option-content`.',
  },
  {
    stickyBottom: true,
    value: 'Footer Option',
    slot: 'option-footer',
    label: 'A footer that has a link!', // This will be used for `filterable`
    disabled: true, // To avoid keyboard interactions
  },
]

const value = ref<SelectValue>()
</script>

<template>
  <HSelect
    v-model="value"
    :options="options"
    filterable
    class="w-80"
  >
    <template #option-content="{ option, searchText, highlights }">
      <div
        class="flex min-w-0 flex-1 items-start"
        v-bind="option.contentProps"
      >
        <HIcon
          v-if="option.creating"
          name="add"
          class="mr-1"
        />
        <HIcon
          v-if="option.icon"
          :name="option.icon"
          class="mr-1"
        />

        <div
          class="min-w-0 flex-1"
          :class="option.children && !option.childrenAsSubmenu && 'font-medium'"
        >
          <span v-if="option.creating || !searchText">{{ option.label }}</span>

          <HTextHighlight
            v-else
            :text="option.label"
            :highlights="highlights"
          />
        </div>
      </div>
    </template>

    <template #option-content-verified="{ option, selected, searchText, highlights, }">
      <div
        class="flex min-w-0 flex-1 items-start"
        :class="!selected && 'text-green-600'"
        v-bind="option.contentProps"
      >
        <div>
          <HTextHighlight
            v-if="searchText"
            :text="option.label"
            :highlights="highlights"
          />
          <span v-else>{{ option.label }}</span>
        </div>

        <HIcon
          name="verified"
          class="ml-1"
        />
      </div>
    </template>

    <template #option-footer>
      <div class="p-2">
        A footer that has a <a
          href="https://holistics.io"
          target="_blank"
          class="text-blue-600 underline"
        >link</a>!
      </div>
    </template>
  </HSelect>
</template>

💡 Default implementation of #option-content

The above code example has the exact same default implementation of #option-content, and will be used as a reference in all version of Select. Therefore, whenever you need to customize the options, try copying that code first, then adjust it to your needs.

For example:

  • #option-content-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.

🏷️ Generics

Select supports generic types by looking at the options prop so that all the emitted events and other props referencing options will have the passed type.

vue
<script setup lang="ts">
import { ref, type Ref } from 'vue'
import { HSelect, type SelectOption } from '@holistics/design-system'

interface MySelectOption extends SelectOption<MySelectOption> {
//        ^ Your option                 ^ `children` of your option
  extraProperty?: boolean
}

const options = ref([]) as Ref<MySelectOption[]>
</script>

<template>
  <HSelect
    v-model:options="options"
    :option-props="(option) => {
      // `option` will have type `MySelectOption`
    }"
    @select="(value, option) => {
      // `option` will have type `MySelectOption`
      if (option.extraProperty) {
        // ...
      }
    }"
  />
</template>

Fixed depth via recursive generic type

You can describe your options structure via recursive generic type to get the best type-safety and convenience.

Example
ts
// A Dataset can only contain Data Models & is always be a parent option
interface DatasetOption extends SelectOption<DataModelOption> {
  type: 'dataset'
  icon: 'data-set'
  children: DataModelOption[]
}
// A Data Model can only contain Fields & is always be a parent option
interface DataModelOption extends SelectOption<FieldOption> {
  type: 'data-model'
  icon: 'data-model'
  children: FieldOption[]
}
// A Field is always a leaf option
interface FieldOption extends SelectOption<never> {
  type: 'field'
  icon: 'type/string' | 'type/number' | 'type/boolean' | 'type/date' | 'type/timestamp' | 'type/unknown'
  children?: never
}

Forcing generic type

Since Vue components are just functions, you can force a generic type when using Select.

Example
vue
<script setup lang="ts">
import { _HSelect, type SelectOption } from '@holistics/design-system'

interface ForcedSelectOption extends SelectOption<ForcedSelectOption> {
  someKey?: boolean
}

const HSelect = _HSelect<ForcedSelectOption>
</script>

<template>
  <HSelect
    @select="(option) => {
      // `option` will have type `ForcedSelectOption` even though `options` prop is not specified
    }"
  />
</template>

API

Pass-through: <HPopper>

What does this mean?

All props, events, and attrs that are not specified in the tables below will be passed to the element/component described above.

Props

NameTypeDescription
options *
readonly SelectOption<any>[]

List of options.

modelValue 
SelectValue | SelectValue[]

Selected/checked value(s):

  • If multiple is falsy: single option value
  • If multiple is truthy: an array of selected option values
disabled 
boolean

When true, prevents the user from interacting with the select.

inputId 
string

The id attribute to assign to the <input> element when filterable: true.

theme 
"normal" | "underline"
= "normal"

The theme of the select.

icon 
"function" | "add-block" | "add-filter" | "add-tag" | "add" | "address-card" | "adhoc-query" | "ai/file" | "ai/generate" | "ai/mascot" | "ai/openai-black-monoblossom" | "ai/sparkle-gradient" | ... 431 more ...

Name of the icon to display at the prepend of the trigger.

iconSpin 
boolean

Whether the trigger icon should spin.

placeholder 
string
= "Select..."

A (muted) text to show when no option is being selected.

clearable 
boolean

When true, shows a button at the append of the trigger to completely remove all selected options.

refreshFn 
(() => void | Promise<void>)

Custom function to refresh the options.

refreshLoading 
boolean

When true, prevents user from interacting with the list of options and the input (when filterable), useful when using with @refresh.

groupSelectable 
boolean

By default, group options can only be expanded/collapsed. To allow selecting them, set this to true.

showCheckmark 
boolean

When true, shows a check mark at the append of the selected options.

multiple 
SelectMultiple

When true, allows user to select multiple options.

indeterminateKeys 
Key[]

List of option values that have some children checked (when checkCascade: true).

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"
filterable 
boolean

When true, allows user to filter options that are matched against a search text.

searchText 
string

The search text used for filtering options.

preserveSearchText 
boolean

By default, the search text will be automatically reset when Select is blurred (@blur is fired) or when an option is selected. To disable this behavior, set this to true. You will need to manually reset it via the searchText prop.

searchDebounce 
number
= 200

Debounce time (in ms) for searching and filtering.

filterIncludeDirectChildren 
boolean

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 this to true. Note that children at deeper levels still won't be shown, hence the term: direct.

filterIncludeStickyOptions 
boolean

By default, sticky options are always visible regardless of the search text (can still be auto-expaned, auto-focused, highlighted,...). To also filter them based on the search text, set this to true.

filterAdvancedSearch 
boolean | SelectSearchStemIndex

Whether to enable stemming with hierarchical search for filtering. This is achieved by generating a stem index which is essentially an inverted index map from a stem to option values, then for each value map to its list of original tokens.

  • When true, the index is collected automatically via selectCollectStemTokenIndex().
  • Can provide a custom index from outside for manual caching or re-using the same index for multiple Selects.
searchLoading 
boolean

When true, prevents user from interacting with the list of options, useful when using with @search.

createable 
boolean

When true, allows user to create new option that have value and label same as the input search text.

While inputting the text, if no existing option has value same as the text, an option with the + icon will be shown at the bottom of the list (called creating option). Upon selecting it, a new option will be created and put after the existing options.

createFn 
SelectCreateOptionFn<SelectOption<any>>

Custom low-level function used to create new options based on the search text.

createAtTop 
boolean

When true, creating & created options will be put at the top of the filterable options list. The order is now reversed: Creating -> Created -> Original.

maxHeight 
string
= "15.5rem"

Max height of the container of filterable options (not the menu).

scrollBottomLoading 
boolean

When true, shows a loading indicator at the bottom of the container of filterable options (not the menu).

preserveFocus 
boolean

In general cases, whenever options changes, the focus will be reset to the first focusable option. This is to ensure the focused option is always valid (otherwise, it will be reset anyway). If you are certain that the previous options is a subset of the updated options (e.g. inserting new options via infinite scroll), you can set this to true.

inline 
boolean

When true, displays the select menu inline without a popover menu, useful for embedding directly in forms or panels.

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

Config to match the Anchor "size". The "size" here is detected by the final side of the Floating element:

  • If the side is vertical (i.e. top, bottom), Anchor "size" is width
  • If the side is horizontal (i.e. left, right), Anchor "size" is height
floatingClass 
HTMLAttributeClass

Custom class for the root options menu.

optionProps 
((option: SelectOption<any>) => HTMLAttributes)

The getter to bind additional props to the root element of the 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<any>) => HTMLAttributes)

The getter to bind 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<any>) => HTMLAttributes)

The getter to bind 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.

optionIndent 
number
= 20

[Nested] The distance (in px) between options of 2 continuous level.

optionEmptyParentText 
string | null
= "(empty)"

Text to display right after the label of options having no children. The text will be hidden when searching (as the children could have been filtered out). To completely hide it in all circumstances, set this to null.

Events

NameParametersDescription
@update:modelValue
[value: SelectValue | SelectValue[] | undefined]
@select
[value: SelectValue, option: SelectOption<any>]
@update:refreshLoading
[loading: boolean]
@deselect
[value: SelectValue, option: SelectOption<any>]
@update:indeterminateKeys
[keys: Key[]]
@update:searchText
[value: string]
@update:searchLoading
[loading: boolean]
@update:scrollBottomLoading
[loading: boolean]
@focus
[event: FocusEvent]
@blur
[event: FocusEvent]
@focusOption
[option?: SelectOption<any> | undefined]

Slots

NameScopedDescription
#placeholder
{ placeholder: string; }
#trigger-selected-options
SelectSlotTriggerSelectedOptionsProps<SelectOption<any>>
#option
SelectSlotOptionProps<SelectOption<any>>
#option-toggle
SelectSlotOptionContentProps<SelectOption<any>>
#option-prepend
SelectSlotOptionContentProps<SelectOption<any>>
#option-content
SelectSlotOptionContentProps<SelectOption<any>>
#option-append
SelectSlotOptionContentProps<SelectOption<any>>
#option-description
SelectSlotOptionContentProps<SelectOption<any>>
#options-empty
any
#options-filter-empty
any