Ariakit
/

Radix Select with Combobox

Rendering a searchable Radix UI Select component with a text field that enables typeahead & autocomplete features using the primitive Ariakit Combobox components.

import {
} from "@ariakit/react";
import * as RadixSelect from "@radix-ui/react-select";
import { matchSorter } from "match-sorter";
import { startTransition, useMemo, useState } from "react";
import { CheckIcon, ChevronUpDownIcon, SearchIcon } from "./icons.tsx";
import { languages } from "./languages.ts";
import "./style.css";
export default function Example() {
const [open, setOpen] = useState(false);
const [value, setValue] = useState("");
const [searchValue, setSearchValue] = useState("");
const matches = useMemo(() => {
if (!searchValue) return languages;
const keys = ["label", "value"];
const matches = matchSorter(languages, searchValue, { keys });
// Radix Select does not work if we don't render the selected item, so we
// make sure to include it in the list of matches.
const selectedLanguage = languages.find((lang) => lang.value === value);
if (selectedLanguage && !matches.includes(selectedLanguage)) {
matches.push(selectedLanguage);
}
return matches;
}, [searchValue, value]);
return (
<RadixSelect.Root
value={value}
onValueChange={setValue}
open={open}
onOpenChange={setOpen}
>
open={open}
setOpen={setOpen}
setValue={(value) => {
startTransition(() => {
setSearchValue(value);
});
}}
>
<RadixSelect.Trigger aria-label="Language" className="select">
<RadixSelect.Value placeholder="Select a language" />
<RadixSelect.Icon className="select-icon">
<ChevronUpDownIcon />
</RadixSelect.Icon>
</RadixSelect.Trigger>
<RadixSelect.Content
role="dialog"
aria-label="Languages"
position="popper"
className="popover"
sideOffset={4}
alignOffset={-16}
>
<div className="combobox-wrapper">
<div className="combobox-icon">
<SearchIcon />
</div>
placeholder="Search languages"
className="combobox"
// Ariakit's Combobox manually triggers a blur event on virtually
// blurred items, making them work as if they had actual DOM
// focus. These blur events might happen after the corresponding
// focus events in the capture phase, leading Radix Select to
// close the popover. This happens because Radix Select relies on
// the order of these captured events to discern if the focus was
// outside the element. Since we don't have access to the
// onInteractOutside prop in the Radix SelectContent component to
// stop this behavior, we can turn off Ariakit's behavior here.
onBlurCapture={(event) => {
event.preventDefault();
event.stopPropagation();
}}
/>
</div>
<ComboboxList className="listbox">
{matches.map(({ label, value }) => (
<RadixSelect.Item
key={value}
value={value}
asChild
className="item"
>
<RadixSelect.ItemText>{label}</RadixSelect.ItemText>
<RadixSelect.ItemIndicator className="item-indicator">
<CheckIcon />
</RadixSelect.ItemIndicator>
</RadixSelect.Item>
))}
</RadixSelect.Content>
</RadixSelect.Root>
);
}

Components

Explore the Ariakit components used in this example:

Basic structure

<RadixSelect.Root>
<RadixSelect.Trigger />
<RadixSelect.Content>
<Combobox />
<RadixSelect.Item asChild>
</RadixSelect.Item>
</RadixSelect.Content>
</RadixSelect.Root>

Sharing state between Ariakit and Radix UI

We can share state between Ariakit and Radix UI by passing our own open state to both Radix's Root and Ariakit's ComboboxProvider:

const [open, setOpen] = useState(false);
<RadixSelect.Root open={open} onOpenChange={setOpen}>
<ComboboxProvider open={open} setOpen={setOpen}>

You can learn more about this Ariakit feature in the guide:

Filtering options

The Ariakit Combobox component doesn't dictate how you filter the items. It focuses solely on the ComboboxItem elements you render. Consequently, you can render items conditionally based on the value state.

We use the setValue callback in combination with React.startTransition to update our search value state without blocking the UI:

const [searchValue, setSearchValue] = useState("");
setValue={(value) => {
React.startTransition(() => {
setSearchValue(value);
});
}}
>

You're free to use any matching algorithm or library to filter the items. In this example, we use match-sorter:

const matches = useMemo(() => {
return matchSorter(languages, searchValue, {
keys: ["label", "value"],
});
}, [languages, searchValue]);

Rendering SelectItem as ComboboxItem

To get the items to function as both a Radix SelectItem and an Ariakit ComboboxItem, we have to combine the two components:

<RadixSelect.Item value="en" asChild>
<RadixSelect.ItemText>English</RadixSelect.ItemText>
</RadixSelect.Item>

More examples

Stay tuned

Join 1,000+ subscribers and receive monthly tips & updates on new Ariakit content.

No spam. Unsubscribe anytime. Read latest issue