DocsHub
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 items

Logic

  1. Store all items in an array
  2. Render all items on page load
  3. Listen for input event on the search box
  4. On every keystroke — filter the array by checking if each item includes the query
  5. If no items match — show a no results message
  6. Highlight the matching part of each item by wrapping it in a <mark> tag
  7. Show a count of how many results are showing

Flow

yes no no yes User types in search box input event fires Read query and trim Query empty? Show all items Filter items that include query Any matches? Show no results message Highlight matching text in each item Render filtered list with count

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":

StepCodeResult
QuerysearchInput.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":

StepCodeResult
Escaped query"sc""sc"
Regexnew RegExp("sc", "gi")matches sc anywhere
Replace"JavaScript".replace(regex, "<mark>$&</mark>")"Java<mark>Sc</mark>ript"
RenderedinnerHTMLJavaScript (highlighted)

Trace through typing "xyz":

StepCodeResult
Filternone of the 12 items include "xyz"[]
filtered.length === 0trueshow 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

On this page