Search box + filter chips + a grid โ the pattern behind dashboards, product lists and this very blog's index page.
The single-function architecture
One state object, one applyFilters() that re-evaluates everything. Never write separate "search logic" and "filter logic" that fight:
const state = { query: "", category: "all" };
function applyFilters() {
let visible = 0;
cards.forEach((card) => {
const matchesQuery = !state.query ||
card.dataset.search.includes(state.query); // pre-lowercased text
const matchesCat = state.category === "all" ||
card.dataset.cat === state.category;
const show = matchesQuery && matchesCat;
card.hidden = !show;
if (show) visible++;
});
emptyMsg.hidden = visible > 0;
countEl.textContent = `${visible} results`;
}
searchInput.addEventListener("input", debounce(() => {
state.query = searchInput.value.trim().toLowerCase();
applyFilters();
}, 150));
chipBar.addEventListener("click", (e) => {
const chip = e.target.closest("[data-cat]");
if (!chip) return;
state.category = chip.dataset.cat;
chips.forEach(c => c.classList.toggle("active", c === chip));
applyFilters();
});Polish that separates portfolios
- Highlight matches: wrap them in
<mark>(careful โ replace text nodes, not innerHTML with user input) - Sync state to the URL for shareable filtered views
- Past ~2000 items, move filtering server-side
See it live on our blog index โ 100+ posts, instant filtering, exactly this code.
โ All Articles