Combobox
Autocomplete input and command palette with a list of suggestions.
"use client";
import * as React from "react";
import { LuCheck, LuChevronsUpDown } from "react-icons/lu";
import { css } from "styled-system/css";
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" },
];
export default function ComboboxDemo() {
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState("");
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
css={{ w: "200px", justifyContent: "space-between" }}
>
{value
? frameworks.find((framework) => framework.value === value)?.label
: "Select framework..."}
<LuChevronsUpDown opacity="0.5" />
</Button>
</PopoverTrigger>
<PopoverContent css={{ w: "200px", p: "0" }}>
<Command>
<CommandInput placeholder="Search framework..." />
<CommandList>
<CommandEmpty>No framework found.</CommandEmpty>
<CommandGroup>
{frameworks.map((framework) => (
<CommandItem
key={framework.value}
value={framework.value}
onSelect={(currentValue) => {
setValue(currentValue === value ? "" : currentValue);
setOpen(false);
}}
>
{framework.label}
<LuCheck
className={css({
ml: "auto",
opacity: value === framework.value ? "1" : "0",
})}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
Installation
The Combobox is built using a composition of the <Popover /> and the <Command /> components.
See installation instructions for the Popover and the Command components.
Usage
"use client";
import * as React from "react";
import { LuCheck, LuChevronsUpDown } from "react-icons/lu";
import { css, cx } from "styled-system/css";
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",
},
];
export default function ComboboxDemo() {
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState("");
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
css={{ w: "200px", justifyContent: "space-between" }}
>
{value
? frameworks.find((framework) => framework.value === value)?.label
: "Select framework..."}
<LuChevronsUpDown className={css({ opacity: "0.5" })} />
</Button>
</PopoverTrigger>
<PopoverContent css={{ w: "200px", p: "0" }}>
<Command>
<CommandInput placeholder="Search framework..." />
<CommandList>
<CommandEmpty>No framework found.</CommandEmpty>
<CommandGroup>
{frameworks.map((framework) => (
<CommandItem
key={framework.value}
value={framework.value}
onSelect={(currentValue) => {
setValue(currentValue === value ? "" : currentValue);
setOpen(false);
}}
>
{framework.label}
<LuCheck
className={cx(
css({ ml: "auto" }),
value === framework.value ? css({ opacity: "1" }) : css({ opacity: "0" })
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
Examples
Popover
Status
"use client";
import * as React from "react";
import { LuCircle, LuCircleArrowUp, LuCircleCheck, LuCircleHelp, LuCircleX } from "react-icons/lu";
import { css } from "styled-system/css";
import { styled } from "styled-system/jsx";
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";
type Status = {
value: string;
label: string;
icon: React.ElementType;
};
const statuses: Status[] = [
{ value: "backlog", label: "Backlog", icon: LuCircleHelp },
{ value: "todo", label: "Todo", icon: LuCircle },
{ value: "in progress", label: "In Progress", icon: LuCircleArrowUp },
{ value: "done", label: "Done", icon: LuCircleCheck },
{ value: "canceled", label: "Canceled", icon: LuCircleX },
];
export default function ComboboxPopover() {
const [open, setOpen] = React.useState(false);
const [selectedStatus, setSelectedStatus] = React.useState<Status | null>(null);
return (
<styled.div css={{ display: "flex", alignItems: "center", gap: "4" }}>
<styled.p css={{ textStyle: "sm", color: "muted.fg" }}>Status</styled.p>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
size="sm"
css={{ width: "150px", justifyContent: "flex-start" }}
>
{selectedStatus ? (
<>
<selectedStatus.icon
className={css({ marginRight: "2", width: "4", height: "4", flexShrink: "0" })}
/>
{selectedStatus.label}
</>
) : (
"+ Set status"
)}
</Button>
</PopoverTrigger>
<PopoverContent side="right" align="start" css={{ p: "0" }}>
<Command>
<CommandInput placeholder="Change status..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
{statuses.map((status) => (
<CommandItem
key={status.value}
value={status.value}
onSelect={(value) => {
setSelectedStatus(statuses.find((s) => s.value === value) || null);
setOpen(false);
}}
>
<status.icon
className={css({
marginRight: "2",
width: "4",
height: "4",
opacity: status.value === selectedStatus?.value ? "1" : "0.4",
})}
/>
<span>{status.label}</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</styled.div>
);
}
Dropdown Menu
"use client";
import { Controller, useForm } from "react-hook-form";
import { LuCheck, LuChevronsUpDown } from "react-icons/lu";
import { zodResolver } from "@hookform/resolvers/zod";
import { css } from "styled-system/css";
import { styled } from "styled-system/jsx";
import { z } from "zod";
import { toast } from "@/hooks/use-toast";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { Field, FieldDescription, FieldError, FieldLabel } from "@/components/ui/field";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
const languages = [
{ label: "English", value: "en" },
{ label: "French", value: "fr" },
{ label: "German", value: "de" },
{ label: "Spanish", value: "es" },
{ label: "Portuguese", value: "pt" },
{ label: "Russian", value: "ru" },
{ label: "Japanese", value: "ja" },
{ label: "Korean", value: "ko" },
{ label: "Chinese", value: "zh" },
] as const;
const formSchema = z.object({
language: z.string({
required_error: "Please select a language.",
}),
});
type FormSchema = z.infer<typeof formSchema>;
export default function ComboboxForm() {
const form = useForm<FormSchema>({
resolver: zodResolver(formSchema),
});
const onSubmit = form.handleSubmit((data) => {
toast({
title: "You submitted the following values:",
description: (
<styled.pre
css={{ mt: "2", w: "340px", rounded: "md", bg: "slate.950", p: "4", borderWidth: "1px" }}
>
<styled.code css={{ color: "white" }}>{JSON.stringify(data, null, 2)}</styled.code>
</styled.pre>
),
});
});
return (
<styled.form onSubmit={onSubmit} css={{ spaceY: "6" }}>
<Controller
control={form.control}
name="language"
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid} css={{ display: "flex", flexDir: "column" }}>
<FieldLabel>Language</FieldLabel>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
css={{
w: "200px",
justifyContent: "space-between",
color: !field.value ? "muted.fg" : undefined,
}}
>
{field.value
? languages.find((language) => language.value === field.value)?.label
: "Select language"}
<LuChevronsUpDown
className={css({
ml: "2",
w: "4",
h: "4",
flexShrink: "0",
opacity: "0.5",
})}
/>
</Button>
</PopoverTrigger>
<PopoverContent css={{ w: "200px", p: "0" }}>
<Command>
<CommandInput placeholder="Search language..." />
<CommandList>
<CommandEmpty>No language found.</CommandEmpty>
<CommandGroup>
{languages.map((language) => (
<CommandItem
value={language.label}
key={language.value}
onSelect={() => {
form.setValue("language", language.value);
}}
>
{language.label}
<LuCheck
className={css({
ml: "auto",
opacity: language.value === field.value ? "1" : "0",
})}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<FieldDescription>
This is the language that will be used in the dashboard.
</FieldDescription>
<FieldError>{fieldState.error?.message}</FieldError>
</Field>
)}
/>
<Button type="submit">Submit</Button>
</styled.form>
);
}
Asynchronous
"use client";
import * as React from "react";
import { LuCheck, LuChevronsUpDown, LuLoader } from "react-icons/lu";
import { css } from "styled-system/css";
import { styled } from "styled-system/jsx";
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";
interface Framework {
value: string;
label: string;
}
export default function ComboboxAsync() {
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState("");
const [searchQuery, setSearchQuery] = React.useState("");
const [frameworks, setFrameworks] = React.useState<Framework[]>([]);
const [isLoading, setIsLoading] = React.useState(false);
React.useEffect(() => {
if (open) {
fetchFrameworks(searchQuery);
}
}, [open, searchQuery]);
const fetchFrameworks = async (query: string) => {
setIsLoading(true);
try {
await new Promise((resolve) => setTimeout(resolve, 1000));
const mockData = [
{ value: "next.js", label: "Next.js" },
{ value: "sveltekit", label: "SvelteKit" },
{ value: "nuxt.js", label: "Nuxt.js" },
{ value: "remix", label: "Remix" },
{ value: "astro", label: "Astro" },
].filter((item) => (query ? item.label.toLowerCase().includes(query.toLowerCase()) : true));
setFrameworks(mockData);
} catch (error) {
console.error("Error fetching frameworks:", error);
} finally {
setIsLoading(false);
}
};
const handleInputChange = (value: string) => {
setSearchQuery(value);
};
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
css={{ w: "200px", justifyContent: "space-between" }}
>
{value
? frameworks.find((framework) => framework.value === value)?.label ||
"Select framework..."
: "Select framework..."}
<LuChevronsUpDown className={css({ opacity: "0.5" })} />
</Button>
</PopoverTrigger>
<PopoverContent css={{ w: "200px", p: "0" }}>
<Command>
<CommandInput
placeholder="Search framework..."
value={searchQuery}
onValueChange={handleInputChange}
/>
<CommandList>
{isLoading ? (
<styled.div
css={{
display: "flex",
alignItems: "center",
justifyContent: "center",
py: "2",
px: "2",
color: "gray.500",
fontSize: "sm",
}}
>
<LuLoader
className={css({
animation: "spin 2s linear infinite",
mr: "2",
})}
/>
<span>Loading frameworks...</span>
</styled.div>
) : (
<>
<CommandEmpty>No framework found.</CommandEmpty>
<CommandGroup>
{frameworks.map((framework) => (
<CommandItem
key={framework.value}
value={framework.value}
onSelect={(currentValue) => {
setValue(currentValue === value ? "" : currentValue);
setOpen(false);
}}
>
{framework.label}
<LuCheck
className={css({
ml: "auto",
opacity: value === framework.value ? "1" : "0",
})}
/>
</CommandItem>
))}
</CommandGroup>
</>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
Form
"use client";
import { Controller, useForm } from "react-hook-form";
import { LuCheck, LuChevronsUpDown } from "react-icons/lu";
import { zodResolver } from "@hookform/resolvers/zod";
import { css } from "styled-system/css";
import { styled } from "styled-system/jsx";
import { z } from "zod";
import { toast } from "@/hooks/use-toast";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { Field, FieldDescription, FieldError, FieldLabel } from "@/components/ui/field";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
const languages = [
{ label: "English", value: "en" },
{ label: "French", value: "fr" },
{ label: "German", value: "de" },
{ label: "Spanish", value: "es" },
{ label: "Portuguese", value: "pt" },
{ label: "Russian", value: "ru" },
{ label: "Japanese", value: "ja" },
{ label: "Korean", value: "ko" },
{ label: "Chinese", value: "zh" },
] as const;
const formSchema = z.object({
language: z.string({
required_error: "Please select a language.",
}),
});
type FormSchema = z.infer<typeof formSchema>;
export default function ComboboxForm() {
const form = useForm<FormSchema>({
resolver: zodResolver(formSchema),
});
const onSubmit = form.handleSubmit((data) => {
toast({
title: "You submitted the following values:",
description: (
<styled.pre
css={{ mt: "2", w: "340px", rounded: "md", bg: "slate.950", p: "4", borderWidth: "1px" }}
>
<styled.code css={{ color: "white" }}>{JSON.stringify(data, null, 2)}</styled.code>
</styled.pre>
),
});
});
return (
<styled.form onSubmit={onSubmit} css={{ spaceY: "6" }}>
<Controller
control={form.control}
name="language"
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid} css={{ display: "flex", flexDir: "column" }}>
<FieldLabel>Language</FieldLabel>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
css={{
w: "200px",
justifyContent: "space-between",
color: !field.value ? "muted.fg" : undefined,
}}
>
{field.value
? languages.find((language) => language.value === field.value)?.label
: "Select language"}
<LuChevronsUpDown
className={css({
ml: "2",
w: "4",
h: "4",
flexShrink: "0",
opacity: "0.5",
})}
/>
</Button>
</PopoverTrigger>
<PopoverContent css={{ w: "200px", p: "0" }}>
<Command>
<CommandInput placeholder="Search language..." />
<CommandList>
<CommandEmpty>No language found.</CommandEmpty>
<CommandGroup>
{languages.map((language) => (
<CommandItem
value={language.label}
key={language.value}
onSelect={() => {
form.setValue("language", language.value);
}}
>
{language.label}
<LuCheck
className={css({
ml: "auto",
opacity: language.value === field.value ? "1" : "0",
})}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<FieldDescription>
This is the language that will be used in the dashboard.
</FieldDescription>
<FieldError>{fieldState.error?.message}</FieldError>
</Field>
)}
/>
<Button type="submit">Submit</Button>
</styled.form>
);
}
Multi Select
"use client";
import * as React from "react";
import { LuCheck, LuChevronsUpDown } from "react-icons/lu";
import { css } from "styled-system/css";
import { styled } from "styled-system/jsx";
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" },
];
type Framework = (typeof frameworks)[number];
export default function ComboboxMulti() {
const [open, setOpen] = React.useState(false);
const [selectedFrameworks, setSelectedFrameworks] = React.useState<Framework[]>([]);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
css={{ w: "fit", minW: "280px", justifyContent: "space-between" }}
>
{selectedFrameworks.length > 0
? selectedFrameworks.map((framework) => framework.label).join(", ")
: "Select framework..."}
<LuChevronsUpDown className={css({ color: "muted.fg" })} />
</Button>
</PopoverTrigger>
<PopoverContent align="start" css={{ w: "300px", p: "0" }}>
<Command>
<CommandInput placeholder="Search framework..." />
<CommandList>
<CommandEmpty>No framework found.</CommandEmpty>
<CommandGroup>
{frameworks.map((framework) => (
<CommandItem
key={framework.value}
value={framework.value}
onSelect={(currentValue) => {
setSelectedFrameworks(
selectedFrameworks.some((f) => f.value === currentValue)
? selectedFrameworks.filter((f) => f.value !== currentValue)
: [...selectedFrameworks, framework]
);
}}
>
<styled.div
data-selected={selectedFrameworks.some((f) => f.value === framework.value)}
css={{
borderWidth: "1px",
borderColor: "input",
pointerEvents: "none",
w: "4",
h: "4",
flexShrink: "0",
rounded: "4px",
transition: "all",
userSelect: "none",
"& > svg": { opacity: "0" },
"&[data-selected=true]": {
borderColor: "primary",
bg: "primary",
color: "primary.fg",
"& > svg": { opacity: "1" },
},
}}
>
<LuCheck className={css({ w: "3.5", h: "3.5", color: "current" })} />
</styled.div>
{framework.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}