const items = ['Alert','Anchor','Button','Card','Heading','List','Modal','Paragraph','Popover','Tooltip',];function getFilteredItems(inputValue) {const lowerCasedInputValue = inputValue.toLowerCase();return items.filter(function filterItems(item) {return item.toLowerCase().includes(lowerCasedInputValue);});}const SampleEmptyState = () => (<Box paddingY="space40" paddingX="space50"><Text as="span" fontStyle="italic" color="colorTextWeak">No results found</Text></Box>);const MultiselectComboboxExample = () => {const [inputValue, setInputValue] = React.useState('');const filteredItems = React.useMemo(() => getFilteredItems(inputValue), [inputValue]);return (<MultiselectComboboxlabelText="Choose a Paste component"selectedItemsLabelText="Selected Paste components"helpText="Paste components are the building blocks of your product UI."items={filteredItems}initialSelectedItems={items.slice(1, 3)}emptyState={SampleEmptyState}onInputValueChange={({inputValue: newInputValue = ''}) => {setInputValue(newInputValue);}}onSelectedItemsChange={(selectedItems) => {console.log(selectedItems);}}/>);}render(<MultiselectComboboxExample />)
A Multiselect Combobox allows a user to either type or select multiple values from a styled listbox of options. Each option can consist of more than just text, e.g. text paired with an icon.
At its most basic, a Select has an options list that’s styled according to the browser default. A Combobox has a Twilio-styled options list and can allow for additional functionality like autocomplete and multiselect.
Use a Select when:
- You need a native picker experience, especially on mobile devices.
- Users will be selecting from a list of 4-10 options, or a sorted list of highly familiar options (e.g., alphabetical list of states or countries).
- You need the component to work out-of-the-box across all operating systems and browsers.
Use a Multiselect Combobox when:
- You need a Twilio-styled options list.
- You need to show more than text in an option (e.g., text paired with an icon).
- You need to group options under labels.
- You need to disable options in the list.
- Users would benefit from autocomplete functionality (e.g., autocomplete, search). For example, autocomplete may be useful when users need to select from a list of more than 10 options.
- You need to lazy load a much longer list of options to improve page load performance.
Multiselect Combobox is built with consideration for the ARIA combobox pattern.
When a user is focused on a Combobox, the listbox opens. When a user makes a selection, the listbox closes so the selected option can be registered to screen readers.
When the user is focused on a Combobox, the following keyboard interactions apply:
- Up and down arrows move the user between the options
- Enter selects the currently active option
When the user is focused within the Form Pill Group, they can use these keyboard interactions:
- Left and right arrow keys move focus within the group.
- If a pill is selectable, spacebar and enter will toggle pill selection.
- If a pill is dismissible, the pill can be removed by pressing the delete or backspace key.
Use a basic Multiselect Combobox to allow users to select multiple values from a list of predefined options.
The height of the Combobox field will increase to fit the selection of Form Pills.
Optionally, you may set a max height using the maxHeight
prop and if there are more
pills than viewable at max height, users can vertically scroll to view all the selected options.
const items = ['Alert','Anchor','Button','Card','Heading','List','Modal','Paragraph','Popover','Tooltip',];function getFilteredItems(inputValue) {const lowerCasedInputValue = inputValue.toLowerCase();return items.filter(function filterItems(item) {return item.toLowerCase().includes(lowerCasedInputValue);});}const SampleEmptyState = () => (<Box paddingY="space40" paddingX="space50"><Text as="span" fontStyle="italic" color="colorTextWeak">No results found</Text></Box>);const MultiselectComboboxExample = () => {const [inputValue, setInputValue] = React.useState('');const filteredItems = React.useMemo(() => getFilteredItems(inputValue), [inputValue]);return (<MultiselectComboboxlabelText="Choose a Paste component"selectedItemsLabelText="Selected Paste components"helpText="Paste components are the building blocks of your product UI."items={filteredItems}initialSelectedItems={items.slice(1, 3)}emptyState={SampleEmptyState}onInputValueChange={({inputValue: newInputValue = ''}) => {setInputValue(newInputValue);}}onSelectedItemsChange={(selectedItems) => {console.log(selectedItems);}}/>);}render(<MultiselectComboboxExample />)
Use add-ons to provide users with guidance on formatting their input and to offer more context about the value a user is entering.
- Prefix/suffix text — Text that can be used as a prefix and/or suffix to the value that is entered. Use prefix/suffix to help users format text.
- Prefix/suffix icon — Icons can be placed in the same area as the prefix and suffix text. Icons should trigger an action (e.g., clearing a field) or in rare cases, provide further context to what value should be entered to make a field's purpose more immediately visible (e.g., a search icon).
const items = ['Alert','Anchor','Button','Card','Heading','List','Modal','Paragraph','Popover','Tooltip',];function getFilteredItems(inputValue) {const lowerCasedInputValue = inputValue.toLowerCase();return items.filter(function filterItems(item) {return item.toLowerCase().includes(lowerCasedInputValue);});}const MultiselectComboboxExample = () => {const [inputValue, setInputValue] = React.useState('');const filteredItems = React.useMemo(() => getFilteredItems(inputValue), [inputValue]);return (<MultiselectComboboxlabelText="Choose a Paste component"selectedItemsLabelText="Selected Paste components"helpText="Paste components are the building blocks of your product UI."items={filteredItems}initialSelectedItems={items.slice(1, 3)}onInputValueChange={({inputValue: newInputValue = ''}) => {setInputValue(newInputValue);}}onSelectedItemsChange={(selectedItems) => {console.log(selectedItems);}}insertBefore={<Text color="colorTextWeak" as="span" fontWeight="fontWeightSemibold">$10.99</Text>}insertAfter={<Anchor href="#" display="flex"><InformationIcon decorative={false} size="sizeIcon20" title="Get more info" /></Anchor>}/>);}render(<MultiselectComboboxExample />)
Use option groups to create labeled sections of options.
Structure your data into an array of objects and use a key on each object as the grouping identifier.
Then, tell the Combobox what you would like to group the items by, by setting groupItemsBy
to match
the intended group identifier.
In the example below, we have a list of components and we are grouping them based on their type.
const groupedItems = [{group: 'Components', label: 'Alert'},{group: 'Components', label: 'Anchor'},{group: 'Components', label: 'Button'},{group: 'Components', label: 'Card'},{group: 'Components', label: 'Heading'},{group: 'Components', label: 'List'},{group: 'Components', label: 'Modal'},{group: 'Components', label: 'Paragraph'},{group: 'Primitives', label: 'Box'},{group: 'Primitives', label: 'Text'},{group: 'Primitives', label: 'Non-modal dialog'},{group: 'Layout', label: 'Grid'},{label: 'Design Tokens'},];function getFilteredGroupedItems(inputValue) {const lowerCasedInputValue = inputValue.toLowerCase();return filter(groupedItems, (item) => item.label.toLowerCase().includes(lowerCasedInputValue));}const MultiselectComboboxExample = () => {const [inputValue, setInputValue] = React.useState('');const filteredItems = React.useMemo(() => getFilteredGroupedItems(inputValue), [inputValue]);return (<MultiselectComboboxgroupItemsBy="group"items={filteredItems}itemToString={(item) => (item ? item.label : '')}onInputValueChange={({inputValue: newInputValue = ''}) => {setInputValue(newInputValue);}}onSelectedItemsChange={(selectedItems) => {console.log(selectedItems);}}labelText="Choose a Paste component"selectedItemsLabelText="Selected Paste components"helpText="Paste components are the building blocks of your product UI."optionTemplate={(item) => <div>{item.label}</div>}/>);}render(<MultiselectComboboxExample />)
Expanding on the previous example, it's also possible to customize the group label.
The groupLabelTemplate
prop accepts a method with a groupName
argument. This method should return
valid JSX, which it will render in place of a group label string.
In the example below, we are checking the groupName
and rendering an icon next to
it based on the name.
const groupedItems = [{group: 'Components', label: 'Alert'},{group: 'Components', label: 'Anchor'},{group: 'Components', label: 'Button'},{group: 'Components', label: 'Card'},{group: 'Components', label: 'Heading'},{group: 'Components', label: 'List'},{group: 'Components', label: 'Modal'},{group: 'Components', label: 'Paragraph'},{group: 'Primitives', label: 'Box'},{group: 'Primitives', label: 'Text'},{group: 'Primitives', label: 'Non-modal dialog'},{group: 'Layout', label: 'Grid'},{label: 'Design Tokens'},];function getFilteredGroupedItems(inputValue) {const lowerCasedInputValue = inputValue.toLowerCase();return filter(groupedItems, (item) => item.label.toLowerCase().includes(lowerCasedInputValue));}const MultiselectComboboxExample = () => {const [inputValue, setInputValue] = React.useState('');const filteredItems = React.useMemo(() => getFilteredGroupedItems(inputValue), [inputValue]);return (<MultiselectComboboxgroupItemsBy="group"items={filteredItems}itemToString={(item) => (item ? item.label : '')}onInputValueChange={({inputValue: newInputValue = ''}) => {setInputValue(newInputValue);}}onSelectedItemsChange={(selectedItems) => {console.log(selectedItems);}}labelText="Choose a Paste component"selectedItemsLabelText="Selected Paste components"helpText="Paste components are the building blocks of your product UI."optionTemplate={(item) => <div>{item.label}</div>}groupLabelTemplate={(groupName) => {if (groupName === 'Components') {return (<MediaObject verticalAlign="center"><MediaFigure spacing="space20"><AttachIcon color="colorTextIcon" decorative={false} title="icon" /></MediaFigure><MediaBody>{groupName}</MediaBody></MediaObject>);}return groupName;}}/>);}render(<MultiselectComboboxExample />)
Use the option template to display more complex options in a listbox.
The optionTemplate
prop allows you to pass jsx
to customize the options. Note that we use native HTML input elements
to build Combobox and these HTML elements don't allow for images, icons, or svgs to be added even with the option template.
const components = [{group: 'Components', label: 'Alert'},{group: 'Components', label: 'Anchor'},{group: 'Components', label: 'Button'},{group: 'Components', label: 'Card'},{group: 'Components', label: 'Heading'},{group: 'Components', label: 'List'},{group: 'Components', label: 'Modal'},{group: 'Components', label: 'Paragraph'},{group: 'Primitives', label: 'Box'},{group: 'Primitives', label: 'Text'},{group: 'Primitives', label: 'Non-modal dialog'},{group: 'Layout', label: 'Grid'},];function getFilteredComponents(inputValue) {const lowerCasedInputValue = inputValue.toLowerCase();return components.filter(function filterComponents(component) {return (component.group.toLowerCase().includes(lowerCasedInputValue) ||component.label.toLowerCase().includes(lowerCasedInputValue));});}const MultiselectComboboxExample = () => {const [inputValue, setInputValue] = React.useState('');const filteredItems = React.useMemo(() => getFilteredComponents(inputValue), [inputValue]);return (<MultiselectComboboxlabelText="Choose a component"selectedItemsLabelText="Selected components:"helpText="Paste components are the building blocks of your product UI."items={filteredItems}itemToString={(item) => (item ? item.label : '')}optionTemplate={({label, group}) => (<Box as="span" display="flex" flexDirection="column"><Box as="span">{label}</Box><Box as="span" color="colorTextWeak">{group}</Box></Box>)}onInputValueChange={({inputValue: newInputValue = ''}) => {setInputValue(newInputValue);}}onSelectedItemsChange={(selectedItems) => {console.log(selectedItems);}}/>);};render(<MultiselectComboboxExample />)
By default the Multiselect Combobox will grow to fit the content inside it. If you want to limit how much
it resizes vertically, you can provide a maxHeight
prop.
const items = Array.from(new Array(100)).map((_,index) => `Item ${index}`)function getFilteredItems(inputValue) {const lowerCasedInputValue = inputValue.toLowerCase();return items.filter(function filterItems(item) {return item.toLowerCase().includes(lowerCasedInputValue);});}const SampleEmptyState = () => (<Box paddingY="space40" paddingX="space50"><Text as="span" fontStyle="italic" color="colorTextWeak">No results found</Text></Box>);const MultiselectComboboxExample = () => {const [inputValue, setInputValue] = React.useState('');const filteredItems = React.useMemo(() => getFilteredItems(inputValue), [inputValue]);return (<MultiselectComboboxmaxHeight="130px"labelText="Choose a Paste component"selectedItemsLabelText="Selected Paste components"helpText="Try searching for an item that doesn't exist in the list."items={filteredItems}initialSelectedItems={items.slice(20, 80)}emptyState={SampleEmptyState}onInputValueChange={({inputValue: newInputValue = ''}) => {setInputValue(newInputValue);}}onSelectedItemsChange={(selectedItems) => {console.log(selectedItems);}}/>);}render(<MultiselectComboboxExample />)
Power user move!
Only use this property if you are a power user. It's very easy to break your implementation and unfortunately the Paste team will not be able to debug this for you. Proceed with extreme caution.
In addition to being a controlled component, the Multiselect Combobox comes with the option of "hooking" into the internal state by using the state hook originally provided by Downshift.
Rather than the state be internal to the component, you can use the useMultiselectCombobox
hook and pass the returned state to MultiselectCombobox
as the state
prop.
This allows you to destructure certain returned props from the state hook,
including action methods like reset
.
An example use case of this might be programmatically providing the user a way to clear or reset the Multiselect Combobox of its previous selections.
It should be noted that when doing so, the state
prop takes precident over the
other properties that affect the state or initial state of the
MultiselectCombobox
. They will be ignored in favour of them being provided as
arguments to the useMultiselectCombobox
hook.
For full details on how to use the state hook, and what props to provide it, follow the Combobox Primitive documentation. It's the same hook, just renamed.
const groupedItems = [{group: 'Components', label: 'Alert'},{group: 'Components', label: 'Anchor'},{group: 'Components', label: 'Button'},{group: 'Components', label: 'Card'},{group: 'Components', label: 'Heading'},{group: 'Components', label: 'List'},{group: 'Components', label: 'Modal'},{group: 'Components', label: 'Paragraph'},{group: 'Primitives', label: 'Box'},{group: 'Primitives', label: 'Text'},{group: 'Primitives', label: 'Non-modal dialog'},{group: 'Layout', label: 'Grid'},{label: 'Design Tokens'},];function getFilteredGroupedItems(inputValue) {const lowerCasedInputValue = inputValue.toLowerCase();return filter(groupedItems, (item) => item.label.toLowerCase().includes(lowerCasedInputValue));}const SampleEmptyState = () => (<Box paddingY="space40" paddingX="space50"><Text as="span" fontStyle="italic" color="colorTextWeak">No results found</Text></Box>);const MultiselectComboboxExample = () => {const [inputValue, setInputValue] = React.useState('');const filteredItems = React.useMemo(() => getFilteredGroupedItems(inputValue), [inputValue]);const onSelectedItemsChange = React.useCallback((selectedItems) => {console.log(selectedItems);}, []);const state = useMultiselectCombobox({initialSelectedItems: filteredItems.slice(0, 2),onSelectedItemsChange,});return (<><Box marginBottom="space40"><Button variant="primary" onClick={() => state.setSelectedItems([])}>Clear selection</Button></Box><MultiselectComboboxstate={state}groupItemsBy="group"items={filteredItems}emptyState={SampleEmptyState}inputValue={inputValue}itemToString={(item) => (item ? item.label : '')}onInputValueChange={({inputValue = ''}) => {setInputValue(newInputValue);}}onSelectedItemsChange={onSelectedItemsChange}labelText="Choose a Paste Component"selectedItemsLabelText="Selected Paste components"helpText="Paste components are the building blocks of your product UI."optionTemplate={(item) => {return <div>{item.label}</div>;}}groupLabelTemplate={(groupName) => {if (groupName === 'Components') {return (<MediaObject verticalAlign="center"><MediaFigure spacing="space20"><AttachIcon color="colorTextIcon" decorative={false} title="icon" /></MediaFigure><MediaBody>{groupName}</MediaBody></MediaObject>);}return groupName;}}/></>);}render(<MultiselectComboboxExample />)
const items = ['Alert','Anchor','Button','Card','Heading','List','Modal','Paragraph','Popover','Tooltip',];function getFilteredItems(inputValue) {const lowerCasedInputValue = inputValue.toLowerCase();return items.filter(function filterItems(item) {return item.toLowerCase().includes(lowerCasedInputValue);});}const MultiselectComboboxExample = () => {const [inputValue, setInputValue] = React.useState('');const filteredItems = React.useMemo(() => getFilteredItems(inputValue), [inputValue]);return (<MultiselectComboboxdisabledlabelText="Choose a Paste component"selectedItemsLabelText="Selected Paste components"helpText="Paste components are the building blocks of your product UI."items={filteredItems}initialSelectedItems={items.slice(1, 3)}onInputValueChange={({inputValue: newInputValue = ''}) => {setInputValue(newInputValue);}}onSelectedItemsChange={(selectedItems) => {console.log(selectedItems);}}/>);}render(<MultiselectComboboxExample />)
const items = ['Alert','Anchor','Button','Card','Heading','List','Modal','Paragraph','Popover','Tooltip',];function getFilteredItems(inputValue) {const lowerCasedInputValue = inputValue.toLowerCase();return items.filter(function filterItems(item) {return item.toLowerCase().includes(lowerCasedInputValue);});}const MultiselectComboboxExample = () => {const [inputValue, setInputValue] = React.useState('');const filteredItems = React.useMemo(() => getFilteredItems(inputValue), [inputValue]);return (<MultiselectComboboxlabelText="Choose a Paste component"selectedItemsLabelText="Selected Paste components"helpText="Paste components are the building blocks of your product UI."items={filteredItems}disabledItems={filteredItems.slice(2, 4)}onInputValueChange={({inputValue: newInputValue = ''}) => {setInputValue(newInputValue);}}onSelectedItemsChange={(selectedItems) => {console.log(selectedItems);}}/>);}render(<MultiselectComboboxExample />)
const items = ['Alert','Anchor','Button','Card','Heading','List','Modal','Paragraph','Popover','Tooltip',];function getFilteredItems(inputValue) {const lowerCasedInputValue = inputValue.toLowerCase();return items.filter(function filterItems(item) {return item.toLowerCase().includes(lowerCasedInputValue);});}const MultiselectComboboxExample = () => {const [inputValue, setInputValue] = React.useState('');const filteredItems = React.useMemo(() => getFilteredItems(inputValue), [inputValue]);return (<MultiselectComboboxhasErrorlabelText="Choose a Paste component"selectedItemsLabelText="Selected Paste components"helpText="Tooltip must be used with an anchor or button."items={filteredItems}initialSelectedItems={items.slice(1, 3)}onInputValueChange={({inputValue: newInputValue = ''}) => {setInputValue(newInputValue);}}onSelectedItemsChange={(selectedItems) => {console.log(selectedItems);}}/>);}render(<MultiselectComboboxExample />)
Use an empty state to indicate to a user that their input does not match any value in the list of options.
const items = ['Alert','Anchor','Button','Card','Heading','List','Modal','Paragraph','Popover','Tooltip',];function getFilteredItems(inputValue) {const lowerCasedInputValue = inputValue.toLowerCase();return items.filter(function filterItems(item) {return item.toLowerCase().includes(lowerCasedInputValue);});}const SampleEmptyState = () => (<Box paddingY="space40" paddingX="space50"><Text as="span" fontStyle="italic" color="colorTextWeak">No results found</Text></Box>);const MultiselectComboboxExample = () => {const [inputValue, setInputValue] = React.useState('');const filteredItems = React.useMemo(() => getFilteredItems(inputValue), [inputValue]);return (<MultiselectComboboxemptyState={SampleEmptyState}labelText="Choose a Paste component"selectedItemsLabelText="Selected Paste components"helpText="Try searching for an item that doesn't exist in the list."items={filteredItems}onInputValueChange={({inputValue: newInputValue = ''}) => {setInputValue(newInputValue);}}onSelectedItemsChange={(selectedItems) => {console.log(selectedItems);}}/>);}render(<MultiselectComboboxExample />)
A Multiselect Combobox is comprised of a label, an input and a listbox.
Stack form fields vertically with $space-80
spacing between each field. Avoid placing multiple form fields on the
same horizontal row to help make it easier to scan a page vertically.
Keep option text concise and simple. Option text will truncate when it’s longer than the width of the container. Use a safe and reversible option as the default selected value.
Use at least 7 options in a Combobox. If there are less than 7 options or if choosing options requires more explanation, consider using a Checkbox instead and add Help Text for each option, or give more explanation through help text. Sort options logically (e.g., alphabetically, by value) or in an order that’s intuitive to your user.
Use help text to provide information to help users avoid errors.
Use Help Text to show an inline error text beneath the combobox to explain how to fix an error. For additional guidance on how to compose error messages, refer to the error state pattern.
Do
Use a Multiselect Combobox when there are multiple options for a user to type or select.
Don't
Don’t use a Multiselect Combobox when a user should only select a single option. Use a Singleselect Combobox instead.
Do
Use a Multiselect Combobox when there are at least 7 options for a user to type or select from.
Don't
Don’t use a Multiselect Combobox if there are less than 7 options in a list for users to select multiple values from. Use a Checkbox Group instead.