Input component that allows users to select from a list of options as they type.
import { matchSorter } from 'match-sorter'; const FRUITS = [ { value: 'Apple', emoji: '🍎' }, { value: 'Grape', emoji: '🍇' }, { value: 'Orange', emoji: '🍊' }, { value: 'Strawberry', emoji: '🍓' }, { value: 'Watermelon', emoji: '🍉' }, { value: 'Banana', emoji: '🍌' }, { value: 'Pineapple', emoji: '🍍' }, { value: 'Peach', emoji: '🍑' }, { value: 'Cherry', emoji: '🍒' }, { value: 'Kiwi', emoji: '🥝' }, { value: 'Mango', emoji: '🥭' }, { value: 'Papaya', emoji: '🍈' }, { value: 'Blueberry', emoji: '🫐' }, { value: 'Coconut', emoji: '🥥' }, ]; function ComboboxExample() { const [searchValue, setSearchValue] = React.useState(''); const [selectedValue, setSelectedValue] = React.useState<string | null>(null); const matches = React.useMemo(() => { return matchSorter(FRUITS, searchValue, { keys: ['value'], baseSort: (a, b) => (a.index < b.index ? -1 : 1), }); }, [searchValue]); return ( <Combobox.Root selectedValue={selectedValue} onSelectedValueChange={setSelectedValue} onValueChange={(value) => { React.startTransition(() => { setSearchValue(value); }); }} > <Combobox.Label>Your favorite fruit</Combobox.Label> <Combobox.Anchor> <Combobox.Input placeholder="e.g., Apple, Banana" /> </Combobox.Anchor> <Combobox.Popover> {matches.length > 0 ? ( <Combobox.Content> <Combobox.ScrollArea> {matches.map((match) => ( <Combobox.Item key={match.value} value={match.value}> {match.emoji} {match.value} </Combobox.Item> ))} </Combobox.ScrollArea> </Combobox.Content> ) : ( <Flex align="center" gap="2" justify="between" px="2" py="1"> <Text size="2">No results found</Text> </Flex> )} </Combobox.Popover> </Combobox.Root> ); }
import { matchSorter } from 'match-sorter'; const FRUITS = [ { value: 'Apple', emoji: '🍎' }, { value: 'Grape', emoji: '🍇' }, { value: 'Orange', emoji: '🍊' }, { value: 'Strawberry', emoji: '🍓' }, { value: 'Watermelon', emoji: '🍉' }, { value: 'Banana', emoji: '🍌' }, { value: 'Pineapple', emoji: '🍍' }, { value: 'Peach', emoji: '🍑' }, { value: 'Cherry', emoji: '🍒' }, { value: 'Kiwi', emoji: '🥝' }, { value: 'Mango', emoji: '🥭' }, { value: 'Papaya', emoji: '🍈' }, { value: 'Blueberry', emoji: '🫐' }, { value: 'Coconut', emoji: '🥥' }, ]; function ComboboxExample() { const [{ searchValue, selectedValues, open, fruits }, dispatch] = React.useReducer( (state, action) => { switch (action.type) { case 'set': { const key = action.key; const value = typeof action.value === 'function' ? action.value(state[key]) : action.value; return value === state[key] ? state : { ...state, [key]: value }; } case 'createFruit': { const { fruits } = state; const searchValue = state.searchValue.trim(); const value = searchValue.toLowerCase().charAt(0).toUpperCase() + searchValue.slice(1); const fruit = FRUITS.find((fruit) => fruit.value === value) || { value, emoji: '🍉', }; return { fruits: fruits.some((fruit) => fruit.value === value) ? fruits : [...fruits, fruit], selectedValues: state.selectedValues.includes(value) ? state.selectedValues : [...state.selectedValues, value], open: false, searchValue: '', }; } default: return state; } }, { searchValue: '', selectedValues: [], open: false, fruits: [...FRUITS].slice(0, 3), }, ); const matches = React.useMemo(() => { return matchSorter(fruits, searchValue, { keys: ['value'], baseSort: (a, b) => (a.index < b.index ? -1 : 1), }); }, [searchValue]); const hasSelectedItems = selectedValues.length > 0; return ( <Combobox.Root selectionType="multiple" selectedValue={selectedValues} onSelectedValueChange={(value) => dispatch({ type: 'set', key: 'selectedValues', value }) } open={open} onOpenChange={(value) => dispatch({ type: 'set', key: 'open', value })} value={searchValue} onValueChange={(value) => { React.startTransition(() => { dispatch({ type: 'set', key: 'searchValue', value }); }); }} > <Flex direction="column" gap="2"> <Flex direction="column" gap="1"> <Combobox.Label>Your favorite fruit</Combobox.Label> <Combobox.Anchor> <Combobox.Input placeholder={ hasSelectedItems ? `${selectedValues.length} items selected` : 'e.g., Apple, Banana' } /> </Combobox.Anchor> <Combobox.Popover> <Combobox.Content> <Combobox.ScrollArea> {matches.length > 0 ? ( matches.map((match) => ( <Combobox.Item key={match.value} value={match.value}> {match.emoji} {match.value} </Combobox.Item> )) ) : ( <Flex align="center" gap="2" justify="between" px="2" py="1"> <Text size="2">No results found</Text> </Flex> )} </Combobox.ScrollArea> <Combobox.Footer> {(() => { const cleanSearchValue = searchValue.trim(); if (!cleanSearchValue) { return null; } const matched = matches.find( (match) => match.value.toLowerCase() === cleanSearchValue.toLowerCase(), ); return ( <Combobox.ActionItem disabled={!!matched} asChild> <button type="button" onClick={() => !matched && dispatch({ type: 'createFruit' }) } > <Text> Create <Text weight="bold">{searchValue}</Text> </Text> </button> </Combobox.ActionItem> ); })()} <Combobox.ActionItem asChild> <a href="https://en.wikipedia.org/wiki/Fruit" target="_blank" rel="noopener noreferrer" > Read about fruits </a> </Combobox.ActionItem> </Combobox.Footer> </Combobox.Content> </Combobox.Popover> {selectedValues.length > 0 && ( <Combobox.SelectionList> {selectedValues.map((value) => ( <Combobox.SelectionListItem key={value} value={value}> {value} </Combobox.SelectionListItem> ))} </Combobox.SelectionList> )} </Flex> </Flex> </Combobox.Root> ); }
TODO: Demo combobox with textarea TODO: Add all component docs