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.
You can use the ui
prop like the Select
component to override the default config. The uiMenu
prop can be used to override the default menu config.
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 = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
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 #leading>
<UIcon v-if="selected.icon" :name="selected.icon" class="w-4 h-4 mx-0.5" />
<UAvatar v-else-if="selected.avatar" v-bind="selected.avatar" size="3xs" class="mx-0.5" />
</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].name)
</script>
<template>
<USelectMenu
v-model="selected"
:options="people"
placeholder="Select people"
value-attribute="name"
option-attribute="name"
/>
</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"
class="w-full lg:w-40"
placeholder="Select a person"
:options="['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']"
/>
Search
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..."
class="w-full lg:w-40"
placeholder="Select a person"
:options="['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']"
/>
Search Attributes
Use the search-attributes
with an array of property names to search on each option object.
Nested attributes can be accessed using dot.notation
. When the property value is an array or object, these are cast to string so these can be searched within.
<script setup>
const options = [
{ id: 1, name: 'Wade Cooper', colors: ['red', 'yellow'] },
{ id: 2, name: 'Arlene Mccoy', colors: ['blue', 'yellow'] },
{ id: 3, name: 'Devon Webb', colors: ['green', 'blue'] },
{ id: 4, name: 'Tom Cook', colors: ['blue', 'red'] },
{ id: 5, name: 'Tanya Fox', colors: ['green', 'red'] },
{ id: 5, name: 'Hellen Schmidt', colors: ['green', 'yellow'] }
]
const selected = ref(options[1])
</script>
<template>
<USelectMenu
v-model="selected"
:options="options"
placeholder="Select a person"
searchable
searchable-placeholder="Search by name or color"
option-attribute="name"
by="id"
:search-attributes="['name', 'colors']"
>
<template #option="{ option: person }">
<span v-for="color in person.colors" :key="color.id" class="h-2 w-2 rounded-full" :class="`bg-${color}-500 dark:bg-${color}-400`" />
<span class="truncate">{{ person.name }}</span>
</template>
</USelectMenu>
</template>
Clear on close New
By default, the search query will be kept after the menu is closed. To clear it on close, set the clear-search-on-close
prop.
<USelectMenu
clear-search-on-close
class="w-full lg:w-40"
placeholder="Select a person"
searchable
searchable-placeholder="Search a person..."
:options="['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']"
/>
Async search
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 = {
id: options.value.length + 1,
name: label.name,
color: generateColorFromString(label.name)
}
options.value.push(response)
return response
})
selected.value = await Promise.all(promises)
}
})
function hashCode (str) {
let hash = 0
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash)
}
return hash
}
function intToRGB (i) {
const c = (i & 0x00FFFFFF)
.toString(16)
.toUpperCase()
return '00000'.substring(0, 6 - c.length) + c
}
function generateColorFromString (str) {
return intToRGB(hashCode(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>
Popper
Use the popper
prop to customize the popper instance.
Arrow
<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" :popper="{ arrow: true }" />
</template>
Placement
<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" :popper="{ placement: 'left-end' }" />
</template>
Offset
<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" :popper="{ offsetDistance: 0 }" />
</template>
Slots
label
You can override the #label
slot and handle the display yourself.
<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([])
</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 = ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
const selected = ref(people[3])
</script>
<template>
<USelectMenu v-slot="{ open }" v-model="selected" :options="people">
<UButton color="gray" class="flex-1 justify-between">
{{ selected }}
<UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform text-gray-400 dark:text-gray-500" :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.
Props
{}
null
"sm"
"2xs"
"xs"
"md"
"lg"
"xl"
null
[]
undefined
""
null
null
null
config.default.color
config.default.variant
"outline"
"none"
{}
config.default.loadingIcon
null
config.default.trailingIcon
configMenu.default.selectedIcon
"label"
null
null
{}
null
false
"Search..."
200
configMenu.default.showCreateOptionWhen
false
false
false
false
false
false
true
configMenu.default.clearSearchOnClose
false
Config
ui
prop to override the select config and the uiMenu
prop to override the menu config.{
"container": "z-20 group",
"trigger": "inline-flex w-full",
"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",
"empty": "text-sm text-gray-400 dark:text-gray-500 px-2 py-1.5",
"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"
},
"create": "block truncate"
},
"transition": {
"leaveActiveClass": "transition ease-in duration-100",
"leaveFromClass": "opacity-100",
"leaveToClass": "opacity-0"
},
"popper": {
"placement": "bottom-end"
},
"default": {
"selectedIcon": "i-heroicons-check-20-solid",
"clearSearchOnClose": false,
"showCreateOptionWhen": "empty"
},
"arrow": {
"base": "invisible before:visible before:block before:rotate-45 before:z-[-1] before:w-2 before:h-2",
"ring": "before:ring-1 before:ring-gray-200 dark:before:ring-gray-700",
"rounded": "before:rounded-sm",
"background": "before:bg-white dark:before:bg-gray-700",
"shadow": "before:shadow",
"placement": "group-data-[popper-placement*='right']:-left-1 group-data-[popper-placement*='left']:-right-1 group-data-[popper-placement*='top']:-bottom-1 group-data-[popper-placement*='bottom']:-top-1"
},
"select": "inline-flex items-center text-left cursor-default",
"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",
"required": "absolute inset-0 w-px opacity-0 cursor-default",
"label": "block truncate"
}
{
"container": "z-20 group",
"trigger": "inline-flex w-full",
"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",
"empty": "text-sm text-gray-400 dark:text-gray-500 px-2 py-1.5",
"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"
},
"create": "block truncate"
},
"transition": {
"leaveActiveClass": "transition ease-in duration-100",
"leaveFromClass": "opacity-100",
"leaveToClass": "opacity-0"
},
"popper": {
"placement": "bottom-end"
},
"default": {
"selectedIcon": "i-heroicons-check-20-solid",
"clearSearchOnClose": false,
"showCreateOptionWhen": "empty"
},
"arrow": {
"base": "invisible before:visible before:block before:rotate-45 before:z-[-1] before:w-2 before:h-2",
"ring": "before:ring-1 before:ring-gray-200 dark:before:ring-gray-700",
"rounded": "before:rounded-sm",
"background": "before:bg-white dark:before:bg-gray-700",
"shadow": "before:shadow",
"placement": "group-data-[popper-placement*='right']:-left-1 group-data-[popper-placement*='left']:-right-1 group-data-[popper-placement*='top']:-bottom-1 group-data-[popper-placement*='bottom']:-top-1"
},
"select": "inline-flex items-center text-left cursor-default",
"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",
"required": "absolute inset-0 w-px opacity-0 cursor-default",
"label": "block truncate"
}