Programmer's Picnic • JavaScript DOM Lesson

Build a Search and Sort Table with JavaScript

In this lesson, you will learn how to search through all table cells, hide rows that do not match, highlight matching text, count visible results, clear the search, and sort a table by clicking the headers.

This lesson is designed for beginners. We will start from the idea, move through the HTML structure, then the JavaScript logic, and finally see a live working demo.

Start lesson Jump to live demo

What are we building?

We are building an HTML table with three important interactive features. First, when the user types into a search box, the page checks every td in each row. Second, if a row has zero matches, that row's tr becomes hidden. Third, when the user clicks a header, the table sorts itself by that column.

querySelector
querySelectorAll
tr and td
DOM manipulation
table sorting
By the end of this lesson, you will understand how to use row.querySelectorAll("td"), row.style.display = "none", and header click events for sorting.

Step 1: Create the table structure

Every table begins with a <table>. Inside it, we usually keep headings in <thead> and data rows in <tbody>. Each row is a <tr>, and each data cell is a <td>.

Step 1 code
<table id="myTable">
  <thead>
    <tr>
      <th>Name</th>
      <th>City</th>
      <th>Skill</th>
    </tr>
  </thead>
  <tbody>
    <tr><td>Champak Roy</td><td>Varanasi</td><td>Java</td></tr>
    <tr><td>Riya Sharma</td><td>Delhi</td><td>Python</td></tr>
  </tbody>
</table>
A table is easier to control in JavaScript when the tbody exists, because we can directly collect and reorder its rows.

Step 2: Search all td cells using querySelectorAll

The search logic works row by row. First we collect all rows from the table body. Then, for each row, we collect all of its cells with row.querySelectorAll("td"). This gives us a list of the data cells inside that row.

Step 2 code
const tbody = document.querySelector("#myTable tbody");

function getRows() {
  return Array.from(tbody.querySelectorAll("tr"));
}

const rows = getRows();

rows.forEach(row => {
  const tds = row.querySelectorAll("td");
  console.log(tds);
});
Here, document.querySelector("#myTable tbody") gets the body of the table, and row.querySelectorAll("td") gets all cells inside one row.

Step 3: Hide a row when there are zero matches

This is the main requirement. We take the search text from the input, convert it to lowercase, and compare it with each cell's text. If at least one cell matches, we show the row. If no cell matches, we hide the row.

Core search logic
function applySearch() {
  const keyword = searchInput.value.trim().toLowerCase();
  const rows = getRows();

  rows.forEach(row => {
    const tds = row.querySelectorAll("td");
    let match = false;

    tds.forEach(td => {
      const text = td.textContent.toLowerCase();

      if (keyword && text.includes(keyword)) {
        match = true;
      }
    });

    if (!keyword || match) {
      row.style.display = "";
    } else {
      row.style.display = "none";
    }
  });
}
Notice the condition: if (!keyword || match). This means: if the box is empty, show everything. Otherwise, only show matching rows.
If you forget to reset hidden rows when the search box becomes empty, some rows may stay hidden. That is why the empty-search condition matters.

Step 4: Sort the table when a header is clicked

To sort the table, we add click listeners to the th elements. Each header knows which column it controls through a data-col attribute. When a header is clicked, we collect all rows, compare the values in that column, sort them, and then put them back into the table body in the new order.

Header click and sorting
headers.forEach(th => {
  th.addEventListener("click", function () {
    const colIndex = Number(th.dataset.col);
    sortTableByColumn(colIndex);
  });
});

function sortTableByColumn(columnIndex) {
  const rows = getRows();
  const sameColumn = sortState.column === columnIndex;

  if (sameColumn) {
    sortState.direction = sortState.direction === "asc" ? "desc" : "asc";
  } else {
    sortState.column = columnIndex;
    sortState.direction = "asc";
  }

  rows.sort((rowA, rowB) => {
    const textA = rowA.children[columnIndex].textContent.trim().toLowerCase();
    const textB = rowB.children[columnIndex].textContent.trim().toLowerCase();
    return sortState.direction === "asc"
      ? textA.localeCompare(textB)
      : textB.localeCompare(textA);
  });

  rows.forEach(row => tbody.appendChild(row));
}
Appending sorted rows back into the same tbody changes the visible order on the page.

Step 5: Highlight matches and count visible rows

A good lesson project should feel alive. So we also highlight matching text with the <mark> tag and show a line telling the learner how many rows are currently visible.

Highlight function
function highlightCell(td, keyword) {
  if (!keyword) return;

  const safeKeyword = escapeRegExp(keyword);
  const regex = new RegExp("(" + safeKeyword + ")", "gi");
  td.innerHTML = td.textContent.replace(regex, "<mark>$1</mark>");
}
Count function
function updateCount() {
  const visibleRows = getRows().filter(
    row => row.style.display !== "none"
  ).length;

  if (!searchInput.value.trim()) {
    countDiv.textContent = "Showing all rows";
  } else {
    countDiv.textContent = "Showing " + visibleRows + " matching row(s)";
  }
}
The highlight part is optional, but it improves clarity for learners. The count line gives quick feedback after every search.

Live demo

Try the demo below. Search for a city like Varanasi, a skill like React, or a person name. Then click the column headers to sort the rows.

Showing all rows
Name City Skill
Champak RoyVaranasiJava
Riya SharmaDelhiPython
Aman VermaLucknowJavaScript
Neha SinghKanpurAI/ML
Rahul GuptaNoidaSpring Boot
Anjali MishraPrayagrajHTML/CSS
Vivek PatelGorakhpurReact
This demo uses the same logic explained in the lesson. You can reuse the full code in your own projects and then modify the data rows.

Complete project code

This section gives a compact version of the working demo logic. Use the copy button to grab it quickly. The full lesson page source can be copied from the top button.

Working JavaScript
(function () {
  "use strict";

  const table = document.getElementById("myTable");
  const tbody = table.querySelector("tbody");
  const searchInput = document.getElementById("searchInput");
  const clearBtn = document.getElementById("clearBtn");
  const countDiv = document.getElementById("count");
  const headers = table.querySelectorAll("th.sortable");

  let sortState = {
    column: -1,
    direction: "asc"
  };

  function escapeRegExp(text) {
    return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  }

  function removeHighlights(td) {
    td.innerHTML = td.textContent;
  }

  function highlightCell(td, keyword) {
    if (!keyword) return;
    const safeKeyword = escapeRegExp(keyword);
    const regex = new RegExp("(" + safeKeyword + ")", "gi");
    td.innerHTML = td.textContent.replace(regex, "<mark>$1</mark>");
  }

  function getRows() {
    return Array.from(tbody.querySelectorAll("tr"));
  }

  function updateCount() {
    const visibleRows = getRows().filter(row => row.style.display !== "none").length;
    const keyword = searchInput.value.trim();

    if (!keyword) {
      countDiv.textContent = "Showing all rows";
    } else {
      countDiv.textContent = "Showing " + visibleRows + " matching row(s)";
    }
  }

  function applySearch() {
    const keyword = searchInput.value.trim().toLowerCase();
    const rows = getRows();

    rows.forEach(row => {
      const tds = row.querySelectorAll("td");
      let match = false;

      tds.forEach(td => {
        removeHighlights(td);

        const text = td.textContent.toLowerCase();
        if (keyword && text.includes(keyword)) {
          match = true;
          highlightCell(td, keyword);
        }
      });

      if (!keyword || match) {
        row.style.display = "";
      } else {
        row.style.display = "none";
      }
    });

    updateCount();
  }

  function clearSortClasses() {
    headers.forEach(th => {
      th.classList.remove("sorted-asc", "sorted-desc");
    });
  }

  function sortTableByColumn(columnIndex) {
    const rows = getRows();
    const sameColumn = sortState.column === columnIndex;

    if (sameColumn) {
      sortState.direction = sortState.direction === "asc" ? "desc" : "asc";
    } else {
      sortState.column = columnIndex;
      sortState.direction = "asc";
    }

    rows.sort((rowA, rowB) => {
      const textA = rowA.children[columnIndex].textContent.trim().toLowerCase();
      const textB = rowB.children[columnIndex].textContent.trim().toLowerCase();

      const numA = parseFloat(textA);
      const numB = parseFloat(textB);

      let comparison = 0;

      if (!Number.isNaN(numA) && !Number.isNaN(numB)) {
        comparison = numA - numB;
      } else {
        comparison = textA.localeCompare(textB);
      }

      return sortState.direction === "asc" ? comparison : -comparison;
    });

    rows.forEach(row => tbody.appendChild(row));

    clearSortClasses();

    const activeHeader = table.querySelector('th[data-col="' + columnIndex + '"]');
    if (activeHeader) {
      activeHeader.classList.add(sortState.direction === "asc" ? "sorted-asc" : "sorted-desc");
    }

    applySearch();
  }

  searchInput.addEventListener("input", applySearch);

  clearBtn.addEventListener("click", function () {
    searchInput.value = "";
    applySearch();
    searchInput.focus();
  });

  headers.forEach(th => {
    th.addEventListener("click", function () {
      const colIndex = Number(th.dataset.col);
      sortTableByColumn(colIndex);
    });
  });

  applySearch();
})();

Practice tasks

  • Add one more column called Experience and make sorting work for it.
  • Add more rows and test whether the search still checks every td.
  • Change the count line so it also shows the total number of rows.
  • Add a No results row or message when zero rows are visible.
  • Create a second table on the same page and reuse the same idea there.
Why do we use textContent instead of innerHTML for searching?
Because textContent gives plain text. That makes search cleaner and safer.
Why does row.style.display become none?
Because setting display: none hides that row from the page when it has zero matches.
Why do we call applySearch again after sorting?
So the current search filter and highlights stay correct even after rows are reordered.