Build a Responsive Product Filter with JavaScript
Challenge overview
Build a responsive product filter that lets users search products by name and filter them by category. This practical challenge will help you practice JavaScript arrays, DOM manipulation, event handling, responsive layouts, and empty-state handling.
What you will build
You will create a small product catalog with:
- A text input for searching products by name
- A category dropdown for narrowing the results
- A responsive product card grid
- A message when no products match the selected filters
- A visible product count that updates automatically
Challenge goal
Create a reusable JavaScript product filter that reads product data from an array, applies the active search and category filters, and renders only the matching products.
Requirements
- Display all products when the page first loads.
- Filter products while the user types in the search field.
- Make the search case-insensitive.
- Allow users to filter products by category.
- Apply the search and category filters at the same time.
- Display a message when no matching products are found.
- Update the displayed product count after every filter change.
- Use a responsive grid that works on mobile, tablet, and desktop screens.
- Keep the filtering and rendering logic in separate functions.
Product data
Use the following array as the input for the challenge:
const products = [
{
id: 1,
name: "Wireless Mouse",
category: "Electronics",
price: 24.99
},
{
id: 2,
name: "Mechanical Keyboard",
category: "Electronics",
price: 79.99
},
{
id: 3,
name: "Office Chair",
category: "Furniture",
price: 149.99
},
{
id: 4,
name: "Standing Desk",
category: "Furniture",
price: 299.99
},
{
id: 5,
name: "JavaScript Beginner Guide",
category: "Books",
price: 19.99
},
{
id: 6,
name: "Clean Code Handbook",
category: "Books",
price: 29.99
}
];
Input and output
Input
- A product array
- A search query entered by the user
- A selected category
Output
A filtered array containing only the products whose names match the search query and whose categories match the selected category.
Example
// Search query
"desk"
// Selected category
"Furniture"
// Expected matching product
[
{
id: 4,
name: "Standing Desk",
category: "Furniture",
price: 299.99
}
]
Constraints and edge cases
- The product array may be empty.
- The search query may contain uppercase letters or extra spaces.
- The selected category may be set to "all".
- No products may match the active filters.
- Products may have similar names but different categories.
- The original product array should not be modified.
Suggested HTML structure
<main class="container">
<h1>Product Catalog</h1>
<section class="filters" aria-label="Product filters">
<label for="searchInput">Search products</label>
<input
id="searchInput"
type="search"
placeholder="Search by product name"
>
<label for="categoryFilter">Category</label>
<select id="categoryFilter">
<option value="all">All categories</option>
<option value="Electronics">Electronics</option>
<option value="Furniture">Furniture</option>
<option value="Books">Books</option>
</select>
</section>
<p id="resultCount" aria-live="polite"></p>
<section id="productGrid" class="product-grid"></section>
<p id="emptyState" hidden>No products found.</p>
</main>
Suggested responsive CSS
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: Arial, sans-serif;
background: #f5f7fa;
color: #1f2937;
}
.container {
width: min(1100px, 92%);
margin: 0 auto;
padding: 2rem 0;
}
.filters {
display: grid;
grid-template-columns: 1fr;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.filters input,
.filters select {
width: 100%;
padding: 0.75rem;
font: inherit;
}
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1rem;
}
.product-card {
padding: 1rem;
background: #ffffff;
border: 1px solid #dbe2ea;
border-radius: 0.75rem;
}
.product-card h2 {
margin-top: 0;
font-size: 1.1rem;
}
@media (min-width: 700px) {
.filters {
grid-template-columns: 1fr 220px;
align-items: end;
}
}
Starter code
const products = [
{ id: 1, name: "Wireless Mouse", category: "Electronics", price: 24.99 },
{ id: 2, name: "Mechanical Keyboard", category: "Electronics", price: 79.99 },
{ id: 3, name: "Office Chair", category: "Furniture", price: 149.99 },
{ id: 4, name: "Standing Desk", category: "Furniture", price: 299.99 },
{ id: 5, name: "JavaScript Beginner Guide", category: "Books", price: 19.99 },
{ id: 6, name: "Clean Code Handbook", category: "Books", price: 29.99 }
];
const searchInput = document.querySelector("#searchInput");
const categoryFilter = document.querySelector("#categoryFilter");
const productGrid = document.querySelector("#productGrid");
const resultCount = document.querySelector("#resultCount");
const emptyState = document.querySelector("#emptyState");
function filterProducts(productList, searchQuery, selectedCategory) {
// Return the matching products.
}
function renderProducts(productList) {
// Render product cards and update the interface.
}
function updateProducts() {
const searchQuery = searchInput.value;
const selectedCategory = categoryFilter.value;
const filteredProducts = filterProducts(
products,
searchQuery,
selectedCategory
);
renderProducts(filteredProducts);
}
searchInput.addEventListener("input", updateProducts);
categoryFilter.addEventListener("change", updateProducts);
updateProducts();
Hints
- Normalize the search query with
trim()andtoLowerCase(). - Use
Array.prototype.filter()to create a new array of matching products. - Check the search condition and category condition separately.
- Treat the category value
"all"as a match for every product. - Use
map()andjoin("")to generate the product card markup. - Use the
hiddenproperty to show or hide the empty-state message.
Solution walkthrough
- Read the current search text and selected category.
- Remove extra spaces from the search query and convert it to lowercase.
- Loop through the products with
filter(). - Check whether each product name contains the normalized search text.
- Check whether the selected category is
"all"or matches the product category. - Keep products that pass both checks.
- Convert the filtered products into product card HTML.
- Update the product count and empty-state message.
- Run the filtering function whenever the user types or changes the category.
Final solution
const products = [
{ id: 1, name: "Wireless Mouse", category: "Electronics", price: 24.99 },
{ id: 2, name: "Mechanical Keyboard", category: "Electronics", price: 79.99 },
{ id: 3, name: "Office Chair", category: "Furniture", price: 149.99 },
{ id: 4, name: "Standing Desk", category: "Furniture", price: 299.99 },
{ id: 5, name: "JavaScript Beginner Guide", category: "Books", price: 19.99 },
{ id: 6, name: "Clean Code Handbook", category: "Books", price: 29.99 }
];
const searchInput = document.querySelector("#searchInput");
const categoryFilter = document.querySelector("#categoryFilter");
const productGrid = document.querySelector("#productGrid");
const resultCount = document.querySelector("#resultCount");
const emptyState = document.querySelector("#emptyState");
function filterProducts(productList, searchQuery, selectedCategory) {
const normalizedQuery = searchQuery.trim().toLowerCase();
return productList.filter((product) => {
const matchesSearch = product.name
.toLowerCase()
.includes(normalizedQuery);
const matchesCategory =
selectedCategory === "all" ||
product.category === selectedCategory;
return matchesSearch && matchesCategory;
});
}
function createProductCard(product) {
return `
<article class="product-card">
<h2>${product.name}</h2>
<p>Category: ${product.category}</p>
<p>Price: $${product.price.toFixed(2)}</p>
</article>
`;
}
function renderProducts(productList) {
productGrid.innerHTML = productList
.map(createProductCard)
.join("");
const productLabel = productList.length === 1
? "product"
: "products";
resultCount.textContent = `${productList.length} ${productLabel} found`;
emptyState.hidden = productList.length !== 0;
}
function updateProducts() {
const filteredProducts = filterProducts(
products,
searchInput.value,
categoryFilter.value
);
renderProducts(filteredProducts);
}
searchInput.addEventListener("input", updateProducts);
categoryFilter.addEventListener("change", updateProducts);
updateProducts();
Test cases
- Leave the search field empty and select all categories. All products should appear.
- Search for
mouse. Only Wireless Mouse should appear. - Search for
DESK. The search should still match Standing Desk. - Select Furniture without entering a search query. Both furniture products should appear.
- Search for
codeand select Books. Only Clean Code Handbook should appear. - Search for a product that does not exist. The empty-state message should appear.
Complexity
Filtering requires checking each product once, so the time complexity is O(n), where n is the number of products. The filtered array may contain up to n products, so the space complexity is O(n).
Stretch goals
- Add minimum and maximum price filters.
- Add sorting by name or price.
- Generate category options automatically from the product data.
- Add a reset filters button.
- Highlight the matching search text inside product names.
- Save the selected filters in
localStorage. - Add pagination for larger product collections.
- Load products from a JSON file or API.
- Add unit tests for the
filterProducts()function.