Combobox
A versatile input component that combines a text input with a listbox, allowing users to filter a list of options and select single or multiple values.
"use client" import { Combobox, Portal, useFilter, useListCollection, } from "@chakra-ui/react" const Demo = () => { const { contains } = useFilter({ sensitivity: "base" }) const { collection, filter } = useListCollection({ initialItems: frameworks, filter: contains, }) return ( <Combobox.Root collection={collection} onInputValueChange={(e) => filter(e.inputValue)} width="320px" > <Combobox.Label>Select framework</Combobox.Label> <Combobox.Control> <Combobox.Input placeholder="Type to search" /> <Combobox.IndicatorGroup> <Combobox.ClearTrigger /> <Combobox.Trigger /> </Combobox.IndicatorGroup> </Combobox.Control> <Portal> <Combobox.Positioner> <Combobox.Content> <Combobox.Empty>No items found</Combobox.Empty> {collection.items.map((item) => ( <Combobox.Item item={item} key={item.value}> {item.label} <Combobox.ItemIndicator /> </Combobox.Item> ))} </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.Root> ) } const frameworks = [ { label: "React", value: "react" }, { label: "Solid", value: "solid" }, { label: "Vue", value: "vue" }, { label: "Angular", value: "angular" }, { label: "Svelte", value: "svelte" }, { label: "Preact", value: "preact" }, { label: "Qwik", value: "qwik" }, { label: "Lit", value: "lit" }, { label: "Alpine.js", value: "alpinejs" }, { label: "Ember", value: "ember" }, { label: "Next.js", value: "nextjs" }, ]
Usage
import { Combobox } from "@chakra-ui/react"
<Combobox.Root> <Combobox.Label /> <Combobox.Control> <Combobox.Input /> <Combobox.IndicatorGroup> <Combobox.ClearTrigger /> <Combobox.Trigger /> </Combobox.IndicatorGroup> </Combobox.Control> <Combobox.Positioner> <Combobox.Content> <Combobox.Empty /> <Combobox.Item /> <Combobox.ItemGroup> <Combobox.ItemGroupLabel /> <Combobox.Item /> </Combobox.ItemGroup> </Combobox.Content> </Combobox.Positioner> </Combobox.Root>
To setup combobox, you might need to import the following hooks:
-
useListCollection
: Used to manage the list of items in the combobox, providing helpful methods for filtering and mutating the list. -
useFilter
: Used to provide the filtering logic for the combobox based onIntl.Collator
APIs.
Examples
Basic
The basic combobox provides a searchable dropdown with single selection.
"use client" import { Combobox, Portal, useFilter, useListCollection, } from "@chakra-ui/react" const Demo = () => { const { contains } = useFilter({ sensitivity: "base" }) const { collection, filter } = useListCollection({ initialItems: frameworks, filter: contains, }) return ( <Combobox.Root collection={collection} onInputValueChange={(e) => filter(e.inputValue)} width="320px" > <Combobox.Label>Select framework</Combobox.Label> <Combobox.Control> <Combobox.Input placeholder="Type to search" /> <Combobox.IndicatorGroup> <Combobox.ClearTrigger /> <Combobox.Trigger /> </Combobox.IndicatorGroup> </Combobox.Control> <Portal> <Combobox.Positioner> <Combobox.Content> <Combobox.Empty>No items found</Combobox.Empty> {collection.items.map((item) => ( <Combobox.Item item={item} key={item.value}> {item.label} <Combobox.ItemIndicator /> </Combobox.Item> ))} </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.Root> ) } const frameworks = [ { label: "React", value: "react" }, { label: "Solid", value: "solid" }, { label: "Vue", value: "vue" }, { label: "Angular", value: "angular" }, { label: "Svelte", value: "svelte" }, { label: "Preact", value: "preact" }, { label: "Qwik", value: "qwik" }, { label: "Lit", value: "lit" }, { label: "Alpine.js", value: "alpinejs" }, { label: "Ember", value: "ember" }, { label: "Next.js", value: "nextjs" }, ]
Sizes
Pass the size
prop to the Combobox.Root
to change the size of the combobox.
"use client" import { Combobox, Portal, Stack, useFilter, useListCollection, } from "@chakra-ui/react" const Demo = () => { return ( <Stack gap="8"> <ComboboxDemo size="xs" /> <ComboboxDemo size="sm" /> <ComboboxDemo size="md" /> <ComboboxDemo size="lg" /> </Stack> ) } const ComboboxDemo = (props: Omit<Combobox.RootProps, "collection">) => { const { contains } = useFilter({ sensitivity: "base" }) const { collection, filter } = useListCollection({ initialItems: frameworks, filter: contains, }) return ( <Combobox.Root {...props} onInputValueChange={(e) => filter(e.inputValue)} collection={collection} > <Combobox.Label> Select framework ({props.size?.toString()}) </Combobox.Label> <Combobox.Control> <Combobox.Input placeholder="Type to search" /> <Combobox.IndicatorGroup> <Combobox.ClearTrigger /> <Combobox.Trigger /> </Combobox.IndicatorGroup> </Combobox.Control> <Portal> <Combobox.Positioner> <Combobox.Content> <Combobox.Empty>No items found</Combobox.Empty> {collection.items.map((item) => ( <Combobox.Item item={item} key={item.value}> {item.label} <Combobox.ItemIndicator /> </Combobox.Item> ))} </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.Root> ) } const frameworks = [ { label: "React", value: "react" }, { label: "Solid", value: "solid" }, { label: "Vue", value: "vue" }, { label: "Angular", value: "angular" }, { label: "Svelte", value: "svelte" }, { label: "Preact", value: "preact" }, { label: "Qwik", value: "qwik" }, { label: "Lit", value: "lit" }, { label: "Alpine.js", value: "alpinejs" }, { label: "Ember", value: "ember" }, { label: "Next.js", value: "nextjs" }, ]
Variants
Pass the variant
prop to the Combobox.Root
to change the appearance of the combobox.
"use client" import { Combobox, Portal, Stack, useFilter, useListCollection, } from "@chakra-ui/react" const Demo = () => { return ( <Stack gap="8"> <ComboboxDemo variant="subtle" /> <ComboboxDemo variant="outline" /> <ComboboxDemo variant="flushed" /> </Stack> ) } const ComboboxDemo = (props: Omit<Combobox.RootProps, "collection">) => { const { contains } = useFilter({ sensitivity: "base" }) const { collection, filter } = useListCollection({ initialItems: frameworks, filter: contains, }) return ( <Combobox.Root {...props} onInputValueChange={(e) => filter(e.inputValue)} collection={collection} > <Combobox.Label> Select framework ({props.variant?.toString()}) </Combobox.Label> <Combobox.Control> <Combobox.Input placeholder="Type to search" /> <Combobox.IndicatorGroup> <Combobox.ClearTrigger /> <Combobox.Trigger /> </Combobox.IndicatorGroup> </Combobox.Control> <Portal> <Combobox.Positioner> <Combobox.Content> <Combobox.Empty>No items found</Combobox.Empty> {collection.items.map((item) => ( <Combobox.Item item={item} key={item.value}> {item.label} <Combobox.ItemIndicator /> </Combobox.Item> ))} </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.Root> ) } const frameworks = [ { label: "React", value: "react" }, { label: "Solid", value: "solid" }, { label: "Vue", value: "vue" }, { label: "Angular", value: "angular" }, { label: "Svelte", value: "svelte" }, { label: "Preact", value: "preact" }, { label: "Qwik", value: "qwik" }, { label: "Lit", value: "lit" }, { label: "Alpine.js", value: "alpinejs" }, { label: "Ember", value: "ember" }, { label: "Next.js", value: "nextjs" }, ]
Multiple
Pass the multiple
prop to the Combobox.Root
to enable multiple selection. This allows users to select multiple items from the list.
When this is set, the combobox will always clear the input value when an item is selected.
"use client" import { Badge, Combobox, Portal, Wrap, createListCollection, } from "@chakra-ui/react" import { useMemo, useState } from "react" const skills = [ "JavaScript", "TypeScript", "React", "Node.js", "GraphQL", "PostgreSQL", ] const Demo = () => { const [searchValue, setSearchValue] = useState("") const [selectedSkills, setSelectedSkills] = useState<string[]>([]) const filteredItems = useMemo( () => skills.filter((item) => item.toLowerCase().includes(searchValue.toLowerCase()), ), [searchValue], ) const collection = useMemo( () => createListCollection({ items: filteredItems }), [filteredItems], ) const handleValueChange = (details: Combobox.ValueChangeDetails) => { setSelectedSkills(details.value) } return ( <Combobox.Root multiple closeOnSelect width="320px" value={selectedSkills} collection={collection} onValueChange={handleValueChange} onInputValueChange={(details) => setSearchValue(details.inputValue)} > <Wrap gap="2"> {selectedSkills.map((skill) => ( <Badge key={skill}>{skill}</Badge> ))} </Wrap> <Combobox.Label>Select Skills</Combobox.Label> <Combobox.Control> <Combobox.Input /> <Combobox.IndicatorGroup> <Combobox.Trigger /> </Combobox.IndicatorGroup> </Combobox.Control> <Portal> <Combobox.Positioner> <Combobox.Content> <Combobox.ItemGroup> <Combobox.ItemGroupLabel>Skills</Combobox.ItemGroupLabel> {filteredItems.map((item) => ( <Combobox.Item key={item} item={item}> {item} <Combobox.ItemIndicator /> </Combobox.Item> ))} <Combobox.Empty>No skills found</Combobox.Empty> </Combobox.ItemGroup> </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.Root> ) }
Async Loading
Here's an example of loading the collection
asynchronously as users type, perfect for API-driven search interfaces.
"use client" import { Combobox, HStack, Portal, Span, Spinner, useListCollection, } from "@chakra-ui/react" import { useState } from "react" import { useAsync } from "react-use" const Demo = () => { const [inputValue, setInputValue] = useState("") const { collection, set } = useListCollection<Character>({ initialItems: [], itemToString: (item) => item.name, itemToValue: (item) => item.name, }) const state = useAsync(async () => { const response = await fetch( `https://swapi.py4e.com/api/people/?search=${inputValue}`, ) const data = await response.json() set(data.results) }, [inputValue, set]) return ( <Combobox.Root width="320px" collection={collection} placeholder="Example: C-3PO" onInputValueChange={(e) => setInputValue(e.inputValue)} positioning={{ sameWidth: false, placement: "bottom-start" }} > <Combobox.Label>Search Star Wars Characters</Combobox.Label> <Combobox.Control> <Combobox.Input placeholder="Type to search" /> <Combobox.IndicatorGroup> <Combobox.ClearTrigger /> <Combobox.Trigger /> </Combobox.IndicatorGroup> </Combobox.Control> <Portal> <Combobox.Positioner> <Combobox.Content minW="sm"> {state.loading ? ( <HStack p="2"> <Spinner size="xs" borderWidth="1px" /> <Span>Loading...</Span> </HStack> ) : state.error ? ( <Span p="2" color="fg.error"> Error fetching </Span> ) : ( collection.items?.map((character) => ( <Combobox.Item key={character.name} item={character}> <HStack justify="space-between" textStyle="sm"> <Span fontWeight="medium" truncate> {character.name} </Span> <Span color="fg.muted" truncate> {character.height}cm / {character.mass}kg </Span> </HStack> <Combobox.ItemIndicator /> </Combobox.Item> )) )} </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.Root> ) } interface Character { name: string height: string mass: string created: string edited: string url: string }
Highlight Matching Text
Here's an example of composing the Combobox.Item
and Highlight
components to highlight matching text in search results.
"use client" import { Combobox, Highlight, Portal, useComboboxContext, useFilter, useListCollection, } from "@chakra-ui/react" const Demo = () => { const { contains } = useFilter({ sensitivity: "base" }) const { collection, filter } = useListCollection({ initialItems: frameworks, filter: contains, }) return ( <Combobox.Root collection={collection} onInputValueChange={(e) => filter(e.inputValue)} width="320px" > <Combobox.Label>Select framework</Combobox.Label> <Combobox.Control> <Combobox.Input placeholder="Type to search" /> <Combobox.IndicatorGroup> <Combobox.ClearTrigger /> <Combobox.Trigger /> </Combobox.IndicatorGroup> </Combobox.Control> <Portal> <Combobox.Positioner> <Combobox.Content> <Combobox.Empty>No items found</Combobox.Empty> {collection.items.map((item) => ( <ComboboxItem item={item} key={item.value} /> ))} </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.Root> ) } function ComboboxItem(props: { item: { label: string; value: string } }) { const { item } = props const combobox = useComboboxContext() return ( <Combobox.Item item={item} key={item.value}> <Combobox.ItemText> <Highlight ignoreCase query={combobox.inputValue} styles={{ bg: "yellow.emphasized", fontWeight: "medium" }} > {item.label} </Highlight> </Combobox.ItemText> </Combobox.Item> ) } const frameworks = [ { label: "React", value: "react" }, { label: "Solid", value: "solid" }, { label: "Vue", value: "vue" }, { label: "Angular", value: "angular" }, { label: "Svelte", value: "svelte" }, { label: "Preact", value: "preact" }, { label: "Qwik", value: "qwik" }, { label: "Lit", value: "lit" }, { label: "Alpine.js", value: "alpinejs" }, { label: "Ember", value: "ember" }, { label: "Next.js", value: "nextjs" }, ]
Open on Click
Use the openOnClick
prop to open the combobox when the user clicks on the input.
"use client" import { Combobox, Portal, useFilter, useListCollection, } from "@chakra-ui/react" const Demo = () => { const { contains } = useFilter({ sensitivity: "base" }) const { collection, filter } = useListCollection({ initialItems: frameworks, filter: contains, }) return ( <Combobox.Root collection={collection} onInputValueChange={(e) => filter(e.inputValue)} width="320px" openOnClick > <Combobox.Label>Select framework</Combobox.Label> <Combobox.Control> <Combobox.Input placeholder="Type to search" /> <Combobox.IndicatorGroup> <Combobox.ClearTrigger /> <Combobox.Trigger /> </Combobox.IndicatorGroup> </Combobox.Control> <Portal> <Combobox.Positioner> <Combobox.Content> <Combobox.Empty>No items found</Combobox.Empty> {collection.items.map((item) => ( <Combobox.Item item={item} key={item.value}> {item.label} <Combobox.ItemIndicator /> </Combobox.Item> ))} </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.Root> ) } const frameworks = [ { label: "React", value: "react" }, { label: "Solid", value: "solid" }, { label: "Vue", value: "vue" }, { label: "Angular", value: "angular" }, { label: "Svelte", value: "svelte" }, { label: "Preact", value: "preact" }, { label: "Qwik", value: "qwik" }, { label: "Lit", value: "lit" }, { label: "Alpine.js", value: "alpinejs" }, { label: "Ember", value: "ember" }, { label: "Next.js", value: "nextjs" }, ]
Custom Objects
By default, the combobox collection expects an array of objects with label
and value
properties. In some cases, you may need to deal with custom objects.
Use the itemToString
and itemToValue
props to map the custom object to the required interface.
const items = [ { country: "United States", code: "US", flag: "🇺🇸" }, { country: "Canada", code: "CA", flag: "🇨🇦" }, { country: "Australia", code: "AU", flag: "🇦🇺" }, // ... ] const { contains } = useFilter({ sensitivity: "base" }) const { collection } = useListCollection({ initialItems: items, itemToString: (item) => item.country, itemToValue: (item) => item.code, filter: contains, })
"use client" import { Combobox, Portal, useFilter, useListCollection, } from "@chakra-ui/react" const Demo = () => { const { contains } = useFilter({ sensitivity: "base" }) const { collection, filter } = useListCollection({ initialItems: countries, itemToString: (item) => item.country, itemToValue: (item) => item.code, filter: contains, }) const handleInputChange = (details: Combobox.InputValueChangeDetails) => { filter(details.inputValue) } return ( <Combobox.Root collection={collection} onInputValueChange={handleInputChange} > <Combobox.Label>Search Countries</Combobox.Label> <Combobox.Control> <Combobox.Input placeholder="e.g. United States" /> <Combobox.IndicatorGroup> <Combobox.ClearTrigger /> </Combobox.IndicatorGroup> </Combobox.Control> <Portal> <Combobox.Positioner> <Combobox.Content> <Combobox.Empty>No items found</Combobox.Empty> {collection.items.map((item) => ( <Combobox.Item key={item.code} item={item}> {item.country} </Combobox.Item> ))} </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.Root> ) } const countries = [ { country: "United States", code: "US", flag: "🇺🇸" }, { country: "Canada", code: "CA", flag: "🇨🇦" }, { country: "Australia", code: "AU", flag: "🇦🇺" }, { country: "United Kingdom", code: "UK", flag: "🇬🇧" }, { country: "New Zealand", code: "NZ", flag: "🇳🇿" }, { country: "South Africa", code: "ZA", flag: "🇿🇦" }, { country: "India", code: "IN", flag: "🇮🇳" }, { country: "China", code: "CN", flag: "🇨🇳" }, { country: "Japan", code: "JP", flag: "🇯🇵" }, { country: "Korea", code: "KR", flag: "🇰🇷" }, { country: "Vietnam", code: "VN", flag: "🇻🇳" }, { country: "Thailand", code: "TH", flag: "🇹ðŸ‡" }, { country: "Malaysia", code: "MY", flag: "🇲🇾" }, { country: "Indonesia", code: "ID", flag: "🇮🇩" }, { country: "Philippines", code: "PH", flag: "🇵ðŸ‡" }, { country: "Singapore", code: "SG", flag: "🇸🇬" }, { country: "Hong Kong", code: "HK", flag: "ðŸ‡ðŸ‡°" }, { country: "Macau", code: "MO", flag: "🇲🇴" }, { country: "Taiwan", code: "TW", flag: "🇹🇼" }, ]
Minimum Characters
Use the openOnChange
prop to set a minimum number of characters before filtering the list.
<Combobox.Root openOnChange={(e) => e.inputValue.length > 2} />
"use client" import { Combobox, Portal, useFilter, useListCollection, } from "@chakra-ui/react" const Demo = () => { const { contains } = useFilter({ sensitivity: "base" }) const { collection, filter } = useListCollection({ initialItems: frameworks, filter: contains, }) return ( <Combobox.Root collection={collection} onInputValueChange={(e) => filter(e.inputValue)} openOnChange={(e) => e.inputValue.length > 2} width="320px" > <Combobox.Label>Select framework</Combobox.Label> <Combobox.Control> <Combobox.Input placeholder="Type to search" /> <Combobox.IndicatorGroup> <Combobox.ClearTrigger /> <Combobox.Trigger /> </Combobox.IndicatorGroup> </Combobox.Control> <Portal> <Combobox.Positioner> <Combobox.Content> <Combobox.Empty>No items found</Combobox.Empty> {collection.items.map((item) => ( <Combobox.Item item={item} key={item.value}> {item.label} <Combobox.ItemIndicator /> </Combobox.Item> ))} </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.Root> ) } const frameworks = [ { label: "React", value: "react" }, { label: "Solid", value: "solid" }, { label: "Vue", value: "vue" }, { label: "Angular", value: "angular" }, { label: "Svelte", value: "svelte" }, { label: "Preact", value: "preact" }, { label: "Qwik", value: "qwik" }, { label: "Lit", value: "lit" }, { label: "Alpine.js", value: "alpinejs" }, { label: "Ember", value: "ember" }, { label: "Next.js", value: "nextjs" }, ]
Field
Compose the Combobox
component with the Field
component to wrap the combobox in a form field. Useful for form layouts.
"use client" import { Combobox, Field, Portal, useFilter, useListCollection, } from "@chakra-ui/react" const Demo = () => { const { contains } = useFilter({ sensitivity: "base" }) const { collection, filter } = useListCollection({ initialItems: frameworks, filter: contains, }) return ( <Field.Root width="320px"> <Field.Label>Select framework</Field.Label> <Combobox.Root collection={collection} onInputValueChange={(e) => filter(e.inputValue)} > <Combobox.Control> <Combobox.Input placeholder="Type to search" /> <Combobox.IndicatorGroup> <Combobox.ClearTrigger /> <Combobox.Trigger /> </Combobox.IndicatorGroup> </Combobox.Control> <Field.HelperText>The framework you love to use</Field.HelperText> <Portal> <Combobox.Positioner> <Combobox.Content> <Combobox.Empty>No items found</Combobox.Empty> {collection.items.map((item) => ( <Combobox.Item item={item} key={item.value}> {item.label} <Combobox.ItemIndicator /> </Combobox.Item> ))} </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.Root> </Field.Root> ) } const frameworks = [ { label: "React", value: "react" }, { label: "Solid", value: "solid" }, { label: "Vue", value: "vue" }, { label: "Angular", value: "angular" }, { label: "Svelte", value: "svelte" }, { label: "Preact", value: "preact" }, { label: "Qwik", value: "qwik" }, { label: "Lit", value: "lit" }, { label: "Alpine.js", value: "alpinejs" }, { label: "Ember", value: "ember" }, { label: "Next.js", value: "nextjs" }, ]
Form + Custom Object
When working with custom objects in forms, you often need to submit the programmatic value rather than the display value. This example shows how to combine custom object mapping with form submission using a hidden input.
The key is using itemToValue
to define what gets submitted, while itemToString
controls what users see. A hidden input captures the programmatic value for form submission.
In this example, users see "🇺🇸 United States" but the form submits "US".
"use client" import { Button, Combobox, Field, Portal, Stack, useComboboxContext, useFilter, useListCollection, } from "@chakra-ui/react" // This is a hidden input that is used to store the value of the combobox const ComboboxHiddenInput = (props: React.ComponentProps<"input">) => { const combobox = useComboboxContext() return <input type="hidden" value={combobox.value[0]} readOnly {...props} /> } const Demo = () => { const { contains } = useFilter({ sensitivity: "base" }) const { collection, filter } = useListCollection({ initialItems: countries, itemToString: (item) => item.country, itemToValue: (item) => item.code, filter: contains, }) const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault() const formData = new FormData(event.currentTarget) const country = formData.get("country") console.log("Form submitted with country code:", country) alert(`Selected country code: ${country}`) } const handleInputChange = (details: Combobox.InputValueChangeDetails) => { filter(details.inputValue) } return ( <form onSubmit={handleSubmit}> <Stack gap="4" align="flex-start"> <Field.Root width="320px"> <Field.Label>Country</Field.Label> <Combobox.Root collection={collection} onInputValueChange={handleInputChange} > <Combobox.Control> <Combobox.Input placeholder="Search countries (e.g. United States)" /> <Combobox.IndicatorGroup> <Combobox.ClearTrigger /> <Combobox.Trigger /> </Combobox.IndicatorGroup> </Combobox.Control> <ComboboxHiddenInput name="country" /> <Portal> <Combobox.Positioner> <Combobox.Content> <Combobox.Empty>No countries found</Combobox.Empty> {collection.items.map((item) => ( <Combobox.Item key={item.code} item={item}> {item.flag} {item.country} <Combobox.ItemIndicator /> </Combobox.Item> ))} </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.Root> <Field.HelperText> The form will submit the country code (e.g. "US"), not the display name </Field.HelperText> </Field.Root> <Button size="sm" type="submit"> Submit </Button> </Stack> </form> ) } const countries = [ { country: "United States", code: "US", flag: "🇺🇸" }, { country: "Canada", code: "CA", flag: "🇨🇦" }, { country: "Australia", code: "AU", flag: "🇦🇺" }, { country: "United Kingdom", code: "GB", flag: "🇬🇧" }, { country: "New Zealand", code: "NZ", flag: "🇳🇿" }, { country: "South Africa", code: "ZA", flag: "🇿🇦" }, { country: "India", code: "IN", flag: "🇮🇳" }, { country: "China", code: "CN", flag: "🇨🇳" }, { country: "Japan", code: "JP", flag: "🇯🇵" }, { country: "Korea", code: "KR", flag: "🇰🇷" }, { country: "Vietnam", code: "VN", flag: "🇻🇳" }, { country: "Thailand", code: "TH", flag: "🇹ðŸ‡" }, { country: "Malaysia", code: "MY", flag: "🇲🇾" }, { country: "Indonesia", code: "ID", flag: "🇮🇩" }, { country: "Philippines", code: "PH", flag: "🇵ðŸ‡" }, { country: "Singapore", code: "SG", flag: "🇸🇬" }, { country: "Hong Kong", code: "HK", flag: "ðŸ‡ðŸ‡°" }, { country: "Macau", code: "MO", flag: "🇲🇴" }, { country: "Taiwan", code: "TW", flag: "🇹🇼" }, ]
Hook Form
This example demonstrates how to integrate the Combobox with React Hook Form using the Controller
component. The form automatically receives the item's value
property without needing a hidden input.
Users see "React" but the form receives "react".
"use client" import { Button, Combobox, Field, Portal, Stack, useFilter, useListCollection, } from "@chakra-ui/react" import { zodResolver } from "@hookform/resolvers/zod" import { Controller, useForm } from "react-hook-form" import { z } from "zod" const formSchema = z.object({ framework: z.string({ message: "Framework is required" }).min(1), }) type FormValues = z.infer<typeof formSchema> const Demo = () => { const { handleSubmit, formState: { errors }, control, } = useForm<FormValues>({ resolver: zodResolver(formSchema), }) const onSubmit = handleSubmit((data) => { console.log("Form submitted with:", data) alert(`Selected framework: ${data.framework}`) }) const { contains } = useFilter({ sensitivity: "base" }) const { collection, filter } = useListCollection({ initialItems: frameworks, filter: contains, }) const handleInputChange = (details: Combobox.InputValueChangeDetails) => { filter(details.inputValue) } return ( <form onSubmit={onSubmit}> <Stack gap="4" align="flex-start"> <Field.Root invalid={!!errors.framework} width="320px"> <Field.Label>Framework</Field.Label> <Controller control={control} name="framework" render={({ field }) => ( <Combobox.Root collection={collection} value={field.value ? [field.value] : []} onValueChange={({ value }) => field.onChange(value[0] || "")} onInputValueChange={handleInputChange} onInteractOutside={() => field.onBlur()} > <Combobox.Control> <Combobox.Input placeholder="Select framework" /> <Combobox.IndicatorGroup> <Combobox.ClearTrigger /> <Combobox.Trigger /> </Combobox.IndicatorGroup> </Combobox.Control> <Portal> <Combobox.Positioner> <Combobox.Content> <Combobox.Empty>No frameworks found</Combobox.Empty> {collection.items.map((item) => ( <Combobox.Item key={item.value} item={item}> {item.label} <Combobox.ItemIndicator /> </Combobox.Item> ))} </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.Root> )} /> <Field.ErrorText>{errors.framework?.message}</Field.ErrorText> </Field.Root> <Button size="sm" type="submit"> Submit </Button> </Stack> </form> ) } const frameworks = [ { label: "React", value: "react" }, { label: "Vue", value: "vue" }, { label: "Angular", value: "angular" }, { label: "Svelte", value: "svelte" }, { label: "Solid", value: "solid" }, { label: "Qwik", value: "qwik" }, { label: "Lit", value: "lit" }, { label: "Alpine", value: "alpine" }, ]
Disabled State
Pass the disabled
prop to the Combobox.Root
to disable the entire combobox.
"use client" import { Combobox, Portal, useFilter, useListCollection, } from "@chakra-ui/react" const Demo = () => { const { contains } = useFilter({ sensitivity: "base" }) const { collection, filter } = useListCollection({ initialItems: frameworks, filter: contains, }) return ( <Combobox.Root collection={collection} onInputValueChange={(e) => filter(e.inputValue)} disabled width="320px" > <Combobox.Label>Select framework</Combobox.Label> <Combobox.Control> <Combobox.Input placeholder="Type to search" /> <Combobox.IndicatorGroup> <Combobox.ClearTrigger /> <Combobox.Trigger /> </Combobox.IndicatorGroup> </Combobox.Control> <Portal> <Combobox.Positioner> <Combobox.Content> <Combobox.Empty>No items found</Combobox.Empty> {collection.items.map((item) => ( <Combobox.Item item={item} key={item.value}> {item.label} <Combobox.ItemIndicator /> </Combobox.Item> ))} </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.Root> ) } const frameworks = [ { label: "React", value: "react" }, { label: "Solid", value: "solid" }, { label: "Vue", value: "vue" }, { label: "Angular", value: "angular" }, { label: "Svelte", value: "svelte" }, { label: "Preact", value: "preact" }, { label: "Qwik", value: "qwik" }, { label: "Lit", value: "lit" }, { label: "Alpine.js", value: "alpinejs" }, { label: "Ember", value: "ember" }, { label: "Next.js", value: "nextjs" }, ]
Disabled Item
Disable specific items in the dropdown, add the disabled
prop to the collection item.
const items = [ { label: "Item 1", value: "item-1", disabled: true }, { label: "Item 2", value: "item-2" }, ] const { collection } = useListCollection({ initialItems: items, // ... })
"use client" import { Combobox, HStack, Icon, Portal, Span, useFilter, useListCollection, } from "@chakra-ui/react" const Demo = () => { const { contains } = useFilter({ sensitivity: "base" }) const { collection, filter } = useListCollection({ initialItems: companies, filter: contains, itemToValue: (item) => item.id, itemToString: (item) => item.name, isItemDisabled: (item) => !!item.disabled, }) const handleInputChange = (details: Combobox.InputValueChangeDetails) => { filter(details.inputValue) } return ( <Combobox.Root width="320px" collection={collection} placeholder="Type to search companies" onInputValueChange={handleInputChange} > <Combobox.Label>Select a Company</Combobox.Label> <Combobox.Control> <Combobox.Input /> <Combobox.IndicatorGroup> <Combobox.ClearTrigger /> <Combobox.Trigger /> </Combobox.IndicatorGroup> </Combobox.Control> <Portal> <Combobox.Positioner> <Combobox.Content> <Combobox.ItemGroup> <Combobox.ItemGroupLabel>Companies</Combobox.ItemGroupLabel> {collection.items.map((country) => { return ( <Combobox.Item item={country} key={country.id}> <HStack gap="3"> <Icon>{country.logo}</Icon> <Span fontWeight="medium">{country.name}</Span> </HStack> <Combobox.ItemIndicator /> </Combobox.Item> ) })} </Combobox.ItemGroup> </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.Root> ) } interface Company { id: string name: string logo: React.ReactElement disabled?: boolean } const companies: Company[] = [ { id: "airbnb", name: "Airbnb", logo: ( <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"> <g clipPath="url(#airbnb)"> <path fill="#EB4C60" d="M0 0h18v18H0V0Z" /> <path fill="#fff" d="m13.565 10.777.051.123c.133.372.173.724.092 1.076a2.142 2.142 0 0 1-1.33 1.672 2.095 2.095 0 0 1-1.096.141 2.737 2.737 0 0 1-1.023-.342c-.41-.231-.819-.564-1.269-1.047-.45.483-.85.816-1.27 1.047a2.73 2.73 0 0 1-1.29.362c-.286 0-.562-.05-.828-.16a2.146 2.146 0 0 1-1.33-1.673 2.211 2.211 0 0 1 .122-1.087c.051-.13.103-.252.153-.362l.112-.242.124-.271.011-.02a115.31 115.31 0 0 1 2.261-4.552l.03-.061c.083-.151.165-.312.246-.473a3.45 3.45 0 0 1 .37-.553 1.725 1.725 0 0 1 1.31-.605c.501 0 .972.221 1.299.625.15.167.25.342.344.51l.025.043c.081.161.163.322.246.473l.03.061a104.224 104.224 0 0 1 2.262 4.552l.01.01.124.271.112.242c.034.073.067.156.102.24Zm-5.6-1.227c.123.544.482 1.188 1.035 1.873.552-.695.911-1.339 1.034-1.873.05-.201.06-.41.03-.615a.968.968 0 0 0-.163-.422C9.715 8.232 9.379 8.07 9 8.07a1.092 1.092 0 0 0-.9.443.968.968 0 0 0-.165.423c-.03.205-.019.414.031.615l-.001-.001Zm4.187 3.524c.503-.201.86-.654.932-1.178.037-.26.013-.526-.071-.775a1.97 1.97 0 0 0-.088-.216 5.032 5.032 0 0 1-.046-.107 7.415 7.415 0 0 1-.118-.251 5.735 5.735 0 0 0-.117-.252v-.01a132.7 132.7 0 0 0-2.242-4.53l-.03-.061-.123-.232-.123-.232a2.211 2.211 0 0 0-.287-.443 1.078 1.078 0 0 0-.819-.372 1.078 1.078 0 0 0-.818.372c-.113.136-.21.284-.287.443-.042.077-.083.155-.123.232-.04.079-.082.157-.123.232l-.03.06a109.354 109.354 0 0 0-2.253 4.521l-.01.02a20.74 20.74 0 0 0-.281.61 1.951 1.951 0 0 0-.087.216 1.639 1.639 0 0 0-.092.785 1.5 1.5 0 0 0 .931 1.178c.235.09.502.13.778.1.257-.03.512-.11.778-.26.369-.202.748-.515 1.167-.978-.665-.816-1.084-1.57-1.239-2.235a2.058 2.058 0 0 1-.051-.855c.041-.253.134-.484.277-.685.317-.443.85-.716 1.442-.716.595 0 1.127.263 1.444.716.143.2.235.432.276.685.031.261.021.543-.051.855-.153.665-.563 1.41-1.239 2.225.43.464.8.776 1.167.977.266.15.522.231.778.262.267.03.533 0 .778-.101Z" /> </g> <defs> <clipPath id="airbnb"> <path fill="#fff" d="M0 0h18v18H0z" /> </clipPath> </defs> </svg> ), }, { id: "tesla", disabled: true, logo: ( <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"> <g clipPath="url(#tesla)"> <path fill="#E31937" d="M0 0h18v18H0V0Z" /> <path fill="#fff" d="m9 15 1.5-8c1.334 0 1.654.272 1.715.872 0 0 .894-.335 1.346-1.016C11.8 6.037 10 6 10 6L9 7.25 8 6s-1.8.037-3.56.856c.45.68 1.345 1.016 1.345 1.016.061-.6.39-.871 1.715-.872L9 15Z" /> <path fill="#fff" d="M9 5.608a11.35 11.35 0 0 1 4.688.955C13.91 6.16 14 6 14 6c-1.823-.724-3.53-.994-5-1-1.47.006-3.177.276-5 1 0 0 .114.2.313.563A11.348 11.348 0 0 1 9 5.608Z" /> </g> <defs> <clipPath id="tesla"> <path fill="#fff" d="M0 0h18v18H0z" /> </clipPath> </defs> </svg> ), name: "Tesla", }, { logo: ( <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"> <g clipPath="url(#nvidia-a)"> <path fill="url(#nvidia-b)" d="M0 0h18v18H0V0Z" /> <path fill="#fff" d="M7.601 7.57v-.656c.065-.004.13-.008.195-.008 1.797-.057 2.975 1.547 2.975 1.547S9.5 10.218 8.136 10.218c-.183 0-.36-.029-.53-.085V8.14c.7.085.841.393 1.258 1.093l.936-.786s-.685-.894-1.834-.894a2.745 2.745 0 0 0-.365.016Zm0-2.17v.98l.195-.012c2.497-.086 4.13 2.048 4.13 2.048s-1.871 2.275-3.819 2.275c-.17 0-.336-.016-.502-.044v.607c.138.016.28.029.417.029 1.814 0 3.126-.928 4.397-2.02.21.17 1.073.578 1.251.756-1.206 1.012-4.02 1.826-5.615 1.826-.154 0-.3-.008-.446-.024v.854H14.5V5.4H7.601Zm0 4.733v.518c-1.676-.3-2.141-2.045-2.141-2.045s.805-.89 2.141-1.036v.567h-.004c-.7-.085-1.25.57-1.25.57s.31 1.106 1.254 1.426Zm-2.975-1.6s.991-1.465 2.98-1.619V6.38C5.402 6.558 3.5 8.42 3.5 8.42s1.077 3.118 4.101 3.401v-.567c-2.218-.275-2.975-2.72-2.975-2.72Z" /> </g> <defs> <linearGradient id="nvidia-b" x1="16" x2="5.5" y1="-.5" y2="18" gradientUnits="userSpaceOnUse" > <stop stopColor="#85B737" /> <stop offset="1" stopColor="#597B20" /> </linearGradient> <clipPath id="nvidia-a"> <path fill="#fff" d="M0 0h18v18H0z" /> </clipPath> </defs> </svg> ), id: "nvida", name: "NVIDA", }, { id: "amazon", name: "Amazon", logo: ( <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"> <g clipPath="url(#amazon)"> <path d="M0 0h18v18H0V0Z" /> <path fill="#fff" d="M12.237 10.734c-.259-.327-.458-.56-.458-1.189V7.46c0-.88-.06-1.703-.708-2.306-.519-.478-1.373-.654-2.047-.654-1.425 0-2.698.58-3.01 2.137-.026.177.104.252.207.278l1.351.123c.13 0 .208-.125.234-.25.104-.529.572-.972 1.09-.972.285 0 .848.287.848.89v.754c-.83 0-1.757.056-2.483.357-.855.353-1.586 1.028-1.586 2.11 0 1.382 1.064 2.137 2.204 2.137.96 0 1.482-.25 2.232-.979.235.352.38.603.82.979.105.051.234.051.31-.024.26-.228.712-.703.996-.929.13-.102.104-.252 0-.377ZM9.744 8.775c0 .502-.098 1.756-1.368 1.756-.653 0-.666-.769-.666-.769 0-.988 1.049-1.317 2.034-1.317v.33Z" /> <path fill="#FFB300" d="M12.917 12.952C11.862 13.601 10.284 14 9.005 14a7.818 7.818 0 0 1-4.713-1.551c-.101-.084 0-.168.1-.126 1.432.685 3 1.036 4.587 1.026 1.154 0 2.609-.209 3.787-.628.174-.042.325.126.15.231Zm.376-.44c-.125-.147-.878-.063-1.204-.043-.101 0-.125-.062-.025-.125.576-.357 1.554-.252 1.655-.126.1.126-.026.943-.577 1.32-.076.064-.176.021-.126-.04.126-.253.402-.84.276-.987Z" /> </g> <defs> <clipPath id="amazon"> <path fill="#fff" d="M0 0h18v18H0z" /> </clipPath> </defs> </svg> ), }, ]
Input Group
Combine with InputGroup to add icons or other elements.
"use client" import { Combobox, InputGroup, Portal, useFilter, useListCollection, } from "@chakra-ui/react" import { LuCode } from "react-icons/lu" const Demo = () => { const { contains } = useFilter({ sensitivity: "base" }) const { collection, filter } = useListCollection({ initialItems: frameworks, filter: contains, }) return ( <Combobox.Root collection={collection} onInputValueChange={(e) => filter(e.inputValue)} width="320px" > <Combobox.Label>Select framework</Combobox.Label> <Combobox.Control> <InputGroup startElement={<LuCode />}> <Combobox.Input placeholder="Type to search" /> </InputGroup> <Combobox.IndicatorGroup> <Combobox.ClearTrigger /> <Combobox.Trigger /> </Combobox.IndicatorGroup> </Combobox.Control> <Portal> <Combobox.Positioner> <Combobox.Content> <Combobox.Empty>No items found</Combobox.Empty> {collection.items.map((item) => ( <Combobox.Item item={item} key={item.value}> {item.label} <Combobox.ItemIndicator /> </Combobox.Item> ))} </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.Root> ) } const frameworks = [ { label: "React", value: "react" }, { label: "Solid", value: "solid" }, { label: "Vue", value: "vue" }, { label: "Angular", value: "angular" }, { label: "Svelte", value: "svelte" }, { label: "Preact", value: "preact" }, { label: "Qwik", value: "qwik" }, { label: "Lit", value: "lit" }, { label: "Alpine.js", value: "alpinejs" }, { label: "Ember", value: "ember" }, { label: "Next.js", value: "nextjs" }, ]
Invalid
Pass the invalid
prop to the Combobox.Root
to show the error state.
"use client" import { Combobox, Portal, useFilter, useListCollection, } from "@chakra-ui/react" const Demo = () => { const { contains } = useFilter({ sensitivity: "base" }) const { collection, filter } = useListCollection({ initialItems: frameworks, filter: contains, }) return ( <Combobox.Root collection={collection} onInputValueChange={(e) => filter(e.inputValue)} width="320px" invalid > <Combobox.Label>Select framework</Combobox.Label> <Combobox.Control> <Combobox.Input placeholder="Type to search" /> <Combobox.IndicatorGroup> <Combobox.ClearTrigger /> <Combobox.Trigger /> </Combobox.IndicatorGroup> </Combobox.Control> <Portal> <Combobox.Positioner> <Combobox.Content> <Combobox.Empty>No items found</Combobox.Empty> {collection.items.map((item) => ( <Combobox.Item item={item} key={item.value}> {item.label} <Combobox.ItemIndicator /> </Combobox.Item> ))} </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.Root> ) } const frameworks = [ { label: "React", value: "react" }, { label: "Solid", value: "solid" }, { label: "Vue", value: "vue" }, { label: "Angular", value: "angular" }, { label: "Svelte", value: "svelte" }, { label: "Preact", value: "preact" }, { label: "Qwik", value: "qwik" }, { label: "Lit", value: "lit" }, { label: "Alpine.js", value: "alpinejs" }, { label: "Ember", value: "ember" }, { label: "Next.js", value: "nextjs" }, ]
Controlled Value
Use the value
and onValueChange
props to control the combobox's value programmatically.
"use client" import { Badge, Combobox, For, HStack, Portal, useFilter, useListCollection, } from "@chakra-ui/react" import { useState } from "react" const Demo = () => { const [value, setValue] = useState<string[]>([]) const { contains } = useFilter({ sensitivity: "base" }) const { collection, filter } = useListCollection({ initialItems: frameworks, filter: contains, }) return ( <Combobox.Root collection={collection} onInputValueChange={(e) => filter(e.inputValue)} value={value} onValueChange={(e) => setValue(e.value)} width="320px" > <HStack textStyle="sm" mb="6"> Selected: <HStack> <For each={value} fallback="N/A"> {(v) => <Badge key={v}>{v}</Badge>} </For> </HStack> </HStack> <Combobox.Label>Select framework</Combobox.Label> <Combobox.Control> <Combobox.Input placeholder="Type to search" /> <Combobox.IndicatorGroup> <Combobox.ClearTrigger /> <Combobox.Trigger /> </Combobox.IndicatorGroup> </Combobox.Control> <Portal> <Combobox.Positioner> <Combobox.Content> <Combobox.Empty>No items found</Combobox.Empty> {collection.items.map((item) => ( <Combobox.Item item={item} key={item.value}> {item.label} <Combobox.ItemIndicator /> </Combobox.Item> ))} </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.Root> ) } const frameworks = [ { label: "React", value: "react" }, { label: "Solid", value: "solid" }, { label: "Vue", value: "vue" }, { label: "Angular", value: "angular" }, { label: "Svelte", value: "svelte" }, { label: "Preact", value: "preact" }, { label: "Qwik", value: "qwik" }, { label: "Lit", value: "lit" }, { label: "Alpine.js", value: "alpinejs" }, { label: "Ember", value: "ember" }, { label: "Next.js", value: "nextjs" }, ]
Store
An alternative way to control the combobox is to use the Combobox.RootProvider
component and the useCombobox
store hook.
import { Combobox, useCombobox } from "@chakra-ui/react" function Demo() { const combobox = useCombobox() return ( <Combobox.RootProvider value={combobox}>{/* ... */}</Combobox.RootProvider> ) }
This way you can access the combobox state and methods from outside the combobox.
"use client" import { Combobox, Portal, useCombobox, useFilter, useListCollection, } from "@chakra-ui/react" const Demo = () => { const { contains } = useFilter({ sensitivity: "base" }) const { collection, filter } = useListCollection({ initialItems: frameworks, filter: contains, }) const combobox = useCombobox({ collection, onInputValueChange(e) { filter(e.inputValue) }, }) return ( <Combobox.RootProvider value={combobox} width="320px"> <Combobox.Label>Select framework</Combobox.Label> <Combobox.Control> <Combobox.Input placeholder="Type to search" /> <Combobox.IndicatorGroup> <Combobox.ClearTrigger /> <Combobox.Trigger /> </Combobox.IndicatorGroup> </Combobox.Control> <Portal> <Combobox.Positioner> <Combobox.Content> {collection.items.map((item) => ( <Combobox.Item item={item} key={item.value}> {item.label} <Combobox.ItemIndicator /> </Combobox.Item> ))} </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.RootProvider> ) } const frameworks = [ { label: "React", value: "react" }, { label: "Solid", value: "solid" }, { label: "Vue", value: "vue" }, { label: "Angular", value: "angular" }, { label: "Svelte", value: "svelte" }, { label: "Preact", value: "preact" }, { label: "Qwik", value: "qwik" }, { label: "Lit", value: "lit" }, { label: "Alpine.js", value: "alpinejs" }, { label: "Ember", value: "ember" }, { label: "Next.js", value: "nextjs" }, ]
Controlled Open
Use the open
and onOpenChange
props to control the combobox's open state programmatically.
"use client" import { Combobox, Portal, useFilter, useListCollection, } from "@chakra-ui/react" import { useState } from "react" const Demo = () => { const [open, setOpen] = useState(false) const { contains } = useFilter({ sensitivity: "base" }) const { collection, filter } = useListCollection({ initialItems: frameworks, filter: contains, }) return ( <Combobox.Root collection={collection} onInputValueChange={(e) => filter(e.inputValue)} width="320px" open={open} onOpenChange={(e) => setOpen(e.open)} > <Combobox.Label>Combobox is {open ? "open" : "closed"}</Combobox.Label> <Combobox.Control> <Combobox.Input placeholder="Type to search" /> <Combobox.IndicatorGroup> <Combobox.ClearTrigger /> <Combobox.Trigger /> </Combobox.IndicatorGroup> </Combobox.Control> <Portal> <Combobox.Positioner> <Combobox.Content> <Combobox.Empty>No items found</Combobox.Empty> {collection.items.map((item) => ( <Combobox.Item item={item} key={item.value}> {item.label} <Combobox.ItemIndicator /> </Combobox.Item> ))} </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.Root> ) } const frameworks = [ { label: "React", value: "react" }, { label: "Solid", value: "solid" }, { label: "Vue", value: "vue" }, { label: "Angular", value: "angular" }, { label: "Svelte", value: "svelte" }, { label: "Preact", value: "preact" }, { label: "Qwik", value: "qwik" }, { label: "Lit", value: "lit" }, { label: "Alpine.js", value: "alpinejs" }, { label: "Ember", value: "ember" }, { label: "Next.js", value: "nextjs" }, ]
Limit Large Datasets
The recommended way of managing large lists is to use the limit
property on the useListCollection
hook. This will limit the number of rendered items in the DOM to improve performance.
"use client" import { Combobox, Portal, useFilter, useListCollection, } from "@chakra-ui/react" import { useRef } from "react" const Demo = () => { const contentRef = useRef<HTMLDivElement | null>(null) const { startsWith } = useFilter({ sensitivity: "base" }) const { collection, filter, reset } = useListCollection({ initialItems: items, filter: startsWith, limit: 10, }) return ( <Combobox.Root collection={collection} onInputValueChange={(e) => filter(e.inputValue)} openOnClick width="320px" > <Combobox.Label>Select framework</Combobox.Label> <Combobox.Control> <Combobox.Input placeholder="Type to search" /> <Combobox.IndicatorGroup> <Combobox.ClearTrigger /> <Combobox.Trigger onClick={reset} /> </Combobox.IndicatorGroup> </Combobox.Control> <Portal> <Combobox.Positioner> <Combobox.Content ref={contentRef}> {collection.items.map((item) => ( <Combobox.Item key={item.value} item={item}> <Combobox.ItemText truncate> <span aria-hidden style={{ marginRight: 4 }}> {item.emoji} </span> {item.label} </Combobox.ItemText> <Combobox.ItemIndicator /> </Combobox.Item> ))} </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.Root> ) } export const items = [ { value: "AD", label: "Andorra", emoji: "🇦🇩" }, { value: "AE", label: "United Arab Emirates", emoji: "🇦🇪" }, { value: "AF", label: "Afghanistan", emoji: "🇦🇫" }, { value: "AG", label: "Antigua and Barbuda", emoji: "🇦🇬" }, { value: "AI", label: "Anguilla", emoji: "🇦🇮" }, { value: "AL", label: "Albania", emoji: "🇦🇱" }, { value: "AM", label: "Armenia", emoji: "🇦🇲" }, { value: "AO", label: "Angola", emoji: "🇦🇴" }, { value: "AQ", label: "Antarctica", emoji: "🇦🇶" }, { value: "AR", label: "Argentina", emoji: "🇦🇷" }, { value: "AS", label: "American Samoa", emoji: "🇦🇸" }, { value: "AT", label: "Austria", emoji: "🇦🇹" }, { value: "AU", label: "Australia", emoji: "🇦🇺" }, { value: "AW", label: "Aruba", emoji: "🇦🇼" }, { value: "AX", label: "Ã…land Islands", emoji: "🇦🇽" }, { value: "AZ", label: "Azerbaijan", emoji: "🇦🇿" }, { value: "BA", label: "Bosnia and Herzegovina", emoji: "🇧🇦" }, { value: "BB", label: "Barbados", emoji: "🇧🇧" }, { value: "BD", label: "Bangladesh", emoji: "🇧🇩" }, { value: "BE", label: "Belgium", emoji: "🇧🇪" }, { value: "BF", label: "Burkina Faso", emoji: "🇧🇫" }, { value: "BG", label: "Bulgaria", emoji: "🇧🇬" }, { value: "BH", label: "Bahrain", emoji: "🇧ðŸ‡" }, { value: "BI", label: "Burundi", emoji: "🇧🇮" }, { value: "BJ", label: "Benin", emoji: "🇧🇯" }, { value: "BL", label: "Saint Barthélemy", emoji: "🇧🇱" }, { value: "BM", label: "Bermuda", emoji: "🇧🇲" }, { value: "BN", label: "Brunei Darussalam", emoji: "🇧🇳" }, { value: "BO", label: "Bolivia, Plurinational State of", emoji: "🇧🇴" }, { value: "BQ", label: "Bonaire, Sint Eustatius and Saba", emoji: "🇧🇶" }, { value: "BR", label: "Brazil", emoji: "🇧🇷" }, { value: "BS", label: "Bahamas", emoji: "🇧🇸" }, { value: "BT", label: "Bhutan", emoji: "🇧🇹" }, { value: "BV", label: "Bouvet Island", emoji: "🇧🇻" }, { value: "BW", label: "Botswana", emoji: "🇧🇼" }, { value: "BY", label: "Belarus", emoji: "🇧🇾" }, { value: "BZ", label: "Belize", emoji: "🇧🇿" }, { value: "CA", label: "Canada", emoji: "🇨🇦" }, { value: "CC", label: "Cocos (Keeling) Islands", emoji: "🇨🇨" }, { value: "CD", label: "Congo, Democratic Republic of the", emoji: "🇨🇩" }, { value: "CF", label: "Central African Republic", emoji: "🇨🇫" }, { value: "CG", label: "Congo", emoji: "🇨🇬" }, { value: "CH", label: "Switzerland", emoji: "🇨ðŸ‡" }, { value: "CI", label: "Côte d'Ivoire", emoji: "🇨🇮" }, { value: "CK", label: "Cook Islands", emoji: "🇨🇰" }, { value: "CL", label: "Chile", emoji: "🇨🇱" }, { value: "CM", label: "Cameroon", emoji: "🇨🇲" }, { value: "CN", label: "China", emoji: "🇨🇳" }, { value: "CO", label: "Colombia", emoji: "🇨🇴" }, { value: "CR", label: "Costa Rica", emoji: "🇨🇷" }, { value: "CU", label: "Cuba", emoji: "🇨🇺" }, { value: "CV", label: "Cabo Verde", emoji: "🇨🇻" }, { value: "CW", label: "Curaçao", emoji: "🇨🇼" }, { value: "CX", label: "Christmas Island", emoji: "🇨🇽" }, { value: "CY", label: "Cyprus", emoji: "🇨🇾" }, { value: "CZ", label: "Czechia", emoji: "🇨🇿" }, { value: "DE", label: "Germany", emoji: "🇩🇪" }, { value: "DJ", label: "Djibouti", emoji: "🇩🇯" }, { value: "DK", label: "Denmark", emoji: "🇩🇰" }, { value: "DM", label: "Dominica", emoji: "🇩🇲" }, { value: "DO", label: "Dominican Republic", emoji: "🇩🇴" }, { value: "DZ", label: "Algeria", emoji: "🇩🇿" }, { value: "EC", label: "Ecuador", emoji: "🇪🇨" }, { value: "EE", label: "Estonia", emoji: "🇪🇪" }, { value: "EG", label: "Egypt", emoji: "🇪🇬" }, { value: "EH", label: "Western Sahara", emoji: "🇪ðŸ‡" }, { value: "ER", label: "Eritrea", emoji: "🇪🇷" }, { value: "ES", label: "Spain", emoji: "🇪🇸" }, { value: "ET", label: "Ethiopia", emoji: "🇪🇹" }, { value: "FI", label: "Finland", emoji: "🇫🇮" }, { value: "FJ", label: "Fiji", emoji: "🇫🇯" }, { value: "FK", label: "Falkland Islands (Malvinas)", emoji: "🇫🇰" }, { value: "FM", label: "Micronesia, Federated States of", emoji: "🇫🇲" }, { value: "FO", label: "Faroe Islands", emoji: "🇫🇴" }, { value: "FR", label: "France", emoji: "🇫🇷" }, { value: "GA", label: "Gabon", emoji: "🇬🇦" }, { value: "GB", label: "United Kingdom of Great Britain and Northern Ireland", emoji: "🇬🇧", }, { value: "GD", label: "Grenada", emoji: "🇬🇩" }, { value: "GE", label: "Georgia", emoji: "🇬🇪" }, { value: "GF", label: "French Guiana", emoji: "🇬🇫" }, { value: "GG", label: "Guernsey", emoji: "🇬🇬" }, { value: "GH", label: "Ghana", emoji: "🇬ðŸ‡" }, { value: "GI", label: "Gibraltar", emoji: "🇬🇮" }, { value: "GL", label: "Greenland", emoji: "🇬🇱" }, { value: "GM", label: "Gambia", emoji: "🇬🇲" }, { value: "GN", label: "Guinea", emoji: "🇬🇳" }, { value: "GP", label: "Guadeloupe", emoji: "🇬🇵" }, { value: "GQ", label: "Equatorial Guinea", emoji: "🇬🇶" }, { value: "GR", label: "Greece", emoji: "🇬🇷" }, { value: "GS", label: "South Georgia and the South Sandwich Islands", emoji: "🇬🇸", }, { value: "GT", label: "Guatemala", emoji: "🇬🇹" }, { value: "GU", label: "Guam", emoji: "🇬🇺" }, { value: "GW", label: "Guinea-Bissau", emoji: "🇬🇼" }, { value: "GY", label: "Guyana", emoji: "🇬🇾" }, { value: "HK", label: "Hong Kong", emoji: "ðŸ‡ðŸ‡°" }, { value: "HM", label: "Heard Island and McDonald Islands", emoji: "ðŸ‡ðŸ‡²" }, { value: "HN", label: "Honduras", emoji: "ðŸ‡ðŸ‡³" }, { value: "HR", label: "Croatia", emoji: "ðŸ‡ðŸ‡·" }, { value: "HT", label: "Haiti", emoji: "ðŸ‡ðŸ‡¹" }, { value: "HU", label: "Hungary", emoji: "ðŸ‡ðŸ‡º" }, { value: "ID", label: "Indonesia", emoji: "🇮🇩" }, { value: "IE", label: "Ireland", emoji: "🇮🇪" }, { value: "IL", label: "Israel", emoji: "🇮🇱" }, { value: "IM", label: "Isle of Man", emoji: "🇮🇲" }, { value: "IN", label: "India", emoji: "🇮🇳" }, { value: "IO", label: "British Indian Ocean Territory", emoji: "🇮🇴" }, { value: "IQ", label: "Iraq", emoji: "🇮🇶" }, { value: "IR", label: "Iran, Islamic Republic of", emoji: "🇮🇷" }, { value: "IS", label: "Iceland", emoji: "🇮🇸" }, { value: "IT", label: "Italy", emoji: "🇮🇹" }, { value: "JE", label: "Jersey", emoji: "🇯🇪" }, { value: "JM", label: "Jamaica", emoji: "🇯🇲" }, { value: "JO", label: "Jordan", emoji: "🇯🇴" }, { value: "JP", label: "Japan", emoji: "🇯🇵" }, { value: "KE", label: "Kenya", emoji: "🇰🇪" }, { value: "KG", label: "Kyrgyzstan", emoji: "🇰🇬" }, { value: "KH", label: "Cambodia", emoji: "🇰ðŸ‡" }, { value: "KI", label: "Kiribati", emoji: "🇰🇮" }, { value: "KM", label: "Comoros", emoji: "🇰🇲" }, { value: "KN", label: "Saint Kitts and Nevis", emoji: "🇰🇳" }, { value: "KP", label: "Korea, Democratic People's Republic of", emoji: "🇰🇵" }, { value: "KR", label: "Korea, Republic of", emoji: "🇰🇷" }, { value: "KW", label: "Kuwait", emoji: "🇰🇼" }, { value: "KY", label: "Cayman Islands", emoji: "🇰🇾" }, { value: "KZ", label: "Kazakhstan", emoji: "🇰🇿" }, { value: "LA", label: "Lao People's Democratic Republic", emoji: "🇱🇦" }, { value: "LB", label: "Lebanon", emoji: "🇱🇧" }, { value: "LC", label: "Saint Lucia", emoji: "🇱🇨" }, { value: "LI", label: "Liechtenstein", emoji: "🇱🇮" }, { value: "LK", label: "Sri Lanka", emoji: "🇱🇰" }, { value: "LR", label: "Liberia", emoji: "🇱🇷" }, { value: "LS", label: "Lesotho", emoji: "🇱🇸" }, { value: "LT", label: "Lithuania", emoji: "🇱🇹" }, { value: "LU", label: "Luxembourg", emoji: "🇱🇺" }, { value: "LV", label: "Latvia", emoji: "🇱🇻" }, { value: "LY", label: "Libya", emoji: "🇱🇾" }, { value: "MA", label: "Morocco", emoji: "🇲🇦" }, { value: "MC", label: "Monaco", emoji: "🇲🇨" }, { value: "MD", label: "Moldova, Republic of", emoji: "🇲🇩" }, { value: "ME", label: "Montenegro", emoji: "🇲🇪" }, { value: "MF", label: "Saint Martin, (French part)", emoji: "🇲🇫" }, { value: "MG", label: "Madagascar", emoji: "🇲🇬" }, { value: "MH", label: "Marshall Islands", emoji: "🇲ðŸ‡" }, { value: "MK", label: "North Macedonia", emoji: "🇲🇰" }, { value: "ML", label: "Mali", emoji: "🇲🇱" }, { value: "MM", label: "Myanmar", emoji: "🇲🇲" }, { value: "MN", label: "Mongolia", emoji: "🇲🇳" }, { value: "MO", label: "Macao", emoji: "🇲🇴" }, { value: "MP", label: "Northern Mariana Islands", emoji: "🇲🇵" }, { value: "MQ", label: "Martinique", emoji: "🇲🇶" }, { value: "MR", label: "Mauritania", emoji: "🇲🇷" }, { value: "MS", label: "Montserrat", emoji: "🇲🇸" }, { value: "MT", label: "Malta", emoji: "🇲🇹" }, { value: "MU", label: "Mauritius", emoji: "🇲🇺" }, { value: "MV", label: "Maldives", emoji: "🇲🇻" }, { value: "MW", label: "Malawi", emoji: "🇲🇼" }, { value: "MX", label: "Mexico", emoji: "🇲🇽" }, { value: "MY", label: "Malaysia", emoji: "🇲🇾" }, { value: "MZ", label: "Mozambique", emoji: "🇲🇿" }, { value: "NA", label: "Namibia", emoji: "🇳🇦" }, { value: "NC", label: "New Caledonia", emoji: "🇳🇨" }, { value: "NE", label: "Niger", emoji: "🇳🇪" }, { value: "NF", label: "Norfolk Island", emoji: "🇳🇫" }, { value: "NG", label: "Nigeria", emoji: "🇳🇬" }, { value: "NI", label: "Nicaragua", emoji: "🇳🇮" }, { value: "NL", label: "Netherlands", emoji: "🇳🇱" }, { value: "NO", label: "Norway", emoji: "🇳🇴" }, { value: "NP", label: "Nepal", emoji: "🇳🇵" }, { value: "NR", label: "Nauru", emoji: "🇳🇷" }, { value: "NU", label: "Niue", emoji: "🇳🇺" }, { value: "NZ", label: "New Zealand", emoji: "🇳🇿" }, { value: "OM", label: "Oman", emoji: "🇴🇲" }, { value: "PA", label: "Panama", emoji: "🇵🇦" }, { value: "PE", label: "Peru", emoji: "🇵🇪" }, { value: "PF", label: "French Polynesia", emoji: "🇵🇫" }, { value: "PG", label: "Papua New Guinea", emoji: "🇵🇬" }, { value: "PH", label: "Philippines", emoji: "🇵ðŸ‡" }, { value: "PK", label: "Pakistan", emoji: "🇵🇰" }, { value: "PL", label: "Poland", emoji: "🇵🇱" }, { value: "PM", label: "Saint Pierre and Miquelon", emoji: "🇵🇲" }, { value: "PN", label: "Pitcairn", emoji: "🇵🇳" }, { value: "PR", label: "Puerto Rico", emoji: "🇵🇷" }, { value: "PS", label: "Palestine, State of", emoji: "🇵🇸" }, { value: "PT", label: "Portugal", emoji: "🇵🇹" }, { value: "PW", label: "Palau", emoji: "🇵🇼" }, { value: "PY", label: "Paraguay", emoji: "🇵🇾" }, { value: "QA", label: "Qatar", emoji: "🇶🇦" }, { value: "RE", label: "Réunion", emoji: "🇷🇪" }, { value: "RO", label: "Romania", emoji: "🇷🇴" }, { value: "RS", label: "Serbia", emoji: "🇷🇸" }, { value: "RU", label: "Russian Federation", emoji: "🇷🇺" }, { value: "RW", label: "Rwanda", emoji: "🇷🇼" }, { value: "SA", label: "Saudi Arabia", emoji: "🇸🇦" }, { value: "SB", label: "Solomon Islands", emoji: "🇸🇧" }, { value: "SC", label: "Seychelles", emoji: "🇸🇨" }, { value: "SD", label: "Sudan", emoji: "🇸🇩" }, { value: "SE", label: "Sweden", emoji: "🇸🇪" }, { value: "SG", label: "Singapore", emoji: "🇸🇬" }, { value: "SH", label: "Saint Helena, Ascension and Tristan da Cunha", emoji: "🇸ðŸ‡", }, { value: "SI", label: "Slovenia", emoji: "🇸🇮" }, { value: "SJ", label: "Svalbard and Jan Mayen", emoji: "🇸🇯" }, { value: "SK", label: "Slovakia", emoji: "🇸🇰" }, { value: "SL", label: "Sierra Leone", emoji: "🇸🇱" }, { value: "SM", label: "San Marino", emoji: "🇸🇲" }, { value: "SN", label: "Senegal", emoji: "🇸🇳" }, { value: "SO", label: "Somalia", emoji: "🇸🇴" }, { value: "SR", label: "Suriname", emoji: "🇸🇷" }, { value: "SS", label: "South Sudan", emoji: "🇸🇸" }, { value: "ST", label: "Sao Tome and Principe", emoji: "🇸🇹" }, { value: "SV", label: "El Salvador", emoji: "🇸🇻" }, { value: "SX", label: "Sint Maarten, (Dutch part)", emoji: "🇸🇽" }, { value: "SY", label: "Syrian Arab Republic", emoji: "🇸🇾" }, { value: "SZ", label: "Eswatini", emoji: "🇸🇿" }, { value: "TC", label: "Turks and Caicos Islands", emoji: "🇹🇨" }, { value: "TD", label: "Chad", emoji: "🇹🇩" }, { value: "TF", label: "French Southern Territories", emoji: "🇹🇫" }, { value: "TG", label: "Togo", emoji: "🇹🇬" }, { value: "TH", label: "Thailand", emoji: "🇹ðŸ‡" }, { value: "TJ", label: "Tajikistan", emoji: "🇹🇯" }, { value: "TK", label: "Tokelau", emoji: "🇹🇰" }, { value: "TL", label: "Timor-Leste", emoji: "🇹🇱" }, { value: "TM", label: "Turkmenistan", emoji: "🇹🇲" }, { value: "TN", label: "Tunisia", emoji: "🇹🇳" }, { value: "TO", label: "Tonga", emoji: "🇹🇴" }, { value: "TR", label: "Türkiye", emoji: "🇹🇷" }, { value: "TT", label: "Trinidad and Tobago", emoji: "🇹🇹" }, { value: "TV", label: "Tuvalu", emoji: "🇹🇻" }, { value: "TW", label: "Taiwan, Province of China", emoji: "🇹🇼" }, { value: "TZ", label: "Tanzania, United Republic of", emoji: "🇹🇿" }, { value: "UA", label: "Ukraine", emoji: "🇺🇦" }, { value: "UG", label: "Uganda", emoji: "🇺🇬" }, { value: "UM", label: "United States Minor Outlying Islands", emoji: "🇺🇲" }, { value: "US", label: "United States of America", emoji: "🇺🇸" }, { value: "UY", label: "Uruguay", emoji: "🇺🇾" }, { value: "UZ", label: "Uzbekistan", emoji: "🇺🇿" }, { value: "VA", label: "Holy See", emoji: "🇻🇦" }, { value: "VC", label: "Saint Vincent and the Grenadines", emoji: "🇻🇨" }, { value: "VE", label: "Venezuela, Bolivarian Republic of", emoji: "🇻🇪" }, { value: "VG", label: "Virgin Islands, British", emoji: "🇻🇬" }, { value: "VI", label: "Virgin Islands, U.S.", emoji: "🇻🇮" }, { value: "VN", label: "Viet Nam", emoji: "🇻🇳" }, { value: "VU", label: "Vanuatu", emoji: "🇻🇺" }, { value: "WF", label: "Wallis and Futuna", emoji: "🇼🇫" }, { value: "WS", label: "Samoa", emoji: "🇼🇸" }, { value: "YE", label: "Yemen", emoji: "🇾🇪" }, { value: "YT", label: "Mayotte", emoji: "🇾🇹" }, { value: "ZA", label: "South Africa", emoji: "🇿🇦" }, { value: "ZM", label: "Zambia", emoji: "🇿🇲" }, { value: "ZW", label: "Zimbabwe", emoji: "🇿🇼" }, ]
Virtualization
Alternatively, you can leverage virtualization from the @tanstack/react-virtual
package to render large datasets efficiently.
"use client" import { Combobox, Portal, useFilter, useListCollection, } from "@chakra-ui/react" import { useVirtualizer } from "@tanstack/react-virtual" import { useRef } from "react" import { flushSync } from "react-dom" const Demo = () => { const contentRef = useRef<HTMLDivElement | null>(null) const { startsWith } = useFilter({ sensitivity: "base" }) const { collection, filter, reset } = useListCollection({ initialItems: items, filter: startsWith, }) const virtualizer = useVirtualizer({ count: collection.size, getScrollElement: () => contentRef.current, estimateSize: () => 28, overscan: 10, scrollPaddingEnd: 32, }) const handleScrollToIndexFn = (details: { index: number }) => { flushSync(() => { virtualizer.scrollToIndex(details.index, { align: "center", behavior: "auto", }) }) } return ( <Combobox.Root collection={collection} onInputValueChange={(e) => filter(e.inputValue)} scrollToIndexFn={handleScrollToIndexFn} width="320px" > <Combobox.Label>Select framework</Combobox.Label> <Combobox.Control> <Combobox.Input placeholder="Type to search" /> <Combobox.IndicatorGroup> <Combobox.ClearTrigger /> <Combobox.Trigger onClick={reset} /> </Combobox.IndicatorGroup> </Combobox.Control> <Portal> <Combobox.Positioner> <Combobox.Content ref={contentRef}> <div style={{ height: `${virtualizer.getTotalSize()}px`, width: "100%", position: "relative", }} > {virtualizer.getVirtualItems().map((virtualItem) => { const item = collection.items[virtualItem.index] return ( <Combobox.Item key={item.value} item={item} style={{ position: "absolute", top: 0, left: 0, width: "100%", height: `${virtualItem.size}px`, transform: `translateY(${virtualItem.start}px)`, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", }} > <Combobox.ItemText truncate> <span aria-hidden style={{ marginRight: 4 }}> {item.emoji} </span> {item.label} </Combobox.ItemText> <Combobox.ItemIndicator /> </Combobox.Item> ) })} </div> </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.Root> ) } export const items = [ { value: "AD", label: "Andorra", emoji: "🇦🇩" }, { value: "AE", label: "United Arab Emirates", emoji: "🇦🇪" }, { value: "AF", label: "Afghanistan", emoji: "🇦🇫" }, { value: "AG", label: "Antigua and Barbuda", emoji: "🇦🇬" }, { value: "AI", label: "Anguilla", emoji: "🇦🇮" }, { value: "AL", label: "Albania", emoji: "🇦🇱" }, { value: "AM", label: "Armenia", emoji: "🇦🇲" }, { value: "AO", label: "Angola", emoji: "🇦🇴" }, { value: "AQ", label: "Antarctica", emoji: "🇦🇶" }, { value: "AR", label: "Argentina", emoji: "🇦🇷" }, { value: "AS", label: "American Samoa", emoji: "🇦🇸" }, { value: "AT", label: "Austria", emoji: "🇦🇹" }, { value: "AU", label: "Australia", emoji: "🇦🇺" }, { value: "AW", label: "Aruba", emoji: "🇦🇼" }, { value: "AX", label: "Ã…land Islands", emoji: "🇦🇽" }, { value: "AZ", label: "Azerbaijan", emoji: "🇦🇿" }, { value: "BA", label: "Bosnia and Herzegovina", emoji: "🇧🇦" }, { value: "BB", label: "Barbados", emoji: "🇧🇧" }, { value: "BD", label: "Bangladesh", emoji: "🇧🇩" }, { value: "BE", label: "Belgium", emoji: "🇧🇪" }, { value: "BF", label: "Burkina Faso", emoji: "🇧🇫" }, { value: "BG", label: "Bulgaria", emoji: "🇧🇬" }, { value: "BH", label: "Bahrain", emoji: "🇧ðŸ‡" }, { value: "BI", label: "Burundi", emoji: "🇧🇮" }, { value: "BJ", label: "Benin", emoji: "🇧🇯" }, { value: "BL", label: "Saint Barthélemy", emoji: "🇧🇱" }, { value: "BM", label: "Bermuda", emoji: "🇧🇲" }, { value: "BN", label: "Brunei Darussalam", emoji: "🇧🇳" }, { value: "BO", label: "Bolivia, Plurinational State of", emoji: "🇧🇴" }, { value: "BQ", label: "Bonaire, Sint Eustatius and Saba", emoji: "🇧🇶" }, { value: "BR", label: "Brazil", emoji: "🇧🇷" }, { value: "BS", label: "Bahamas", emoji: "🇧🇸" }, { value: "BT", label: "Bhutan", emoji: "🇧🇹" }, { value: "BV", label: "Bouvet Island", emoji: "🇧🇻" }, { value: "BW", label: "Botswana", emoji: "🇧🇼" }, { value: "BY", label: "Belarus", emoji: "🇧🇾" }, { value: "BZ", label: "Belize", emoji: "🇧🇿" }, { value: "CA", label: "Canada", emoji: "🇨🇦" }, { value: "CC", label: "Cocos (Keeling) Islands", emoji: "🇨🇨" }, { value: "CD", label: "Congo, Democratic Republic of the", emoji: "🇨🇩" }, { value: "CF", label: "Central African Republic", emoji: "🇨🇫" }, { value: "CG", label: "Congo", emoji: "🇨🇬" }, { value: "CH", label: "Switzerland", emoji: "🇨ðŸ‡" }, { value: "CI", label: "Côte d'Ivoire", emoji: "🇨🇮" }, { value: "CK", label: "Cook Islands", emoji: "🇨🇰" }, { value: "CL", label: "Chile", emoji: "🇨🇱" }, { value: "CM", label: "Cameroon", emoji: "🇨🇲" }, { value: "CN", label: "China", emoji: "🇨🇳" }, { value: "CO", label: "Colombia", emoji: "🇨🇴" }, { value: "CR", label: "Costa Rica", emoji: "🇨🇷" }, { value: "CU", label: "Cuba", emoji: "🇨🇺" }, { value: "CV", label: "Cabo Verde", emoji: "🇨🇻" }, { value: "CW", label: "Curaçao", emoji: "🇨🇼" }, { value: "CX", label: "Christmas Island", emoji: "🇨🇽" }, { value: "CY", label: "Cyprus", emoji: "🇨🇾" }, { value: "CZ", label: "Czechia", emoji: "🇨🇿" }, { value: "DE", label: "Germany", emoji: "🇩🇪" }, { value: "DJ", label: "Djibouti", emoji: "🇩🇯" }, { value: "DK", label: "Denmark", emoji: "🇩🇰" }, { value: "DM", label: "Dominica", emoji: "🇩🇲" }, { value: "DO", label: "Dominican Republic", emoji: "🇩🇴" }, { value: "DZ", label: "Algeria", emoji: "🇩🇿" }, { value: "EC", label: "Ecuador", emoji: "🇪🇨" }, { value: "EE", label: "Estonia", emoji: "🇪🇪" }, { value: "EG", label: "Egypt", emoji: "🇪🇬" }, { value: "EH", label: "Western Sahara", emoji: "🇪ðŸ‡" }, { value: "ER", label: "Eritrea", emoji: "🇪🇷" }, { value: "ES", label: "Spain", emoji: "🇪🇸" }, { value: "ET", label: "Ethiopia", emoji: "🇪🇹" }, { value: "FI", label: "Finland", emoji: "🇫🇮" }, { value: "FJ", label: "Fiji", emoji: "🇫🇯" }, { value: "FK", label: "Falkland Islands (Malvinas)", emoji: "🇫🇰" }, { value: "FM", label: "Micronesia, Federated States of", emoji: "🇫🇲" }, { value: "FO", label: "Faroe Islands", emoji: "🇫🇴" }, { value: "FR", label: "France", emoji: "🇫🇷" }, { value: "GA", label: "Gabon", emoji: "🇬🇦" }, { value: "GB", label: "United Kingdom of Great Britain and Northern Ireland", emoji: "🇬🇧", }, { value: "GD", label: "Grenada", emoji: "🇬🇩" }, { value: "GE", label: "Georgia", emoji: "🇬🇪" }, { value: "GF", label: "French Guiana", emoji: "🇬🇫" }, { value: "GG", label: "Guernsey", emoji: "🇬🇬" }, { value: "GH", label: "Ghana", emoji: "🇬ðŸ‡" }, { value: "GI", label: "Gibraltar", emoji: "🇬🇮" }, { value: "GL", label: "Greenland", emoji: "🇬🇱" }, { value: "GM", label: "Gambia", emoji: "🇬🇲" }, { value: "GN", label: "Guinea", emoji: "🇬🇳" }, { value: "GP", label: "Guadeloupe", emoji: "🇬🇵" }, { value: "GQ", label: "Equatorial Guinea", emoji: "🇬🇶" }, { value: "GR", label: "Greece", emoji: "🇬🇷" }, { value: "GS", label: "South Georgia and the South Sandwich Islands", emoji: "🇬🇸", }, { value: "GT", label: "Guatemala", emoji: "🇬🇹" }, { value: "GU", label: "Guam", emoji: "🇬🇺" }, { value: "GW", label: "Guinea-Bissau", emoji: "🇬🇼" }, { value: "GY", label: "Guyana", emoji: "🇬🇾" }, { value: "HK", label: "Hong Kong", emoji: "ðŸ‡ðŸ‡°" }, { value: "HM", label: "Heard Island and McDonald Islands", emoji: "ðŸ‡ðŸ‡²" }, { value: "HN", label: "Honduras", emoji: "ðŸ‡ðŸ‡³" }, { value: "HR", label: "Croatia", emoji: "ðŸ‡ðŸ‡·" }, { value: "HT", label: "Haiti", emoji: "ðŸ‡ðŸ‡¹" }, { value: "HU", label: "Hungary", emoji: "ðŸ‡ðŸ‡º" }, { value: "ID", label: "Indonesia", emoji: "🇮🇩" }, { value: "IE", label: "Ireland", emoji: "🇮🇪" }, { value: "IL", label: "Israel", emoji: "🇮🇱" }, { value: "IM", label: "Isle of Man", emoji: "🇮🇲" }, { value: "IN", label: "India", emoji: "🇮🇳" }, { value: "IO", label: "British Indian Ocean Territory", emoji: "🇮🇴" }, { value: "IQ", label: "Iraq", emoji: "🇮🇶" }, { value: "IR", label: "Iran, Islamic Republic of", emoji: "🇮🇷" }, { value: "IS", label: "Iceland", emoji: "🇮🇸" }, { value: "IT", label: "Italy", emoji: "🇮🇹" }, { value: "JE", label: "Jersey", emoji: "🇯🇪" }, { value: "JM", label: "Jamaica", emoji: "🇯🇲" }, { value: "JO", label: "Jordan", emoji: "🇯🇴" }, { value: "JP", label: "Japan", emoji: "🇯🇵" }, { value: "KE", label: "Kenya", emoji: "🇰🇪" }, { value: "KG", label: "Kyrgyzstan", emoji: "🇰🇬" }, { value: "KH", label: "Cambodia", emoji: "🇰ðŸ‡" }, { value: "KI", label: "Kiribati", emoji: "🇰🇮" }, { value: "KM", label: "Comoros", emoji: "🇰🇲" }, { value: "KN", label: "Saint Kitts and Nevis", emoji: "🇰🇳" }, { value: "KP", label: "Korea, Democratic People's Republic of", emoji: "🇰🇵" }, { value: "KR", label: "Korea, Republic of", emoji: "🇰🇷" }, { value: "KW", label: "Kuwait", emoji: "🇰🇼" }, { value: "KY", label: "Cayman Islands", emoji: "🇰🇾" }, { value: "KZ", label: "Kazakhstan", emoji: "🇰🇿" }, { value: "LA", label: "Lao People's Democratic Republic", emoji: "🇱🇦" }, { value: "LB", label: "Lebanon", emoji: "🇱🇧" }, { value: "LC", label: "Saint Lucia", emoji: "🇱🇨" }, { value: "LI", label: "Liechtenstein", emoji: "🇱🇮" }, { value: "LK", label: "Sri Lanka", emoji: "🇱🇰" }, { value: "LR", label: "Liberia", emoji: "🇱🇷" }, { value: "LS", label: "Lesotho", emoji: "🇱🇸" }, { value: "LT", label: "Lithuania", emoji: "🇱🇹" }, { value: "LU", label: "Luxembourg", emoji: "🇱🇺" }, { value: "LV", label: "Latvia", emoji: "🇱🇻" }, { value: "LY", label: "Libya", emoji: "🇱🇾" }, { value: "MA", label: "Morocco", emoji: "🇲🇦" }, { value: "MC", label: "Monaco", emoji: "🇲🇨" }, { value: "MD", label: "Moldova, Republic of", emoji: "🇲🇩" }, { value: "ME", label: "Montenegro", emoji: "🇲🇪" }, { value: "MF", label: "Saint Martin, (French part)", emoji: "🇲🇫" }, { value: "MG", label: "Madagascar", emoji: "🇲🇬" }, { value: "MH", label: "Marshall Islands", emoji: "🇲ðŸ‡" }, { value: "MK", label: "North Macedonia", emoji: "🇲🇰" }, { value: "ML", label: "Mali", emoji: "🇲🇱" }, { value: "MM", label: "Myanmar", emoji: "🇲🇲" }, { value: "MN", label: "Mongolia", emoji: "🇲🇳" }, { value: "MO", label: "Macao", emoji: "🇲🇴" }, { value: "MP", label: "Northern Mariana Islands", emoji: "🇲🇵" }, { value: "MQ", label: "Martinique", emoji: "🇲🇶" }, { value: "MR", label: "Mauritania", emoji: "🇲🇷" }, { value: "MS", label: "Montserrat", emoji: "🇲🇸" }, { value: "MT", label: "Malta", emoji: "🇲🇹" }, { value: "MU", label: "Mauritius", emoji: "🇲🇺" }, { value: "MV", label: "Maldives", emoji: "🇲🇻" }, { value: "MW", label: "Malawi", emoji: "🇲🇼" }, { value: "MX", label: "Mexico", emoji: "🇲🇽" }, { value: "MY", label: "Malaysia", emoji: "🇲🇾" }, { value: "MZ", label: "Mozambique", emoji: "🇲🇿" }, { value: "NA", label: "Namibia", emoji: "🇳🇦" }, { value: "NC", label: "New Caledonia", emoji: "🇳🇨" }, { value: "NE", label: "Niger", emoji: "🇳🇪" }, { value: "NF", label: "Norfolk Island", emoji: "🇳🇫" }, { value: "NG", label: "Nigeria", emoji: "🇳🇬" }, { value: "NI", label: "Nicaragua", emoji: "🇳🇮" }, { value: "NL", label: "Netherlands", emoji: "🇳🇱" }, { value: "NO", label: "Norway", emoji: "🇳🇴" }, { value: "NP", label: "Nepal", emoji: "🇳🇵" }, { value: "NR", label: "Nauru", emoji: "🇳🇷" }, { value: "NU", label: "Niue", emoji: "🇳🇺" }, { value: "NZ", label: "New Zealand", emoji: "🇳🇿" }, { value: "OM", label: "Oman", emoji: "🇴🇲" }, { value: "PA", label: "Panama", emoji: "🇵🇦" }, { value: "PE", label: "Peru", emoji: "🇵🇪" }, { value: "PF", label: "French Polynesia", emoji: "🇵🇫" }, { value: "PG", label: "Papua New Guinea", emoji: "🇵🇬" }, { value: "PH", label: "Philippines", emoji: "🇵ðŸ‡" }, { value: "PK", label: "Pakistan", emoji: "🇵🇰" }, { value: "PL", label: "Poland", emoji: "🇵🇱" }, { value: "PM", label: "Saint Pierre and Miquelon", emoji: "🇵🇲" }, { value: "PN", label: "Pitcairn", emoji: "🇵🇳" }, { value: "PR", label: "Puerto Rico", emoji: "🇵🇷" }, { value: "PS", label: "Palestine, State of", emoji: "🇵🇸" }, { value: "PT", label: "Portugal", emoji: "🇵🇹" }, { value: "PW", label: "Palau", emoji: "🇵🇼" }, { value: "PY", label: "Paraguay", emoji: "🇵🇾" }, { value: "QA", label: "Qatar", emoji: "🇶🇦" }, { value: "RE", label: "Réunion", emoji: "🇷🇪" }, { value: "RO", label: "Romania", emoji: "🇷🇴" }, { value: "RS", label: "Serbia", emoji: "🇷🇸" }, { value: "RU", label: "Russian Federation", emoji: "🇷🇺" }, { value: "RW", label: "Rwanda", emoji: "🇷🇼" }, { value: "SA", label: "Saudi Arabia", emoji: "🇸🇦" }, { value: "SB", label: "Solomon Islands", emoji: "🇸🇧" }, { value: "SC", label: "Seychelles", emoji: "🇸🇨" }, { value: "SD", label: "Sudan", emoji: "🇸🇩" }, { value: "SE", label: "Sweden", emoji: "🇸🇪" }, { value: "SG", label: "Singapore", emoji: "🇸🇬" }, { value: "SH", label: "Saint Helena, Ascension and Tristan da Cunha", emoji: "🇸ðŸ‡", }, { value: "SI", label: "Slovenia", emoji: "🇸🇮" }, { value: "SJ", label: "Svalbard and Jan Mayen", emoji: "🇸🇯" }, { value: "SK", label: "Slovakia", emoji: "🇸🇰" }, { value: "SL", label: "Sierra Leone", emoji: "🇸🇱" }, { value: "SM", label: "San Marino", emoji: "🇸🇲" }, { value: "SN", label: "Senegal", emoji: "🇸🇳" }, { value: "SO", label: "Somalia", emoji: "🇸🇴" }, { value: "SR", label: "Suriname", emoji: "🇸🇷" }, { value: "SS", label: "South Sudan", emoji: "🇸🇸" }, { value: "ST", label: "Sao Tome and Principe", emoji: "🇸🇹" }, { value: "SV", label: "El Salvador", emoji: "🇸🇻" }, { value: "SX", label: "Sint Maarten, (Dutch part)", emoji: "🇸🇽" }, { value: "SY", label: "Syrian Arab Republic", emoji: "🇸🇾" }, { value: "SZ", label: "Eswatini", emoji: "🇸🇿" }, { value: "TC", label: "Turks and Caicos Islands", emoji: "🇹🇨" }, { value: "TD", label: "Chad", emoji: "🇹🇩" }, { value: "TF", label: "French Southern Territories", emoji: "🇹🇫" }, { value: "TG", label: "Togo", emoji: "🇹🇬" }, { value: "TH", label: "Thailand", emoji: "🇹ðŸ‡" }, { value: "TJ", label: "Tajikistan", emoji: "🇹🇯" }, { value: "TK", label: "Tokelau", emoji: "🇹🇰" }, { value: "TL", label: "Timor-Leste", emoji: "🇹🇱" }, { value: "TM", label: "Turkmenistan", emoji: "🇹🇲" }, { value: "TN", label: "Tunisia", emoji: "🇹🇳" }, { value: "TO", label: "Tonga", emoji: "🇹🇴" }, { value: "TR", label: "Türkiye", emoji: "🇹🇷" }, { value: "TT", label: "Trinidad and Tobago", emoji: "🇹🇹" }, { value: "TV", label: "Tuvalu", emoji: "🇹🇻" }, { value: "TW", label: "Taiwan, Province of China", emoji: "🇹🇼" }, { value: "TZ", label: "Tanzania, United Republic of", emoji: "🇹🇿" }, { value: "UA", label: "Ukraine", emoji: "🇺🇦" }, { value: "UG", label: "Uganda", emoji: "🇺🇬" }, { value: "UM", label: "United States Minor Outlying Islands", emoji: "🇺🇲" }, { value: "US", label: "United States of America", emoji: "🇺🇸" }, { value: "UY", label: "Uruguay", emoji: "🇺🇾" }, { value: "UZ", label: "Uzbekistan", emoji: "🇺🇿" }, { value: "VA", label: "Holy See", emoji: "🇻🇦" }, { value: "VC", label: "Saint Vincent and the Grenadines", emoji: "🇻🇨" }, { value: "VE", label: "Venezuela, Bolivarian Republic of", emoji: "🇻🇪" }, { value: "VG", label: "Virgin Islands, British", emoji: "🇻🇬" }, { value: "VI", label: "Virgin Islands, U.S.", emoji: "🇻🇮" }, { value: "VN", label: "Viet Nam", emoji: "🇻🇳" }, { value: "VU", label: "Vanuatu", emoji: "🇻🇺" }, { value: "WF", label: "Wallis and Futuna", emoji: "🇼🇫" }, { value: "WS", label: "Samoa", emoji: "🇼🇸" }, { value: "YE", label: "Yemen", emoji: "🇾🇪" }, { value: "YT", label: "Mayotte", emoji: "🇾🇹" }, { value: "ZA", label: "South Africa", emoji: "🇿🇦" }, { value: "ZM", label: "Zambia", emoji: "🇿🇲" }, { value: "ZW", label: "Zimbabwe", emoji: "🇿🇼" }, ]
Links
Use the asChild
prop to render the combobox items as links.
"use client" import { Combobox, Portal, useFilter, useListCollection, } from "@chakra-ui/react" import { LuExternalLink } from "react-icons/lu" const Demo = () => { const { contains } = useFilter({ sensitivity: "base" }) const { collection, filter } = useListCollection({ initialItems: frameworks, filter: contains, }) return ( <Combobox.Root collection={collection} onInputValueChange={(e) => filter(e.inputValue)} width="320px" selectionBehavior="clear" > <Combobox.Label>Select framework</Combobox.Label> <Combobox.Control> <Combobox.Input placeholder="Type to search" /> <Combobox.IndicatorGroup> <Combobox.ClearTrigger /> <Combobox.Trigger /> </Combobox.IndicatorGroup> </Combobox.Control> <Portal> <Combobox.Positioner> <Combobox.Content> <Combobox.Empty>No items found</Combobox.Empty> {collection.items.map((item) => ( <Combobox.Item asChild item={item} key={item.value}> <a href={item.docs}> {item.label} <LuExternalLink size={10} /> </a> </Combobox.Item> ))} </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.Root> ) } const frameworks = [ { label: "React", value: "react", docs: "https://react.dev" }, { label: "Solid", value: "solid", docs: "https://solidjs.com" }, { label: "Vue", value: "vue", docs: "https://vuejs.org" }, { label: "Angular", value: "angular", docs: "https://angular.io" }, { label: "Svelte", value: "svelte", docs: "https://svelte.dev" }, { label: "Preact", value: "preact", docs: "https://preactjs.com" }, { label: "Qwik", value: "qwik", docs: "https://qwik.builder.io" }, { label: "Lit", value: "lit", docs: "https://lit.dev" }, { label: "Alpine.js", value: "alpinejs", docs: "https://alpinejs.dev" }, { label: "Ember", value: "ember", docs: "https://emberjs.com" }, { label: "Next.js", value: "nextjs", docs: "https://nextjs.org" }, ]
For custom router links, you can customize the navigate
prop on the Combobox.Root
component.
Here's an example of using the Tanstack Router.
import { Combobox } from "@chakra-ui/react" import { useNavigate } from "@tanstack/react-router" function Demo() { const navigate = useNavigate() return ( <Combobox.Root navigate={({ href }) => { navigate({ to: href }) }} > {/* ... */} </Combobox.Root> ) }
Rehydrate Value
In some cases, where a combobox has a defaultValue
but the collection is not loaded yet, here's an example of how to rehydrate the value and populate the input value.
"use client" import { Combobox, For, HStack, Portal, Span, Spinner, useCombobox, useListCollection, } from "@chakra-ui/react" import { useRef, useState } from "react" import { useAsync } from "react-use" const Demo = () => { const [inputValue, setInputValue] = useState("") const { collection, set } = useListCollection<Character>({ initialItems: [], itemToString: (item) => item.name, itemToValue: (item) => item.name, }) const combobox = useCombobox({ collection, defaultValue: ["C-3PO"], placeholder: "Example: Dexter", inputValue, onInputValueChange: (e) => setInputValue(e.inputValue), }) const state = useAsync(async () => { const response = await fetch( `https://swapi.py4e.com/api/people/?search=${inputValue}`, ) const data = await response.json() set(data.results) }, [inputValue, set]) // Rehydrate the value const hydrated = useRef(false) if (combobox.value.length && collection.size && !hydrated.current) { combobox.syncSelectedItems() hydrated.current = true } return ( <Combobox.RootProvider value={combobox} width="320px"> <Combobox.Label>Search Star Wars Characters</Combobox.Label> <Combobox.Control> <Combobox.Input placeholder="Type to search" /> </Combobox.Control> <Portal> <Combobox.Positioner> <Combobox.Content> {state.loading ? ( <HStack p="2"> <Spinner size="xs" /> <Span>Loading...</Span> </HStack> ) : state.error ? ( <Span p="2" color="fg.error"> {state.error.message} </Span> ) : ( <For each={collection.items} fallback={<Combobox.Empty>No items</Combobox.Empty>} > {(item) => ( <Combobox.Item key={item.name} item={item}> <HStack justify="space-between" textStyle="sm"> <Span fontWeight="medium">{item.name}</Span> <Span color="fg.muted"> {item.height}cm / {item.mass}kg </Span> </HStack> <Combobox.ItemIndicator /> </Combobox.Item> )} </For> )} </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.RootProvider> ) } interface Character { name: string height: string mass: string created: string edited: string url: string }
Custom Item
Customize the appearance of items in the dropdown with your own components.





























"use client" import { Combobox, HStack, Image, Portal, Span, Stack, useComboboxContext, useFilter, useListCollection, } from "@chakra-ui/react" function ComboboxValue() { const combobox = useComboboxContext() const selectedItems = combobox.selectedItems as (typeof items)[number][] return ( <Stack mt="2"> {selectedItems.map((item) => ( <HStack key={item.value} textStyle="sm" p="1" borderWidth="1px"> <Image boxSize="10" p="2" src={item.logo} alt={item.label + " logo"} /> <span>{item.label}</span> </HStack> ))} </Stack> ) } const Demo = () => { const { contains } = useFilter({ sensitivity: "base" }) const { collection, filter } = useListCollection({ initialItems: items, filter: contains, }) return ( <Combobox.Root collection={collection} onInputValueChange={(e) => filter(e.inputValue)} width="320px" placeholder="Example: Audi" multiple closeOnSelect > <Combobox.Label>Search and select car brands</Combobox.Label> <Combobox.Control> <Combobox.Input /> <Combobox.IndicatorGroup> <Combobox.Trigger /> </Combobox.IndicatorGroup> </Combobox.Control> <ComboboxValue /> <Portal> <Combobox.Positioner> <Combobox.Content> <Combobox.Empty>No items found</Combobox.Empty> {collection.items.map((item) => ( <Combobox.Item item={item} key={item.value}> <Image boxSize="5" src={item.logo} alt={item.label + " logo"} /> <Span flex="1">{item.label}</Span> <Combobox.ItemIndicator /> </Combobox.Item> ))} </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.Root> ) } export const items = [ { label: "Audi", value: "audi", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/audi-logo.png", }, { label: "BMW", value: "bmw", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/bmw-logo.png", }, { label: "Citroen", value: "citroen", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/citroen-logo.png", }, { label: "Dacia", value: "dacia", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/dacia-logo.png", }, { label: "Fiat", value: "fiat", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/fiat-logo.png", }, { label: "Ford", value: "ford", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/ford-logo.png", }, { label: "Ferrari", value: "ferrari", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/ferrari-logo.png", }, { label: "Honda", value: "honda", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/honda-logo.png", }, { label: "Hyundai", value: "hyundai", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/hyundai-logo.png", }, { label: "Jaguar", value: "jaguar", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/jaguar-logo.png", }, { label: "Jeep", value: "jeep", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/jeep-logo.png", }, { label: "Kia", value: "kia", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/kia-logo.png", }, { label: "Land Rover", value: "land rover", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/land-rover-logo.png", }, { label: "Mazda", value: "mazda", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/mazda-logo.png", }, { label: "Mercedes", value: "mercedes", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/mercedes-logo.png", }, { label: "Mini", value: "mini", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/mini-logo.png", }, { label: "Mitsubishi", value: "mitsubishi", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/mitsubishi-logo.png", }, { label: "Nissan", value: "nissan", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/nissan-logo.png", }, { label: "Opel", value: "opel", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/opel-logo.png", }, { label: "Peugeot", value: "peugeot", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/peugeot-logo.png", }, { label: "Porsche", value: "porsche", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/porsche-logo.png", }, { label: "Renault", value: "renault", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/renault-logo.png", }, { label: "Saab", value: "saab", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/saab-logo.png", }, { label: "Skoda", value: "skoda", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/skoda-logo.png", }, { label: "Subaru", value: "subaru", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/subaru-logo.png", }, { label: "Suzuki", value: "suzuki", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/suzuki-logo.png", }, { label: "Toyota", value: "toyota", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/toyota-logo.png", }, { label: "Volkswagen", value: "volkswagen", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/volkswagen-logo.png", }, { label: "Volvo", value: "volvo", logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/volvo-logo.png", }, ]
Custom Filter
Here's an example of a custom filter that matches multiple properties of an item.
"use client" import { Combobox, Portal, Span, Stack, useListCollection, } from "@chakra-ui/react" const Demo = () => { const { collection, set } = useListCollection({ initialItems: people, itemToString: (item) => item.name, itemToValue: (item) => item.id.toString(), }) const handleInputChange = (details: Combobox.InputValueChangeDetails) => { const filteredItems = people.filter((item) => { const searchLower = details.inputValue.toLowerCase() const nameParts = item.name.toLowerCase().split(" ") const emailParts = item.email.toLowerCase().split("@")[0].split(".") return ( item.name.toLowerCase().includes(searchLower) || nameParts.some((part) => part.includes(searchLower)) || emailParts.some((part) => part.includes(searchLower)) || item.role.toLowerCase().includes(searchLower) ) }) set(filteredItems) } return ( <Combobox.Root width="320px" collection={collection} inputBehavior="autocomplete" placeholder="Search by name, email, or role..." onInputValueChange={handleInputChange} > <Combobox.Label>Select Person</Combobox.Label> <Combobox.Control> <Combobox.Input /> <Combobox.IndicatorGroup> <Combobox.ClearTrigger /> <Combobox.Trigger /> </Combobox.IndicatorGroup> </Combobox.Control> <Portal> <Combobox.Positioner> <Combobox.Content> <Combobox.Empty>No matches found</Combobox.Empty> {collection.items.map((person) => ( <Combobox.Item item={person} key={person.id}> <Stack gap={0}> <Span textStyle="sm" fontWeight="medium"> {person.name} </Span> <Span textStyle="xs" color="fg.muted"> {person.email} </Span> </Stack> <Combobox.ItemIndicator /> </Combobox.Item> ))} </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.Root> ) } const people = [ { id: 1, name: "John Smith", email: "john@example.com", role: "Sales Manager", }, { id: 2, name: "Sarah Johnson", email: "sarah@example.com", role: "UI Designer", }, { id: 3, name: "Michael Brown", email: "michael@example.com", role: "Software Engineer", }, { id: 4, name: "Emily Davis", email: "emily@example.com", role: "AI Engineer", }, { id: 5, name: "James Wilson", email: "james@example.com", role: "Chief Executive Officer", }, ]
Custom Animation
To customize the animation of the combobox, pass the _open
and _closed
prop to the Combobox.Content
component.
"use client" import { Combobox, Portal, useFilter, useListCollection, } from "@chakra-ui/react" const Demo = () => { const { contains } = useFilter({ sensitivity: "base" }) const { collection, filter } = useListCollection({ initialItems: frameworks, filter: contains, }) return ( <Combobox.Root collection={collection} onInputValueChange={(e) => filter(e.inputValue)} width="320px" positioning={{ flip: false, gutter: 2 }} > <Combobox.Label>Select framework</Combobox.Label> <Combobox.Control> <Combobox.Input placeholder="Type to search" /> <Combobox.IndicatorGroup> <Combobox.ClearTrigger /> <Combobox.Trigger /> </Combobox.IndicatorGroup> </Combobox.Control> <Portal> <Combobox.Positioner> <Combobox.Content _open={{ animationStyle: "scale-fade-in" }} _closed={{ animationStyle: "scale-fade-out", animationDuration: "fast", }} > <Combobox.Empty>No items found</Combobox.Empty> {collection.items.map((item) => ( <Combobox.Item item={item} key={item.value}> {item.label} <Combobox.ItemIndicator /> </Combobox.Item> ))} </Combobox.Content> </Combobox.Positioner> </Portal> </Combobox.Root> ) } const frameworks = [ { label: "React", value: "react" }, { label: "Solid", value: "solid" }, { label: "Vue", value: "vue" }, { label: "Angular", value: "angular" }, { label: "Svelte", value: "svelte" }, { label: "Preact", value: "preact" }, { label: "Qwik", value: "qwik" }, { label: "Lit", value: "lit" }, { label: "Alpine.js", value: "alpinejs" }, { label: "Ember", value: "ember" }, { label: "Next.js", value: "nextjs" }, ]
Within Dialog
To use the combobox within a dialog or popover component, avoid wrapping the Combobox.Positioner
within the Portal
.
-<Portal> <Combobox.Positioner> <Combobox.Content> {/* ... */} </Combobox.Content> </Combobox.Positioner> -</Portal>
If you use a Dialog
and have set scrollBehavior="inside"
, you need to:
- Set the combobox positioning to
fixed
to avoid the combobox from being clipped by the dialog. - Set
hideWhenDetached
totrue
to hide the combobox when the trigger is scrolled out of view.
<Combobox.Root positioning={{ strategy: "fixed", hideWhenDetached: true }}> {/* ... */} </Combobox.Root>
"use client" import { Button, Combobox, Popover, Portal, useFilter, useListCollection, } from "@chakra-ui/react" const Demo = () => { return ( <Popover.Root size="xs"> <Popover.Trigger asChild> <Button variant="outline" size="sm"> Toggle popover </Button> </Popover.Trigger> <Portal> <Popover.Positioner> <Popover.Content> <Popover.Header>Select framework</Popover.Header> <Popover.Body> <ComboboxDemo /> </Popover.Body> </Popover.Content> </Popover.Positioner> </Portal> </Popover.Root> ) } const ComboboxDemo = () => { const { contains } = useFilter({ sensitivity: "base" }) const { collection, filter } = useListCollection({ initialItems: frameworks, filter: contains, }) return ( <Combobox.Root collection={collection} onInputValueChange={(e) => filter(e.inputValue)} > <Combobox.Control> <Combobox.Input placeholder="Type to search" /> <Combobox.IndicatorGroup> <Combobox.ClearTrigger /> <Combobox.Trigger /> </Combobox.IndicatorGroup> </Combobox.Control> <Combobox.Positioner> <Combobox.Content> <Combobox.Empty>No items found</Combobox.Empty> {collection.items.map((item) => ( <Combobox.Item item={item} key={item.value}> {item.label} <Combobox.ItemIndicator /> </Combobox.Item> ))} </Combobox.Content> </Combobox.Positioner> </Combobox.Root> ) } const frameworks = [ { label: "React", value: "react" }, { label: "Solid", value: "solid" }, { label: "Vue", value: "vue" }, { label: "Angular", value: "angular" }, { label: "Svelte", value: "svelte" }, { label: "Preact", value: "preact" }, { label: "Qwik", value: "qwik" }, { label: "Lit", value: "lit" }, { label: "Alpine.js", value: "alpinejs" }, { label: "Ember", value: "ember" }, { label: "Next.js", value: "nextjs" }, ]
Props
Root
Prop | Default | Type |
---|---|---|
collection * | ListCollection<T> The collection of items | |
composite | true | boolean Whether the combobox is a composed with other composite widgets like tabs |
defaultInputValue | '\'\'' | string The initial value of the combobox's input when rendered. Use when you don't need to control the value of the combobox's input. |
defaultValue | '[]' | string[] The initial value of the combobox's selected items when rendered. Use when you don't need to control the value of the combobox's selected items. |
inputBehavior | '\'none\'' | 'none' | 'autohighlight' | 'autocomplete' Defines the auto-completion behavior of the combobox. - `autohighlight`: The first focused item is highlighted as the user types - `autocomplete`: Navigating the listbox with the arrow keys selects the item and the input is updated |
lazyMount | false | boolean Whether to enable lazy mounting |
loopFocus | true | boolean Whether to loop the keyboard navigation through the items |
openOnChange | true | boolean | ((details: InputValueChangeDetails) => boolean) Whether to show the combobox when the input value changes |
openOnClick | false | boolean Whether to open the combobox popup on initial click on the input |
openOnKeyPress | true | boolean Whether to open the combobox on arrow key press |
positioning | '{ placement: \'bottom-start\' }' | PositioningOptions The positioning options to dynamically position the menu |
selectionBehavior | '\'replace\'' | 'replace' | 'clear' | 'preserve' The behavior of the combobox input when an item is selected - `replace`: The selected item string is set as the input value - `clear`: The input value is cleared - `preserve`: The input value is preserved |
skipAnimationOnMount | false | boolean Whether to allow the initial presence animation. |
unmountOnExit | false | boolean Whether to unmount on exit. |
colorPalette | 'gray' | 'gray' | 'red' | 'orange' | 'yellow' | 'green' | 'teal' | 'blue' | 'cyan' | 'purple' | 'pink' The color palette of the component |
variant | 'outline' | 'outline' | 'subtle' | 'flushed' The variant of the component |
size | 'md' | 'xs' | 'sm' | 'md' | 'lg' The size of the component |
as | React.ElementType The underlying element to render. | |
asChild | boolean Use the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. | |
unstyled | boolean Whether to remove the component's style. | |
allowCustomValue | boolean Whether to allow typing custom values in the input | |
autoFocus | boolean Whether to autofocus the input on mount | |
closeOnSelect | boolean Whether to close the combobox when an item is selected. | |
defaultHighlightedValue | string The initial highlighted value of the combobox when rendered. Use when you don't need to control the highlighted value of the combobox. | |
defaultOpen | boolean The initial open state of the combobox when rendered. Use when you don't need to control the open state of the combobox. | |
disabled | boolean Whether the combobox is disabled | |
disableLayer | boolean Whether to disable registering this a dismissable layer | |
form | string The associate form of the combobox. | |
highlightedValue | string The controlled highlighted value of the combobox | |
id | string The unique identifier of the machine. | |
ids | Partial<{ root: string label: string control: string input: string content: string trigger: string clearTrigger: string item: (id: string, index?: number | undefined) => string positioner: string itemGroup: (id: string | number) => string itemGroupLabel: (id: string | number) => string }> The ids of the elements in the combobox. Useful for composition. | |
immediate | boolean Whether to synchronize the present change immediately or defer it to the next frame | |
inputValue | string The controlled value of the combobox's input | |
invalid | boolean Whether the combobox is invalid | |
multiple | boolean Whether to allow multiple selection. **Good to know:** When `multiple` is `true`, the `selectionBehavior` is automatically set to `clear`. It is recommended to render the selected items in a separate container. | |
name | string The `name` attribute of the combobox's input. Useful for form submission | |
navigate | (details: NavigateDetails) => void Function to navigate to the selected item | |
onExitComplete | VoidFunction Function called when the animation ends in the closed state | |
onFocusOutside | (event: FocusOutsideEvent) => void Function called when the focus is moved outside the component | |
onHighlightChange | (details: HighlightChangeDetails<T>) => void Function called when an item is highlighted using the pointer or keyboard navigation. | |
onInputValueChange | (details: InputValueChangeDetails) => void Function called when the input's value changes | |
onInteractOutside | (event: InteractOutsideEvent) => void Function called when an interaction happens outside the component | |
onOpenChange | (details: OpenChangeDetails) => void Function called when the popup is opened | |
onPointerDownOutside | (event: PointerDownOutsideEvent) => void Function called when the pointer is pressed down outside the component | |
onSelect | (details: SelectionDetails) => void Function called when an item is selected | |
onValueChange | (details: ValueChangeDetails<T>) => void Function called when a new item is selected | |
open | boolean The controlled open state of the combobox | |
placeholder | string The placeholder text of the combobox's input | |
present | boolean Whether the node is present (controlled by the user) | |
readOnly | boolean Whether the combobox is readonly. This puts the combobox in a "non-editable" mode but the user can still interact with it | |
required | boolean Whether the combobox is required | |
scrollToIndexFn | (details: ScrollToIndexDetails) => void Function to scroll to a specific index | |
translations | IntlTranslations Specifies the localized strings that identifies the accessibility elements and their states | |
value | string[] The controlled value of the combobox's selected items |
Item
Prop | Default | Type |
---|---|---|
as | React.ElementType The underlying element to render. | |
asChild | boolean Use the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. | |
item | any The item to render | |
persistFocus | boolean Whether hovering outside should clear the highlighted state |
Explorer
Explore the Combobox
component parts interactively. Click on parts in the sidebar to highlight them in the preview.
Component Anatomy
Hover to highlight, click to select parts
root
clearTrigger
content
control
input
item
itemGroup
itemGroupLabel
itemIndicator
itemText
label
list
positioner
trigger
empty
indicatorGroup
combobox.recipe.ts