JavascriptIntermediate
Live Search Filter
Build a live search filter that filters a list of items as the user types.
Live Search Filter
Problem
Build a live search filter where the user types in a search box and a list of items filters instantly to show only matching results. The matching part of each result should be highlighted.
Items: JavaScript, Python, MongoDB, React, Vue, Angular, Node.js, TypeScript
User types: "a"
Showing: JavaSCript, ReACt, AngulAr, TypeScript (case insensitive)
User types: "sc"
Showing: JavaSCript, TypeSCript
User types: "xyz"
Showing: No results found
User clears input
Showing: All 8 itemsLogic
- Store all items in an array
- Render all items on page load
- Listen for
inputevent on the search box - On every keystroke — filter the array by checking if each item includes the query
- If no items match — show a no results message
- Highlight the matching part of each item by wrapping it in a
<mark>tag - Show a count of how many results are showing
Flow
HTML Structure
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Live Search Filter</title>
<style>
body {
font-family: sans-serif;
max-width: 480px;
margin: 60px auto;
padding: 0 20px;
}
/* search input wrapper with icon */
.search-wrapper {
position: relative;
margin-bottom: 12px;
}
.search-wrapper input {
width: 100%;
padding: 12px 12px 12px 40px;
font-size: 1rem;
border: 2px solid #ccc;
border-radius: 8px;
box-sizing: border-box;
outline: none;
}
.search-wrapper input:focus {
border-color: #555;
}
/* search icon inside the input */
.search-wrapper .search-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #aaa;
font-size: 1rem;
}
/* result count shown below search */
#result-count {
font-size: 0.85rem;
color: #888;
margin-bottom: 14px;
}
/* the list of items */
#item-list {
list-style: none;
padding: 0;
margin: 0;
}
/* each item row */
#item-list li {
padding: 12px 16px;
border-radius: 8px;
border: 1px solid #e0e0e0;
margin-bottom: 8px;
font-size: 1rem;
color: #333;
transition: background 0.15s;
}
#item-list li:hover {
background: #f9f9f9;
}
/* no results message */
#no-results {
padding: 20px;
text-align: center;
color: #aaa;
font-size: 0.95rem;
display: none;
}
/* highlighted matching text */
mark {
background: #fff3a3;
color: #111;
border-radius: 3px;
padding: 0 2px;
}
</style>
</head>
<body>
<h1>Live Search Filter</h1>
<!-- search input -->
<div class="search-wrapper">
<span class="search-icon">🔍</span>
<input
type="text"
id="search-input"
placeholder="Search technologies..."
/>
</div>
<!-- shows how many results are visible -->
<div id="result-count"></div>
<!-- filtered items rendered here -->
<ul id="item-list"></ul>
<!-- shown when no items match -->
<div id="no-results">No results found.</div>
<script src="script.js"></script>
</body>
</html>Solution
// Step 1 — the data — all items to search through
const items = [
"JavaScript",
"Python",
"MongoDB",
"React",
"Vue",
"Angular",
"Node.js",
"TypeScript",
"CSS",
"HTML",
"GraphQL",
"PostgreSQL",
];
// Step 2 — select elements
const searchInput = document.querySelector("#search-input");
const itemList = document.querySelector("#item-list");
const noResults = document.querySelector("#no-results");
const resultCount = document.querySelector("#result-count");
// Step 3 — render all items on page load
renderItems(items, "");
// Step 4 — listen for input event
searchInput.addEventListener("input", () => {
// read the query and trim whitespace
const query = searchInput.value.trim();
if (query === "") {
// if search is cleared — show everything
renderItems(items, "");
return;
}
// Step 5 — filter items
// .toLowerCase() on both sides makes the search case insensitive
const filtered = items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
);
// Step 6 — render filtered results
renderItems(filtered, query);
});
function renderItems(filteredItems, query) {
// clear the current list
itemList.innerHTML = "";
// Step 7 — handle no results
if (filteredItems.length === 0) {
noResults.style.display = "block";
resultCount.textContent = "No results found";
return;
}
// hide no results message
noResults.style.display = "none";
// update result count
resultCount.textContent = query
? `${filteredItems.length} of ${items.length} results`
: `Showing all ${items.length} items`;
// Step 8 — render each matching item
filteredItems.forEach((item) => {
const li = document.createElement("li");
// Step 9 — highlight the matching part
// if no query — show plain text
// if query exists — wrap the matching part in <mark>
if (query === "") {
li.textContent = item;
} else {
li.innerHTML = highlightMatch(item, query);
}
itemList.appendChild(li);
});
}
// Step 10 — highlight function
// finds the matching part and wraps it in a <mark> tag
// uses a regex with 'gi' flags — global and case insensitive
function highlightMatch(text, query) {
// escape any special regex characters in the query
// so searching "." does not match everything
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
// create a regex that matches the query — case insensitive
const regex = new RegExp(escapedQuery, "gi");
// replace the matching part with the same text wrapped in <mark>
// $& in the replacement means "the matched text" — preserves original casing
return text.replace(regex, "<mark>$&</mark>");
}Code Execution
Trace through typing "sc":
| Step | Code | Result |
|---|---|---|
| Query | searchInput.value.trim() | "sc" |
| Filter | "JavaScript".toLowerCase().includes("sc") | true — match |
| Filter | "Python".toLowerCase().includes("sc") | false — no match |
| Filter | "TypeScript".toLowerCase().includes("sc") | true — match |
| Filtered | ["JavaScript", "TypeScript"] | |
| Count | "2 of 12 results" |
Highlight trace for "JavaScript" with query "sc":
| Step | Code | Result |
|---|---|---|
| Escaped query | "sc" | "sc" |
| Regex | new RegExp("sc", "gi") | matches sc anywhere |
| Replace | "JavaScript".replace(regex, "<mark>$&</mark>") | "Java<mark>Sc</mark>ript" |
| Rendered | innerHTML | JavaScript (highlighted) |
Trace through typing "xyz":
| Step | Code | Result |
|---|---|---|
| Filter | none of the 12 items include "xyz" | [] |
filtered.length === 0 | true | show no results message |
Output
Page loads → All 12 items shown
Type "a" → JavaScript, React, Angular, TypeScript — "a" highlighted in each
Type "sc" → JavaScript, TypeScript — "sc" highlighted
Type "xyz" → No results found
Clear input → All 12 items shown