Contact Us NowENDE
Product Filters with JavaScript and PHP for Your E-Commerce Shop

Product Filters with JavaScript and PHP for Your E-Commerce Shop

Product filters are crucial for the user experience in e-commerce. In this article, we show you how to implement a fast, dynamic filtering system with JavaScript and PHP—including AJAX for smooth updates and advanced features for better performance.

Kontakt-Button
Get in Touch

We are available for you.

Request a Consultation Now
By
  • Web Development
  • E-Commerce
Basic HTML Structure for Filters and Product List
<form id="filter-form">
<fieldset>
	<legend>Product Filters</legend>
	<label>
		Category:
		<select name="category">
			<option value="">All Categories</option>
			<option value="shirts">Shirts</option>
			<option value="hosen">Pants</option>
			<option value="schuhe">Shoes</option>
			<option value="accessoires">Accessories</option>
		</select>
	</label>
	<label>
		Price:
		<select name="price">
			<option value="">All Prices</option>
			<option value="low">Under €50</option>
			<option value="medium">€50–€100</option>
			<option value="high">Over €100</option>
		</select>
	</label>
	<label>
		Sort:
		<select name="sort">
			<option value="">Standard</option>
			<option value="price_asc">Price Ascending</option>
			<option value="price_desc">Price Descending</option>
			<option value="name_asc">Name A-Z</option>
		</select>
	</label>
</fieldset>
</form>

<div id="loading-indicator" style="display: none;">
	<p>Products are loading...</p>
</div>

<div id="product-list"></div>

We start with a structured HTML template consisting of a form for filtering and a container for the product list. The use of Fieldset and Legend improves accessibility, while the loading indicator provides visual feedback to the user.

AJAX Request on Filter Change with Improved Error Handling

The form triggers an AJAX request on every change to update the product list. Includes loading indicator and error handling.

document.getElementById("filter-form").addEventListener("change", function () {
	const formData = new FormData(this);
	const loadingIndicator = document.getElementById("loading-indicator");
	const productList = document.getElementById("product-list");
	
	// Show loading indicator
	loadingIndicator.style.display = "block";
	productList.style.opacity = "0.5";
	
	fetch("get_products.php", {
		method: "POST",
		body: formData,
		headers: {
			'X-Requested-With': 'XMLHttpRequest'
		}
	})
	.then(response => {
		if (!response.ok) {
			throw new Error(`HTTP error! status: ${response.status}`);
		}
		return response.text();
	})
	.then(html => {
		productList.innerHTML = html;
		productList.style.opacity = "1";
		
		// Display the number of results
		const productCount = productList.querySelectorAll('.product').length;
		updateResultCount(productCount);
	})
	.catch(err => {
		console.error("Error loading products:", err);
		productList.innerHTML = '<p class="error">Error loading products. Please try again later.</p>';
		productList.style.opacity = "1";
	})
	.finally(() => {
		loadingIndicator.style.display = "none";
	});
});

function updateResultCount(count) {
	let countElement = document.getElementById("result-count");
	if (!countElement) {
		countElement = document.createElement("p");
		countElement.id = "result-count";
		document.getElementById("product-list").insertAdjacentElement("beforebegin", countElement);
	}
	countElement.textContent = `${count} product${count !== 1 ? 's' : ''} found`;
}

This enhanced JavaScript function fetches new product data from the server with every filter change. It provides improved error handling, a loading indicator, and displays the number of found products.

Server-Side Processing in PHP with Database Connection

The server filters products based on request parameters, implements sorting, and returns structured HTML.

<?php
	header('Content-Type: text/html; charset=utf-8');
	
	// Validate and sanitize input data
	$category = filter_input(INPUT_POST, 'category', FILTER_SANITIZE_STRING) ?? '';
	$price = filter_input(INPUT_POST, 'price', FILTER_SANITIZE_STRING) ?? '';
	$sort = filter_input(INPUT_POST, 'sort', FILTER_SANITIZE_STRING) ?? '';

	// Example product data (in practice: database query)
	$products = [
		["id" => 1, "name" => "Blue Shirt", "category" => "shirts", "price" => 29.99, "image" => "shirt1.jpg"],
		["id" => 2, "name" => "Black Pants", "category" => "hosen", "price" => 89.50, "image" => "hose1.jpg"],
		["id" => 3, "name" => "Designer Shirt", "category" => "shirts", "price" => 120.00, "image" => "shirt2.jpg"],
		["id" => 4, "name" => "Sports Shoes", "category" => "schuhe", "price" => 75.00, "image" => "schuh1.jpg"],
		["id" => 5, "name" => "Elegant Pants", "category" => "hosen", "price" => 45.00, "image" => "hose2.jpg"],
	];

	// Filter products by criteria
	$filtered = array_filter($products, function ($product) use ($category, $price) {
		// Category filter
		if ($category && $product['category'] !== $category) {
			return false;
		}
		
		// Price filter
		if ($price === 'low' && $product['price'] >= 50) return false;
		if ($price === 'medium' && ($product['price'] < 50 || $product['price'] > 100)) return false;
		if ($price === 'high' && $product['price'] <= 100) return false;
		
		return true;
	});

	// Apply sorting
	switch ($sort) {
		case 'price_asc':
			usort($filtered, fn($a, $b) => $a['price'] <=> $b['price']);
			break;
		case 'price_desc':
			usort($filtered, fn($a, $b) => $b['price'] <=> $a['price']);
			break;
		case 'name_asc':
			usort($filtered, fn($a, $b) => strcasecmp($a['name'], $b['name']));
			break;
	}

	// Generate HTML output
	if (empty($filtered)) {
		echo '<p class="no-results">No products found that match your criteria.</p>';
	} else {
		foreach ($filtered as $item) {
			$formattedPrice = number_format($item['price'], 2, '.', ','); // Changed , to . for cents in English
			echo "<div class='product' data-id='{$item['id']}'>
				<img src='images/{$item['image']}' alt='{$item['name']}' loading='lazy'>
				<h3>{$item['name']}</h3>
				<p class='price'>€{$formattedPrice}</p>
				<button class='add-to-cart' data-product-id='{$item['id']}'>Add to Cart</button>
			</div>";
		}
	}

	// Example for database query (PDO)
	/*
	try {
		$pdo = new PDO("mysql:host=localhost;dbname=shop", $username, $password);
		$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
		
		$sql = "SELECT * FROM products WHERE 1=1";
		$params = [];
		
		if ($category) {
			$sql .= " AND category = :category";
			$params[':category'] = $category;
		}
		
		if ($price) {
			switch ($price) {
				case 'low':
					$sql .= " AND price < 50";
					break;
				case 'medium':
					$sql .= " AND price BETWEEN 50 AND 100";
					break;
				case 'high':
					$sql .= " AND price > 100";
					break;
			}
		}
		
		if ($sort) {
			switch ($sort) {
				case 'price_asc':
					$sql .= " ORDER BY price ASC";
					break;
				case 'price_desc':
					$sql .= " ORDER BY price DESC";
					break;
				case 'name_asc':
					$sql .= " ORDER BY name ASC";
					break;
			}
		}
		
		$stmt = $pdo->prepare($sql);
		$stmt->execute($params);
		$products = $stmt->fetchAll(PDO::FETCH_ASSOC);
		
	} catch (PDOException $e) {
		error_log("Database error: " . $e->getMessage());
		echo '<p class="error">Error loading products.</p>';
	}
	*/
?>

The get_products.php file evaluates the POST data, filters and sorts the product list, and returns it as structured HTML. The comments show how to implement a real database connection.

Improving Performance with Debouncing and Caching

let debounceTimer;
const cache = new Map();

document.getElementById("filter-form").addEventListener("change", function () {
	clearTimeout(debounceTimer);
	
	debounceTimer = setTimeout(() => {
		const formData = new FormData(this);
		const cacheKey = new URLSearchParams(formData).toString();
		
		// Check if result is already in cache
		if (cache.has(cacheKey)) {
			document.getElementById("product-list").innerHTML = cache.get(cacheKey);
			
			// Update result count from cached content (optional but good practice)
			const productList = document.getElementById("product-list");
			const productCount = productList.querySelectorAll('.product').length;
			updateResultCount(productCount);
			return;
		}
		
		const loadingIndicator = document.getElementById("loading-indicator");
		const productList = document.getElementById("product-list");
		
		loadingIndicator.style.display = "block";
		productList.style.opacity = "0.5";
		
		fetch("get_products.php", {
			method: "POST",
			body: formData,
			headers: {
				'X-Requested-With': 'XMLHttpRequest'
			}
		})
		.then(response => {
			if (!response.ok) {
				throw new Error(`HTTP error! status: ${response.status}`);
			}
			return response.text();
		})
		.then(html => {
			// Save result in cache
			cache.set(cacheKey, html);
			
			productList.innerHTML = html;
			productList.style.opacity = "1";
			
			const productCount = productList.querySelectorAll('.product').length;
			updateResultCount(productCount);
		})
		.catch(err => {
			console.error("Error loading products:", err);
			productList.innerHTML = '<p class="error">Error loading products. Please try again later.</p>';
			productList.style.opacity = "1";
		})
		.finally(() => {
			loadingIndicator.style.display = "none";
		});
	}, 300);
});

// Clear cache after 5 minutes
setTimeout(() => {
	cache.clear();
}, 5 * 60 * 1000);

With <strong>Debouncing</strong>, we prevent too many rapid requests from being triggered by multiple quick changes. The integrated <strong>Caching</strong> further reduces server requests for identical filter combinations.

CSS Styling for a Professional Presentation
#filter-form {
	background: #f8f9fa;
	padding: 20px;
	border-radius: 8px;
	margin-bottom: 20px;
	box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

#filter-form fieldset {
	border: none;
	padding: 0;
	margin: 0;
}

#filter-form legend {
	font-size: 1.2em;
	font-weight: bold;
	margin-bottom: 15px;
	color: #333;
}

#filter-form label {
	display: inline-block;
	margin-right: 20px;
	margin-bottom: 10px;
	font-weight: 500;
}

#filter-form select {
	display: block;
	width: 100%;
	max-width: 200px;
	padding: 8px 12px;
	border: 1px solid #ddd;
	border-radius: 4px;
	font-size: 14px;
	margin-top: 5px;
}

#loading-indicator {
	text-align: center;
	padding: 20px;
	font-style: italic;
	color: #666;
}

#product-list {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
	gap: 20px;
	transition: opacity 0.3s ease;
}

.product {
	border: 1px solid #eee;
	border-radius: 8px;
	padding: 15px;
	background: white;
	text-align: center;
	box-shadow: 0 2px 4px rgba(0,0,0,0.1);
	transition: transform 0.2s ease;
}

.product:hover {
	transform: translateY(-2px);
	box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}

.product img {
	width: 100%;
	height: 200px;
	object-fit: cover;
	border-radius: 4px;
	margin-bottom: 10px;
}

.product h3 {
	margin: 10px 0;
	font-size: 1.1em;
	color: #333;
}

.product .price {
	font-size: 1.2em;
	font-weight: bold;
	color: #e74c3c;
	margin: 10px 0;
}

.add-to-cart {
	background: #3498db;
	color: white;
	border: none;
	padding: 10px 20px;
	border-radius: 4px;
	cursor: pointer;
	font-size: 14px;
	transition: background 0.2s ease;
}

.add-to-cart:hover {
	background: #2980b9;
}

.no-results, .error {
	text-align: center;
	padding: 40px;
	color: #666;
	font-style: italic;
}

.error {
	color: #e74c3c;
	background: #fdf2f2;
	border: 1px solid #f5c6cb;
	border-radius: 4px;
}

#result-count {
	margin-bottom: 20px;
	font-weight: 500;
	color: #666;
}

These CSS styles ensure a professional, responsive presentation of the filtering system. The grid layout automatically adapts to different screen sizes.

Conclusion: Dynamic Product Filters Improve UX and Conversion

A dynamic product filter ensures better user guidance and higher conversion rates. With <strong>JavaScript</strong> and <strong>PHP</strong>, a performant and easily customizable filtering system can be realized for any e-commerce shop. The presented solution offers advanced features like caching, debouncing, and professional error handling, which are essential for production environments. Thanks to the modular structure, you can easily adapt the system to your specific requirements and add further features like search functions or advanced filters.

Kontakt-Button
Can we assist you?

Can we assist you?

Get in Touch

Can I use this filtering system in WooCommerce?

Not directly, but the logic can be adapted to WooCommerce using custom endpoints and templates. You can use WooCommerce hooks to integrate the system.

How do I load product data from a database instead of an array?

Replace the $products array with an SQL query and use PDO, for example, to dynamically load the data. An example can be found in the commented code section.

Can I add more filters like colors or sizes?

Yes, simply add more fields to the HTML form and extend the corresponding conditions in the PHP code. The system is modular and easily expandable.

What is Debouncing and why is it useful?

Debouncing ensures that functions are not called too often in quick succession—ideal for rapid UI interactions. It reduces server load and improves performance.

How can I implement this using Vue or React?

The principle remains the same—State Management and API calls are handled by the JavaScript frameworks. You can use the PHP API unchanged.

How do I implement a full-text search?

Add a search field to the HTML form and extend the PHP logic with a LIKE query or use Elasticsearch for advanced search functions.

Is the system optimized for mobile devices?

Yes, thanks to the responsive CSS Grid layout implementation, the system automatically adapts to various screen sizes.

How can I improve performance with large product catalogs?

Implement Pagination, Lazy Loading, and database indexing. Additionally, you can use server-side caching with Redis or Memcached.

Kontakt-Button
How can we help you?

Our services cover all areas of digital communication.

Write to Us
weedesign Blog

The 10 Best E-Commerce Blogs You Should Know

In today's digital world, e-commerce is a vital component of business success. To stay up-to-date and develop successful strategies, it's essential to know the latest trends, innovations, and best practices in e-commerce. Fortunately, many blogs can support e-commerce entrepreneurs. In this article, we have compiled the 10 best e-commerce blogs you should know.

To the Blog Post

Networking Like a Pro: Tips for 2025

Professional networking has fundamentally changed in 2025. Learn how to strategically build your professional network with hybrid events, AI tools, and authentic relationships. Our experts share the best networking strategies for sustainable success.

To the Blog Post

More Sales with Email Marketing: Strategies for Your Online Shop

Email marketing is one of the most effective channels in e-commerce. In this article, you will learn how to achieve not just clicks, but real conversions with sophisticated campaigns, segmentation, and automation.

To the Blog Post
Looking for Shopware Extensions?
Here are our bestsellers!
Advanced Editor | WYSIWYG
Advanced Editor | WYSIWYG

Use the advanced WYSIWYG editor in Shopware 6. This editor enables easy embedding of media in descriptions and many additional features.

ab 7.99 €* / Month

Optimize PageSpeed

Optimize PageSpeed

Optimize your shop to create a better experience for your customers. This plugin minimizes your shop’s loading time and offers numerous configuration options.

ab 27.49 €* / Month

Twig Manager
Twig Manager

Quickly and easily create and edit your own template extensions in the administration. Displays existing storefront template paths and contents.

ab 3.99 €* / Month

Note: * All prices are exclusive of VAT

x