forms

SelectMenu

Display a select menu with advanced features.

Usage

The SelectMenu component renders by default a Select component and is based on the ui.select preset. You can use most of the Select props to configure the display if you don't want to override the default slot such as color, variant, size, placeholder, icon, disabled, etc.

Like the Select component, you can use the options prop to pass an array of strings or objects.

<script setup>
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref(people[0])
</script>
<template>
  <USelectMenu v-model="selected" :options="people" />
</template>

Multiple

You can use the multiple prop to select multiple values.

<script setup>
const people = [...]
const selected = ref([])
</script>
<template>
  <USelectMenu v-model="selected" :options="people" multiple placeholder="Select people" />
</template>

Objects

You can pass an array of objects to options and either compare on the whole object or use the by prop to compare on a specific key. You can configure which field will be used to display the label through the option-attribute prop that defaults to label.

<script setup>
const people = [{
  id: 'benjamincanac',
  label: 'benjamincanac',
  href: 'https://github.com/benjamincanac',
  target: '_blank',
  avatar: { src: 'https://avatars.githubusercontent.com/u/739984?v=4' }
},
{
  id: 'Atinux',
  label: 'Atinux',
  href: 'https://github.com/Atinux',
  target: '_blank',
  avatar: { src: 'https://avatars.githubusercontent.com/u/904724?v=4' }
},
{
  id: 'smarroufin',
  label: 'smarroufin',
  href: 'https://github.com/smarroufin',
  target: '_blank',
  avatar: { src: 'https://avatars.githubusercontent.com/u/7547335?v=4' }
},
{
  id: 'nobody',
  label: 'Nobody',
  icon: 'i-heroicons-user-circle'
}]
const selected = ref(people[0])
</script>
<template>
  <USelectMenu v-model="selected" :options="people">
    <template #label>
      <UIcon v-if="selected.icon" :name="selected.icon" class="w-4 h-4" />
      <UAvatar v-else-if="selected.avatar" v-bind="selected.avatar" size="3xs" />
      {{ selected.label }}
    </template>
  </USelectMenu>
</template>

If you only want to select a single object property rather than the whole object as value, you can set the value-attribute property. This prop defaults to null.

<script setup>
const people = [{
  id: 1,
  name: 'Wade Cooper'
}, {
  id: 2,
  name: 'Arlene Mccoy'
}, {
  id: 3,
  name: 'Devon Webb'
}, {
  id: 4,
  name: 'Tom Cook'
}]
const selected = ref(people[0].id)
const current = computed(() => people.find(person => person.id === selected.value))
</script>
<template>
  <USelectMenu
    v-model="selected"
    :options="people"
    placeholder="Select people"
    value-attribute="id"
    option-attribute="name"
  >
    <template #label>
      {{ current.name }}
    </template>
  </USelectMenu>
</template>

Icon

Use the selected-icon prop to set a different icon or change it globally in ui.selectMenu.default.selectedIcon. Defaults to i-heroicons-check-20-solid.

<USelectMenu selected-icon="i-heroicons-hand-thumb-up-solid" />
Learn how to customize icons from the Select component.

Use the searchable prop to enable search.

Use the searchable-placeholder prop to set a different placeholder.

This will use Headless UI Combobox component instead of Listbox.

<USelectMenu searchable searchable-placeholder="Search a person..." />

Pass a function to the searchable prop to customize the search behavior and filter options according to your needs. The function will receive the query as its first argument and should return an array.

Use the debounce prop to adjust the delay of the function.

<script setup>
const search = async (q) => {
  const users = await $fetch('https://jsonplaceholder.typicode.com/users', { params: { q } })
  return users.map(user => ({ id: user.id, label: user.name, suffix: user.email })).filter(Boolean)
}
const selected = ref([])
</script>
<template>
  <USelectMenu
    v-model="selected"
    :searchable="search"
    placeholder="Search for a user..."
    multiple
    by="id"
  />
</template>

Create option

Use the creatable prop to enable the creation of new options when the search doesn't return any results (only works with searchable).

Try to search for something that doesn't exist in the example below.

<script setup>
const options = ref([
  { id: 1, name: 'bug', color: 'd73a4a' },
  { id: 2, name: 'documentation', color: '0075ca' },
  { id: 3, name: 'duplicate', color: 'cfd3d7' },
  { id: 4, name: 'enhancement', color: 'a2eeef' },
  { id: 5, name: 'good first issue', color: '7057ff' },
  { id: 6, name: 'help wanted', color: '008672' },
  { id: 7, name: 'invalid', color: 'e4e669' },
  { id: 8, name: 'question', color: 'd876e3' },
  { id: 9, name: 'wontfix', color: 'ffffff' }
])
const selected = ref([])
const labels = computed({
  get: () => selected.value,
  set: async (labels) => {
    const promises = labels.map(async (label) => {
      if (label.id) {
        return label
      }
      // In a real app, you would make an API call to create the label
      const response = {
        name: label.name,
        color: generateColorFromString(label.name)
      }
      options.value.push(response)
      return response
    })
    selected.value = await Promise.all(promises)
  }
})
// Look at the component example to see how this is used
function generateColorFromString (str) {
  // ...
}
</script>
<template>
  <USelectMenu
    v-model="labels"
    by="id"
    name="labels"
    :options="options"
    option-attribute="name"
    multiple
    searchable
    creatable
  >
    <template #label>
      <template v-if="labels.length">
        <span class="flex items-center -space-x-1">
          <span v-for="label of labels" :key="label.id" class="flex-shrink-0 w-2 h-2 mt-px rounded-full" :style="{ background: `#${label.color}` }" />
        </span>
        <span>{{ labels.length }} label{{ labels.length > 1 ? 's' : '' }}</span>
      </template>
      <template v-else>
        <span class="text-gray-500 dark:text-gray-400 truncate">Select labels</span>
      </template>
    </template>
    <template #option="{ option }">
      <span
        class="flex-shrink-0 w-2 h-2 mt-px rounded-full"
        :style="{ background: `#${option.color}` }"
      />
      <span class="truncate">{{ option.name }}</span>
    </template>
    <template #option-create="{ option }">
      <span class="flex-shrink-0">New label:</span>
      <span
        class="flex-shrink-0 w-2 h-2 mt-px rounded-full -mx-1"
        :style="{ background: `#${generateColorFromString(option.name)}` }"
      />
      <span class="block truncate">{{ option.name }}</span>
    </template>
  </USelectMenu>
</template>

Slots

label

You can override the #label slot and handle the display yourself.

<script setup>
const people = [...]
const selected = ref([])
</script>
<template>
  <USelectMenu v-model="selected" :options="people" multiple>
    <template #label>
      <span v-if="selected.length" class="truncate">{{ selected.join(', ') }}</span>
      <span v-else>Select people</span>
    </template>
  </USelectMenu>
</template>

default

You can also override the #default slot entirely.

<script setup>
const people = [...]
const selected = ref(people[3])
</script>
<template>
  <USelectMenu v-slot="{ open }" v-model="selected" :options="people">
    <UButton color="gray">
      {{ selected }}
      <UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform" :class="[open && 'transform rotate-90']" />
    </UButton>
  </USelectMenu>
</template>

option

Use the #option slot to customize the option content. You will have access to the option, active and selected properties in the slot scope.

<script setup>
const people = [
  { name: 'Wade Cooper', online: true },
  { name: 'Arlene Mccoy', online: false },
  { name: 'Devon Webb', online: false },
  { name: 'Tom Cook', online: true },
  { name: 'Tanya Fox', online: false },
  { name: 'Hellen Schmidt', online: true },
  { name: 'Caroline Schultz', online: true },
  { name: 'Mason Heaney', online: false },
  { name: 'Claudie Smitham', online: true },
  { name: 'Emil Schaefer', online: false }
]
const selected = ref(people[3])
</script>
<template>
  <USelectMenu v-model="selected" :options="people" option-attribute="name">
    <template #label>
      <span :class="[selected.online ? 'bg-green-400' : 'bg-gray-200', 'inline-block h-2 w-2 flex-shrink-0 rounded-full']" aria-hidden="true" />
      <span class="truncate">{{ selected.name }}</span>
    </template>
    <template #option="{ option: person }">
      <span :class="[person.online ? 'bg-green-400' : 'bg-gray-200', 'inline-block h-2 w-2 flex-shrink-0 rounded-full']" aria-hidden="true" />
      <span class="truncate">{{ person.name }}</span>
    </template>
  </USelectMenu>
</template>

option-empty

Use the #option-empty slot to customize the content displayed when the searchable prop is true and there is no options. You will have access to the query property in the slot scope.

<script setup>
const people = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref(people[0])
</script>
<template>
  <USelectMenu v-model="selected" :options="people" searchable>
    <template #option-empty="{ query }">
      <q>{{ query }}</q> not found
    </template>
  </USelectMenu>
</template>

option-create

Use the #option-create slot to customize the content displayed when the creatable prop is true and there is no options. You will have access to the query property in the slot scope.

An example is available in the Create option section.

Props

name
string

null

size
string

appConfig.ui.select.default.size

ui
any

{}

icon
string

null

placeholder
string

null

popper
{}

{}

modelValue
string | number | Record<string, any> | unknown[]

""

color
string

appConfig.ui.select.default.color

options
string[] | { [key: string]: any; disabled?: boolean; }[]

[]

loadingIcon
string

appConfig.ui.input.default.loadingIcon

leadingIcon
string

null

trailingIcon
string

appConfig.ui.select.default.trailingIcon

variant
string

appConfig.ui.select.default.variant

optionAttribute
string

"label"

valueAttribute
string

null

selectClass
string

null

by
string

undefined

selectedIcon
string

appConfig.ui.selectMenu.default.selectedIcon

searchable
boolean | ((query: string) => any[] | Promise<any[]>)

false

searchablePlaceholder
string

"Search..."

debounce
number

200

searchAttributes
unknown[]

null

uiMenu
any

{}

required
boolean

false

disabled
boolean

false

trailing
boolean

false

leading
boolean

false

loading
boolean

false

padded
boolean

true

multiple
boolean

false

creatable
boolean

false

Preset

appConfig.ui.selectMenu
{
  "container": "z-20",
  "width": "w-full",
  "height": "max-h-60",
  "base": "relative focus:outline-none overflow-y-auto scroll-py-1",
  "background": "bg-white dark:bg-gray-800",
  "shadow": "shadow-lg",
  "rounded": "rounded-md",
  "padding": "p-1",
  "ring": "ring-1 ring-gray-200 dark:ring-gray-700",
  "input": "block w-[calc(100%+0.5rem)] focus:ring-transparent text-sm px-3 py-1.5 text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 border-0 border-b border-gray-200 dark:border-gray-700 focus:border-inherit sticky -top-1 -mt-1 mb-1 -mx-1 z-10 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none",
  "option": {
    "base": "cursor-default select-none relative flex items-center justify-between gap-1",
    "rounded": "rounded-md",
    "padding": "px-2 py-1.5",
    "size": "text-sm",
    "color": "text-gray-900 dark:text-white",
    "container": "flex items-center gap-2 min-w-0",
    "active": "bg-gray-100 dark:bg-gray-900",
    "inactive": "",
    "selected": "pe-7",
    "disabled": "cursor-not-allowed opacity-50",
    "empty": "text-sm text-gray-400 dark:text-gray-500 px-2 py-1.5",
    "icon": {
      "base": "flex-shrink-0 h-4 w-4",
      "active": "text-gray-900 dark:text-white",
      "inactive": "text-gray-400 dark:text-gray-500"
    },
    "selectedIcon": {
      "wrapper": "absolute inset-y-0 end-0 flex items-center",
      "padding": "pe-2",
      "base": "h-4 w-4 text-gray-900 dark:text-white flex-shrink-0"
    },
    "avatar": {
      "base": "flex-shrink-0",
      "size": "3xs"
    },
    "chip": {
      "base": "flex-shrink-0 w-2 h-2 mx-1 rounded-full"
    }
  },
  "transition": {
    "leaveActiveClass": "transition ease-in duration-100",
    "leaveFromClass": "opacity-100",
    "leaveToClass": "opacity-0"
  },
  "popper": {
    "placement": "bottom-end"
  },
  "default": {
    "selectedIcon": "i-heroicons-check-20-solid"
  }
}