Product Filter App with HTML, CSS & JavaScript

20 DAYS 20 PROJECT CHALLENGE

Day #19

Project Overview

A dynamic Product Filter App that lets users search, filter, and sort products in real time.
It demonstrates practical use of Array.filter(), Array.sort(), event handling, and DOM rendering.
Users can search by keyword, filter by category or price range, show only in-stock items, and sort results instantly.
The UI updates smoothly without page reloads, making this a great beginner-friendly project to understand interactive lists.

Key Features

  • Live search (name + description)
  • Category filter generated from product data
  • Price range filter (min–max)
  • In-stock only toggle
  • Sorting by price or name
  • Clear filters with one click
  • Responsive grid layout
  • Clean use of Array.filter(), Array.sort(), and template cloning

HTML Code

<!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

: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

// 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.