A search bar is a great way to make content on your website discoverable. In this tutorial, we’ll be building an accessible search bar component using React. We’ll also be adding a couple of unit tests with React Testing Library.
Here's our final product:
The source code for this tutorial is available at react-search-bar.
Render your search bar component in the app
To get started, create a new file for your search component. I've called mine search.js
:
// src/search.js
const Search = () => {
return <div>Hello world!</div>
}
export default Search;
Then, render this component from inside of your main app file:
// src/App.js
import Search from './search';
const App = () => {
return (
<Search />
);
}
export default App;
💡 Where did
import React from 'react'
go?You may notice that we no longer have an import statement at the top of every file. From the release of React 17, this is no longer necessary. (Yay!) If you happen to be on an older version, you may still need this import statement.
Add your HTML elements
Our search bar component will contain a couple of HTML elements. Add a label, input and button, and then wrap it all in a form element:
// src/search.js
const SearchBar = () => (
<form action="/" method="get">
<label htmlFor="header-search">
<span className="visually-hidden">Search blog posts</span>
</label>
<input
type="text"
id="header-search"
placeholder="Search blog posts"
name="s"
/>
<button type="submit">Search</button>
</form>
);
export default SearchBar;
This will render like this:
Accessibility and labels
You might be wondering why we are doubling up on the label and placeholder text.
This is because placeholders aren’t accessible. By adding a label, we can tell screen reader users what the input field is for.
We can hide our label using a visually-hidden CSS class:
// src/App.css
.visually-hidden {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
This keeps it visible to screen reader users, but invisible to everyone else.
Now we have a functioning search bar! When you search, you will navigate to /?s=<your_query_here>
.
Add a list of posts
Now that we can search, we’ll need a list of items to search from. I've created a list of fake posts:
const posts = [
{ id: '1', name: 'This first post is about React' },
{ id: '2', name: 'This next post is about Preact' },
{ id: '3', name: 'We have yet another React post!' },
{ id: '4', name: 'This is the fourth and final post' },
];
Use the map
function to loop through and render them:
// src/App.js
const App = () => {
return (
<div>
<Search />
<ul>
{posts.map((post) => (
<li key={post.id}>{post.name}</li>
))}
</ul>
</div>
);
}
Filter the list based on your search query
Our search bar will navigate us to a new URL when we perform a search. We can grab this value from the URL:
const { search } = window.location;
const query = new URLSearchParams(search).get('s');
We’ll also need a function that filters out posts depending on the search query. If the list you’re querying over is simple, you can write your own:
const filterPosts = (posts, query) => {
if (!query) {
return posts;
}
return posts.filter((post) => {
const postName = post.name.toLowerCase();
return postName.includes(query);
});
};
You can also rely on third-party search libraries like js-search to filter posts for you.
Using your search query and filter function, you can render the posts that match your search:
// src/App.js
const App = () => {
const { search } = window.location;
const query = new URLSearchParams(search).get('s');
const filteredPosts = filterPosts(posts, query);
return (
<div>
<Search />
<ul>
{filteredPosts.map(post => (
<li key={post.key}>{post.name}</li>
))}
</ul>
</div>
);
}
Now when you type in a query, you will be able to filter your posts!
Adding immediate search or “search as you type”
Instead of pressing enter to submit your search, you may also want the list to filter as the user begins typing. This immediate response can be more pleasant from a user-experience perspective.
To add this feature, you can store a searchQuery
value in your component’s state, and change this value as the user begins typing:
// src/App.js
import { useState } from 'react';
function App() {
const { search } = window.location;
const query = new URLSearchParams(search).get('s');
const [searchQuery, setSearchQuery] = useState(query || '');
const filteredPosts = filterPosts(posts, searchQuery);
return (
<div>
<Search
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
/>
<ul>
{filteredPosts.map(post => (
<li key={post.key}>{post.name}</li>
))}
</ul>
</div>
);
}
After you pass in the searchQuery
and setSearchQuery
props, you’ll need to make use of it in your input element:
// src/search.js
const SearchBar = ({ searchQuery, setSearchQuery }) => (
<form action="/" method="get">
<label htmlFor="header-search">
<span className="visually-hidden">Search blog posts</span>
</label>
<input
value={searchQuery}
onInput={e => setSearchQuery(e.target.value)}
type="text"
id="header-search"
placeholder="Search blog posts"
name="s"
/>
<button type="submit">Search</button>
</form>
);
Now, as soon as you start typing, your posts will begin filtering!
Adding SPA navigation with React Router
Currently your search bar will do a full-page refresh when you press enter.
If you're looking to build a single-page app (SPA), you'll want to use a routing library like React Router. You can install it with the following command:
yarn add react-router-dom
After installing it, wrap your app in the Router
component:
// src/App.js
import { BrowserRouter as Router } from "react-router-dom";
const App = () => {
return <Router>
{ /* ... */ }
</Router>
}
And then add the following to the top of your search component:
// src/search.js
import { useHistory } from 'react-router-dom';
const SearchBar = ({ searchQuery, setSearchQuery }) => {
const history = useHistory();
const onSubmit = e => {
history.push(`?s=${searchQuery}`)
e.preventDefault()
};
return <form action="/" method="get" autoComplete="off" onSubmit={onSubmit}>
Now when a user presses enter, the app's URL will change without a full-page refresh.
“Search as you type”, SPA navigation and accessibility concerns
Without a full-page refresh, you won't be notifying screen reader users if the items in the list change.
We can send these notifications using ARIA live regions.
After some Googling, there are packages like react-aria-live and react-a11y-announcer that will help you do this.
Unfortunately, it seems like neither of these have been updated in over a year.
Luckily, it is simple to write your own announcer component:
// src/announcer.js
const Announcer = ({ message }) =>
<div role="region" aria-live="polite" className="visually-hidden">{message}</div>
export default Announcer;
And then render this in your main app component:
// src/App.js
<Announcer message={`List has ${filteredPosts.length} posts`}/>
Whenever the message changes in your Announcer
component, screen readers will read out the message.
Now, as you search, screen reader users will receive an update letting them know how many posts are on the page.
This isn't a perfect solution, but it's much better than having your items silently change.
If you are on a Mac and testing its VoiceOver feature, make sure to use Safari! I find that other browsers don't work as well with screen readers.
Testing your component with React Testing Library
To wrap things up, we’ll be testing our component using React Testing Library. This library comes out of the box with create-react-app.
The first test we’ll be adding is an accessibility check using axe
. To use it, add the jest-axe
package to your repository:
yarn add jest-axe
We can use axe to test that our search component does not have any accessibility violations:
// src/search.test.js
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import Search from '../src/search';
expect.extend(toHaveNoViolations);
test('should not have any accessibility violations', async () => {
const { container } = render(<Search searchQuery='' />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
This is a super easy way to catch simple accessibility issues. For instance, if we removed our label component, our test will fail:
We should also add a test for the functionality of your component. Let’s add one that tests that when you type “preact”, it only shows one post:
// src/App.test.js
test('should render one post when user searches for preact', () => {
render(<App />);
let posts = screen.getAllByRole('listitem');
expect(posts.length).toEqual(4);
const searchBar = screen.getByRole('textbox');
userEvent.type(searchBar, 'preact');
posts = screen.getAllByRole('listitem');
expect(posts.length).toEqual(1);
});
Conclusion
After reading this tutorial, you will be able to create an accessible search bar component for your React app. With unit tests! You can see the full source code at react-search-bar.
If you know more about how to make search bars accessible, I would love to hear from you. Feel free to reach out to me on Twitter at @emma_goto.
Top comments (19)
Hello Emma! Loved the accessibility topics, I love axe and I was happy you mentioned it :)
Quick nit: Don't pass setters to child components, this allows a child component to freely mutate the state of the parent. Children should only react to changes and then mutate the state of a parent accordingly. That way the data flow is predictable.
Hi, thanks for the feedback - do you have suggestions on how you would approach this instead?
First off what you did wasn't wrong, with that in mind read the following from the React docs:
So, the idea is to only have the parent have full control over the state, the child should receive the parent's state and control to that state as props. This way if there's a bug, you always know where to look for - the parent. You can grant control to the state via props by wrapping the setter in a function in the parent. Doing so will also allow you to control how the child mutates the parent's state too.
TLDR: Wrapping the setter in a function and passing it as a prop, then calling it in the child makes it look like the parent is changing the state, not the child.
Makes sense, thanks for the detailed explanation :)
Mydrax:
Do you mean this? I think it's just another one level of bureaucracy. The only difference is that you cannot do this with the wrapper:
But if you use TypeScript and will declare prop-type like:
you would not be able to do it anyway
I think it's more than "okay" to pass setters below the tree. And it doesn't violate top-down principle. The parent component in both cases is the only source of truth. And the only way to change the state is to use those tools that parent components provide to its children.
P.S. also if you're interested in strong performance you will wrap you wrapper by another wrapper (useCallback) :)
Hello Emma! 😀 Thank you for this tutorial. I have also implemented a search 🧐 in react. But I am using a different approach. 🤯 I think my code is a bit shorter. Please share your thoughts. As I am new in react and still I am learning 😅😅.
dark-todo.netlify.app/
My Code: github.com/Starboy-Sharma/react-to...
I want to try, but i use vue :D
I'm sure some of this is probably quite similar to Vue - especially the accessibility bits!
That's a great breakdown! Will feature this article in my newsletter. 😍
Wow that's awesome! I'll make sure to subscribe :)
Thank you so much!
Thank you, Emma! Very good read!
This is so great. One of the first mini how-to's that goes this far in-depth on the accessibility aspect of components. I love it, thanks so much for sharing!
Thanks Keith! I had to do a lot of Googling to try and find out the answers to my accessibility questions - definitely an area of improvement for all of us as developers!
Nice seems to work well.
Highly informative post, will be coming back to it. Great job 👏
Thanks Doaa, glad you like it!
Thanks for sharing! Happy to see that works on accessibility, so nice👍👍👍
Thanks for this post!!