Filterable list with React

How to make a list with a search field so the user can filter the list.

Demo

This is what we’re building:

  • foo
  • fooz
  • bar
  • baz
  • quix

The React component

Here’s code for that example:

import type { ChangeEvent, ComponentProps } from 'react'
import { useState } from 'react'
 
const items = ['foo', 'fooz', 'bar', 'baz', 'quix']
 
export function FilterableList({ style, ...props }: ComponentProps<'div'>) {
  const [searchResults, setSearchResults] = useState(items)
 
  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
    const value = event.target.value.toLowerCase()
 
    if (value) {
      const results = items.filter(item => item.toLowerCase().includes(value))
      setSearchResults(results.length > 0 || value ? results : items)
    } else {
      setSearchResults(items)
    }
  }
 
  return (
    <div
      {...props}
      style={{
        border: '1px solid #ddd',
        padding: '20px 20px 10px 20px',
        ...style,
      }}
    >
      <input
        aria-controls='tag-list'
        onChange={handleChange}
        placeholder='Search'
        style={{ padding: '5px 10px', marginBottom: '10px', ...style }}
        type='text'
      />
      <ul aria-live='polite' id='tag-list' role='region'>
        {searchResults.length === 0 ? (
          <li>No results</li>
        ) : (
          searchResults.map(item => <li key={item}>{item}</li>)
        )}
      </ul>
    </div>
  )
}

Accessibility

We need to tell screen readers to announce the changes that have happened on the page when filtering. This can be done by defining the list as an ARIA live region, see the highlighted lines and the explanation below:

<>
  <input
    aria-controls='tag-list'
    aria-label='search'
    onChange={handleChange}
    placeholder='Search'
    style={{ padding: '5px 10px', marginBottom: '10px', ...style }}
    type='text'
  />
  <ul aria-live='polite' id='tag-list' role='region'>
    {searchResults.length === 0 ? (
      <li>No results</li>
    ) : (
      searchResults.map(item => <li key={item}>{item}</li>)
    )}
  </ul>
</>
aria-controls="tag-list"

This attribute tells the screen reader that this field controls the list below with the id of tag-list.

aria-label="search"

We don’t have a label on the form field, so we can label it like this.

aria-live="polite"

This marks the element as a live region, meaning it might update dynamically. The value of the attribute defines how the changes to the tag-list are announced. Other values can be assertive or off.

role="region"

The region role is used to identify document areas the author deems significant. It is a generic landmark available to aid in navigation when none of the other landmark roles are appropriate.

Read more about regions

.