Combobox
Combobox lets the user search a list and pick one option. Use it for lists too long to scan. For a short list without search, use Select.
'use client';
import * as React from 'react';
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxTrigger,
} from '@/components/nxds/components/combobox/combobox';
type ComboboxItemData = {
value: string;
label: string;
disabled?: boolean;
group?: string;
};
const frameworks: ComboboxItemData[] = [
{ value: 'next', label: 'Next.js' },
{ value: 'sveltekit', label: 'SvelteKit' },
{ value: 'nuxt', label: 'Nuxt.js' },
{ value: 'remix', label: 'Remix' },
{ value: 'astro', label: 'Astro' },
];
export default function ComboboxControlled() {
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState('astro');
const selectedLabel = frameworks.find((f) => f.value === value)?.label;
const handleSelect = (next: string) => {
setValue(next === value ? '' : next);
setOpen(false);
};
return (
<div className="nx:flex nx:flex-col nx:gap-3">
<Combobox open={open} onOpenChange={setOpen}>
<ComboboxTrigger className="nx:w-64">
<span className="nx:min-w-0 nx:flex-1 nx:truncate nx:text-left">
{selectedLabel ?? 'Select framework'}
</span>
</ComboboxTrigger>
<ComboboxContent label="Framework">
<ComboboxInput placeholder="Search framework..." />
<ComboboxList>
<ComboboxEmpty>No framework found.</ComboboxEmpty>
<ComboboxGroup>
{frameworks.map((framework) => (
<ComboboxItem
key={framework.value}
value={framework.value}
keywords={[framework.label]}
selected={value === framework.value}
onSelect={handleSelect}
>
{framework.label}
</ComboboxItem>
))}
</ComboboxGroup>
</ComboboxList>
</ComboboxContent>
</Combobox>
<output className="nx:typography-body-small nx:text-muted-foreground">
Selected: {value || 'none'}
</output>
</div>
);
}
Installation
npx nxds-cli@latest add combobox
Run npx nxds-cli@latest init first if nxds isn’t set up in your app yet. See Install.
Usage
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxTrigger,
} from '@/components/nxds/components/combobox/combobox';
Composition
Combobox
├── ComboboxTrigger
└── ComboboxContent
├── ComboboxInput
└── ComboboxList
├── ComboboxEmpty
└── ComboboxGroup
└── ComboboxItem
Examples
Default
Open the trigger, type to filter the options, and pick one.
'use client';
import * as React from 'react';
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxTrigger,
} from '@/components/nxds/components/combobox/combobox';
import { cn } from '@/components/nxds/lib/utils';
type ComboboxItemData = {
value: string;
label: string;
disabled?: boolean;
group?: string;
};
const frameworks: ComboboxItemData[] = [
{ value: 'next', label: 'Next.js' },
{ value: 'sveltekit', label: 'SvelteKit' },
{ value: 'nuxt', label: 'Nuxt.js' },
{ value: 'remix', label: 'Remix' },
{ value: 'astro', label: 'Astro' },
];
function toGroups(items: ComboboxItemData[]) {
if (!items.some((item) => item.group))
return [{ heading: undefined, options: items }];
const groups = new Map<string, ComboboxItemData[]>();
for (const item of items) {
const heading = item.group ?? 'Other';
groups.set(heading, [...(groups.get(heading) ?? []), item]);
}
return [...groups.entries()].map(([heading, options]) => ({
heading,
options,
}));
}
function ComboboxExample({
defaultValue = '',
emptyText = 'No framework found.',
items = frameworks,
placeholder = 'Select framework',
searchPlaceholder = 'Search framework...',
triggerClassName = 'nx:w-64',
...triggerProps
}: {
defaultValue?: string;
emptyText?: string;
items?: ComboboxItemData[];
placeholder?: string;
searchPlaceholder?: string;
triggerClassName?: string;
} & Omit<
React.ComponentProps<typeof ComboboxTrigger>,
'children' | 'className'
>) {
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState(defaultValue);
const selectedLabel = items.find((item) => item.value === value)?.label;
const handleSelect = (next: string) => {
setValue(next === value ? '' : next);
setOpen(false);
};
return (
<Combobox open={open} onOpenChange={setOpen}>
<ComboboxTrigger className={triggerClassName} {...triggerProps}>
<span
className={cn(
'nx:min-w-0 nx:flex-1 nx:truncate nx:text-left',
selectedLabel ? undefined : 'nx:text-muted-foreground'
)}
>
{selectedLabel ?? placeholder}
</span>
</ComboboxTrigger>
<ComboboxContent label="Framework">
<ComboboxInput placeholder={searchPlaceholder} />
<ComboboxList>
<ComboboxEmpty>{emptyText}</ComboboxEmpty>
{toGroups(items).map(({ heading, options }) => (
<ComboboxGroup key={heading ?? '_root'} heading={heading}>
{options.map((option) => (
<ComboboxItem
key={option.value}
value={option.value}
keywords={[option.label]}
disabled={option.disabled}
selected={value === option.value}
onSelect={handleSelect}
>
{option.label}
</ComboboxItem>
))}
</ComboboxGroup>
))}
</ComboboxList>
</ComboboxContent>
</Combobox>
);
}
export default function ComboboxDefault() {
return <ComboboxExample />;
}
Sizes
Set size on the trigger to sm, the default, or lg.
'use client';
import * as React from 'react';
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxTrigger,
} from '@/components/nxds/components/combobox/combobox';
import { cn } from '@/components/nxds/lib/utils';
type ComboboxItemData = {
value: string;
label: string;
disabled?: boolean;
group?: string;
};
const frameworks: ComboboxItemData[] = [
{ value: 'next', label: 'Next.js' },
{ value: 'sveltekit', label: 'SvelteKit' },
{ value: 'nuxt', label: 'Nuxt.js' },
{ value: 'remix', label: 'Remix' },
{ value: 'astro', label: 'Astro' },
];
function toGroups(items: ComboboxItemData[]) {
if (!items.some((item) => item.group))
return [{ heading: undefined, options: items }];
const groups = new Map<string, ComboboxItemData[]>();
for (const item of items) {
const heading = item.group ?? 'Other';
groups.set(heading, [...(groups.get(heading) ?? []), item]);
}
return [...groups.entries()].map(([heading, options]) => ({
heading,
options,
}));
}
function ComboboxExample({
defaultValue = '',
emptyText = 'No framework found.',
items = frameworks,
placeholder = 'Select framework',
searchPlaceholder = 'Search framework...',
triggerClassName = 'nx:w-64',
...triggerProps
}: {
defaultValue?: string;
emptyText?: string;
items?: ComboboxItemData[];
placeholder?: string;
searchPlaceholder?: string;
triggerClassName?: string;
} & Omit<
React.ComponentProps<typeof ComboboxTrigger>,
'children' | 'className'
>) {
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState(defaultValue);
const selectedLabel = items.find((item) => item.value === value)?.label;
const handleSelect = (next: string) => {
setValue(next === value ? '' : next);
setOpen(false);
};
return (
<Combobox open={open} onOpenChange={setOpen}>
<ComboboxTrigger className={triggerClassName} {...triggerProps}>
<span
className={cn(
'nx:min-w-0 nx:flex-1 nx:truncate nx:text-left',
selectedLabel ? undefined : 'nx:text-muted-foreground'
)}
>
{selectedLabel ?? placeholder}
</span>
</ComboboxTrigger>
<ComboboxContent label="Framework">
<ComboboxInput placeholder={searchPlaceholder} />
<ComboboxList>
<ComboboxEmpty>{emptyText}</ComboboxEmpty>
{toGroups(items).map(({ heading, options }) => (
<ComboboxGroup key={heading ?? '_root'} heading={heading}>
{options.map((option) => (
<ComboboxItem
key={option.value}
value={option.value}
keywords={[option.label]}
disabled={option.disabled}
selected={value === option.value}
onSelect={handleSelect}
>
{option.label}
</ComboboxItem>
))}
</ComboboxGroup>
))}
</ComboboxList>
</ComboboxContent>
</Combobox>
);
}
export default function ComboboxSizes() {
return (
<div className="nx:flex nx:w-64 nx:flex-col nx:gap-3">
<ComboboxExample
size="sm"
placeholder="Small"
triggerClassName="nx:w-full"
/>
<ComboboxExample placeholder="Default" triggerClassName="nx:w-full" />
<ComboboxExample
size="lg"
placeholder="Large"
triggerClassName="nx:w-full"
/>
</div>
);
}
Grouped
Wrap options in ComboboxGroup with a heading to sort them into labeled groups.
'use client';
import * as React from 'react';
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxTrigger,
} from '@/components/nxds/components/combobox/combobox';
import { cn } from '@/components/nxds/lib/utils';
type ComboboxItemData = {
value: string;
label: string;
disabled?: boolean;
group?: string;
};
const frameworks: ComboboxItemData[] = [
{ value: 'next', label: 'Next.js' },
{ value: 'sveltekit', label: 'SvelteKit' },
{ value: 'nuxt', label: 'Nuxt.js' },
{ value: 'remix', label: 'Remix' },
{ value: 'astro', label: 'Astro' },
];
const groupedFrameworks: ComboboxItemData[] = [
{ value: 'next', label: 'Next.js', group: 'React' },
{ value: 'remix', label: 'Remix', group: 'React' },
{ value: 'sveltekit', label: 'SvelteKit', group: 'Svelte' },
{ value: 'nuxt', label: 'Nuxt.js', group: 'Vue' },
{ value: 'astro', label: 'Astro', group: 'Other' },
];
function toGroups(items: ComboboxItemData[]) {
if (!items.some((item) => item.group))
return [{ heading: undefined, options: items }];
const groups = new Map<string, ComboboxItemData[]>();
for (const item of items) {
const heading = item.group ?? 'Other';
groups.set(heading, [...(groups.get(heading) ?? []), item]);
}
return [...groups.entries()].map(([heading, options]) => ({
heading,
options,
}));
}
function ComboboxExample({
defaultValue = '',
emptyText = 'No framework found.',
items = frameworks,
placeholder = 'Select framework',
searchPlaceholder = 'Search framework...',
triggerClassName = 'nx:w-64',
...triggerProps
}: {
defaultValue?: string;
emptyText?: string;
items?: ComboboxItemData[];
placeholder?: string;
searchPlaceholder?: string;
triggerClassName?: string;
} & Omit<
React.ComponentProps<typeof ComboboxTrigger>,
'children' | 'className'
>) {
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState(defaultValue);
const selectedLabel = items.find((item) => item.value === value)?.label;
const handleSelect = (next: string) => {
setValue(next === value ? '' : next);
setOpen(false);
};
return (
<Combobox open={open} onOpenChange={setOpen}>
<ComboboxTrigger className={triggerClassName} {...triggerProps}>
<span
className={cn(
'nx:min-w-0 nx:flex-1 nx:truncate nx:text-left',
selectedLabel ? undefined : 'nx:text-muted-foreground'
)}
>
{selectedLabel ?? placeholder}
</span>
</ComboboxTrigger>
<ComboboxContent label="Framework">
<ComboboxInput placeholder={searchPlaceholder} />
<ComboboxList>
<ComboboxEmpty>{emptyText}</ComboboxEmpty>
{toGroups(items).map(({ heading, options }) => (
<ComboboxGroup key={heading ?? '_root'} heading={heading}>
{options.map((option) => (
<ComboboxItem
key={option.value}
value={option.value}
keywords={[option.label]}
disabled={option.disabled}
selected={value === option.value}
onSelect={handleSelect}
>
{option.label}
</ComboboxItem>
))}
</ComboboxGroup>
))}
</ComboboxList>
</ComboboxContent>
</Combobox>
);
}
export default function ComboboxGrouped() {
return <ComboboxExample items={groupedFrameworks} />;
}
With disabled option
Set disabled on an item so it shows but cannot be picked.
'use client';
import * as React from 'react';
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxTrigger,
} from '@/components/nxds/components/combobox/combobox';
import { cn } from '@/components/nxds/lib/utils';
type ComboboxItemData = {
value: string;
label: string;
disabled?: boolean;
group?: string;
};
const frameworks: ComboboxItemData[] = [
{ value: 'next', label: 'Next.js' },
{ value: 'sveltekit', label: 'SvelteKit' },
{ value: 'nuxt', label: 'Nuxt.js' },
{ value: 'remix', label: 'Remix' },
{ value: 'astro', label: 'Astro' },
];
function toGroups(items: ComboboxItemData[]) {
if (!items.some((item) => item.group))
return [{ heading: undefined, options: items }];
const groups = new Map<string, ComboboxItemData[]>();
for (const item of items) {
const heading = item.group ?? 'Other';
groups.set(heading, [...(groups.get(heading) ?? []), item]);
}
return [...groups.entries()].map(([heading, options]) => ({
heading,
options,
}));
}
function ComboboxExample({
defaultValue = '',
emptyText = 'No framework found.',
items = frameworks,
placeholder = 'Select framework',
searchPlaceholder = 'Search framework...',
triggerClassName = 'nx:w-64',
...triggerProps
}: {
defaultValue?: string;
emptyText?: string;
items?: ComboboxItemData[];
placeholder?: string;
searchPlaceholder?: string;
triggerClassName?: string;
} & Omit<
React.ComponentProps<typeof ComboboxTrigger>,
'children' | 'className'
>) {
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState(defaultValue);
const selectedLabel = items.find((item) => item.value === value)?.label;
const handleSelect = (next: string) => {
setValue(next === value ? '' : next);
setOpen(false);
};
return (
<Combobox open={open} onOpenChange={setOpen}>
<ComboboxTrigger className={triggerClassName} {...triggerProps}>
<span
className={cn(
'nx:min-w-0 nx:flex-1 nx:truncate nx:text-left',
selectedLabel ? undefined : 'nx:text-muted-foreground'
)}
>
{selectedLabel ?? placeholder}
</span>
</ComboboxTrigger>
<ComboboxContent label="Framework">
<ComboboxInput placeholder={searchPlaceholder} />
<ComboboxList>
<ComboboxEmpty>{emptyText}</ComboboxEmpty>
{toGroups(items).map(({ heading, options }) => (
<ComboboxGroup key={heading ?? '_root'} heading={heading}>
{options.map((option) => (
<ComboboxItem
key={option.value}
value={option.value}
keywords={[option.label]}
disabled={option.disabled}
selected={value === option.value}
onSelect={handleSelect}
>
{option.label}
</ComboboxItem>
))}
</ComboboxGroup>
))}
</ComboboxList>
</ComboboxContent>
</Combobox>
);
}
export default function ComboboxWithDisabledOption() {
return (
<ComboboxExample
items={[
{ value: 'next', label: 'Next.js' },
{ value: 'sveltekit', label: 'SvelteKit', disabled: true },
{ value: 'astro', label: 'Astro' },
]}
/>
);
}
Disabled
Set disabled on the trigger so the popover cannot open.
'use client';
import * as React from 'react';
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxTrigger,
} from '@/components/nxds/components/combobox/combobox';
import { cn } from '@/components/nxds/lib/utils';
type ComboboxItemData = {
value: string;
label: string;
disabled?: boolean;
group?: string;
};
const frameworks: ComboboxItemData[] = [
{ value: 'next', label: 'Next.js' },
{ value: 'sveltekit', label: 'SvelteKit' },
{ value: 'nuxt', label: 'Nuxt.js' },
{ value: 'remix', label: 'Remix' },
{ value: 'astro', label: 'Astro' },
];
function toGroups(items: ComboboxItemData[]) {
if (!items.some((item) => item.group))
return [{ heading: undefined, options: items }];
const groups = new Map<string, ComboboxItemData[]>();
for (const item of items) {
const heading = item.group ?? 'Other';
groups.set(heading, [...(groups.get(heading) ?? []), item]);
}
return [...groups.entries()].map(([heading, options]) => ({
heading,
options,
}));
}
function ComboboxExample({
defaultValue = '',
emptyText = 'No framework found.',
items = frameworks,
placeholder = 'Select framework',
searchPlaceholder = 'Search framework...',
triggerClassName = 'nx:w-64',
...triggerProps
}: {
defaultValue?: string;
emptyText?: string;
items?: ComboboxItemData[];
placeholder?: string;
searchPlaceholder?: string;
triggerClassName?: string;
} & Omit<
React.ComponentProps<typeof ComboboxTrigger>,
'children' | 'className'
>) {
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState(defaultValue);
const selectedLabel = items.find((item) => item.value === value)?.label;
const handleSelect = (next: string) => {
setValue(next === value ? '' : next);
setOpen(false);
};
return (
<Combobox open={open} onOpenChange={setOpen}>
<ComboboxTrigger className={triggerClassName} {...triggerProps}>
<span
className={cn(
'nx:min-w-0 nx:flex-1 nx:truncate nx:text-left',
selectedLabel ? undefined : 'nx:text-muted-foreground'
)}
>
{selectedLabel ?? placeholder}
</span>
</ComboboxTrigger>
<ComboboxContent label="Framework">
<ComboboxInput placeholder={searchPlaceholder} />
<ComboboxList>
<ComboboxEmpty>{emptyText}</ComboboxEmpty>
{toGroups(items).map(({ heading, options }) => (
<ComboboxGroup key={heading ?? '_root'} heading={heading}>
{options.map((option) => (
<ComboboxItem
key={option.value}
value={option.value}
keywords={[option.label]}
disabled={option.disabled}
selected={value === option.value}
onSelect={handleSelect}
>
{option.label}
</ComboboxItem>
))}
</ComboboxGroup>
))}
</ComboboxList>
</ComboboxContent>
</Combobox>
);
}
export default function ComboboxDisabled() {
return <ComboboxExample disabled defaultValue="next" />;
}
Invalid field
Set aria-invalid and place the combobox in a Field to show an error.
Choose the framework used by this project.
'use client';
import * as React from 'react';
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxTrigger,
} from '@/components/nxds/components/combobox/combobox';
import {
Field,
FieldDescription,
FieldError,
FieldLabel,
} from '@/components/nxds/components/field/field';
import { cn } from '@/components/nxds/lib/utils';
type ComboboxItemData = {
value: string;
label: string;
disabled?: boolean;
group?: string;
};
const frameworks: ComboboxItemData[] = [
{ value: 'next', label: 'Next.js' },
{ value: 'sveltekit', label: 'SvelteKit' },
{ value: 'nuxt', label: 'Nuxt.js' },
{ value: 'remix', label: 'Remix' },
{ value: 'astro', label: 'Astro' },
];
function toGroups(items: ComboboxItemData[]) {
if (!items.some((item) => item.group))
return [{ heading: undefined, options: items }];
const groups = new Map<string, ComboboxItemData[]>();
for (const item of items) {
const heading = item.group ?? 'Other';
groups.set(heading, [...(groups.get(heading) ?? []), item]);
}
return [...groups.entries()].map(([heading, options]) => ({
heading,
options,
}));
}
function ComboboxExample({
defaultValue = '',
emptyText = 'No framework found.',
items = frameworks,
placeholder = 'Select framework',
searchPlaceholder = 'Search framework...',
triggerClassName = 'nx:w-64',
...triggerProps
}: {
defaultValue?: string;
emptyText?: string;
items?: ComboboxItemData[];
placeholder?: string;
searchPlaceholder?: string;
triggerClassName?: string;
} & Omit<
React.ComponentProps<typeof ComboboxTrigger>,
'children' | 'className'
>) {
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState(defaultValue);
const selectedLabel = items.find((item) => item.value === value)?.label;
const handleSelect = (next: string) => {
setValue(next === value ? '' : next);
setOpen(false);
};
return (
<Combobox open={open} onOpenChange={setOpen}>
<ComboboxTrigger className={triggerClassName} {...triggerProps}>
<span
className={cn(
'nx:min-w-0 nx:flex-1 nx:truncate nx:text-left',
selectedLabel ? undefined : 'nx:text-muted-foreground'
)}
>
{selectedLabel ?? placeholder}
</span>
</ComboboxTrigger>
<ComboboxContent label="Framework">
<ComboboxInput placeholder={searchPlaceholder} />
<ComboboxList>
<ComboboxEmpty>{emptyText}</ComboboxEmpty>
{toGroups(items).map(({ heading, options }) => (
<ComboboxGroup key={heading ?? '_root'} heading={heading}>
{options.map((option) => (
<ComboboxItem
key={option.value}
value={option.value}
keywords={[option.label]}
disabled={option.disabled}
selected={value === option.value}
onSelect={handleSelect}
>
{option.label}
</ComboboxItem>
))}
</ComboboxGroup>
))}
</ComboboxList>
</ComboboxContent>
</Combobox>
);
}
export default function ComboboxInvalidField() {
return (
<Field data-invalid>
<FieldLabel htmlFor="framework-invalid">Framework</FieldLabel>
<ComboboxExample
id="framework-invalid"
aria-invalid
aria-describedby="framework-invalid-description framework-invalid-error"
/>
<FieldDescription id="framework-invalid-description">
Choose the framework used by this project.
</FieldDescription>
<FieldError id="framework-invalid-error">
Select a supported framework.
</FieldError>
</Field>
);
}
This example also needs: Field.
Props
Combobox
Combobox adds no props of its own; it accepts the props of the element or primitive it wraps.
ComboboxContent
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | "Options" | Accessible name for the search field and popover. Use the field's label text. |
ComboboxEmpty
ComboboxEmpty adds no props of its own; it accepts the props of the element or primitive it wraps.
ComboboxGroup
ComboboxGroup adds no props of its own; it accepts the props of the element or primitive it wraps.
ComboboxInput
ComboboxInput adds no props of its own; it accepts the props of the element or primitive it wraps.
ComboboxItem
| Prop | Type | Default | Description |
|---|---|---|---|
selected | boolean | false | Marks the option as the current selection with a check. |
ComboboxList
ComboboxList adds no props of its own; it accepts the props of the element or primitive it wraps.
ComboboxSeparator
ComboboxSeparator adds no props of its own; it accepts the props of the element or primitive it wraps.
ComboboxTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | —No default documented | Renders the child, such as an |
endIcon | ReactNode | —No default documented | Decorative icon after the label. Use |
loading | boolean | —No default documented | Disables the button and shows a spinner in place of its content. |
size | "default" | "sm" | "lg" | "icon-sm" | "icon" | "icon-lg" | null | "default" | —No description |
startIcon | ReactNode | —No default documented | Decorative icon before the label. Use |
variant | "link" | "default" | "error" | "destructive" | "outline" | "dashed" | "secondary" | "ghost" | null | "outline" | —No description |
Combobox lets the user search a list and pick one option. Use it for lists too long to scan. For a short list without search, use Select.
<script setup lang="ts">
import { ref } from 'vue';
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxTrigger,
} from '@/components/combobox';
const open = ref(false);
const value = ref('astro');
const frameworks = [
{ value: 'next', label: 'Next.js' },
{ value: 'sveltekit', label: 'SvelteKit' },
{ value: 'nuxt', label: 'Nuxt.js' },
{ value: 'remix', label: 'Remix' },
{ value: 'astro', label: 'Astro' },
];
</script>
<template>
<div class="nx:flex nx:flex-col nx:gap-3">
<Combobox v-model:open="open">
<ComboboxTrigger class="nx:w-64">
<span class="nx:min-w-0 nx:flex-1 nx:truncate nx:text-left">{{
frameworks.find((framework) => framework.value === value)?.label ??
'Select framework'
}}</span>
</ComboboxTrigger>
<ComboboxContent label="Framework">
<ComboboxInput placeholder="Search framework..." />
<ComboboxList>
<ComboboxEmpty>No framework found.</ComboboxEmpty>
<ComboboxGroup>
<ComboboxItem
v-for="framework in frameworks"
:key="framework.value"
:value="framework.value"
:keywords="[framework.label]"
:selected="value === framework.value"
@select="
value = $event === value ? '' : $event;
open = false;
"
>{{ framework.label }}</ComboboxItem
>
</ComboboxGroup>
</ComboboxList>
</ComboboxContent>
</Combobox>
<output class="nx:typography-body-small nx:text-muted-foreground"
>Selected: {{ value || 'none' }}</output
>
</div>
</template>
Installation
- 1
- 2
Copy and paste the following code into your project.
components/combobox/ComboboxContent.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { type PopoverContentEmits, useForwardPropsEmits } from 'reka-ui'; import type { HTMLAttributes } from 'vue'; import { cn } from '../../lib/utils'; import { Command } from '../command'; import { PopoverContent, type PopoverContentProps } from '../popover'; export interface ComboboxContentProps extends Omit< PopoverContentProps, 'class' > { /** * Accessible name for the search field and popover. Use the field's label text. */ label?: string; class?: HTMLAttributes['class']; } const props = withDefaults(defineProps<ComboboxContentProps>(), { align: 'start', sideOffset: 4, label: 'Options', }); const emits = defineEmits<PopoverContentEmits>(); const delegated = reactiveOmit(props, 'class', 'label'); const forwarded = useForwardPropsEmits(delegated, emits); </script> <template> <PopoverContent v-bind="forwarded" data-slot="combobox-content" :aria-label="label" :class=" cn('nx:w-(--reka-popover-trigger-width) nx:min-w-48 nx:p-0', props.class) " > <Command data-slot="combobox" :label="label" class="nx:bg-transparent"> <slot /> </Command> </PopoverContent> </template>components/combobox/ComboboxEmpty.vue <script setup lang="ts"> import type { HTMLAttributes } from 'vue'; import { cn } from '../../lib/utils'; import { CommandEmpty } from '../command'; export interface ComboboxEmptyProps { class?: HTMLAttributes['class']; } const props = defineProps<ComboboxEmptyProps>(); </script> <template> <CommandEmpty data-slot="combobox-empty" :class="cn('nx:text-muted-foreground', props.class)" > <slot /> </CommandEmpty> </template>components/combobox/ComboboxGroup.vue <script setup lang="ts"> import { CommandGroup, type CommandGroupProps } from '../command'; export type ComboboxGroupProps = CommandGroupProps; const props = withDefaults(defineProps<ComboboxGroupProps>(), { forceMount: false, }); </script> <template> <CommandGroup v-bind="props" data-slot="combobox-group"> <slot /> </CommandGroup> </template>components/combobox/ComboboxInput.vue <script setup lang="ts"> import { CommandInput, type CommandInputProps } from '../command'; export type ComboboxInputProps = CommandInputProps; const props = defineProps<ComboboxInputProps>(); const emit = defineEmits<{ 'update:modelValue': [value: string] }>(); </script> <template> <CommandInput v-bind="props" data-slot="combobox-input" @update:model-value="emit('update:modelValue', $event)" /> </template>components/combobox/ComboboxItem.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { IconCheck } from '../../lib/icons'; import { cn } from '../../lib/utils'; import { CommandItem, type CommandItemProps } from '../command'; export interface ComboboxItemProps extends CommandItemProps { /** * Marks the option as the current selection with a check. */ selected?: boolean; } const props = withDefaults(defineProps<ComboboxItemProps>(), { selected: false, disabled: false, forceMount: undefined, }); const emit = defineEmits<{ select: [value: string] }>(); const delegated = reactiveOmit(props, 'selected'); </script> <template> <CommandItem v-bind="delegated" data-slot="combobox-item" @select="emit('select', $event)" > <slot /> <IconCheck aria-hidden="true" data-slot="combobox-item-indicator" :class=" cn( 'nx:ml-auto nx:transition-opacity', selected ? 'nx:opacity-100' : 'nx:opacity-0' ) " /> </CommandItem> </template>components/combobox/ComboboxList.vue <script setup lang="ts"> import { CommandList, type CommandListProps } from '../command'; export type ComboboxListProps = CommandListProps; const props = defineProps<ComboboxListProps>(); </script> <template> <CommandList v-bind="props" data-slot="combobox-list"> <slot /> </CommandList> </template>components/combobox/ComboboxSeparator.vue <script setup lang="ts"> import { CommandSeparator, type CommandSeparatorProps } from '../command'; export type ComboboxSeparatorProps = CommandSeparatorProps; const props = withDefaults(defineProps<ComboboxSeparatorProps>(), { alwaysRender: false, }); </script> <template> <CommandSeparator v-bind="props" data-slot="combobox-separator" /> </template>components/combobox/ComboboxTrigger.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { PopoverTrigger } from 'reka-ui'; import type { HTMLAttributes, VNode } from 'vue'; import { IconSelector } from '../../lib/icons'; import { cn } from '../../lib/utils'; import { Button, type ButtonProps } from '../button'; export interface ComboboxTriggerProps extends Omit<ButtonProps, 'class'> { class?: HTMLAttributes['class']; } defineOptions({ inheritAttrs: false }); const props = withDefaults(defineProps<ComboboxTriggerProps>(), { as: 'button', variant: 'outline', type: 'button', }); const slots = defineSlots<{ default?: () => VNode[]; 'start-icon'?: () => VNode[]; 'end-icon'?: () => VNode[]; }>(); const delegated = reactiveOmit(props, 'class'); </script> <template> <PopoverTrigger as-child> <Button v-bind="{ ...delegated, ...$attrs }" data-slot="combobox-trigger" :class=" cn( 'nx:w-full nx:justify-between', 'nx:aria-invalid:border-error-border nx:aria-invalid:focus-visible:outline-focus-error', props.class ) " > <template v-if="slots['start-icon']" #start-icon> <slot name="start-icon" /> </template> <slot /> <IconSelector aria-hidden="true" data-slot="combobox-trigger-icon" class="nx:shrink-0 nx:text-muted-foreground" /> <template v-if="slots['end-icon']" #end-icon> <slot name="end-icon" /> </template> </Button> </PopoverTrigger> </template>components/combobox/index.ts export { default as ComboboxContent, type ComboboxContentProps, } from './ComboboxContent.vue'; export { default as ComboboxEmpty, type ComboboxEmptyProps, } from './ComboboxEmpty.vue'; export { default as ComboboxGroup, type ComboboxGroupProps, } from './ComboboxGroup.vue'; export { default as ComboboxInput, type ComboboxInputProps, } from './ComboboxInput.vue'; export { default as ComboboxItem, type ComboboxItemProps, } from './ComboboxItem.vue'; export { default as ComboboxList, type ComboboxListProps, } from './ComboboxList.vue'; export { default as ComboboxSeparator, type ComboboxSeparatorProps, } from './ComboboxSeparator.vue'; export { default as ComboboxTrigger, type ComboboxTriggerProps, } from './ComboboxTrigger.vue'; export { PopoverRoot as Combobox, type PopoverRootProps as ComboboxProps, } from 'reka-ui'; - 3
Keep each file at the path shown above it.
Paths are relative to
packages/vue/src/. Keep the same folders under your@/root, and the imports between files work without edits.
Usage
import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxList, ComboboxTrigger } from '@/components/combobox';
Examples
Default
Open the trigger, type to filter the options, and pick one.
<script setup lang="ts">
import { ref } from 'vue';
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxTrigger,
} from '@/components/combobox';
const open = ref(false);
const value = ref('');
const frameworks = [
{ value: 'next', label: 'Next.js' },
{ value: 'sveltekit', label: 'SvelteKit' },
{ value: 'nuxt', label: 'Nuxt.js' },
{ value: 'remix', label: 'Remix' },
{ value: 'astro', label: 'Astro' },
];
</script>
<template>
<Combobox v-model:open="open">
<ComboboxTrigger class="nx:w-64">
<span
:class="[
'nx:min-w-0 nx:flex-1 nx:truncate nx:text-left',
{ 'nx:text-muted-foreground': !value },
]"
>{{
frameworks.find((framework) => framework.value === value)?.label ??
'Select framework'
}}</span
>
</ComboboxTrigger>
<ComboboxContent label="Framework">
<ComboboxInput placeholder="Search framework..." />
<ComboboxList>
<ComboboxEmpty>No framework found.</ComboboxEmpty>
<ComboboxGroup>
<ComboboxItem
v-for="framework in frameworks"
:key="framework.value"
:value="framework.value"
:keywords="[framework.label]"
:selected="value === framework.value"
@select="
value = $event === value ? '' : $event;
open = false;
"
>{{ framework.label }}</ComboboxItem
>
</ComboboxGroup>
</ComboboxList>
</ComboboxContent>
</Combobox>
</template>
Sizes
Set size on the trigger to sm, the default, or lg.
<script setup lang="ts">
import { reactive } from 'vue';
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxTrigger,
} from '@/components/combobox';
const pickers = reactive([
{ size: 'sm' as const, placeholder: 'Small', open: false, value: '' },
{ size: undefined, placeholder: 'Default', open: false, value: '' },
{ size: 'lg' as const, placeholder: 'Large', open: false, value: '' },
]);
const frameworks = [
{ value: 'next', label: 'Next.js' },
{ value: 'sveltekit', label: 'SvelteKit' },
{ value: 'nuxt', label: 'Nuxt.js' },
{ value: 'remix', label: 'Remix' },
{ value: 'astro', label: 'Astro' },
];
</script>
<template>
<div class="nx:flex nx:w-64 nx:flex-col nx:gap-3">
<Combobox
v-for="picker in pickers"
:key="picker.placeholder"
v-model:open="picker.open"
>
<ComboboxTrigger :size="picker.size" class="nx:w-full">
<span
:class="[
'nx:min-w-0 nx:flex-1 nx:truncate nx:text-left',
{ 'nx:text-muted-foreground': !picker.value },
]"
>{{
frameworks.find((framework) => framework.value === picker.value)
?.label ?? picker.placeholder
}}</span
>
</ComboboxTrigger>
<ComboboxContent label="Framework">
<ComboboxInput placeholder="Search framework..." />
<ComboboxList>
<ComboboxEmpty>No framework found.</ComboboxEmpty>
<ComboboxGroup>
<ComboboxItem
v-for="framework in frameworks"
:key="framework.value"
:value="framework.value"
:keywords="[framework.label]"
:selected="picker.value === framework.value"
@select="
picker.value = $event === picker.value ? '' : $event;
picker.open = false;
"
>{{ framework.label }}</ComboboxItem
>
</ComboboxGroup>
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
</template>
Grouped
Wrap options in ComboboxGroup with a heading to sort them into labeled groups.
<script setup lang="ts">
import { ref } from 'vue';
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxTrigger,
} from '@/components/combobox';
const open = ref(false);
const value = ref('');
const groups = [
{
heading: 'React',
options: [
{ value: 'next', label: 'Next.js' },
{ value: 'remix', label: 'Remix' },
],
},
{
heading: 'Svelte',
options: [{ value: 'sveltekit', label: 'SvelteKit' }],
},
{ heading: 'Vue', options: [{ value: 'nuxt', label: 'Nuxt.js' }] },
{ heading: 'Other', options: [{ value: 'astro', label: 'Astro' }] },
];
</script>
<template>
<Combobox v-model:open="open">
<ComboboxTrigger class="nx:w-64">
<span
:class="[
'nx:min-w-0 nx:flex-1 nx:truncate nx:text-left',
{ 'nx:text-muted-foreground': !value },
]"
>{{
groups
.flatMap((group) => group.options)
.find((option) => option.value === value)?.label ??
'Select framework'
}}</span
>
</ComboboxTrigger>
<ComboboxContent label="Framework">
<ComboboxInput placeholder="Search framework..." />
<ComboboxList>
<ComboboxEmpty>No framework found.</ComboboxEmpty>
<ComboboxGroup
v-for="group in groups"
:key="group.heading"
:heading="group.heading"
>
<ComboboxItem
v-for="option in group.options"
:key="option.value"
:value="option.value"
:keywords="[option.label]"
:selected="value === option.value"
@select="
value = $event === value ? '' : $event;
open = false;
"
>{{ option.label }}</ComboboxItem
>
</ComboboxGroup>
</ComboboxList>
</ComboboxContent>
</Combobox>
</template>
With disabled option
Set disabled on an item so it shows but cannot be picked.
<script setup lang="ts">
import { ref } from 'vue';
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxTrigger,
} from '@/components/combobox';
const open = ref(false);
const value = ref('');
const frameworks = [
{ value: 'next', label: 'Next.js', disabled: false },
{ value: 'sveltekit', label: 'SvelteKit', disabled: true },
{ value: 'astro', label: 'Astro', disabled: false },
];
</script>
<template>
<Combobox v-model:open="open">
<ComboboxTrigger class="nx:w-64">
<span
:class="[
'nx:min-w-0 nx:flex-1 nx:truncate nx:text-left',
{ 'nx:text-muted-foreground': !value },
]"
>{{
frameworks.find((framework) => framework.value === value)?.label ??
'Select framework'
}}</span
>
</ComboboxTrigger>
<ComboboxContent label="Framework">
<ComboboxInput placeholder="Search framework..." />
<ComboboxList>
<ComboboxEmpty>No framework found.</ComboboxEmpty>
<ComboboxGroup>
<ComboboxItem
v-for="framework in frameworks"
:key="framework.value"
:value="framework.value"
:keywords="[framework.label]"
:disabled="framework.disabled"
:selected="value === framework.value"
@select="
value = $event === value ? '' : $event;
open = false;
"
>{{ framework.label }}</ComboboxItem
>
</ComboboxGroup>
</ComboboxList>
</ComboboxContent>
</Combobox>
</template>
Disabled
Set disabled on the trigger so the popover cannot open.
<script setup lang="ts">
import { ref } from 'vue';
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxTrigger,
} from '@/components/combobox';
const open = ref(false);
const value = ref('next');
const frameworks = [
{ value: 'next', label: 'Next.js' },
{ value: 'sveltekit', label: 'SvelteKit' },
{ value: 'nuxt', label: 'Nuxt.js' },
{ value: 'remix', label: 'Remix' },
{ value: 'astro', label: 'Astro' },
];
</script>
<template>
<Combobox v-model:open="open">
<ComboboxTrigger class="nx:w-64" disabled>
<span
:class="[
'nx:min-w-0 nx:flex-1 nx:truncate nx:text-left',
{ 'nx:text-muted-foreground': !value },
]"
>{{
frameworks.find((framework) => framework.value === value)?.label ??
'Select framework'
}}</span
>
</ComboboxTrigger>
<ComboboxContent label="Framework">
<ComboboxInput placeholder="Search framework..." />
<ComboboxList>
<ComboboxEmpty>No framework found.</ComboboxEmpty>
<ComboboxGroup>
<ComboboxItem
v-for="framework in frameworks"
:key="framework.value"
:value="framework.value"
:keywords="[framework.label]"
:selected="value === framework.value"
@select="
value = $event === value ? '' : $event;
open = false;
"
>{{ framework.label }}</ComboboxItem
>
</ComboboxGroup>
</ComboboxList>
</ComboboxContent>
</Combobox>
</template>
Invalid field
Set aria-invalid and place the combobox in a Field to show an error.
<script setup lang="ts">
import { ref } from 'vue';
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxTrigger,
} from '@/components/combobox';
import {
Field,
FieldDescription,
FieldError,
FieldLabel,
} from '@/components/field';
const open = ref(false);
const value = ref('');
const frameworks = [
{ value: 'next', label: 'Next.js' },
{ value: 'sveltekit', label: 'SvelteKit' },
{ value: 'nuxt', label: 'Nuxt.js' },
{ value: 'remix', label: 'Remix' },
{ value: 'astro', label: 'Astro' },
];
</script>
<template>
<Field data-invalid>
<FieldLabel for="framework-invalid">Framework</FieldLabel>
<Combobox v-model:open="open">
<ComboboxTrigger
id="framework-invalid"
class="nx:w-64"
aria-invalid="true"
aria-describedby="framework-invalid-description framework-invalid-error"
>
<span
:class="[
'nx:min-w-0 nx:flex-1 nx:truncate nx:text-left',
{ 'nx:text-muted-foreground': !value },
]"
>{{
frameworks.find((framework) => framework.value === value)?.label ??
'Select framework'
}}</span
>
</ComboboxTrigger>
<ComboboxContent label="Framework">
<ComboboxInput placeholder="Search framework..." />
<ComboboxList>
<ComboboxEmpty>No framework found.</ComboboxEmpty>
<ComboboxGroup>
<ComboboxItem
v-for="framework in frameworks"
:key="framework.value"
:value="framework.value"
:keywords="[framework.label]"
:selected="value === framework.value"
@select="
value = $event === value ? '' : $event;
open = false;
"
>{{ framework.label }}</ComboboxItem
>
</ComboboxGroup>
</ComboboxList>
</ComboboxContent>
</Combobox>
<FieldDescription id="framework-invalid-description">
Choose the framework used by this project.
</FieldDescription>
<FieldError id="framework-invalid-error">
Select a supported framework.
</FieldError>
</Field>
</template>
This example also needs: Field.
Props
ComboboxContent
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | "Options" | Accessible name for the search field and popover. Use the field's label text. |
ComboboxEmpty
ComboboxEmpty adds no props of its own; it accepts the props of the element or primitive it wraps.
ComboboxGroup
| Prop | Type | Default | Description |
|---|---|---|---|
forceMount | boolean | false | Shows the group whatever the search. |
heading | string | —No default documented | Heading shown above the group's items. Screen readers read it as the group's name. |
ComboboxInput
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | —No default documented | The search text. Bind it with |
ComboboxItem
| Prop | Type | Default | Description |
|---|---|---|---|
disabled | boolean | false | Shows the item dimmed and skips it in keyboard navigation. |
forceMount | boolean | —No default documented | Shows the item whatever the search. |
keywords | string[] | —No default documented | More words the search matches, such as synonyms. |
selected | boolean | false | Marks the option as the current selection with a check. |
value | string | —No default documented | What the search matches and |
ComboboxList
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | —No default documented | Accessible name for the list. |
ComboboxSeparator
| Prop | Type | Default | Description |
|---|---|---|---|
alwaysRender | boolean | false | Keeps the separator while a search is active. |
ComboboxTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
disabled | boolean | —No default documented | —No description |
loading | boolean | —No default documented | Disables the button and shows a spinner in place of its content. |
size | "default" | "sm" | "lg" | "icon-sm" | "icon" | "icon-lg" | null | "default" | —No description |
type | "button" | "submit" | "reset" | "button" | —No description |
variant | "default" | "destructive" | "error" | "outline" | "dashed" | "secondary" | "ghost" | "link" | null | "outline" | —No description |