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"
export const ComboboxBasic = () => {
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 '@saas-ui/react/combobox'
<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"
export const ComboboxBasic = () => {
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"
export const ComboboxWithSizes = () => {
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"
export const ComboboxWithVariants = () => {
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",
]
export const ComboboxWithMultiple = () => {
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"
export const ComboboxWithAsyncContent = () => {
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"
export const ComboboxWithHighlight = () => {
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"
export const ComboboxOpenOnClick = () => {
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 { collection } = useListCollection({
initialItems: items,
itemToString: (item) => item.country,
itemToValue: (item) => item.code,
})
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"
export const ComboboxMinCharacter = () => {
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"
export const ComboboxWithField = () => {
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" },
]
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"
export const ComboboxWithDisabled = () => {
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"
export const ComboboxWithDisabledItem = () => {
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"
export const ComboboxWithInputGroup = () => {
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"
export const ComboboxWithInvalid = () => {
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"
export const ComboboxControlled = () => {
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"
export const ComboboxWithStore = () => {
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"
export const ComboboxOpenControlled = () => {
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"
export const ComboboxWithLimit = () => {
const contentRef = useRef<HTMLDivElement>(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"
export const ComboboxVirtualized = () => {
const contentRef = useRef<HTMLDivElement>(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"
export const ComboboxWithLinks = () => {
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,
HStack,
Portal,
Span,
Spinner,
useCombobox,
useListCollection,
} from "@chakra-ui/react"
import { useRef, useState } from "react"
import { useAsync } from "react-use"
export const ComboboxRehydrateValue = () => {
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) {
const inputValue = collection.stringify(combobox.value[0])
combobox.setInputValue(inputValue || "")
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>
) : (
collection.items.map((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>
))
)}
</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>
)
}
export const ComboboxWithCustomItem = () => {
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"
export const ComboboxWithCustomFilter = () => {
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"
export const ComboboxWithCustomAnimation = () => {
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" },
]
Popover
To use the combobox within a popover component, avoid wrapping the
Combobox.Positioner
within the Portal
.
-<Portal>
<Combobox.Positioner>
<Combobox.Content>
{/* ... */}
</Combobox.Content>
</Combobox.Positioner>
-</Portal>
"use client"
import {
Button,
Combobox,
Popover,
Portal,
useFilter,
useListCollection,
} from "@chakra-ui/react"
export const ComboboxInPopover = () => {
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' | 'zinc' | 'neutral' | 'stone' | 'red' | 'orange' | 'amber' | 'yellow' | 'lime' | 'green' | 'emerald' | 'teal' | 'cyan' | 'sky' | 'blue' | 'indigo' | 'violet' | 'purple' | 'fuchsia' | 'pink' | 'rose' | 'presence' | 'status' | 'sidebar' | 'sidebar.accent' | 'accent' | 'slate' 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 |
allowCustomValue | boolean Whether to allow typing custom values in the input | |
asChild | boolean Use the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. | |
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 |
---|---|---|
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 |