Components
- Accordion
- Alert
- Alert Dialog
- Aspect Ratio
- Avatar
- Badge
- Breadcrumb
- Button
- Button Group
- Calendar
- Card
- Carousel
- Chart
- Checkbox
- Collapsible
- Combobox
- Command
- Context Menu
- Data Table
- Date Picker
- Dialog
- Drawer
- Dropdown Menu
- Empty
- Field
- Form
- Hover Card
- Input
- Input Group
- Input OTP
- Item
- Kbd
- Label
- Marker
- Menubar
- Native Select
- Navigation Menu
- Number Field
- Pagination
- Pin Input
- Popover
- Progress
- Radio Group
- Range Calendar
- Resizable
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Sonner
- Spinner
- Stepper
- Switch
- Table
- Tabs
- Tags Input
- Textarea
- Toast
- Toggle
- Toggle Group
- Tooltip
- Typography
Forms
Utilities
<script setup lang="ts">
import { CheckIcon, ChevronsUpDownIcon } from '@lucide/vue'
import { ref } from 'vue'
import { Button } from '@/components/ui/button'
import {
Combobox,
ComboboxAnchor,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxItemIndicator,
ComboboxList,
ComboboxTrigger,
} from '@/components/ui/combobox'
const frameworks = [
{
value: 'next.js',
label: 'Next.js',
},
{
value: 'sveltekit',
label: 'SvelteKit',
},
{
value: 'nuxt.js',
label: 'Nuxt.js',
},
{
value: 'remix',
label: 'Remix',
},
{
value: 'astro',
label: 'Astro',
},
]
const selectedFramework = ref<(typeof frameworks)[number]>()
</script>
<template>
<Combobox v-model="selectedFramework" by="label">
<ComboboxAnchor as-child>
<ComboboxTrigger as-child>
<Button
variant="outline"
class="w-[200px] justify-between"
>
{{ selectedFramework?.label ?? 'Select framework...' }}
<ChevronsUpDownIcon class="opacity-50" />
</Button>
</ComboboxTrigger>
</ComboboxAnchor>
<ComboboxList>
<ComboboxInput placeholder="Search framework..." />
<ComboboxEmpty>No framework found.</ComboboxEmpty>
<ComboboxGroup>
<ComboboxItem
v-for="framework in frameworks"
:key="framework.value"
:value="framework"
>
{{ framework.label }}
<ComboboxItemIndicator>
<CheckIcon />
</ComboboxItemIndicator>
</ComboboxItem>
</ComboboxGroup>
</ComboboxList>
</Combobox>
</template>A combobox can be built in two ways:
- Using the
Comboboxcomponents, built on top of the Reka UI Combobox. - Composing the
Popoverand theCommandcomponents as a listbox.
Installation
Combobox
pnpm dlx shadcn-vue@latest add combobox
Popover and Command
The listbox variant is built using a composition of the Popover and the Command components.
See installation instructions for the Popover and the Command components.
Usage
Combobox
<script setup lang="ts">
import { CheckIcon, ChevronsUpDownIcon } from '@lucide/vue'
import { ref } from 'vue'
import { Button } from '@/components/ui/button'
import {
Combobox,
ComboboxAnchor,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxItemIndicator,
ComboboxList,
ComboboxTrigger,
} from '@/components/ui/combobox'
const frameworks = [
{ value: 'next.js', label: 'Next.js' },
{ value: 'sveltekit', label: 'SvelteKit' },
{ value: 'nuxt.js', label: 'Nuxt.js' },
{ value: 'remix', label: 'Remix' },
{ value: 'astro', label: 'Astro' },
]
const selectedFramework = ref<(typeof frameworks)[number]>()
</script>
<template>
<Combobox v-model="selectedFramework" by="label">
<ComboboxAnchor as-child>
<ComboboxTrigger as-child>
<Button variant="outline" class="w-[200px] justify-between">
{{ selectedFramework?.label ?? 'Select framework...' }}
<ChevronsUpDownIcon class="opacity-50" />
</Button>
</ComboboxTrigger>
</ComboboxAnchor>
<ComboboxList>
<ComboboxInput placeholder="Search framework..." />
<ComboboxEmpty>No framework found.</ComboboxEmpty>
<ComboboxGroup>
<ComboboxItem
v-for="framework in frameworks"
:key="framework.value"
:value="framework"
>
{{ framework.label }}
<ComboboxItemIndicator>
<CheckIcon />
</ComboboxItemIndicator>
</ComboboxItem>
</ComboboxGroup>
</ComboboxList>
</Combobox>
</template>Popover and Command
<script setup lang="ts">
import { CheckIcon, ChevronsUpDownIcon } from '@lucide/vue'
import { ref } from 'vue'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
const frameworks = [
{ value: 'next.js', label: 'Next.js' },
{ value: 'sveltekit', label: 'SvelteKit' },
{ value: 'nuxt.js', label: 'Nuxt.js' },
{ value: 'remix', label: 'Remix' },
{ value: 'astro', label: 'Astro' },
]
const open = ref(false)
const value = ref('')
</script>
<template>
<Popover v-model:open="open">
<PopoverTrigger as-child>
<Button
variant="outline"
role="combobox"
:aria-expanded="open"
class="w-[200px] justify-between"
>
{{
value
? frameworks.find(framework => framework.value === value)?.label
: 'Select framework...'
}}
<ChevronsUpDownIcon class="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent class="w-[200px] p-0">
<Command>
<CommandInput placeholder="Search framework..." />
<CommandList>
<CommandEmpty>No framework found.</CommandEmpty>
<CommandGroup>
<CommandItem
v-for="framework in frameworks"
:key="framework.value"
:value="framework.value"
@select="() => {
value = value === framework.value ? '' : framework.value
open = false
}"
>
<CheckIcon
:class="cn(
'mr-2 h-4 w-4',
value === framework.value ? 'opacity-100' : 'opacity-0',
)"
/>
{{ framework.label }}
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</template>Examples
User
A combobox with custom item rendering and an action item.
<script setup lang="ts">
import { CheckIcon, ChevronsUpDownIcon, PlusCircleIcon } from '@lucide/vue'
import { ref } from 'vue'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import {
Combobox,
ComboboxAnchor,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxItemIndicator,
ComboboxList,
ComboboxSeparator,
ComboboxTrigger,
} from '@/components/ui/combobox'
const users = [
{
id: '1',
username: 'shadcn',
},
{
id: '2',
username: 'leerob',
},
{
id: '3',
username: 'evilrabbit',
},
]
const selectedUser = ref<(typeof users)[number]>()
</script>
<template>
<Combobox v-model="selectedUser" by="username">
<ComboboxAnchor as-child>
<ComboboxTrigger as-child>
<Button
variant="outline"
class="w-[200px] justify-between"
>
<template v-if="selectedUser">
<div class="flex items-center gap-2">
<Avatar class="size-5">
<AvatarImage :src="`https://github.com/${selectedUser.username}.png`" />
<AvatarFallback>{{ selectedUser.username[0] }}</AvatarFallback>
</Avatar>
{{ selectedUser.username }}
</div>
</template>
<template v-else>
Select user...
</template>
<ChevronsUpDownIcon class="opacity-50" />
</Button>
</ComboboxTrigger>
</ComboboxAnchor>
<ComboboxList>
<ComboboxInput placeholder="Search user..." />
<ComboboxEmpty>No user found.</ComboboxEmpty>
<ComboboxGroup>
<ComboboxItem
v-for="user in users"
:key="user.id"
:value="user"
>
<Avatar class="size-5">
<AvatarImage :src="`https://github.com/${user.username}.png`" />
<AvatarFallback>{{ user.username[0] }}</AvatarFallback>
</Avatar>
{{ user.username }}
<ComboboxItemIndicator>
<CheckIcon />
</ComboboxItemIndicator>
</ComboboxItem>
</ComboboxGroup>
<ComboboxSeparator />
<ComboboxGroup>
<ComboboxItem :value="null">
<PlusCircleIcon />
Create user
</ComboboxItem>
</ComboboxGroup>
</ComboboxList>
</Combobox>
</template>Timezone
A combobox with grouped items and a scrollable viewport.
<script setup lang="ts">
import { CheckIcon, ChevronDownIcon, PlusCircleIcon } from '@lucide/vue'
import { computed, ref } from 'vue'
import { Button } from '@/components/ui/button'
import {
Combobox,
ComboboxAnchor,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxItemIndicator,
ComboboxList,
ComboboxSeparator,
ComboboxTrigger,
ComboboxViewport,
} from '@/components/ui/combobox'
const timezones = [
{
label: 'Americas',
timezones: [
{ value: 'America/New_York', label: '(GMT-5) New York' },
{ value: 'America/Los_Angeles', label: '(GMT-8) Los Angeles' },
{ value: 'America/Chicago', label: '(GMT-6) Chicago' },
{ value: 'America/Toronto', label: '(GMT-5) Toronto' },
{ value: 'America/Vancouver', label: '(GMT-8) Vancouver' },
{ value: 'America/Sao_Paulo', label: '(GMT-3) São Paulo' },
],
},
{
label: 'Europe',
timezones: [
{ value: 'Europe/London', label: '(GMT+0) London' },
{ value: 'Europe/Paris', label: '(GMT+1) Paris' },
{ value: 'Europe/Berlin', label: '(GMT+1) Berlin' },
{ value: 'Europe/Rome', label: '(GMT+1) Rome' },
{ value: 'Europe/Madrid', label: '(GMT+1) Madrid' },
{ value: 'Europe/Amsterdam', label: '(GMT+1) Amsterdam' },
],
},
{
label: 'Asia/Pacific',
timezones: [
{ value: 'Asia/Tokyo', label: '(GMT+9) Tokyo' },
{ value: 'Asia/Shanghai', label: '(GMT+8) Shanghai' },
{ value: 'Asia/Singapore', label: '(GMT+8) Singapore' },
{ value: 'Asia/Dubai', label: '(GMT+4) Dubai' },
{ value: 'Australia/Sydney', label: '(GMT+11) Sydney' },
{ value: 'Asia/Seoul', label: '(GMT+9) Seoul' },
],
},
]
const selectedTimezone = ref<(typeof timezones)[number]['timezones'][number]>(
timezones[0].timezones[0],
)
const selectedGroup = computed(() =>
timezones.find(group =>
group.timezones.find(tz => tz.value === selectedTimezone.value?.value),
),
)
</script>
<template>
<Combobox v-model="selectedTimezone" by="label">
<ComboboxAnchor as-child>
<ComboboxTrigger as-child>
<Button
variant="outline"
class="h-12 w-[200px] justify-between px-2.5"
>
<template v-if="selectedTimezone">
<div class="flex flex-col items-start gap-0.5">
<span class="text-muted-foreground text-xs font-normal">
{{ selectedGroup?.label }}
</span>
<span>{{ selectedTimezone.label }}</span>
</div>
</template>
<template v-else>
Select timezone
</template>
<ChevronDownIcon class="text-muted-foreground" />
</Button>
</ComboboxTrigger>
</ComboboxAnchor>
<ComboboxList class="w-72" align="start">
<ComboboxInput placeholder="Search timezone..." />
<ComboboxViewport class="max-h-[260px]">
<ComboboxEmpty>No timezone found.</ComboboxEmpty>
<ComboboxGroup
v-for="region in timezones"
:key="region.label"
:heading="region.label"
>
<ComboboxItem
v-for="timezone in region.timezones"
:key="timezone.value"
:value="timezone"
>
{{ timezone.label }}
<ComboboxItemIndicator>
<CheckIcon />
</ComboboxItemIndicator>
</ComboboxItem>
</ComboboxGroup>
</ComboboxViewport>
<ComboboxSeparator />
<ComboboxGroup class="bg-popover">
<ComboboxItem :value="null">
<PlusCircleIcon />
Create timezone
</ComboboxItem>
</ComboboxGroup>
</ComboboxList>
</Combobox>
</template>Multiple
A combobox with multiple selection.
<script setup lang="ts">
import { CheckIcon, ChevronsUpDownIcon } from '@lucide/vue'
import { ref } from 'vue'
import { Button } from '@/components/ui/button'
import {
Combobox,
ComboboxAnchor,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxTrigger,
} from '@/components/ui/combobox'
const frameworks = [
{
value: 'next.js',
label: 'Next.js',
},
{
value: 'sveltekit',
label: 'SvelteKit',
},
{
value: 'nuxt.js',
label: 'Nuxt.js',
},
{
value: 'remix',
label: 'Remix',
},
{
value: 'astro',
label: 'Astro',
},
]
const selectedFrameworks = ref<typeof frameworks>([])
</script>
<template>
<Combobox v-model="selectedFrameworks" multiple by="label">
<ComboboxAnchor as-child>
<ComboboxTrigger as-child>
<Button
variant="outline"
class="w-[280px] justify-between"
>
<span class="truncate">
{{
selectedFrameworks.length > 0
? selectedFrameworks.map(framework => framework.label).join(', ')
: 'Select frameworks...'
}}
</span>
<ChevronsUpDownIcon class="opacity-50" />
</Button>
</ComboboxTrigger>
</ComboboxAnchor>
<ComboboxList class="w-[280px]" align="start">
<ComboboxInput placeholder="Search framework..." />
<ComboboxEmpty>No framework found.</ComboboxEmpty>
<ComboboxGroup>
<ComboboxItem
v-for="framework in frameworks"
:key="framework.value"
:value="framework"
>
<div
class="border-input data-[selected=true]:border-primary data-[selected=true]:bg-primary data-[selected=true]:text-primary-foreground pointer-events-none size-4 shrink-0 rounded-[4px] border transition-all select-none *:[svg]:opacity-0 data-[selected=true]:*:[svg]:opacity-100"
:data-selected="selectedFrameworks.some(f => f.value === framework.value)"
>
<CheckIcon class="size-3.5 text-current" />
</div>
{{ framework.label }}
</ComboboxItem>
</ComboboxGroup>
</ComboboxList>
</Combobox>
</template>With Listbox
A combobox built with the Popover and the Command components.
<script setup lang="ts">
import { CheckIcon, ChevronsUpDownIcon } from '@lucide/vue'
import { computed, ref } from 'vue'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
const frameworks = [
{
value: 'next.js',
label: 'Next.js',
},
{
value: 'sveltekit',
label: 'SvelteKit',
},
{
value: 'nuxt.js',
label: 'Nuxt.js',
},
{
value: 'remix',
label: 'Remix',
},
{
value: 'astro',
label: 'Astro',
},
]
const open = ref(false)
const value = ref('')
const selectedFramework = computed(() =>
frameworks.find(framework => framework.value === value.value),
)
function selectFramework(selectedValue: string) {
value.value = selectedValue === value.value ? '' : selectedValue
open.value = false
}
</script>
<template>
<Popover v-model:open="open">
<PopoverTrigger as-child>
<Button
variant="outline"
role="combobox"
:aria-expanded="open"
class="w-[200px] justify-between"
>
{{ selectedFramework?.label || "Select framework..." }}
<ChevronsUpDownIcon class="opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent class="w-[200px] p-0">
<Command>
<CommandInput class="h-9" placeholder="Search framework..." />
<CommandList>
<CommandEmpty>No framework found.</CommandEmpty>
<CommandGroup>
<CommandItem
v-for="framework in frameworks"
:key="framework.value"
:value="framework.value"
@select="(ev) => {
selectFramework(ev.detail.value as string)
}"
>
{{ framework.label }}
<CheckIcon
:class="cn(
'ml-auto',
value === framework.value ? 'opacity-100' : 'opacity-0',
)"
/>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</template>