Product Filter App with HTML, CSS & JavaScript

20 DAYS 20 PROJECT CHALLENGE

Day #19

Project Overview

The Product Filter App is a beginner-friendly project built using HTML, CSS, and JavaScript. It allows users to search, filter, and sort products in real time without reloading the page. Users can search for products using keywords, select a specific category, set a minimum or maximum price, and display only products that are currently in stock. This project is best for beginners to understand the fundamentals of product listing pages and how they can manipulate and sort the data with JavaScript. It also includes sorting options that allow users to arrange products by price or name. It also provides practical experience with JavaScript arrays, Array.filter(), Array.sort(), event listeners, DOM manipulation, and dynamic content rendering.

Key Features

  • Live product search: This project provides a search option where the users type their desired keyword, and the items matching that keyword appear. The search option scans through the product name and description to retrieve similar items and updates the search results in real-time.
  • Category filter: The application allows searching products based on their category. The categories are generated based on the available items in the database. This approach is efficient because it is dynamic; it automatically updates when new products are added or existing ones removed.
  • In stock filter: The application provides an option for users to choose whether they want products that are in stock only.
  • Price sorting: The application has an option of sorting items by price from low to high and high to low.
  • Name sorting: This feature allows the users to sort the items according to their name in alphabetical order.
  • Clear filters option: This option allows the users to clear all the filters and sort options to restart their search.
  • Dynamic search: All the above-mentioned features are dynamic, meaning, whenever a change is detected, the JavaScript code updates the product cards on the page.
  • Product counter: It displays the number of products currently matching the selected filters. This gives users clear feedback about the search results.
  • No Results Message: When no products match the selected filters, the application displays a message informing the user that no products were found.
  • Dynamic Product cards: Each product card displays information such as the product image, name, description, price, and stock status.
  • Responsive Grid Layout: CSS Grid organizes the products into a responsive layout that adjusts according to the available screen size
  • JavaScript-Based Filtering: The project demonstrates how Array.filter() and Array.sort() can process product data and create an interactive filtering experience.

What You'll Learn

  • Understand how to store and manage product information using JavaScript arrays and objects.
  • Learn how Array.filter() can filter data according to multiple conditions.
  • Practice using Array.sort() to arrange products by price or name.
  • Understand how to read values from search fields, number inputs, checkboxes, and select menus.
  • Learn how to dynamically create and display product cards using JavaScript.
  • Practice selecting and manipulating HTML elements using the DOM.
  • Understand how event listeners respond to user input and filter changes.
  • Learn how to implement a live search feature.
  • Practice working with minimum and maximum price values.
  • Understand how conditional statements can control which products appear.
  • Learn how JavaScript templates can help create repeated product elements.
  • Practice updating the user interface without reloading the webpage.

HTML Code

HTML creates the complete structure of the Product Filter App. First, the main .card container holds the application heading, filtering controls, product results, and other interface elements, #search input allows users to search for products using keywords. Next, the #category select element provides category filtering options an the #minPrice and #maxPrice inputs allow users to enter a price range, while the #inStock checkbox lets users display only products that are currently available.

This #sort select menu provides options for arranging products by price or name. In addition, the #clearFilters button resets all selected filters. The #resultCount element displays the number of matching products, while the #products section acts as the main area where JavaScript displays the product cards. Finally, the <template> element stores the structure of a product card. JavaScript can clone this template and insert product information dynamically. The script.js file is loaded at the bottom of the <body> so JavaScript can access the page elements and add the required functionality.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>Day 19 — Product Filter List</title>
  <link rel="stylesheet" href="styles.css" />
</head>
<body>
  <main class="card" role="main" aria-labelledby="title">
    <header>
      <div class="logo">PF</div>
      <div>
        <h1 id="title">Day 19: Product Filter List</h1>
        <p class="lead">Filter and search products in real-time using <code>Array.filter()</code> and DOM rendering.</p>
      </div>
    </header>

    <section class="controls-grid">
      <div class="control">
        <label for="search" class="small">Search</label>
        <input id="search" type="search" placeholder="Search by name or description" />
      </div>

      <div class="control">
        <label for="category" class="small">Category</label>
        <select id="category">
          <option value="">All categories</option>
        </select>
      </div>

      <div class="control">
        <label class="small">Price range</label>
        <div class="range-row">
          <input id="minPrice" type="number" min="0" step="1" placeholder="Min" />
          <span class="muted">—</span>
          <input id="maxPrice" type="number" min="0" step="1" placeholder="Max" />
        </div>
      </div>

      <div class="control">
        <label class="small" style="display:flex;align-items:center;gap:8px;">
          <input id="inStock" type="checkbox" /> <span>In stock only</span>
        </label>

        <label class="small" style="margin-top:8px;display:block;">
          Sort
          <select id="sort">
            <option value="default">Default</option>
            <option value="price-asc">Price: Low → High</option>
            <option value="price-desc">Price: High → Low</option>
            <option value="name-asc">Name: A → Z</option>
            <option value="name-desc">Name: Z → A</option>
          </select>
        </label>
      </div>
    </section>

    <section class="meta-row">
      <div id="resultCount" class="muted">Showing 0 products</div>
      <div id="clearBtnWrap"><button id="clearFilters" class="btn secondary">Clear filters</button></div>
    </section>

    <section id="products" class="products-grid" aria-live="polite">
      <!-- product cards rendered here -->
    </section>

    <template id="productTpl">
      <article class="product-card">
        <img class="p-img" src="" alt="" />
        <div class="p-body">
          <h3 class="p-title"></h3>
          <p class="p-desc"></p>
          <div class="p-meta">
            <div class="p-price"></div>
            <div class="p-stock"></div>
          </div>
        </div>
      </article>
    </template>

    <details style="margin-top:12px">
      <summary>How it works (short)</summary>
      <p class="small">The JS keeps a products array. On any control change, it builds a filtered array using `filter()` chained with conditions from search text, category, price, and stock flags. Then it sorts (if requested) and re-renders the product cards.</p>
    </details>
  </main>

  <script src="script.js"></script>
</body>
</html>

CSS Code

CSS controls the appearance and layout of the Product Filter App. First the.card class styles the main application container by adding width, padding, rounded corners, borders, and shadows. Next, the .controls-grid class uses CSS Grid to arrange the search field and filtering controls in a flexible layout, input fields and select menus receive consistent padding, borders, and spacing so users can interact with the filtering options comfortably. The .range-row class keeps the minimum and maximum price inputs organized in the same row.

The .products-grid class uses CSS Grid to display product cards in multiple columns. Each .product-card contains an image and product details, while classes such as .p-title, .p-desc, .p-price, and .p-stock style the individual pieces of product information. Finally, the media query changes the layout on smaller screens. This allows the Product Filter App to remain easy to use on desktops, tablets, and mobile devices.

:root {
  --bg: #071026;
  --card: #0b1220;
  --accent: #7c3aed;
  --muted: #9aa4b2;
  --white: #e6eef6;
  --card-2: rgba(255, 255, 255, 0.02);
  font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, Arial;
}

html,
body {
  height: 100%;
}

body {
  margin: 0;
  background:#002252;
  color: var(--white);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 28px;
}

.card {
  width: min(1100px, 96%);
  background: linear-gradient(180deg, var(--card-2), rgba(255, 255, 255, 0.01));
  border-radius: 12px;
  padding: 18px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
  border: 1px solid rgba(255, 255, 255, 0.03);
}

header {
  display: flex;
  gap: 12px;
  align-items: center;
}

.logo {
  width: 44px;
  height: 44px;
  border-radius: 10px;
  background: linear-gradient(135deg, var(--accent), #22c1c3);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
}

h1 {
  margin: 0;
  font-size: 18px;
}

.lead {
  margin: 4px 0 12px;
  color: var(--muted);
  font-size: 14px
}

.controls-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 12px;
  margin-top: 12px;
}

.control label,
.control .small {
  display: block;
  color: var(--muted);
  font-size: 13px;
  margin-bottom: 6px;
}

input[type="search"],
input[type="number"],
select {
  padding: 10px 12px;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.04);
  background: transparent;
  color: inherit;
  outline: none;
  width: 100%;
}

.range-row {
  display: flex;
  gap: 8px;
  align-items: center
}

.range-row input {
  width: 100%
}

.meta-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 12px
}

.muted {
  color: var(--muted);
  font-size: 13px
}

.btn {
  padding: 8px 12px;
  border-radius: 8px;
  border: 0;
  background: linear-gradient(90deg, var(--accent), #22c1c3);
  color: white;
  font-weight: 600;
  cursor: pointer
}

.btn.secondary {
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.06);
  color: var(--muted)
}

.products-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 14px;
  margin-top: 14px
}

.product-card {
  background: rgba(255, 255, 255, 0.02);
  border-radius: 10px;
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.03);
  display: flex;
  flex-direction: column;
}

.p-img {
  width: 100%;
  height: 140px;
  object-fit: cover;
  background: #0b1220
}

.p-body {
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  flex: 1
}

.p-title {
  margin: 0;
  font-size: 16px
}

.p-desc {
  margin: 0;
  color: var(--muted);
  font-size: 13px;
  flex: 1
}

.p-meta {
  display: flex;
  justify-content: space-between;
  align-items: center
}

.p-price {
  font-weight: 700;
  font-family: ui-monospace, monospace
}

.p-stock {
  font-size: 13px;
  color: var(--muted)
}

/* empty state */
.empty {
  padding: 36px;
  text-align: center;
  color: var(--muted);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.01);
}

/* small screens */
@media (max-width:540px) {
  .products-grid {
    grid-template-columns: repeat(2, 1fr)
  }

  .controls-grid {
    grid-template-columns: 1fr
  }
}

Javascript Code

JavaScript provides the main functionality of the Product Filter App. First, the PRODUCTS array stores information about the available products, including their names, descriptions, categories, prices, images, and stock status. Next, JavaScript selects the search field, category selector, price inputs, stock checkbox, sorting menu, product container, and other required HTML elements. The filters object stores the current filtering preferences selected by the user, populateCategories() function collects the available categories from the product data and adds them dynamically to the category select menu, applyFilters() function performs the main filtering process. It uses Array.filter() to check whether each product matches the search keyword, selected category, minimum price, maximum price, and stock availability. Products that do not meet the selected conditions are removed from the results.

After filtering the products, the application uses Array.sort() when the user selects a sorting option. Products can then be arranged by price or alphabetically by name, renderProducts() function creates the visible product cards. It clones the HTML template, adds the product image and information, and inserts the completed cards into the product container. If no products match the selected filters, the application displays a message instead. Finally, event listeners detect changes to the search field, filters, checkbox, sorting menu, and Clear Filters button. Each interaction updates the filters object and runs the filtering process again, allowing the results to change dynamically without refreshing the page.

// Day 19 — Product Filter List
// Demonstrates Array.filter(), DOM rendering, and simple UI wiring.

// --- Sample product data (replace with your own or API fetch) ---
const PRODUCTS = [
  { id: 1, name: 'Classic Leather Wallet', description: 'Handmade full-grain leather wallet.', category: 'Accessories', price: 1299, image: 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?q=80&w=800&auto=format&fit=crop', inStock: true },
  { id: 2, name: 'Running Sneakers', description: 'Lightweight trainers for daily runs.', category: 'Footwear', price: 3499, image: 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?q=80&w=800&auto=format&fit=crop', inStock: true },
  { id: 3, name: 'Vintage Watch', description: 'Classic mechanical watch with leather strap.', category: 'Accessories', price: 8999, image: 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?q=80&w=800&auto=format&fit=crop', inStock: false },
  { id: 4, name: 'Denim Jacket', description: 'Comfortable, durable denim jacket.', category: 'Clothing', price: 2999, image: 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?q=80&w=800&auto=format&fit=crop', inStock: true },
  { id: 5, name: 'Smartphone XS', description: 'High performance smartphone with great camera.', category: 'Electronics', price: 24999, image: 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?q=80&w=800&auto=format&fit=crop', inStock: true },
  { id: 6, name: 'Bluetooth Headphones', description: 'Noise-cancelling wireless headphones.', category: 'Electronics', price: 4999, image: 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?q=80&w=800&auto=format&fit=crop', inStock: false },
  { id: 7, name: 'Casual T-Shirt', description: 'Soft cotton t-shirt.', category: 'Clothing', price: 799, image: 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?q=80&w=800&auto=format&fit=crop', inStock: true },
  { id: 8, name: 'Trail Backpack', description: 'Rugged backpack for hiking and travel.', category: 'Accessories', price: 4199, image: 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?q=80&w=800&auto=format&fit=crop', inStock: true }
];

// --- DOM refs ---
const searchEl = document.getElementById('search');
const categoryEl = document.getElementById('category');
const minPriceEl = document.getElementById('minPrice');
const maxPriceEl = document.getElementById('maxPrice');
const inStockEl = document.getElementById('inStock');
const sortEl = document.getElementById('sort');
const productsEl = document.getElementById('products');
const resultCountEl = document.getElementById('resultCount');
const clearBtn = document.getElementById('clearFilters');

const tpl = document.getElementById('productTpl');

// --- State ---
let products = PRODUCTS.slice(); // working copy (could be from API)
let filters = {
  q: '',
  category: '',
  minPrice: null,
  maxPrice: null,
  inStockOnly: false,
  sort: 'default'
};

// --- Helpers ---
function formatCurrency(n) {
  return '₹' + Number(n).toLocaleString(undefined, { minimumFractionDigits: 0 });
}

// Debounce helper
function debounce(fn, wait = 300) {
  let t;
  return (...args) => {
    clearTimeout(t);
    t = setTimeout(() => fn(...args), wait);
  };
}

// Populate category options from products
function populateCategories() {
  const cats = Array.from(new Set(products.map(p => p.category))).sort();
  categoryEl.innerHTML = '<option value="">All categories</option>';
  cats.forEach(c => {
    const opt = document.createElement('option');
    opt.value = c;
    opt.textContent = c;
    categoryEl.appendChild(opt);
  });
}

// Render products array (cards)
function renderProducts(list) {
  productsEl.innerHTML = '';
  if (!list.length) {
    const empty = document.createElement('div');
    empty.className = 'empty';
    empty.textContent = 'No products found for these filters.';
    productsEl.appendChild(empty);
    resultCountEl.textContent = 'Showing 0 products';
    return;
  }

  const frag = document.createDocumentFragment();
  list.forEach(p => {
    const node = tpl.content.cloneNode(true);
    const article = node.querySelector('.product-card');
    article.querySelector('.p-img').src = p.image;
    article.querySelector('.p-img').alt = p.name;
    article.querySelector('.p-title').textContent = p.name;
    article.querySelector('.p-desc').textContent = p.description;
    article.querySelector('.p-price').textContent = formatCurrency(p.price);
    article.querySelector('.p-stock').textContent = p.inStock ? 'In stock' : 'Out of stock';
    frag.appendChild(node);
  });
  productsEl.appendChild(frag);
  resultCountEl.textContent = `Showing ${list.length} product${list.length>1?'s':''}`;
}

// Main filter function using Array.filter()
function applyFilters() {
  const q = (filters.q || '').trim().toLowerCase();
  const cat = filters.category;
  const minP = filters.minPrice != null ? Number(filters.minPrice) : null;
  const maxP = filters.maxPrice != null ? Number(filters.maxPrice) : null;
  const inStockOnly = !!filters.inStockOnly;

  let res = products.filter(p => {
    // text search (name + description)
    if (q) {
      const hay = (p.name + ' ' + (p.description || '')).toLowerCase();
      if (!hay.includes(q)) return false;
    }
    // category
    if (cat && p.category !== cat) return false;
    // min price
    if (minP != null && !Number.isNaN(minP) && p.price < minP) return false;
    // max price
    if (maxP != null && !Number.isNaN(maxP) && p.price > maxP) return false;
    // stock
    if (inStockOnly && !p.inStock) return false;

    return true;
  });

  // sorting
  switch (filters.sort) {
    case 'price-asc':
      res = res.sort((a,b) => a.price - b.price);
      break;
    case 'price-desc':
      res = res.sort((a,b) => b.price - a.price);
      break;
    case 'name-asc':
      res = res.sort((a,b) => a.name.localeCompare(b.name));
      break;
    case 'name-desc':
      res = res.sort((a,b) => b.name.localeCompare(a.name));
      break;
    default:
      // keep original order
      break;
  }

  renderProducts(res);
}

// --- Event wiring ---
const onSearchInput = debounce((e) => {
  filters.q = e.target.value;
  applyFilters();
}, 300);

searchEl.addEventListener('input', onSearchInput);

categoryEl.addEventListener('change', (e) => {
  filters.category = e.target.value;
  applyFilters();
});

minPriceEl.addEventListener('input', (e) => {
  const v = e.target.value;
  filters.minPrice = v === '' ? null : Number(v);
  applyFilters();
});
maxPriceEl.addEventListener('input', (e) => {
  const v = e.target.value;
  filters.maxPrice = v === '' ? null : Number(v);
  applyFilters();
});

inStockEl.addEventListener('change', (e) => {
  filters.inStockOnly = e.target.checked;
  applyFilters();
});

sortEl.addEventListener('change', (e) => {
  filters.sort = e.target.value;
  applyFilters();
});

clearBtn.addEventListener('click', (e) => {
  // reset filters & UI controls
  filters = { q: '', category: '', minPrice: null, maxPrice: null, inStockOnly: false, sort: 'default' };
  searchEl.value = '';
  categoryEl.value = '';
  minPriceEl.value = '';
  maxPriceEl.value = '';
  inStockEl.checked = false;
  sortEl.value = 'default';
  applyFilters();
});

// --- Initialize ---
(function init(){
  // In real app you might fetch products via API; here we use sample PRODUCTS
  // Populate categories, render initial product list
  populateCategories();
  applyFilters();
})();
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments

Related Projects

Day 11 : Drum Kit

Play drum sounds when clicking buttons or pressing keys.

Concepts: Keyboard events, Audio API.

Day 12 : QR Code Generator

Generates a QR code from text input.

Concepts: Third-party API (QRServer API), fetch().

Day 13 : Currency Converter

Converts one currency to another using real-time exchange rates.

Concepts: API fetch, DOM updates.