Product Filtering UI With Vanilla JS & Tailwind CSS
Feb 26, 2024It's been a while since I did any vanilla JavaScript projects and I know some of the more intermediate and advanced developers may not be into this, but I know the people in the beginner range appreciate this stuff and honestly I like doing these types of projects.
This project is fairly simple but will get you more familiar with the DOM (Document Object Model) as we will be dynamically updating the page without having to refresh. We'll be dynamically changing CSS values through JavaScript. We'll also be working with high order array methods like forEach
, map
and filter
. If you guys would like me to re-create this project in React, Vue or Svelte or even something else, let me know.
This project was actually just added to my 20 Vanilla JavaScript Projects course. I will have the repo link below, however I have changed some of the naming conventions. The exact code from this tutorial can be found in this Codepen.
So let's go over what we'll be building. It's an interface for an online store product section. We will be creating it from scratch using Tailwind CSS, however if you're not interested in writing the HTML, I'll have timestamps so you can go right to the JavaScript.
Once we create the interface, we're going to first add the ability to add and remove products to and from the cart, which will increment and decrement this number here. You can take that further if you want and have an actual cart with local storage, but that's not the main part of the project. The main part is the filtering. So we want to be able to check these categories on the side and only show products from the selected categories. We also want to be able to search for products with the search box. We also want to do this in a way where if categories are selected, then whatever we add in the searchbox should only apply to products in those categories.
Images and products
You will need the images for this project. I'm including the final code for the project below. You can get the images from the Github Repo or the demo.
Project Demo: https://codepen.io/bradtraversy/pen/oNVKXBo
Github Repo (Some naming conventions are different) https://github.com/bradtraversy/vanillawebprojects
The Files
Let's create the files. All that we need is an index.html
, script.js
and a folder named images
. You can get the images from the link above.
The HTML
Let's add the HTML. I will be hardcoding the products and their info, however, when we get into the JavaScript, we will remove all of the hardcoded HTML because it will be added daynamically through the JS.
Let's start with the main html, head, body tags and include the Tailwind CDN as well as a link to the script.js
file.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.tailwindcss.com"> </script>
<title>Tech Store</title>
</head>
<body class="bg-gray-800 text-white">
<script src="/script.js"> </script>
</body>
</html>
We added a background and text color on to the body.
Navbar
It's not really a navbar because there is no navigation, but we will add a search input and the cart with the number of items:
<nav class="bg-gray-900 p-4 mb-6">
<div
class="container max-w-6xl mx-auto flex flex-col sm:flex-row gap-8 items-center"
>
<!-- Search area -->
<div class="relative w-full">
<input
type="text"
id="search"
class="bg-gray-700 rounded-full p-2 pl-10 transition focus:w-full"
placeholder="Search products..."
/>
<svg
class="absolute left-2 top-1/2 -translate-y-1/2"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" />
<path d="M21 21l-6 -6" />
</svg>
</div>
<!-- Cart icon -->
<span class="relative text-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
#
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M6.331 8h11.339a2 2 0 0 1 1.977 2.304l-1.255 8.152a3 3 0 0 1 -2.966 2.544h-6.852a3 3 0 0 1 -2.965 -2.544l-1.255 -8.152a2 2 0 0 1 1.977 -2.304z"
/>
<path d="M9 11v-5a3 3 0 0 1 6 0v5" />
</svg>
<small
id="cart-count"
class="bg-red-500 text-xs text-white w-4 h-4 absolute -top-2 -right-2 rounded-full"
>0</small
>
</span>
</div>
</nav>
We also included some SVG icons for the search and the cart.
Products & Categories
Under the <nav>
, let's add a <main>
the categories with checkboxes and the products wrapper:
<main class="flex flex-col md:flex-row container mx-auto max-w-6xl">
<div class="space-y-4 p-2 w-full md:max-w-[10rem]">
<h2 class="text-2xl">Filters</h2>
<h3 class="text-xl mb-2">Category</h3>
<div id="filters-container" class="text-xl space-y-2">
<div>
<input type="checkbox" class="check" id="cameras" />
<label for="cameras">Cameras</label>
</div>
<div>
<input type="checkbox" class="check" id="smartphones" />
<label for="smartphones">Smartphones</label>
</div>
<div>
<input type="checkbox" class="check" id="games" />
<label for="games">Games</label>
</div>
<div>
<input type="checkbox" class="check" id="televisions" />
<label for="televisions">Televisions</label>
</div>
</div>
</div>
<!-- Products wrapper -->
<div
id="products-wrapper"
class="w-full max-w-4xl mx-auto grid grid-cols-2 sm:grid-cols-3 xl:grid-cols-4 gap-6 place-content-center p-4"
></div>
</main>
The wrapper is empty, which is what we want because the actual products will be added from the JavaScript. However, let's put a few hardcoded products in there for now:
<!-- Products wrapper -->
<div
id="products-wrapper"
class="w-full max-w-4xl mx-auto grid grid-cols-2 sm:grid-cols-3 xl:grid-cols-4 gap-6 place-content-center p-4"
>
<div class="item space-y-2">
<div
class="bg-gray-100 flex justify-center relative overflow-hidden group cursor-pointer border rounded-xl"
>
<img
src="/images/playstation_5.png"
alt=""
class="w-full h-full object-cover"
/>
<button
class="status bg-black text-white absolute bottom-0 left-0 right-0 text-center py-2 translate-y-full transition group-hover:translate-y-0"
>
Add To Cart
</button>
</div>
<p class="text-xl">Playstation 5</p>
<strong>$399</strong>
</div>
<div class="item space-y-2">
<div
class="bg-gray-100 flex justify-center relative overflow-hidden group cursor-pointer border rounded-xl"
>
<img
src="/images/samsung_galaxy.png"
alt=""
class="w-full h-full object-cover"
/>
<button
class="status bg-black text-white absolute bottom-0 left-0 right-0 text-center py-2 translate-y-full transition group-hover:translate-y-0"
>
Add To Cart
</button>
</div>
<p class="text-xl">Samsung Galaxy</p>
<strong>$399</strong>
</div>
<div class="item space-y-2">
<div
class="bg-gray-100 flex justify-center relative overflow-hidden group cursor-pointer border rounded-xl"
>
<img
src="/images/playstation_5.png"
alt=""
class="w-full h-full object-cover"
/>
<span
class="status bg-black text-white absolute bottom-0 left-0 right-0 text-center py-2 translate-y-full transition group-hover:translate-y-0"
>Add To Cart</span
>
</div>
<p class="text-xl">Playstation 5</p>
<strong>$399</strong>
</div>
<div class="item space-y-2">
<div
class="bg-gray-100 flex justify-center relative overflow-hidden group cursor-pointer border rounded-xl"
>
<img
src="/images/cannon_eos_camera.png"
alt=""
class="w-full h-full object-cover"
/>
<button
class="status bg-black text-white absolute bottom-0 left-0 right-0 text-center py-2 translate-y-full transition group-hover:translate-y-0"
>
Add To Cart
</button>
</div>
<p class="text-xl">Cannon EOS Camera</p>
<strong>$749</strong>
</div>
</div>
That's it for the HTML. Now we can start on the JavaScript.
The Data
In a real applicatiion, this data would most likely come from an API that interacts with a database, but we're just going to have it hardcoded in an array. Your script.js
file should have this products
array:
const products = [
{
name: 'Sony Playstation 5',
url: 'images/playstation_5.png',
category: 'games',
price: 499.99,
},
{
name: 'Samsung Galaxy',
url: 'images/samsung_galaxy.png',
category: 'smartphones',
price: 399.99,
},
{
name: 'Cannon EOS Camera',
url: 'images/cannon_eos_camera.png',
category: 'cameras',
price: 749.99,
},
{
name: 'Sony A7 Camera',
url: 'images/sony_a7_camera.png',
category: 'cameras',
price: 1999.99,
},
{
name: 'LG TV',
url: 'images/lg_tv.png',
category: 'televisions',
price: 799.99,
},
{
name: 'Nintendo Switch',
url: 'images/nintendo_switch.png',
category: 'games',
price: 299.99,
},
{
name: 'Xbox Series X',
url: 'images/xbox_series_x.png',
category: 'games',
price: 499.99,
},
{
name: 'Samsung TV',
url: 'images/samsung_tv.png',
category: 'televisions',
price: 1099.99,
},
{
name: 'Google Pixel',
url: 'images/google_pixel.png',
category: 'smartphones',
price: 499.99,
},
{
name: 'Sony ZV1F Camera',
url: 'images/sony_zv1f_camera.png',
category: 'cameras',
price: 799.99,
},
{
name: 'Toshiba TV',
url: 'images/toshiba_tv.png',
category: 'televisions',
price: 499.99,
},
{
name: 'iPhone 14',
url: 'images/iphone_14.png',
category: 'smartphones',
price: 999.99,
},
];
Now we want to display each of these products on the page via JavaScript.
Selecting the DOM Elements
Lets select the items from the DOM that we need:
// Get DOM elements
const productsWrapper = document.getElementById('products-wrapper');
const checkboxes = document.querySelectorAll('.check');
const filtersContainer = document.getElementById('filters-container');
const searchInput = document.getElementById('search');
const cartCount = document.getElementById('cart-count');
We will also intitialze the cart item count and the array of product elements. When I say elements, I mean the actual HTML output for each product:
// Initialize cart item count
let cartItemCount = 0;
// Initialize products
const productElements = [];
Now we need to loop over the products and create a new product element for each one and push it on to the productElements
array:
// Loop over the products and create the product elements
products.forEach((product) => {
const productElement = document.createElement('div');
productElement.className = 'item space-y-2';
productElement.innerHTML = `<div
class="bg-gray-100 flex justify-center relative overflow-hidden group cursor-pointer border rounded-xl"
>
<img
src="${product.url}"
alt="${product.name}"
class="w-full h-full object-cover"
/>
<button
class="status bg-black text-white absolute bottom-0 left-0 right-0 text-center py-2 translate-y-full transition group-hover:translate-y-0"
>Add To Cart</
>
</div>
<p class="text-xl">${product.name}</p>
<strong>$${product.price.toLocaleString()}</strong>`;
productElements.push(productElement);
productsWrapper.appendChild(productElement);
});
We first create the product element by calling the createProductElement
method. We then add a class to the product element and set the innerHTML to the product image, name and price. We then push the product element on to the productElements
array and append it to the productsWrapper
.
You should now see all of the products on the page.
Function Refactor
This is a bit messy. So let's refactor the code and create a function for creating the product element:
// Create product element
function createProductElement(product) {
const productElement = document.createElement('div');
productElement.className = 'item space-y-2';
productElement.innerHTML = `<div
class="bg-gray-100 flex justify-center relative overflow-hidden group cursor-pointer border rounded-xl"
>
<img
src="${product.url}"
alt="${product.name}"
class="w-full h-full object-cover"
/>
<button
class="status bg-black text-white absolute bottom-0 left-0 right-0 text-center py-2 translate-y-full transition group-hover:translate-y-0"
>Add To Cart</button
>
</div>
<p class="text-xl">${product.name}</p>
<strong>$${product.price.toLocaleString()}</strong>`;
return productElement;
}
Now we have a function that we can call to create the product element. This makes the code a bit cleaner and easier to read. Let's use it within the loop:
// Loop over the products and create the product elements
products.forEach((product) => {
const productElement = createProductElement(product);
productElements.push(productElement);
productsWrapper.appendChild(productElement);
});
Add & Remove From Cart
Let's add the ability to add and remove products from the cart. We will do this by adding an event listener and a new function.
In the createProductElement
function, add the following line right above the return statement:
productElement.querySelector('.status').addEventListener('click', updateCart);
This will add an event listener to the button that will call the updateCart
function when clicked.
Now let's create the updateCart
function:
// Toggle add/remove from cart
function updateCart(e) {
const statusEl = e.target;
if (statusEl.classList.contains('added')) {
// Remove from cart
statusEl.classList.remove('added');
statusEl.innerText = 'Add To Cart';
statusEl.classList.remove('bg-red-600');
statusEl.classList.add('bg-gray-800');
cartItemCount--;
} else {
// Add to cart
statusEl.classList.add('added');
statusEl.innerText = 'Remove From Cart';
statusEl.classList.remove('bg-gray-800');
statusEl.classList.add('bg-red-600');
cartItemCount++;
}
// Update cart item count
cartCount.innerText = cartItemCount.toString();
}
This function will toggle the class of the button and update the cart item count.
Filtering
Now let's add the ability to filter the products by category. We will do this by adding an event listener to the checkboxes and a new function.
Add this code under the forEach
loop:
// Add filter event listeners
filtersContainer.addEventListener('change', filterProducts);
searchInput.addEventListener('input', filterProducts);
We will have both the search box and the checkboxes call the same function. This function will filter the products based on the search input and the checked categories.
Now let's create the filterProducts
function:
// Filter products by search or checkbox
function filterProducts( ) {
// Get search term
const searchTerm = searchInput.value.trim().toLowerCase();
// Get checked categories
const checkedCategories = Array.from(checkboxes)
.filter((check) => check.checked)
.map((check) => check.id);
// Loop over products and check for matches
productElements.forEach((productElement, index) => {
const product = products[index];
// Check to see if product matches the search or checked items
const matchesSearchTerm = product.name.toLowerCase().includes(searchTerm);
const isInCheckedCategory =
checkedCategories.length === 0 ||
checkedCategories.includes(product.category);
// Show or hide product based on matches
if (matchesSearchTerm && isInCheckedCategory) {
productElement.classList.remove('hidden');
} else {
productElement.classList.add('hidden');
}
});
}
This function will get the search term and the checked categories. It will then loop over the products and check if the product matches the search term and the checked categories. If it does, it will remove the hidden
class, if not, it will add the hidden
class.
That's it. You should now have a fully functional product filtering UI. You can add products to the cart, filter by category and search for products. If you want to take it further, you can add a cart with local storage and a checkout page. You can also add more categories and products.
Stay connected with news and updates!
Join our mailing list to receive the latest news and updates from our team.
Don't worry, your information will not be shared.
We hate SPAM. We will never sell your information, for any reason.