Usage
Use the v-model directive to control the value of the Listbox or the default-value prop to set the initial value when you do not need to control its state.
<script setup lang="ts">
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[]>([
{
label: 'France',
icon: 'i-lucide-map-pin',
value: 'FR'
},
{
label: 'Germany',
icon: 'i-lucide-map-pin',
value: 'DE'
},
{
label: 'Italy',
icon: 'i-lucide-map-pin',
value: 'IT'
},
{
label: 'Spain',
icon: 'i-lucide-map-pin',
value: 'ES'
},
{
label: 'Netherlands',
icon: 'i-lucide-map-pin',
value: 'NL'
},
{
label: 'Poland',
icon: 'i-lucide-map-pin',
value: 'PL'
},
{
label: 'Belgium',
icon: 'i-lucide-map-pin',
value: 'BE'
},
{
label: 'Portugal',
icon: 'i-lucide-map-pin',
value: 'PT'
},
{
label: 'Austria',
icon: 'i-lucide-map-pin',
value: 'AT'
},
{
label: 'Sweden',
icon: 'i-lucide-map-pin',
value: 'SE'
}
])
const value = ref({
label: 'France',
icon: 'i-lucide-map-pin',
value: 'FR'
})
</script>
<template>
<UListbox v-model="value" :items="items" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[]>([
{
label: 'France',
icon: 'i-lucide-map-pin',
value: 'FR'
},
{
label: 'Germany',
icon: 'i-lucide-map-pin',
value: 'DE'
},
{
label: 'Italy',
icon: 'i-lucide-map-pin',
value: 'IT'
},
{
label: 'Spain',
icon: 'i-lucide-map-pin',
value: 'ES'
},
{
label: 'Netherlands',
icon: 'i-lucide-map-pin',
value: 'NL'
},
{
label: 'Poland',
icon: 'i-lucide-map-pin',
value: 'PL'
},
{
label: 'Belgium',
icon: 'i-lucide-map-pin',
value: 'BE'
},
{
label: 'Portugal',
icon: 'i-lucide-map-pin',
value: 'PT'
},
{
label: 'Austria',
icon: 'i-lucide-map-pin',
value: 'AT'
},
{
label: 'Sweden',
icon: 'i-lucide-map-pin',
value: 'SE'
}
])
const value = ref({
label: 'France',
icon: 'i-lucide-map-pin',
value: 'FR'
})
</script>
<template>
<UListbox v-model="value" :items="items" />
</template>
Items
Use the items prop as an array of objects with the following properties:
label?: stringdescription?: stringtype?: "label" | "separator" | "item"icon?: stringavatar?: AvatarPropschip?: ChipPropsdisabled?: booleanonSelect?: (e: Event) => voidclass?: anyui?: { label?: ClassNameValue, separator?: ClassNameValue, item?: ClassNameValue, itemLeadingIcon?: ClassNameValue, ... }
<script setup lang="ts">
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[]>([
{
label: 'France',
description: 'The Hexagon',
icon: 'i-lucide-map-pin',
value: 'FR'
},
{
label: 'Germany',
description: 'The Federal Republic',
icon: 'i-lucide-map-pin',
value: 'DE'
},
{
label: 'Italy',
description: 'The Boot',
icon: 'i-lucide-map-pin',
value: 'IT'
},
{
label: 'Spain',
description: 'The Bull Skin',
icon: 'i-lucide-map-pin',
value: 'ES'
}
])
</script>
<template>
<UListbox :items="items" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[]>([
{
label: 'France',
description: 'The Hexagon',
icon: 'i-lucide-map-pin',
value: 'FR'
},
{
label: 'Germany',
description: 'The Federal Republic',
icon: 'i-lucide-map-pin',
value: 'DE'
},
{
label: 'Italy',
description: 'The Boot',
icon: 'i-lucide-map-pin',
value: 'IT'
},
{
label: 'Spain',
description: 'The Bull Skin',
icon: 'i-lucide-map-pin',
value: 'ES'
}
])
</script>
<template>
<UListbox :items="items" />
</template>
You can also pass an array of arrays to the items prop to display separated groups of items.
<script setup lang="ts">
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[][]>([
[
{
label: 'France',
icon: 'i-lucide-map-pin',
value: 'FR'
},
{
label: 'Germany',
icon: 'i-lucide-map-pin',
value: 'DE'
},
{
label: 'Italy',
icon: 'i-lucide-map-pin',
value: 'IT'
}
],
[
{
label: 'Brazil',
icon: 'i-lucide-map-pin',
value: 'BR'
},
{
label: 'Argentina',
icon: 'i-lucide-map-pin',
value: 'AR'
}
]
])
</script>
<template>
<UListbox :items="items" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[][]>([
[
{
label: 'France',
icon: 'i-lucide-map-pin',
value: 'FR'
},
{
label: 'Germany',
icon: 'i-lucide-map-pin',
value: 'DE'
},
{
label: 'Italy',
icon: 'i-lucide-map-pin',
value: 'IT'
}
],
[
{
label: 'Brazil',
icon: 'i-lucide-map-pin',
value: 'BR'
},
{
label: 'Argentina',
icon: 'i-lucide-map-pin',
value: 'AR'
}
]
])
</script>
<template>
<UListbox :items="items" />
</template>
Multiple
Use the multiple prop to allow selecting multiple items. When enabled, the v-model will be an array.
<script setup lang="ts">
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[]>([
{
label: 'France',
icon: 'i-lucide-map-pin',
value: 'FR'
},
{
label: 'Germany',
icon: 'i-lucide-map-pin',
value: 'DE'
},
{
label: 'Italy',
icon: 'i-lucide-map-pin',
value: 'IT'
},
{
label: 'Spain',
icon: 'i-lucide-map-pin',
value: 'ES'
}
])
</script>
<template>
<UListbox multiple :items="items" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[]>([
{
label: 'France',
icon: 'i-lucide-map-pin',
value: 'FR'
},
{
label: 'Germany',
icon: 'i-lucide-map-pin',
value: 'DE'
},
{
label: 'Italy',
icon: 'i-lucide-map-pin',
value: 'IT'
},
{
label: 'Spain',
icon: 'i-lucide-map-pin',
value: 'ES'
}
])
</script>
<template>
<UListbox multiple :items="items" />
</template>
Value Key
You can choose to bind a single property of the object rather than the whole object by using the value-key prop. Defaults to undefined.
<script setup lang="ts">
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[]>([
{
label: 'France',
icon: 'i-lucide-map-pin',
value: 'FR'
},
{
label: 'Germany',
icon: 'i-lucide-map-pin',
value: 'DE'
},
{
label: 'Italy',
icon: 'i-lucide-map-pin',
value: 'IT'
},
{
label: 'Spain',
icon: 'i-lucide-map-pin',
value: 'ES'
}
])
const value = ref('FR')
</script>
<template>
<UListbox v-model="value" value-key="value" :items="items" class="w-full" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[]>([
{
label: 'France',
icon: 'i-lucide-map-pin',
value: 'FR'
},
{
label: 'Germany',
icon: 'i-lucide-map-pin',
value: 'DE'
},
{
label: 'Italy',
icon: 'i-lucide-map-pin',
value: 'IT'
},
{
label: 'Spain',
icon: 'i-lucide-map-pin',
value: 'ES'
}
])
const value = ref('FR')
</script>
<template>
<UListbox v-model="value" value-key="value" :items="items" class="w-full" />
</template>
Filter
Use the filter prop to display a filter input or pass an object to customize the Input component. Defaults to false.
<script setup lang="ts">
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[]>([
{
label: 'France',
icon: 'i-lucide-map-pin',
value: 'FR'
},
{
label: 'Germany',
icon: 'i-lucide-map-pin',
value: 'DE'
},
{
label: 'Italy',
icon: 'i-lucide-map-pin',
value: 'IT'
},
{
label: 'Spain',
icon: 'i-lucide-map-pin',
value: 'ES'
},
{
label: 'Netherlands',
icon: 'i-lucide-map-pin',
value: 'NL'
},
{
label: 'Poland',
icon: 'i-lucide-map-pin',
value: 'PL'
}
])
</script>
<template>
<UListbox :filter="{
placeholder: 'Filter...',
icon: 'i-lucide-search'
}" :items="items" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[]>([
{
label: 'France',
icon: 'i-lucide-map-pin',
value: 'FR'
},
{
label: 'Germany',
icon: 'i-lucide-map-pin',
value: 'DE'
},
{
label: 'Italy',
icon: 'i-lucide-map-pin',
value: 'IT'
},
{
label: 'Spain',
icon: 'i-lucide-map-pin',
value: 'ES'
},
{
label: 'Netherlands',
icon: 'i-lucide-map-pin',
value: 'NL'
},
{
label: 'Poland',
icon: 'i-lucide-map-pin',
value: 'PL'
}
])
</script>
<template>
<UListbox :filter="{
placeholder: 'Filter...',
icon: 'i-lucide-search'
}" :items="items" />
</template>
Selected Icon
Use the selected-icon prop to customize the icon when an item is selected. Defaults to i-lucide-check.
<script setup lang="ts">
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[]>([
{
label: 'France',
icon: 'i-lucide-map-pin',
value: 'FR'
},
{
label: 'Germany',
icon: 'i-lucide-map-pin',
value: 'DE'
},
{
label: 'Italy',
icon: 'i-lucide-map-pin',
value: 'IT'
},
{
label: 'Spain',
icon: 'i-lucide-map-pin',
value: 'ES'
}
])
const value = ref('FR')
</script>
<template>
<UListbox v-model="value" selected-icon="i-lucide-flame" value-key="value" :items="items" class="w-full" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[]>([
{
label: 'France',
icon: 'i-lucide-map-pin',
value: 'FR'
},
{
label: 'Germany',
icon: 'i-lucide-map-pin',
value: 'DE'
},
{
label: 'Italy',
icon: 'i-lucide-map-pin',
value: 'IT'
},
{
label: 'Spain',
icon: 'i-lucide-map-pin',
value: 'ES'
}
])
const value = ref('FR')
</script>
<template>
<UListbox v-model="value" selected-icon="i-lucide-flame" value-key="value" :items="items" class="w-full" />
</template>
Size
Use the size prop to change the size of the Listbox.
<script setup lang="ts">
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[]>([
{
label: 'France',
icon: 'i-lucide-map-pin',
value: 'FR'
},
{
label: 'Germany',
icon: 'i-lucide-map-pin',
value: 'DE'
},
{
label: 'Italy',
icon: 'i-lucide-map-pin',
value: 'IT'
},
{
label: 'Spain',
icon: 'i-lucide-map-pin',
value: 'ES'
}
])
</script>
<template>
<UListbox size="xl" :items="items" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[]>([
{
label: 'France',
icon: 'i-lucide-map-pin',
value: 'FR'
},
{
label: 'Germany',
icon: 'i-lucide-map-pin',
value: 'DE'
},
{
label: 'Italy',
icon: 'i-lucide-map-pin',
value: 'IT'
},
{
label: 'Spain',
icon: 'i-lucide-map-pin',
value: 'ES'
}
])
</script>
<template>
<UListbox size="xl" :items="items" />
</template>
Loading
Use the loading prop to display a loading indicator. Use the loading-icon prop to customize the icon.
<script setup lang="ts">
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[]>([
{
label: 'France',
icon: 'i-lucide-map-pin',
value: 'FR'
},
{
label: 'Germany',
icon: 'i-lucide-map-pin',
value: 'DE'
}
])
</script>
<template>
<UListbox loading :items="items" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[]>([
{
label: 'France',
icon: 'i-lucide-map-pin',
value: 'FR'
},
{
label: 'Germany',
icon: 'i-lucide-map-pin',
value: 'DE'
}
])
</script>
<template>
<UListbox loading :items="items" />
</template>
Disabled
Use the disabled prop to prevent any user interaction with the Listbox.
<script setup lang="ts">
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[]>([
{
label: 'France',
icon: 'i-lucide-map-pin',
value: 'FR'
},
{
label: 'Germany',
icon: 'i-lucide-map-pin',
value: 'DE'
},
{
label: 'Italy',
icon: 'i-lucide-map-pin',
value: 'IT'
},
{
label: 'Spain',
icon: 'i-lucide-map-pin',
value: 'ES'
}
])
</script>
<template>
<UListbox disabled :items="items" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[]>([
{
label: 'France',
icon: 'i-lucide-map-pin',
value: 'FR'
},
{
label: 'Germany',
icon: 'i-lucide-map-pin',
value: 'DE'
},
{
label: 'Italy',
icon: 'i-lucide-map-pin',
value: 'IT'
},
{
label: 'Spain',
icon: 'i-lucide-map-pin',
value: 'ES'
}
])
</script>
<template>
<UListbox disabled :items="items" />
</template>
Examples
With items type
You can use the type property with separator to display a separator between items or label to display a label.
<script setup lang="ts">
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[][]>([
[
{
type: 'label',
label: 'Fruits'
},
{
label: 'Apple'
},
{
label: 'Banana'
},
{
label: 'Blueberry'
},
{
label: 'Grapes'
},
{
label: 'Pineapple'
}
],
[
{
type: 'label',
label: 'Vegetables'
},
{
label: 'Aubergine'
},
{
label: 'Broccoli'
},
{
label: 'Carrot'
},
{
label: 'Courgette'
},
{
label: 'Leek'
}
]
])
</script>
<template>
<UListbox :items="items" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[][]>([
[
{
type: 'label',
label: 'Fruits'
},
{
label: 'Apple'
},
{
label: 'Banana'
},
{
label: 'Blueberry'
},
{
label: 'Grapes'
},
{
label: 'Pineapple'
}
],
[
{
type: 'label',
label: 'Vegetables'
},
{
label: 'Aubergine'
},
{
label: 'Broccoli'
},
{
label: 'Carrot'
},
{
label: 'Courgette'
},
{
label: 'Leek'
}
]
])
</script>
<template>
<UListbox :items="items" />
</template>
With icon in items
You can use the icon property to display an Icon inside the items.
<script setup lang="ts">
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[]>([
{
label: 'Backlog',
icon: 'i-lucide-circle-help',
value: 'backlog'
},
{
label: 'Todo',
icon: 'i-lucide-circle-plus',
value: 'todo'
},
{
label: 'In Progress',
icon: 'i-lucide-circle-arrow-up',
value: 'in_progress'
},
{
label: 'Done',
icon: 'i-lucide-circle-check',
value: 'done'
}
])
</script>
<template>
<UListbox :items="items" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[]>([
{
label: 'Backlog',
icon: 'i-lucide-circle-help',
value: 'backlog'
},
{
label: 'Todo',
icon: 'i-lucide-circle-plus',
value: 'todo'
},
{
label: 'In Progress',
icon: 'i-lucide-circle-arrow-up',
value: 'in_progress'
},
{
label: 'Done',
icon: 'i-lucide-circle-check',
value: 'done'
}
])
</script>
<template>
<UListbox :items="items" />
</template>
With avatar in items
You can use the avatar property to display an Avatar inside the items.
<script setup lang="ts">
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[]>([
{
label: 'benjamincanac',
avatar: {
src: 'https://github.com/benjamincanac.png'
}
},
{
label: 'romhml',
avatar: {
src: 'https://github.com/romhml.png'
}
},
{
label: 'atinux',
avatar: {
src: 'https://github.com/atinux.png'
}
},
{
label: 'HugoRCD',
avatar: {
src: 'https://github.com/HugoRCD.png'
}
}
])
</script>
<template>
<UListbox :items="items" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[]>([
{
label: 'benjamincanac',
avatar: {
src: 'https://github.com/benjamincanac.png'
}
},
{
label: 'romhml',
avatar: {
src: 'https://github.com/romhml.png'
}
},
{
label: 'atinux',
avatar: {
src: 'https://github.com/atinux.png'
}
},
{
label: 'HugoRCD',
avatar: {
src: 'https://github.com/HugoRCD.png'
}
}
])
</script>
<template>
<UListbox :items="items" />
</template>
With chip in items
You can use the chip property to display a Chip inside the items.
<script setup lang="ts">
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[]>([
{
label: 'bug',
chip: {
color: 'error'
}
},
{
label: 'feature',
chip: {
color: 'success'
}
},
{
label: 'enhancement',
chip: {
color: 'info'
}
}
])
</script>
<template>
<UListbox :items="items" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[]>([
{
label: 'bug',
chip: {
color: 'error'
}
},
{
label: 'feature',
chip: {
color: 'success'
}
},
{
label: 'enhancement',
chip: {
color: 'info'
}
}
])
</script>
<template>
<UListbox :items="items" />
</template>
With description in items
You can use the description property to display additional text below the label.
<script setup lang="ts">
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[]>([
{
label: 'France',
description: 'The Hexagon',
icon: 'i-lucide-map-pin',
value: 'FR'
},
{
label: 'Germany',
description: 'The Federal Republic',
icon: 'i-lucide-map-pin',
value: 'DE'
},
{
label: 'Italy',
description: 'The Boot',
icon: 'i-lucide-map-pin',
value: 'IT'
},
{
label: 'Spain',
description: 'The Bull Skin',
icon: 'i-lucide-map-pin',
value: 'ES'
}
])
</script>
<template>
<UListbox :items="items" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ListboxItem } from '@nuxt/ui'
const items = ref<ListboxItem[]>([
{
label: 'France',
description: 'The Hexagon',
icon: 'i-lucide-map-pin',
value: 'FR'
},
{
label: 'Germany',
description: 'The Federal Republic',
icon: 'i-lucide-map-pin',
value: 'DE'
},
{
label: 'Italy',
description: 'The Boot',
icon: 'i-lucide-map-pin',
value: 'IT'
},
{
label: 'Spain',
description: 'The Bull Skin',
icon: 'i-lucide-map-pin',
value: 'ES'
}
])
</script>
<template>
<UListbox :items="items" />
</template>
Control selected items
You can control the selected item by using the default-value prop or the v-model directive.
<script setup lang="ts">
import type { ListboxItem } from '@nuxt/ui'
const items: ListboxItem[] = [
{ label: 'France', icon: 'i-lucide-map-pin', value: 'FR' },
{ label: 'Germany', icon: 'i-lucide-map-pin', value: 'DE' },
{ label: 'Italy', icon: 'i-lucide-map-pin', value: 'IT' },
{ label: 'Spain', icon: 'i-lucide-map-pin', value: 'ES' },
{ label: 'Netherlands', icon: 'i-lucide-map-pin', value: 'NL' },
{ label: 'Poland', icon: 'i-lucide-map-pin', value: 'PL' }
]
const value = ref<ListboxItem>(items[2]!)
</script>
<template>
<UListbox
v-model="value"
:items="items"
class="w-full"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ListboxItem } from '@nuxt/ui'
const items: ListboxItem[] = [
{ label: 'France', icon: 'i-lucide-map-pin', value: 'FR' },
{ label: 'Germany', icon: 'i-lucide-map-pin', value: 'DE' },
{ label: 'Italy', icon: 'i-lucide-map-pin', value: 'IT' },
{ label: 'Spain', icon: 'i-lucide-map-pin', value: 'ES' },
{ label: 'Netherlands', icon: 'i-lucide-map-pin', value: 'NL' },
{ label: 'Poland', icon: 'i-lucide-map-pin', value: 'PL' }
]
const value = ref<ListboxItem>(items[2]!)
</script>
<template>
<UListbox
v-model="value"
:items="items"
class="w-full"
/>
</template>
Control search term
Use the v-model:search-term directive to control the search term.
<script setup lang="ts">
const searchTerm = ref('F')
const items = ref([
{ label: 'France', icon: 'i-lucide-map-pin', value: 'FR' },
{ label: 'Germany', icon: 'i-lucide-map-pin', value: 'DE' },
{ label: 'Italy', icon: 'i-lucide-map-pin', value: 'IT' },
{ label: 'Spain', icon: 'i-lucide-map-pin', value: 'ES' },
{ label: 'Netherlands', icon: 'i-lucide-map-pin', value: 'NL' },
{ label: 'Poland', icon: 'i-lucide-map-pin', value: 'PL' }
])
const value = ref()
</script>
<template>
<UListbox v-model="value" v-model:search-term="searchTerm" filter :items="items" class="w-full" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
const searchTerm = ref('F')
const items = ref([
{ label: 'France', icon: 'i-lucide-map-pin', value: 'FR' },
{ label: 'Germany', icon: 'i-lucide-map-pin', value: 'DE' },
{ label: 'Italy', icon: 'i-lucide-map-pin', value: 'IT' },
{ label: 'Spain', icon: 'i-lucide-map-pin', value: 'ES' },
{ label: 'Netherlands', icon: 'i-lucide-map-pin', value: 'NL' },
{ label: 'Poland', icon: 'i-lucide-map-pin', value: 'PL' }
])
const value = ref()
</script>
<template>
<UListbox v-model="value" v-model:search-term="searchTerm" filter :items="items" class="w-full" />
</template>
With ignore filter
Set the ignore-filter prop to true to disable the internal search and use your own search logic.
<script setup lang="ts">
import { refDebounced } from '@vueuse/core'
const searchTerm = ref('')
const searchTermDebounced = refDebounced(searchTerm, 200)
const { data: users, status, execute } = await useLazyFetch('https://jsonplaceholder.typicode.com/users', {
key: 'listbox-users-search',
params: { q: searchTermDebounced },
transform: (data: { id: number, name: string }[]) => {
return data?.map(user => ({
label: user.name,
value: String(user.id),
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, loading: 'lazy' as const }
}))
},
immediate: false
})
onMounted(() => {
execute()
})
</script>
<template>
<UListbox
v-model:search-term="searchTerm"
:items="users || []"
:filter="{
icon: 'i-lucide-search',
loading: status === 'pending'
}"
ignore-filter
class="w-full"
/>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { refDebounced } from '@vueuse/core'
const searchTerm = ref('')
const searchTermDebounced = refDebounced(searchTerm, 200)
const { data: users, status, execute } = await useLazyFetch('https://jsonplaceholder.typicode.com/users', {
key: 'listbox-users-search',
params: { q: searchTermDebounced },
transform: (data: { id: number, name: string }[]) => {
return data?.map(user => ({
label: user.name,
value: String(user.id),
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, loading: 'lazy' as const }
}))
},
immediate: false
})
onMounted(() => {
execute()
})
</script>
<template>
<UListbox
v-model:search-term="searchTerm"
:items="users || []"
:filter="{
icon: 'i-lucide-search',
loading: status === 'pending'
}"
ignore-filter
class="w-full"
/>
</template>
refDebounced to debounce the API calls.With filter fields
Use the filter-fields prop with an array of fields to filter on. Defaults to [labelKey].
<script setup lang="ts">
const { data: users, status, execute } = await useLazyFetch('https://jsonplaceholder.typicode.com/users', {
key: 'typicode-users-email',
transform: (data: { id: number, name: string, email: string }[]) => {
return data?.map(user => ({
label: user.name,
email: user.email,
value: String(user.id),
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, loading: 'lazy' as const }
}))
},
immediate: false
})
onMounted(() => {
execute()
})
</script>
<template>
<UListbox
:items="users || []"
:loading="status === 'pending'"
:filter-fields="['label', 'email']"
filter
class="w-full"
>
<template #item-label="{ item }">
{{ item.label }}
<span class="text-muted">
{{ item.email }}
</span>
</template>
</UListbox>
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
const { data: users, status, execute } = await useLazyFetch('https://jsonplaceholder.typicode.com/users', {
key: 'typicode-users-email',
transform: (data: { id: number, name: string, email: string }[]) => {
return data?.map(user => ({
label: user.name,
email: user.email,
value: String(user.id),
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, loading: 'lazy' as const }
}))
},
immediate: false
})
onMounted(() => {
execute()
})
</script>
<template>
<UListbox
:items="users || []"
:loading="status === 'pending'"
:filter-fields="['label', 'email']"
filter
class="w-full"
>
<template #item-label="{ item }">
{{ item.label }}
<span class="text-muted">
{{ item.email }}
</span>
</template>
</UListbox>
</template>
With virtualization
Use the virtualize prop to enable virtualization for large lists as a boolean or an object with options like { estimateSize: 32, overscan: 12 }.
<script setup lang="ts">
import type { ListboxItem } from '@nuxt/ui'
const items: ListboxItem[] = Array.from({ length: 1000 }, (_, i) => ({
label: `Item ${i + 1}`,
icon: 'i-lucide-file',
value: i + 1
}))
const value = ref<ListboxItem[]>([])
</script>
<template>
<UListbox
v-model="value"
:items="items"
multiple
virtualize
class="w-full"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ListboxItem } from '@nuxt/ui'
const items: ListboxItem[] = Array.from({ length: 1000 }, (_, i) => ({
label: `Item ${i + 1}`,
icon: 'i-lucide-file',
value: i + 1
}))
const value = ref<ListboxItem[]>([])
</script>
<template>
<UListbox
v-model="value"
:items="items"
multiple
virtualize
class="w-full"
/>
</template>
As a transfer list
You can compose two Listbox components with Button controls to build a transfer list pattern.
<script setup lang="ts">
import type { ListboxItem } from '@nuxt/ui'
const items: ListboxItem[] = [
{ label: 'France', icon: 'i-lucide-map-pin', value: 'FR' },
{ label: 'Germany', icon: 'i-lucide-map-pin', value: 'DE' },
{ label: 'Italy', icon: 'i-lucide-map-pin', value: 'IT' },
{ label: 'Spain', icon: 'i-lucide-map-pin', value: 'ES' },
{ label: 'Netherlands', icon: 'i-lucide-map-pin', value: 'NL' },
{ label: 'Poland', icon: 'i-lucide-map-pin', value: 'PL' },
{ label: 'Belgium', icon: 'i-lucide-map-pin', value: 'BE' },
{ label: 'Portugal', icon: 'i-lucide-map-pin', value: 'PT' }
]
const targetItems = ref<ListboxItem[]>([])
const sourceSelection = ref<ListboxItem[]>([])
const targetSelection = ref<ListboxItem[]>([])
const sourceItems = computed(() => items.filter(item => !targetItems.value.some(t => t.value === item.value)))
function transferSelected() {
targetItems.value = [...targetItems.value, ...sourceSelection.value]
sourceSelection.value = []
}
function removeSelected() {
targetItems.value = targetItems.value.filter(item => !targetSelection.value.some(t => t.value === item.value))
targetSelection.value = []
}
</script>
<template>
<div class="flex items-stretch gap-4 w-full">
<div class="flex flex-col flex-1 gap-1">
<span class="text-sm font-medium text-highlighted">Available</span>
<UListbox
v-model="sourceSelection"
:items="sourceItems"
multiple
filter
class="size-full"
/>
</div>
<div class="flex flex-col items-center justify-center gap-1">
<UButton
icon="i-lucide-chevron-right"
color="neutral"
variant="outline"
:disabled="!sourceSelection.length"
@click="transferSelected"
/>
<UButton
icon="i-lucide-chevron-left"
color="neutral"
variant="outline"
:disabled="!targetSelection.length"
@click="removeSelected"
/>
</div>
<div class="flex flex-col flex-1 gap-1">
<span class="text-sm font-medium text-highlighted">Selected</span>
<UListbox
v-model="targetSelection"
:items="targetItems"
multiple
filter
class="size-full"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { ListboxItem } from '@nuxt/ui'
const items: ListboxItem[] = [
{ label: 'France', icon: 'i-lucide-map-pin', value: 'FR' },
{ label: 'Germany', icon: 'i-lucide-map-pin', value: 'DE' },
{ label: 'Italy', icon: 'i-lucide-map-pin', value: 'IT' },
{ label: 'Spain', icon: 'i-lucide-map-pin', value: 'ES' },
{ label: 'Netherlands', icon: 'i-lucide-map-pin', value: 'NL' },
{ label: 'Poland', icon: 'i-lucide-map-pin', value: 'PL' },
{ label: 'Belgium', icon: 'i-lucide-map-pin', value: 'BE' },
{ label: 'Portugal', icon: 'i-lucide-map-pin', value: 'PT' }
]
const targetItems = ref<ListboxItem[]>([])
const sourceSelection = ref<ListboxItem[]>([])
const targetSelection = ref<ListboxItem[]>([])
const sourceItems = computed(() => items.filter(item => !targetItems.value.some(t => t.value === item.value)))
function transferSelected() {
targetItems.value = [...targetItems.value, ...sourceSelection.value]
sourceSelection.value = []
}
function removeSelected() {
targetItems.value = targetItems.value.filter(item => !targetSelection.value.some(t => t.value === item.value))
targetSelection.value = []
}
</script>
<template>
<div class="flex items-stretch gap-4 w-full">
<div class="flex flex-col flex-1 gap-1">
<span class="text-sm font-medium text-highlighted">Available</span>
<UListbox
v-model="sourceSelection"
:items="sourceItems"
multiple
filter
class="size-full"
/>
</div>
<div class="flex flex-col items-center justify-center gap-1">
<UButton
icon="i-lucide-chevron-right"
color="neutral"
variant="outline"
:disabled="!sourceSelection.length"
@click="transferSelected"
/>
<UButton
icon="i-lucide-chevron-left"
color="neutral"
variant="outline"
:disabled="!targetSelection.length"
@click="removeSelected"
/>
</div>
<div class="flex flex-col flex-1 gap-1">
<span class="text-sm font-medium text-highlighted">Selected</span>
<UListbox
v-model="targetSelection"
:items="targetItems"
multiple
filter
class="size-full"
/>
</div>
</div>
</template>
API
Props
| Prop | Default | Type |
|---|---|---|
as | 'div' | anyThe element or component this component should render as. |
id | string | |
color | 'primary' | "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" |
size | 'md' | "sm" | "md" | "xs" | "lg" | "xl" |
items | TThe items to display in the list. | |
modelValue | _Number<_Optional<_Nullable<GetModelValue<T, VK, M, undefined>, Mod>, Mod>, Mod>The controlled value of the Listbox. Can be bound with | |
modelModifiers | Mod | |
defaultValue | _Number<_Optional<_Nullable<GetModelValue<T, VK, M, undefined>, Mod>, Mod>, Mod>The default value when not controlled. | |
multiple | false | MWhether multiple items can be selected. |
valueKey | undefined | VKWhen |
labelKey | 'label' | keyof Extract<NestedItem<T>, object> & string | DotPathKeys<Extract<NestedItem<T>, object>>The key used to get the label from the item. |
descriptionKey | 'description' | keyof Extract<NestedItem<T>, object> & string | DotPathKeys<Extract<NestedItem<T>, object>>The key used to get the description from the item. |
loading | boolean Whether the list is in a loading state. | |
loadingIcon | appConfig.ui.icons.loading | anyThe icon displayed when loading. |
filter | false | boolean | Omit<InputProps<AcceptableValue, ModelModifiers>, "modelValue" | "defaultValue"> Whether to display a filter input or not.
Can be an object to pass additional props to the input.
|
filterFields | [labelKey] | string[]The fields to filter by. |
ignoreFilter | false | boolean When |
selectedIcon | appConfig.ui.icons.check | anyThe icon displayed when an item is selected. |
virtualize | false | boolean | { overscan?: number ; estimateSize?: number | ((index: number) => number) | undefined; } | undefinedEnable virtualization for large lists. |
highlight | boolean Highlight the ring color like a focus state. | |
autofocus | boolean | |
autofocusDelay | 0 | number |
by | string | (a: AcceptableValue, b: AcceptableValue): booleanUse this to compare objects by a particular field, or pass your own comparison function for complete control over how objects are compared. | |
disabled | boolean When | |
highlightOnHover | true | boolean When |
name | stringThe name of the field. Submitted with its owning form as part of a name/value pair. | |
orientation | "vertical" | "horizontal"The orientation of the listbox. | |
required | boolean When | |
selectionBehavior | 'toggle' | "replace" | "toggle"How multiple selection should behave in the collection. |
searchTerm | '' | string |
ui | { root?: ClassNameValue; input?: ClassNameValue; content?: ClassNameValue; group?: ClassNameValue; label?: ClassNameValue; separator?: ClassNameValue; empty?: ClassNameValue; loading?: ClassNameValue; loadingIcon?: ClassNameValue; item?: ClassNameValue; itemLeadingIcon?: ClassNameValue; itemLeadingAvatar?: ClassNameValue; itemLeadingAvatarSize?: ClassNameValue; itemLeadingChip?: ClassNameValue; itemLeadingChipSize?: ClassNameValue; itemWrapper?: ClassNameValue; itemLabel?: ClassNameValue; itemDescription?: ClassNameValue; itemTrailing?: ClassNameValue; itemTrailingIcon?: ClassNameValue; } |
Slots
| Slot | Type |
|---|---|
loading | {} |
empty | { searchTerm: string; } |
item | { item: NestedItem<T>; index: number; ui: object; } |
item-leading | { item: NestedItem<T>; index: number; ui: object; } |
item-label | { item: NestedItem<T>; index: number; } |
item-description | { item: NestedItem<T>; index: number; } |
item-trailing | { item: NestedItem<T>; index: number; ui: object; } |
Emits
| Event | Type |
|---|---|
entryFocus | [event: CustomEvent<any>] |
highlight | [payload: { ref: HTMLElement; value: AcceptableValue; } | undefined] |
leave | [event: Event] |
change | [event: Event] |
update:modelValue | [value: _Number<_Optional<_Nullable<GetModelValue<T, VK, M, undefined>, Mod>, Mod>, Mod>] |
update:searchTerm | [value: string] |
Theme
export default defineAppConfig({
ui: {
listbox: {
slots: {
root: 'flex flex-col min-h-0 min-w-0 ring ring-inset ring-default rounded-lg overflow-hidden',
input: 'border-b border-default',
content: 'relative overflow-y-auto flex-1 max-h-60 scroll-py-1 focus:outline-none',
group: 'p-1 isolate',
label: 'font-semibold text-highlighted',
separator: '-mx-1 my-1 h-px bg-border',
empty: 'text-center text-muted',
loading: 'flex items-center justify-center text-muted',
loadingIcon: 'animate-spin shrink-0',
item: [
'group relative w-full flex items-start select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75 text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50',
'transition-colors before:transition-colors'
],
itemLeadingIcon: [
'shrink-0 text-dimmed group-data-highlighted:not-group-data-disabled:text-default',
'transition-colors'
],
itemLeadingAvatar: 'shrink-0',
itemLeadingAvatarSize: '',
itemLeadingChip: 'shrink-0',
itemLeadingChipSize: '',
itemWrapper: 'flex-1 flex flex-col min-w-0',
itemLabel: 'truncate',
itemDescription: 'truncate text-muted',
itemTrailing: 'ms-auto inline-flex gap-1.5 items-center',
itemTrailingIcon: 'shrink-0'
},
variants: {
size: {
xs: {
label: 'p-1 text-[10px]/3 gap-1',
empty: 'py-3 text-xs',
loading: 'py-3',
loadingIcon: 'size-4',
item: 'p-1 text-xs gap-1',
itemLeadingIcon: 'size-4',
itemLeadingAvatarSize: '3xs',
itemLeadingChip: 'size-4',
itemLeadingChipSize: 'sm',
itemTrailingIcon: 'size-4'
},
sm: {
label: 'p-1.5 text-[10px]/3 gap-1.5',
empty: 'py-4 text-xs',
loading: 'py-4',
loadingIcon: 'size-4',
item: 'p-1.5 text-xs gap-1.5',
itemLeadingIcon: 'size-4',
itemLeadingAvatarSize: '3xs',
itemLeadingChip: 'size-4',
itemLeadingChipSize: 'sm',
itemTrailingIcon: 'size-4'
},
md: {
label: 'p-1.5 text-xs gap-1.5',
empty: 'py-6 text-sm',
loading: 'py-6',
loadingIcon: 'size-5',
item: 'p-1.5 text-sm gap-1.5',
itemLeadingIcon: 'size-5',
itemLeadingAvatarSize: '2xs',
itemLeadingChip: 'size-5',
itemLeadingChipSize: 'md',
itemTrailingIcon: 'size-5'
},
lg: {
label: 'p-2 text-xs gap-2',
empty: 'py-7 text-sm',
loading: 'py-7',
loadingIcon: 'size-5',
item: 'p-2 text-sm gap-2',
itemLeadingIcon: 'size-5',
itemLeadingAvatarSize: '2xs',
itemLeadingChip: 'size-5',
itemLeadingChipSize: 'md',
itemTrailingIcon: 'size-5'
},
xl: {
label: 'p-2 text-sm gap-2',
empty: 'py-8 text-base',
loading: 'py-8',
loadingIcon: 'size-6',
item: 'p-2 text-base gap-2',
itemLeadingIcon: 'size-6',
itemLeadingAvatarSize: 'xs',
itemLeadingChip: 'size-6',
itemLeadingChipSize: 'lg',
itemTrailingIcon: 'size-6',
itemDescription: 'text-sm'
}
},
color: {
primary: '',
secondary: '',
success: '',
info: '',
warning: '',
error: '',
neutral: ''
},
virtualize: {
true: {
content: 'p-1 isolate'
},
false: {
content: 'divide-y divide-default'
}
},
disabled: {
true: {
root: 'opacity-75 cursor-not-allowed'
}
},
highlight: {
true: ''
}
},
compoundVariants: [
{
color: 'primary',
highlight: true,
class: {
root: 'ring ring-inset ring-primary'
}
},
{
color: 'neutral',
highlight: true,
class: {
root: 'ring ring-inset ring-inverted'
}
}
],
defaultVariants: {
size: 'md'
}
}
}
})
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
ui: {
listbox: {
slots: {
root: 'flex flex-col min-h-0 min-w-0 ring ring-inset ring-default rounded-lg overflow-hidden',
input: 'border-b border-default',
content: 'relative overflow-y-auto flex-1 max-h-60 scroll-py-1 focus:outline-none',
group: 'p-1 isolate',
label: 'font-semibold text-highlighted',
separator: '-mx-1 my-1 h-px bg-border',
empty: 'text-center text-muted',
loading: 'flex items-center justify-center text-muted',
loadingIcon: 'animate-spin shrink-0',
item: [
'group relative w-full flex items-start select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75 text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50',
'transition-colors before:transition-colors'
],
itemLeadingIcon: [
'shrink-0 text-dimmed group-data-highlighted:not-group-data-disabled:text-default',
'transition-colors'
],
itemLeadingAvatar: 'shrink-0',
itemLeadingAvatarSize: '',
itemLeadingChip: 'shrink-0',
itemLeadingChipSize: '',
itemWrapper: 'flex-1 flex flex-col min-w-0',
itemLabel: 'truncate',
itemDescription: 'truncate text-muted',
itemTrailing: 'ms-auto inline-flex gap-1.5 items-center',
itemTrailingIcon: 'shrink-0'
},
variants: {
size: {
xs: {
label: 'p-1 text-[10px]/3 gap-1',
empty: 'py-3 text-xs',
loading: 'py-3',
loadingIcon: 'size-4',
item: 'p-1 text-xs gap-1',
itemLeadingIcon: 'size-4',
itemLeadingAvatarSize: '3xs',
itemLeadingChip: 'size-4',
itemLeadingChipSize: 'sm',
itemTrailingIcon: 'size-4'
},
sm: {
label: 'p-1.5 text-[10px]/3 gap-1.5',
empty: 'py-4 text-xs',
loading: 'py-4',
loadingIcon: 'size-4',
item: 'p-1.5 text-xs gap-1.5',
itemLeadingIcon: 'size-4',
itemLeadingAvatarSize: '3xs',
itemLeadingChip: 'size-4',
itemLeadingChipSize: 'sm',
itemTrailingIcon: 'size-4'
},
md: {
label: 'p-1.5 text-xs gap-1.5',
empty: 'py-6 text-sm',
loading: 'py-6',
loadingIcon: 'size-5',
item: 'p-1.5 text-sm gap-1.5',
itemLeadingIcon: 'size-5',
itemLeadingAvatarSize: '2xs',
itemLeadingChip: 'size-5',
itemLeadingChipSize: 'md',
itemTrailingIcon: 'size-5'
},
lg: {
label: 'p-2 text-xs gap-2',
empty: 'py-7 text-sm',
loading: 'py-7',
loadingIcon: 'size-5',
item: 'p-2 text-sm gap-2',
itemLeadingIcon: 'size-5',
itemLeadingAvatarSize: '2xs',
itemLeadingChip: 'size-5',
itemLeadingChipSize: 'md',
itemTrailingIcon: 'size-5'
},
xl: {
label: 'p-2 text-sm gap-2',
empty: 'py-8 text-base',
loading: 'py-8',
loadingIcon: 'size-6',
item: 'p-2 text-base gap-2',
itemLeadingIcon: 'size-6',
itemLeadingAvatarSize: 'xs',
itemLeadingChip: 'size-6',
itemLeadingChipSize: 'lg',
itemTrailingIcon: 'size-6',
itemDescription: 'text-sm'
}
},
color: {
primary: '',
secondary: '',
success: '',
info: '',
warning: '',
error: '',
neutral: ''
},
virtualize: {
true: {
content: 'p-1 isolate'
},
false: {
content: 'divide-y divide-default'
}
},
disabled: {
true: {
root: 'opacity-75 cursor-not-allowed'
}
},
highlight: {
true: ''
}
},
compoundVariants: [
{
color: 'primary',
highlight: true,
class: {
root: 'ring ring-inset ring-primary'
}
},
{
color: 'neutral',
highlight: true,
class: {
root: 'ring ring-inset ring-inverted'
}
}
],
defaultVariants: {
size: 'md'
}
}
}
})
]
})