Skip to content

Combobox

Accessible combobox (autocomplete or autosuggest) component for React.

A combobox is the combination of an <input type="text"/> and a list. The list is designed to help the user arrive at a value, but the value does not necessarily have to come from that list. Don't think of it like a <select/>, but more of an <input type="text"/> with some suggestions. You can, however, validate that the value comes from the list, that's up to your app.

Installation

From the command line in your project directory, run npm install @reach/combobox or yarn add @reach/combobox. Then import the components and styles that you need:

npm install @reach/combobox# oryarn add @reach/combobox
import {  Combobox,  ComboboxInput,  ComboboxPopover,  ComboboxList,  ComboboxOption,  ComboboxOptionText,} from "@reach/combobox";import "@reach/combobox/styles.css";

Accessibility

Reach UI aims to handle most ARIA and accessibility concerns so that developers don't have to worry about it. Labeling is often the one thing Reach can't do for you by default since there are many ways to accomplish it, and some of those methods require app-level context.

However, we still aim to make accessibility as easy as possible. Labels for the compound Combobox component can go on the parent and we will forward the label to the correct nested component where it belongs.

For instance, instead of adding aria-label to <ComboboxInput>, we can add it to <Combobox>. The same goes for aria-labelledby.

<Combobox aria-label="choose a fruit">  <ComboboxInput />  <ComboboxPopover>    <ComboboxList>      <ComboboxOption value="Apple" />      <ComboboxOption value="Banana" />    </ComboboxList>  </ComboboxPopover></Combobox>

One benefit reaped from this pattern is that it alleviates the need for developers to think about where the aria-label or aria-labelledby attributes belong in the component tree. Another benefit is that if the ARIA spec changes in the future (as it did from 1.1 to 1.2 for Combobox), Reach doesn't introduce a breaking API change to make accessibility improvements.

NOTE: You can still pass either aria-label or aria-labelledby directly to ComboboxInput if you'd prefer and those values will override either respective prop passed into Combobox, though we discourage this. It's helpful for the component's context to keep a reference to its label. We may remove this option in a future release.

Examples

To get you started, let's take a look at a few examples that grow from simple to complex, after the examples you can see the API for each component.

Basic, Fixed List Combobox

Like a <table><tr><td/></tr></table>, a full combobox is made up of multiple components. This example demonstrates all of the pieces you need in the simplest form possible.

function Example() {  return (    <div>      <h4 id="demo">Basic, Fixed List Combobox</h4>      <Combobox aria-labelledby="demo">        <ComboboxInput />        <ComboboxPopover>          <ComboboxList>            <ComboboxOption value="Apple" />            <ComboboxOption value="Banana" />            <ComboboxOption value="Orange" />            <ComboboxOption value="Pineapple" />            <ComboboxOption value="Kiwi" />          </ComboboxList>        </ComboboxPopover>      </Combobox>    </div>  );}

Custom Rendering in ComboboxOption

Sometimes your items need to be more than just text, in these cases you can pass children to ComboboxOption, and then render a <ComboboxOptionText/> to keep the built-in text highlighting. Only the value is used to match, not the children.

function Example() {  return (    <Combobox aria-label="custom option demo">      <ComboboxInput        placeholder="Custom Option Rendering"        style={{ width: 300 }}      />      <ComboboxPopover>        <ComboboxList>          <ComboboxOption value="Apple">            🍎 <ComboboxOptionText />          </ComboboxOption>          <ComboboxOption value="Banana">            🍌 <ComboboxOptionText />          </ComboboxOption>          <ComboboxOption value="Orange">            🍊 <ComboboxOptionText />          </ComboboxOption>          <ComboboxOption value="Pineapple">            🍍 <ComboboxOptionText />          </ComboboxOption>          <ComboboxOption value="Kiwi">            🥝 <ComboboxOptionText />          </ComboboxOption>        </ComboboxList>      </ComboboxPopover>    </Combobox>  );}

This demo searches a client-side list of all US Cities. Combobox does not implement any matching on your list (aside from highlighting the matched phrases in an option). Instead, you render an Option for each result you want in the list. So your job is to:

  • Establish the search term state
  • Match the search to your list
  • Render a ComboboxOption for each match

There is nothing special about managing state for a combobox, it's like managing state for any other list in your app. As the input changes, you figure out what state you need, then render as many ComboboxOption elements as you want.

(() => {  function Example() {    const [term, setTerm] = React.useState("");    const results = useCityMatch(term);    const handleChange = (event) => setTerm(event.target.value);    return (      <div>        <h4>Clientside Search</h4>        <Combobox aria-label="Cities">          <ComboboxInput            className="city-search-input"            onChange={handleChange}          />          {results && (            <ComboboxPopover className="shadow-popup">              {results.length > 0 ? (                <ComboboxList>                  {results.slice(0, 10).map((result, index) => (                    <ComboboxOption                      key={index}                      value={`${result.city}, ${result.state}`}                    />                  ))}                </ComboboxList>              ) : (                <span style={{ display: "block", margin: 8 }}>                  No results found                </span>              )}            </ComboboxPopover>          )}        </Combobox>      </div>    );  }  function useCityMatch(term) {    const throttledTerm = useThrottle(term, 100);    return React.useMemo(      () =>        term.trim() === ""          ? null          : matchSorter(cities, term, {              keys: [(item) => `${item.city}, ${item.state}`],            }),      [throttledTerm]    );  }  return <Example />;})();

This is the same demo as above, except this time we're going to a server to get the match. This is recommended as the previous example had to download 350kb of city text! Again, there is nothing special about a ComboboxList as any other list in React. As the input changes, fetch data, set state, render options.

(() => {  function Example() {    const [searchTerm, setSearchTerm] = React.useState("");    const cities = useCitySearch(searchTerm);    const handleSearchTermChange = (event) => {      setSearchTerm(event.target.value);    };    return (      <Combobox aria-label="Cities">        <ComboboxInput          className="city-search-input"          onChange={handleSearchTermChange}        />        {cities && (          <ComboboxPopover className="shadow-popup">            {cities.length > 0 ? (              <ComboboxList>                {cities.map((city) => {                  const str = `${city.city}, ${city.state}`;                  return <ComboboxOption key={str} value={str} />;                })}              </ComboboxList>            ) : (              <span style={{ display: "block", margin: 8 }}>                No results found              </span>            )}          </ComboboxPopover>        )}      </Combobox>    );  }  function useCitySearch(searchTerm) {    const [cities, setCities] = React.useState([]);    React.useEffect(() => {      if (searchTerm.trim() !== "") {        let isFresh = true;        fetchCities(searchTerm).then((cities) => {          if (isFresh) setCities(cities);        });        return () => (isFresh = false);      }    }, [searchTerm]);    return cities;  }  const cache = {};  function fetchCities(value) {    if (cache[value]) {      return Promise.resolve(cache[value]);    }    return fetch("https://city-search.chaance.vercel.app/api?" + value)      .then((res) => res.json())      .then((result) => {        cache[value] = result;        return result;      });  }  return <Example />;})();

Lots of arbitrary elements

Sometimes your list is a bit more complicated, like categories of results, and lots of elements besides options inside the popover.

You can even have other interactive elements inside the popover, it won't close when the user interacts with them.

(() => {  function Example() {    const [term, setTerm] = React.useState("");    const results = useCityMatch(term);    const handleChange = (event) => setTerm(event.target.value);    return (      <div>        <h4>Lots of stuff going on</h4>        <Combobox>          <ComboboxInput            onChange={handleChange}            style={{ width: 300, margin: 0 }}          />          {results && (            <ComboboxPopover style={{ width: 300 }}>              {results.length > 0 ? (                <ComboboxList>                  <h5 style={heading}>Top 3 results!</h5>                  {results.slice(0, 3).map((result, index) => (                    <ComboboxOption                      key={index}                      value={`${result.city}, ${result.state}`}                    />                  ))}                  {results.length > 3 && (                    <React.Fragment>                      <h5 style={heading}>The Rest</h5>                      {results.slice(3, 10).map((result, index) => (                        <ComboboxOption                          key={index}                          value={`${result.city}, ${result.state}`}                        />                      ))}                    </React.Fragment>                  )}                </ComboboxList>              ) : (                <div>                  <p style={{ padding: 10, textAlign: "center" }}>                    No results 😞                  </p>                </div>              )}              <p style={{ textAlign: "center", padding: 10 }}>                <button>Create a new record</button>              </p>            </ComboboxPopover>          )}        </Combobox>      </div>    );  }  function useCityMatch(term) {    const throttledTerm = useThrottle(term, 100);    return React.useMemo(      () =>        term.trim() === ""          ? null          : matchSorter(cities, term, {              keys: [(item) => `${item.city}, ${item.state}`],            }),      [throttledTerm]    );  }  const heading = {    fontSize: "100%",    color: "red",    fontWeight: "bold",    textTransform: "uppercase",    margin: 0,    padding: 5,  };  return <Example />;})();

Custom styling

This demo shows how you can control a lot about the styling. It uses portal={false} on the ComboboxPopover which allows us to create a continuous outline around the entire thing.

(() => {  function Example() {    let [term, setTerm] = React.useState("");    let results = useCityMatch(term);    const handleChange = (event) => setTerm(event.target.value);    return (      <Combobox className="pink">        <ComboboxInput onChange={handleChange} />        {results && (          <ComboboxPopover portal={false}>            <hr />            {results.length > 0 ? (              <ComboboxList>                {results.slice(0, 10).map((result, index) => (                  <ComboboxOption                    key={index}                    value={`${result.city}, ${result.state}`}                  />                ))}              </ComboboxList>            ) : (              <p                style={{                  margin: 0,                  color: "#454545",                  padding: "0.25rem 1rem 0.75rem 1rem",                  fontStyle: "italic",                }}              >                No results :(              </p>            )}          </ComboboxPopover>        )}      </Combobox>    );  }  function useCityMatch(term) {    let throttledTerm = useThrottle(term, 100);    return React.useMemo(      () =>        term.trim() === ""          ? null          : matchSorter(cities, term, {              keys: [(item) => `${item.city}, ${item.state}`],            }),      [throttledTerm]    );  }  return <Example />;})();

CSS Selectors

Please see the styling guide.

[data-reach-combobox] {}[data-reach-combobox-input] {}[data-reach-combobox-popover] {}[data-reach-combobox-list] {}[data-reach-combobox-option] {}[data-reach-combobox-option][data-highlighted] {}[data-reach-combobox-button] {}/* the string portions that match what the user has typed */[data-user-value] {}/* the string portions are suggested */[data-suggested-value] {}

Component API

Combobox

Parent component that sets up the proper aria roles and context for the rest of the components.

Combobox Props

PropTypeRequired
asstring | Componentfalse
childrennode | funcfalse
openOnFocusbooleanfalse
onSelectfuncfalse
Combobox as

as?: keyof JSX.IntrinsicElements | React.ComponentType

A string representing an HTML element or a React component that will tell the ComboboxOption what element to render. Defaults to div.

NOTE: Many semantic elements, such as button elements, have meaning to assistive devices and browsers that provide context for the user and, in many cases, provide or restrict interactive behaviors. Use caution when overriding our defaults and make sure that the element you choose to render provides the same experience for all users.

Combobox children

children: React.ReactNode | ((props: { id: string | undefined; isExpanded: boolean }) => React.ReactNode)

Combobox expects to receive ComboboxInput and ComboboxPopover as children. You can also pass a render function to expose data for Combobox to its descendants.

Combobox openOnFocus

openOnFocus?: boolean

<Combobox openOnFocus />

Defaults to false.

If true, the popover opens when focus is on the text box.

Combobox onSelect

onSelect?(value: string): void

Called with the selection value when the user makes a selection from the list.

<Combobox onSelect={(item) => {}} />

ComboboxInput

Wraps an <input/> with a couple extra props that work with the combobox.

ComboboxInput Props

PropTypeRequired
asstring | Componentfalse
selectOnClickbooleanfalse
autocompletebooleanfalse
ComboboxInput as

as?: keyof JSX.IntrinsicElements | React.ComponentType

A string representing an HTML element or a React component that will tell the ComboboxInput what element to render. Defaults to input.

NOTE: Recreating native input behavior and all of its nuance with a non-semantic element is extremely difficult and may make the component inaccessible to many users. We do not recommend doing this.

ComboboxInput selectOnClick

selectOnClick?: boolean

<ComboboxInput selectOnClick />

Defaults to false.

If true, when the user clicks inside the text box the current value will be selected. Use this if the user is likely to delete all the text anyway (like the URL bar in browsers).

However, if the user is likely to want to tweak the value, leave this false, like a google search--the user is likely wanting to edit their search, not replace it completely.

ComboboxInput autocomplete

autocomplete?: boolean

Defaults to true.

Determines if the value in the input changes or not as the user navigates with the keyboard. If true, the value changes, if false the value doesn't change.

Set this to false when you don't really need the value from the input but want to populate some other state (like the recipient selector in Gmail). But if your input is more like a normal <input type="text"/>, then leave the true default.

<ComboboxInput autocomplete={false} />

ComboboxPopover

Contains the popup that renders the list. Because some UI needs to render more than the list in the popup, you need to render one of these around the list. For example, maybe you want to render the number of results suggested.

ComboboxPopover Props

PropTypeRequired
portalbooleanfalse
ComboboxPopover portal

If you pass <ComboboxPopover portal={false} /> the popover will not render inside of a portal, but in the same order as the React tree. This is mostly useful for styling the entire component together, like the pink focus outline in the example earlier in this page.

Defaults to true.

ComboboxList

Contains the ComboboxOption elements and sets up the proper aria attributes for the list.

ComboboxList Props

PropTypeRequired
asstring | Componentfalse
persistSelectionbooleanfalse
ComboboxList as

as?: keyof JSX.IntrinsicElements | React.ComponentType

A string representing an HTML element or a React component that will tell the ComboboxList what element to render. Defaults to ul.

NOTE: Many semantic elements, such as button elements, have meaning to assistive devices and browsers that provide context for the user and, in many cases, provide or restrict interactive behaviors. Use caution when overriding our defaults and make sure that the element you choose to render provides the same experience for all users.

ComboboxList persistSelection

persistSelection?: boolean

<ComboboxList persistSelection />

Defaults to false. When true and the list is opened, if an option's value matches the value in the input, it will automatically be highlighted and be the starting point for any keyboard navigation of the list.

This allows you to treat a Combobox more like a <select> than an <input/>, but be mindful that the user is still able to put any arbitrary value into the input, so if the only valid values for the input are from the list, your app will need to do that validation on blur or submit of the form.

ComboboxOption

An option that is suggested to the user as they interact with the combobox.

ComboboxOption Props

PropTypeRequired
asstring | Componentfalse
valuestringtrue
childrennodefalse
ComboboxOption as

as?: keyof JSX.IntrinsicElements | React.ComponentType

A string representing an HTML element or a React component that will tell the ComboboxOption what element to render. Defaults to li.

NOTE: Many semantic elements, such as button elements, have meaning to assistive devices and browsers that provide context for the user and, in many cases, provide or restrict interactive behaviors. Use caution when overriding our defaults and make sure that the element you choose to render provides the same experience for all users.

ComboboxOption value

value?: string

The value to match against when suggesting.

<ComboboxOption value="Salt Lake City, Utah" />
ComboboxOption children

children?: React.ReactNode

Optional. If omitted, the value will be used as the children like this:

<ComboboxOption value="Seattle, Tacoma, Washington" />

But if you need to control a bit more, you can put whatever children you want, but make sure to render a ComboboxOptionText as well, so the value is still displayed with the text highlighting on the matched portions.

<ComboboxOption value="Apple" />🍎 <ComboboxOptionText/></ComboboxOption>

ComboboxOptionText

Renders the value of a ComboboxOption as text but with spans wrapping the matching and non-matching segments of text.

So given an option like this:

<ComboboxOption value="Seattle">  🌧 <ComboboxOptionText /></ComboboxOption>

And the user typed Sea, the out would be:

<span data-user-value>Sea</span><span data-suggested-value>ttle</span>

ComboboxOptionText CSS Selectors

/* the matching segments of text */[data-user-value] {}/* the unmatching segments */[data-suggested-value] {}

useComboboxContext

function useComboboxContext(): { id: string | undefined; isExpanded: boolean }

A hook that exposes data for a given Combobox component to its descendants.