Skip to main content

Project 11 · Intermediate

Search Filter

A list of countries that shrinks as you type. You'll build it twice: once by hiding the items already on the page, and once by keeping the countries as data, searching capitals too, and highlighting what matched.

  • 3 HTML ideas
  • 1 CSS idea
  • 8 JavaScript ideas
  • 2 ways to build it

Why build it

Filtering a list as someone types lets you build a shop's product search, a searchable team directory, a country picker in a form, or a filter for a long FAQ. Building it both ways shows when hiding what's already on the page is enough, and when you need to filter the data itself, as you will once a list comes from a server.

Skills you'll earn

  • Run code on every keystroke with the input event
  • Match text whether it's typed in capitals or not
  • Keep only the items you want from an array with filter
  • Highlight the matching text safely, without innerHTML
  • Tell screen readers how many results there are as they change

01

Try it

Play with it first, then read the code that makes it work.

2 ways to build it

Every country is already in the HTML. JavaScript checks each one as you type and hides the ones that don't match.

Result0px
New tab (opens in a new tab)
Download
<!doctype html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>Search Filter</title>
		<link rel="stylesheet" href="style.css" />
	</head>
	<body>
		<main class="search">
			<h1 class="search-title">Find a country</h1>

			<label class="search-label" for="search-input">Search countries</label>
			<!-- type="search" adds a clear button and a search key on phones. -->
			<input
				class="search-input"
				type="search"
				id="search-input"
				autocomplete="off"
			/>

			<!-- The count is announced, so screen reader users hear what's left. -->
			<p class="search-count" id="search-count" aria-live="polite">
				12 countries
			</p>
			<!-- Every country is already in the page; JavaScript only hides some. -->
			<ul class="results" id="results">
				<li>Brazil</li>
				<li>Canada</li>
				<li>Egypt</li>
				<li>France</li>
				<li>Ghana</li>
				<li>India</li>
				<li>Japan</li>
				<li>Kenya</li>
				<li>Mexico</li>
				<li>Nigeria</li>
				<li>Norway</li>
				<li>Peru</li>
			</ul>
			<p class="no-results" id="no-results" hidden>
				No countries match that search.
			</p>
		</main>

		<script src="script.js"></script>
	</body>
</html>

02

How it works

The ideas behind it, one at a time, each next to the code that does it.

2 ways to build it

Every country is already in the HTML. JavaScript checks each one as you type and hides the ones that don't match.

HTMLthe structure

  1. 01

    Use type="search" with a label

    type="search" is a text input meant for searching. Most browsers add a button to clear it, and phones show a search key on the keyboard. The <label> gives the field its name. A placeholder can't do that job reliably: it disappears once you type, and not every screen reader reads it.

    			<label class="search-label" for="search-input">Search countries</label>
    			<!-- type="search" adds a clear button and a search key on phones. -->
    			<input
    				class="search-input"
    				type="search"
    				id="search-input"
    				autocomplete="off"
    			/>
  2. 02

    Announce how many match

    As you type, sighted users can see the list shrink, but screen reader users get no sign that anything changed. The <p> above the list says how many countries match, like "2 countries", and aria-live="polite" makes screen readers read the new number each time it changes, without interrupting.

    			<!-- The count is announced, so screen reader users hear what's left. -->
    			<p class="search-count" id="search-count" aria-live="polite">
    				12 countries
    			</p>

JavaScriptthe behaviour

  1. 03

    React as you type with the input event

    Events are the browser's way of telling your code that something happened. The input event fires every time the field's value changes: typing, deleting, pasting, or pressing the clear button. That's why the list updates as you type. The change event would only fire once you leave the field, which is too late for a live search.

    // "input" fires on every keystroke, paste, and clear, not just on Enter.
    input.addEventListener("input", () => {
  2. 04

    Match without caring about capitals

    includes() cares about capitals, so "gha" wouldn't find "Ghana" on its own. toLowerCase() makes a lowercase copy of some text, and using it on both the query and each country means "gha", "GHA", and "Gha" all match. trim() removes spaces from both ends of what was typed, so a stray space doesn't stop a match.

    	// Lowercase both sides, so "gha" finds "Ghana".
    	const query = input.value.trim().toLowerCase();
  3. 05

    Check each item with includes()

    forEach() runs a function once for every <li> in the list. includes() returns true if one piece of text appears anywhere in another, so "an" finds Canada, France, Ghana, and Japan. Setting an element's hidden property to true hides it, so each country that doesn't match disappears from the list.

    	items.forEach((item) => {
    		// includes() is true if the text contains the query anywhere.
    		const isMatch = item.textContent.toLowerCase().includes(query);
    		item.hidden = !isMatch;
    		if (isMatch) matches++;
    	});
  4. 06

    Show a message when nothing matches

    An empty list with no explanation looks broken. matches starts at 0 and goes up by one for every country that matches. After the loop, that number fills the count text, and the "No countries match" message is shown only when it's still 0.

    	let matches = 0;
    
    	items.forEach((item) => {
    		// includes() is true if the text contains the query anywhere.
    		const isMatch = item.textContent.toLowerCase().includes(query);
    		item.hidden = !isMatch;
    		if (isMatch) matches++;
    	});
    
    	countDisplay.textContent = `${matches} ${matches === 1 ? "country" : "countries"}`;
    	noResults.hidden = matches > 0;

03

Wrap up

Track your progress, then go deeper or keep going.

Take it further

Once it works, try these on your own. Each one uses what you've just learned, plus one new idea.

  1. Challenge 01

    Wait until typing pauses before filtering, a technique called debouncing.

  2. Challenge 02

    Add buttons to filter by continent as well as by text.

  3. Challenge 03

    Keep the search in the page's address with URLSearchParams, so a filtered list can be shared.

Finished Search Filter?

Mark it done to track your progress.

You finished Search Filter!

0 of 15 projects done.

Go deeper

The full article explains each idea in more depth, why the code is written this way, and more than fits on this page.

Article coming soon. See the series so far (opens in a new tab)