Nuxt UI v3-alpha has been released!

Try it out
Components
Display an autocomplete input with real-time suggestions.

Usage

The InputMenu component renders by default an Input component and is based on the ui.input preset. You can use most of the Input props to configure the display such as color, variant, size, placeholder, icon, disabled, etc.

You can use the ui prop like the Input component to override the default config. The uiMenu prop can be used to override the default menu config.

Pass an array of strings or objects to the options prop to display in the menu.

<script setup lang="ts">
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>
  <UInputMenu v-model="selected" :options="people" />
</template>
This component does not support multiple values. Use the SelectMenu component instead.

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. Additionally, you can use dot notation (e.g., user.name) to access nested object properties.

<script setup lang="ts">
import type { Avatar } from '#ui/types'

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>
  <UInputMenu v-model="selected" :options="people">
    <template #leading>
      <UIcon v-if="selected.icon" :name="(selected.icon as string)" class="w-5 h-5" />
      <UAvatar v-else-if="selected.avatar" v-bind="(selected.avatar as Avatar)" size="2xs" />
    </template>
  </UInputMenu>
</template>

Use the search-attributes prop 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 lang="ts">
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>
  <UInputMenu
    v-model="selected"
    :options="options"
    placeholder="Select a person"
    by="id"
    option-attribute="name"
    :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>
  </UInputMenu>
</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 lang="ts">
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)
</script>

<template>
  <UInputMenu
    v-model="selected"
    :options="people"
    value-attribute="id"
    option-attribute="name"
  />
</template>

Icon

The InputMenu has a button on the right to toggle the menu. Use the trailing-icon prop to set a different icon or change it globally in ui.inputMenu.default.trailingIcon. Defaults to i-heroicons-chevron-down-20-solid.

<template>
  <UInputMenu
    trailing-icon="i-heroicons-chevron-up-down-20-solid"
    class="w-full lg:w-48"
    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']"
  />
</template>

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

<template>
  <UInputMenu
    selected-icon="i-heroicons-hand-thumb-up-solid"
    class="w-full lg:w-48"
    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']"
  />
</template>
Learn how to customize icons from the Input component.

Searchable

Attributes

Use the search-attributes prop 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 lang="ts">
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>
  <UInputMenu
    v-model="selected"
    :options="options"
    placeholder="Select a person"
    by="id"
    option-attribute="name"
    :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>
  </UInputMenu>
</template>

Control the query

Use a v-model:query to control the search query.

<script setup lang="ts">
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()
const query = ref('Wade')
</script>

<template>
  <UInputMenu
    v-model="selected"
    v-model:query="query"
    :options="people"
    placeholder="Select a person"
  />
</template>

Pass a function to the search 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.

Use the searchLazy prop to control the immediacy of data requests.

<script setup lang="ts">
const loading = ref(false)
const selected = ref()

async function search(q: string) {
  loading.value = true

  const users: any[] = await $fetch('https://jsonplaceholder.typicode.com/users', { params: { q } })

  loading.value = false

  return users
}
</script>

<template>
  <UInputMenu
    v-model="selected"
    :search="search"
    :loading="loading"
    placeholder="Search for a user..."
    option-attribute="name"
    trailing
    by="id"
  />
</template>

Popper

Use the popper prop to customize the popper instance.

Arrow

<script setup lang="ts">
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>
  <UInputMenu v-model="selected" :options="people" :popper="{ arrow: true }" />
</template>

Placement

<script setup lang="ts">
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>
  <UInputMenu v-model="selected" :options="people" :popper="{ placement: 'right-start' }" />
</template>

Offset

<script setup lang="ts">
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>
  <UInputMenu v-model="selected" :options="people" :popper="{ offsetDistance: 0 }" />
</template>

Slots

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 lang="ts">
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>
  <UInputMenu v-model="selected" :options="people" option-attribute="name">
    <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>
  </UInputMenu>
</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.

You can also configure this globally through the ui.inputMenu.default.optionEmpty.label config. The token {query} will be replaced by query property. Defaults to No results for "{query}"..

<script setup lang="ts">
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>
  <UInputMenu v-model="selected" :options="people" searchable>
    <template #option-empty="{ query }">
      <q>{{ query }}</q> not found
    </template>
  </UInputMenu>
</template>

empty

Use the #empty slot to customize the content displayed when there is no options. Defaults to No options..

You can also configure this globally through the ui.inputMenu.default.empty.label config. Defaults to No options..

<script setup lang="ts">
const people = []

const selected = ref()
</script>

<template>
  <UInputMenu v-model="selected" :options="people">
    <template #empty>
      No people
    </template>
  </UInputMenu>
</template>

Props

name
string
null
size
InputSize
null
"md""2xs""xs""sm""lg""xl"
search
(query: string) => any[] | Promise<any[]>
undefined
color
string
config.default.color
icon
string
null
ui
{ wrapper?: string; base?: string; form?: string; rounded?: string; placeholder?: string; file?: DeepPartial<{ base: string; }, any>; size?: DeepPartial<{ '2xs': string; xs: string; sm: string; md: string; lg: string; xl: string; }, any>; ... 7 more ...; default?: DeepPartial<...>; } & { ...; } & { ...; }
{}
id
string
null
modelValue
string | number | object | any[]
""
inputClass
string
null
options
string[] | { [key: string]: any; disabled?: boolean; }[]
[]
variant
InputVariant
config.default.variant
"outline""none"
placeholder
string
null
loadingIcon
string
config.default.loadingIcon
leadingIcon
string
null
trailingIcon
string
configMenu.default.trailingIcon
optionAttribute
string
"label"
valueAttribute
string
null
by
string
undefined
popper
PopperOptions
{}
query
string
null
selectedIcon
string
configMenu.default.selectedIcon
searchAttributes
unknown[]
null
debounce
number
200
uiMenu
{ container?: string; trigger?: string; width?: string; height?: string; base?: string; background?: string; shadow?: string; rounded?: string; padding?: string; ring?: string; empty?: string; option?: DeepPartial<{ base: string; ... 13 more ...; chip: { ...; }; }, any>; transition?: DeepPartial<...>; popper?: DeepP...
{}
required
boolean
false
disabled
boolean
false
nullable
boolean
false
leading
boolean
false
trailing
boolean
false
loading
boolean
false
padded
boolean
true
searchLazy
boolean
false

Config

Use the ui prop to override the input config and the uiMenu prop to override the menu config.
{
  wrapper: 'relative',
  base: 'relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0',
  form: 'form-input',
  rounded: 'rounded-md',
  placeholder: 'placeholder-gray-400 dark:placeholder-gray-500',
  file: {
    base: 'file:mr-1.5 file:font-medium file:text-gray-500 dark:file:text-gray-400 file:bg-transparent file:border-0 file:p-0 file:outline-none'
  },
  size: {
    '2xs': 'text-xs',
    xs: 'text-xs',
    sm: 'text-sm',
    md: 'text-sm',
    lg: 'text-sm',
    xl: 'text-base'
  },
  gap: {
    '2xs': 'gap-x-1',
    xs: 'gap-x-1.5',
    sm: 'gap-x-1.5',
    md: 'gap-x-2',
    lg: 'gap-x-2.5',
    xl: 'gap-x-2.5'
  },
  padding: {
    '2xs': 'px-2 py-1',
    xs: 'px-2.5 py-1.5',
    sm: 'px-2.5 py-1.5',
    md: 'px-3 py-2',
    lg: 'px-3.5 py-2.5',
    xl: 'px-3.5 py-2.5'
  },
  leading: {
    padding: {
      '2xs': 'ps-7',
      xs: 'ps-8',
      sm: 'ps-9',
      md: 'ps-10',
      lg: 'ps-11',
      xl: 'ps-12'
    }
  },
  trailing: {
    padding: {
      '2xs': 'pe-7',
      xs: 'pe-8',
      sm: 'pe-9',
      md: 'pe-10',
      lg: 'pe-11',
      xl: 'pe-12'
    }
  },
  color: {
    white: {
      outline: 'shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400'
    },
    gray: {
      outline: 'shadow-sm bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400'
    }
  },
  variant: {
    outline: 'shadow-sm bg-transparent text-gray-900 dark:text-white ring-1 ring-inset ring-{color}-500 dark:ring-{color}-400 focus:ring-2 focus:ring-{color}-500 dark:focus:ring-{color}-400',
    none: 'bg-transparent focus:ring-0 focus:shadow-none'
  },
  icon: {
    base: 'flex-shrink-0 text-gray-400 dark:text-gray-500',
    color: 'text-{color}-500 dark:text-{color}-400',
    loading: 'animate-spin',
    size: {
      '2xs': 'h-4 w-4',
      xs: 'h-4 w-4',
      sm: 'h-5 w-5',
      md: 'h-5 w-5',
      lg: 'h-5 w-5',
      xl: 'h-6 w-6'
    },
    leading: {
      wrapper: 'absolute inset-y-0 start-0 flex items-center',
      pointer: 'pointer-events-none',
      padding: {
        '2xs': 'px-2',
        xs: 'px-2.5',
        sm: 'px-2.5',
        md: 'px-3',
        lg: 'px-3.5',
        xl: 'px-3.5'
      }
    },
    trailing: {
      wrapper: 'absolute inset-y-0 end-0 flex items-center',
      pointer: 'pointer-events-none',
      padding: {
        '2xs': 'px-2',
        xs: 'px-2.5',
        sm: 'px-2.5',
        md: 'px-3',
        lg: 'px-3.5',
        xl: 'px-3.5'
      }
    }
  },
  default: {
    size: 'sm',
    color: 'white',
    variant: 'outline',
    loadingIcon: 'i-heroicons-arrow-path-20-solid'
  }
}
{
  container: 'z-20 group',
  trigger: 'flex items-center 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-1.5 py-1.5',
    size: 'text-sm',
    color: 'text-gray-900 dark:text-white',
    container: 'flex items-center gap-1.5 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-5 w-5',
      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-5 w-5 text-gray-900 dark:text-white flex-shrink-0'
    },
    avatar: {
      base: 'flex-shrink-0',
      size: '2xs'
    },
    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',
    trailingIcon: 'i-heroicons-chevron-down-20-solid',
    empty: {
      label: 'No options.'
    },
    optionEmpty: {
      label: 'No results for "{query}".'
    }
  },
  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"
  }
}