Build a Product Catalog in JavaScript
Fetch live API data and build an interactive product catalog with vanilla JS.
Introduction
30 Second Summary
Every online store you browse uses JavaScript to pull product data from a server and display it on your screen. That grid of items updating when you click a filter button? JavaScript is making it happen.
In this project, you will build a product catalog web app that fetches real e-commerce data from a live API and displays it as a filterable grid of product cards.
What You'll Build
A responsive product catalog that fetches product data from the Fake Store API, renders it as styled cards with images, titles, prices, and ratings, and lets users filter products by category.
By the end of this project, you'll have:
- A working product catalog webpage that fetches and displays real e-commerce data from a live API.
- Hands-on experience with core JavaScript concepts including async/await, array methods, and DOM manipulation.
- A clear understanding of how JavaScript controls the browser, setting the foundation for learning React next.
- Secret Mission: Add a search bar and sort-by-price feature to your catalog.
Are there any prerequisites?
You should have a basic understanding of programming concepts like variables, loops, and functions from any language. No prior JavaScript experience is needed.
Set Up Your Development Environment
Every web project starts with three files working together: HTML for structure, CSS for styling, and JavaScript for interactivity. Your product-catalog folder already has all three files ready to go.
In this step, you'll wire them together with a basic HTML skeleton and confirm your development server works. Once Live Server is running, every change you save will appear instantly in the browser.
In this step, get ready to:
- Add a basic HTML skeleton to index.html that links your CSS and JavaScript files.
- Launch Live Server and confirm the page opens with no errors.
Add the HTML skeleton to your page
An HTML skeleton is the minimal structure every web page needs. It tells the browser which language the page is in, how to scale on mobile, and where to find the linked CSS and JavaScript files.
- In VS Code, click on index.html in the Explorer sidebar to open it.
- Replace everything in the file with this code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Product Catalog</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<script src="app.js" defer></script>
</body>
</html>
What does this code do?
- <!DOCTYPE html> tells the browser this is a modern HTML5 page.
- The <meta name="viewport"> tag ensures the page scales properly on mobile devices.
- <link rel="stylesheet" href="style.css"> connects your CSS file so styles apply to the page.
- <script src="app.js" defer> loads your JavaScript file. The defer attribute ensures the script runs only after the HTML has been fully parsed.
Why use defer instead of putting the script in the head?
JavaScript often needs to interact with HTML elements on the page. If the script runs before those elements exist, it fails silently.
The defer attribute solves this by telling the browser to wait until the entire HTML document is parsed before executing the script. This keeps your code simple without needing to wrap everything in a "document ready" listener.
- Save index.html by pressing Ctrl+S.
Launch Live Server to verify your setup
Live Server runs a local web server on your machine so the browser can load your files over HTTP. This is required for JavaScript features like fetch() to work properly.
- In the VS Code Explorer sidebar, right-click on index.html.
- Select Open with Live Server from the context menu.
Your default browser should open automatically showing a blank white page at http://127.0.0.1:5500.
- Open DevTools by pressing Ctrl+Shift+I and click the Console tab.
- Confirm there are no red error messages in the console.
Browser didn't open automatically?
- Check the VS Code status bar at the bottom. If you see Port: 5500, the server is running. Open your browser manually and navigate to http://127.0.0.1:5500.
- If you don't see a port number, make sure you right-clicked index.html specifically, not another file or the folder.
- Try clicking Go Live in the bottom-right of the VS Code status bar as an alternative way to start the server.
Still stuck? Help me troubleshoot why Live Server isn't opening my page.
✔️ Awesome, I've got everything!
Great. Double check you've saved index.html and your console is error-free.
ⓧ I'd like to double check the full code
Here is the complete index.html at this point:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Product Catalog</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<script src="app.js" defer></script>
</body>
</html>
Your style.css and app.js files should still be empty at this point.
Your development environment is ready. Next up, you'll build the HTML structure for your product catalog and style it with CSS so there's a real layout waiting for your JavaScript to fill.
Build the HTML Structure and Base Styles
Your development environment is ready and Live Server is showing a blank page. Now it's time to give that page some structure and visual design.
Every web app starts with two layers: the HTML skeleton that defines what elements exist on the page, and the CSS that controls how they look. In this step, you'll build both layers so JavaScript has a ready-made stage to fill with product data.
In this step, get ready to:
- Build the HTML page structure with a header, filter section, and product grid area.
- Write CSS styles including a responsive grid layout and card hover effects.
- Test your styles with a temporary hardcoded product card.
Build the HTML page structure
Your index.html currently has an empty body with just the script tag. You need to add the elements that JavaScript will later fill with product data.
- In VS Code, click on index.html in the Explorer sidebar to open it.
- Find the opening <body> tag. Add the following HTML between <body> and the <script> tag:
<header>
<h1>Product Catalog</h1>
</header>
<main>
<!-- Container for category filter buttons -->
<div id="category-filters"></div>
<!-- Container where product cards will be rendered -->
<div id="product-grid"></div>
</main>
What does this HTML do?
The <header> holds your page title. The <main> element wraps all primary content.
The two div elements are empty containers on purpose. JavaScript will fill category-filters with clickable buttons and product-grid with product cards in the next step.
- Save index.html by pressing Ctrl+S.
- Your browser should auto-refresh. You should see "Product Catalog" as a heading at the top of the page.
Don't see the heading?
- Make sure your new HTML is placed between the <body> tag and the <script> tag, not after the script.
- Check that Live Server is still running. Look for "Port: 5500" in the VS Code status bar at the bottom.
- help me figure out why my HTML isn't showing in the browser
✔️ Awesome, I've got everything!
Great. Make sure you've saved index.html before moving on to CSS.
ⓧ I'd like to double check the full code
Here's the complete index.html file at this point:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Product Catalog</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>Product Catalog</h1>
</header>
<main>
<div id="category-filters"></div>
<div id="product-grid"></div>
</main>
<script src="app.js" defer></script>
</body>
</html>
Style the page foundation
With the HTML in place, you'll now write the CSS that controls how everything looks. You'll start with the page-level styles: a reset, typography, and layout for the header and main content area.
- Open style.css in VS Code by clicking on it in the Explorer sidebar.
- Add the page foundation styles by pasting this code:
/* Reset browser defaults */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Base page typography and background */
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #f5f5f5;
color: #333;
line-height: 1.6;
}
/* Dark header bar */
header {
background-color: #1a1a2e;
color: #fff;
padding: 20px;
text-align: center;
}
header h1 {
font-size: 1.8rem;
}
/* Center the main content area */
main {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
What does this code do?
- The * selector resets all margins and padding to zero. Every browser adds its own default spacing, so this gives you a clean slate.
- The body rule sets a system font stack and a light gray background so product cards stand out.
- The main element is capped at 1200px wide and centered with margin: 0 auto, preventing content from stretching edge-to-edge on large screens.
- Save style.css and check your browser.
- You should see a dark navy header with white "Product Catalog" text, and the rest of the page should have a light gray background.
Page still looks unstyled?
- Check that your index.html has <link rel="stylesheet" href="style.css"> in the <head> section.
- Make sure the filename is exactly style.css (not styles.css or style.CSS). The filename in the link tag must match the actual file.
- help me debug why my CSS isn't loading
Next, add the styles for the category filter buttons. Paste this code below your existing CSS in style.css:
- Add the filter button styles below the main rule by pasting this code:
/* Flexbox container for filter buttons */
#category-filters {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 30px;
justify-content: center;
}
/* Individual filter button styling */
.filter-btn {
padding: 8px 16px;
border: 2px solid #1a1a2e;
background-color: #fff;
color: #1a1a2e;
border-radius: 20px;
cursor: pointer;
font-size: 0.9rem;
text-transform: capitalize;
transition: background-color 0.3s, color 0.3s;
}
/* Hover and active states for filter buttons */
.filter-btn:hover {
background-color: #1a1a2e;
color: #fff;
}
.filter-btn.active {
background-color: #1a1a2e;
color: #fff;
}
What does this code do?
- The #category-filters container uses display: flex with flex-wrap: wrap so buttons flow onto a new line on smaller screens.
- The .filter-btn class uses border-radius: 20px to create pill-shaped buttons. The transition property makes color changes animate smoothly on hover.
- The .active class highlights whichever filter is currently selected. JavaScript will add and remove this class later.
- Save style.css.
- You won't see a visible change yet because the category-filters div is still empty. The next chunk adds the product grid styles.
Finally, add the product grid and card styles. This is where CSS Grid creates the responsive layout that automatically adjusts how many cards fit per row.
- Add the product grid and card styles below the filter button rules by pasting this code:
/* Responsive grid that auto-fills columns */
#product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
/* Individual product card */
.product-card {
background-color: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
transition: box-shadow 0.3s;
}
/* Hover effect adds a shadow */
.product-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
/* Product image sizing */
.product-card img {
width: 150px;
height: 150px;
object-fit: contain;
margin-bottom: 15px;
}
/* Product title with line clamping */
.product-card h3 {
font-size: 1rem;
text-align: center;
margin-bottom: 10px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Price styling */
.product-card .price {
font-size: 1.2rem;
font-weight: bold;
color: #e63946;
margin-bottom: 8px;
}
/* Rating text */
.product-card .rating {
font-size: 0.9rem;
color: #666;
}
What does this code do?
- The grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)) rule tells the browser to create as many 280px-wide columns as will fit. On a wide screen you'll see 4 cards per row. On a phone, just 1.
- Each .product-card uses flexbox in column direction so the image, title, price, and rating stack vertically and stay centered.
- The -webkit-line-clamp: 2 on h3 truncates long product titles to 2 lines, keeping cards a consistent height.
- Save style.css.
- You won't see the grid or cards yet because the product-grid div is still empty. You'll verify these styles work in the next substep.
✔️ Awesome, I've got everything!
Great. Make sure you've saved style.css before moving on.
ⓧ I'd like to double check the full code
Here's the complete style.css file at this point:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #f5f5f5;
color: #333;
line-height: 1.6;
}
header {
background-color: #1a1a2e;
color: #fff;
padding: 20px;
text-align: center;
}
header h1 {
font-size: 1.8rem;
}
main {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
#category-filters {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 30px;
justify-content: center;
}
.filter-btn {
padding: 8px 16px;
border: 2px solid #1a1a2e;
background-color: #fff;
color: #1a1a2e;
border-radius: 20px;
cursor: pointer;
font-size: 0.9rem;
text-transform: capitalize;
transition: background-color 0.3s, color 0.3s;
}
.filter-btn:hover {
background-color: #1a1a2e;
color: #fff;
}
.filter-btn.active {
background-color: #1a1a2e;
color: #fff;
}
#product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
.product-card {
background-color: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
transition: box-shadow 0.3s;
}
.product-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.product-card img {
width: 150px;
height: 150px;
object-fit: contain;
margin-bottom: 15px;
}
.product-card h3 {
font-size: 1rem;
text-align: center;
margin-bottom: 10px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.product-card .price {
font-size: 1.2rem;
font-weight: bold;
color: #e63946;
margin-bottom: 8px;
}
.product-card .rating {
font-size: 0.9rem;
color: #666;
}
Test your styles with a temporary product card
Your CSS targets classes like .product-card and .price, but those elements don't exist in the HTML yet. To confirm your styles work before writing JavaScript, you'll add a temporary hardcoded card.
- Switch back to index.html in VS Code.
- Inside the <div id="product-grid"> element, add this temporary test card:
<div class="product-card">
<img src="https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg" alt="Test Product">
<h3>Fjallraven - Foldsack No. 1 Backpack</h3>
<p class="price">$109.95</p>
<p class="rating">Rating: 3.9/5 (120 reviews)</p>
</div>
Why add a temporary card?
This is a common frontend workflow. You hardcode sample content to visually verify your CSS before wiring up dynamic data. Once you confirm the styles look right, you remove the static content and let JavaScript generate it instead.
- Save index.html and check your browser.
- You should see a white card with rounded corners in the content area. It displays a backpack image, the product title, a red price, and a rating line. Hover over the card to see the shadow effect.
Card doesn't look right?
- If the card stretches full-width, check that your #product-grid rule has display: grid set. Without it, the grid won't constrain card widths.
- If the image is missing, check your internet connection. The image loads from the Fake Store API server.
- Make sure the class name product-card in your HTML matches .product-card in your CSS exactly.
- help me debug my product card styling
Now that you've confirmed the styles work, remove the temporary card. JavaScript will generate these cards dynamically in the next step.
- In index.html, delete the entire <div class="product-card">...</div> block you just added.
- Your <div id="product-grid"> should be empty again: <div id="product-grid"></div>.
- Save index.html. The page should show just the dark header and light gray background below it.
Your page now has a solid structure and polished styling ready to receive real data. Next up, you'll write JavaScript that fetches live product data from an API and fills that empty grid with actual product cards.
Fetch Data from the API and Render Products
Your page has structure and style, but it is completely static right now. The product grid area sits empty, waiting for real data to fill it.
In this step, you will write JavaScript that reaches out to a live API, pulls back product data, and turns that data into visible cards on the page. This is where your catalog comes to life.
In this step, get ready to:
- Write a function that fetches product data from a live API.
- Build a function that transforms product data into visible HTML cards.
- Connect both functions so products display automatically on page load.
Write the fetchProducts function
Your catalog needs real product data to display. The Fake Store API provides 20 e-commerce products with images, prices, and ratings, all accessible without an API key.
- In VS Code, switch back to app.js (currently empty) and add the following code:
// Store all products for later use (filtering)
let allProducts = [];
// Fetch product data from the Fake Store API
async function fetchProducts() {
try {
const response = await fetch("https://fakestoreapi.com/products");
const products = await response.json();
return products;
} catch (error) {
console.error("Failed to fetch products:", error);
return [];
}
}
What does this code do?
This code defines a variable and a function that work together to get product data from the internet.
- let allProducts = [] creates an empty array that will later hold all 20 products so other parts of your code can access them.
- async function fetchProducts() defines an asynchronous function. The async keyword lets the function pause while waiting for the API to respond, without freezing the browser.
- await fetch("https://fakestoreapi.com/products") sends a request to the Fake Store API and waits for the response to come back.
- await response.json() converts the raw response into a JSON array of product objects that JavaScript can work with.
- The try/catch block handles errors gracefully. If the network fails, it logs the error and returns an empty array instead of crashing.
- Save app.js.
- Open your browser's DevTools by pressing F12 (Windows) or Cmd+Option+I (macOS).
- Click the Console tab and type the following to test your function:
fetchProducts().then(data => console.log(data))
- Press Enter. You should see an array of 20 product objects logged, each with fields like title, price, image, and rating.
Seeing an empty array or an error?
- Make sure your page is served through Live Server (http://127.0.0.1:5500), not opened as a file. The fetch function does not work on file:// URLs.
- Check that the URL in your code is exactly https://fakestoreapi.com/products with no typos.
- If you see a network error, check your internet connection. The API is a live external service.
Why isn't my fetchProducts function returning data?
Write the renderProducts function
Now that you can fetch data, you need a way to turn those product objects into something visible on the page. This function will build HTML cards from the data and inject them into the product grid.
- In app.js, add the following function below fetchProducts:
// Build product cards and display them in the grid
function renderProducts(products) {
const productGrid = document.getElementById("product-grid");
// Show a message if no products match
if (products.length === 0) {
productGrid.innerHTML = "<p>No products found.</p>";
return;
}
// Transform each product object into an HTML card string
const cardsHTML = products.map(function(product) {
return `
<div class="product-card">
<img src="${product.image}" alt="${product.title}">
<h3>${product.title}</h3>
<p class="price">$${product.price.toFixed(2)}</p>
<p class="rating">Rating: ${product.rating.rate}/5 (${product.rating.count} reviews)</p>
</div>
`;
}).join("");
// Insert all cards into the page
productGrid.innerHTML = cardsHTML;
}
What does this code do?
This function takes an array of products and turns them into visible cards on the page.
- document.getElementById("product-grid") grabs the empty div from your HTML where cards will go.
- .map() loops through every product and transforms it into an HTML string. Each string uses template literals (the backtick characters) to embed product values directly into the HTML.
- .join("") combines all the individual card strings into one long HTML string.
- .toFixed(2) formats prices to always show two decimal places (e.g. $9.99 instead of $9.9).
- Setting innerHTML replaces whatever is inside the grid div with your freshly built cards.
- Save app.js.
- You will not see a change in the browser yet. The function exists but nothing calls it. The next chunk wires everything together.
Connect everything with the init function
Both functions are ready, but neither runs on its own. You need a startup function that fetches the data, stores it, and passes it to the renderer.
- At the bottom of app.js, add the init function and immediately call it:
// Start the app: fetch data, then render it
async function init() {
allProducts = await fetchProducts();
renderProducts(allProducts);
}
init();
What does this code do?
The init function is the entry point for your application.
- It calls fetchProducts() and waits for the data to arrive.
- It saves the full product array into allProducts so you can reference it later for filtering.
- It passes the data to renderProducts which builds and displays the cards.
- The standalone init() call at the bottom runs the function immediately when the script loads.
- Save app.js and check your browser.
- You should see a grid of 20 product cards, each showing a product image, title, price in red, and a star rating.
Not seeing any product cards?
- Open DevTools (F12) and check the Console tab for red error messages. A typo in the API URL or function name will show up here.
- Confirm that init(); appears at the very bottom of app.js outside any function. Without it, nothing runs.
- Make sure your HTML still has <div id="product-grid"></div> in index.html. The JavaScript looks for this exact ID.
Help me figure out why my product cards are not appearing.
✔️ Awesome, I've got everything!
Great. Double check you have saved app.js and confirmed the 20 product cards display in your browser.
ⓧ I'd like to double check the full code
Here is the complete app.js at this point:
let allProducts = [];
async function fetchProducts() {
try {
const response = await fetch("https://fakestoreapi.com/products");
const products = await response.json();
return products;
} catch (error) {
console.error("Failed to fetch products:", error);
return [];
}
}
function renderProducts(products) {
const productGrid = document.getElementById("product-grid");
if (products.length === 0) {
productGrid.innerHTML = "<p>No products found.</p>";
return;
}
const cardsHTML = products.map(function(product) {
return `
<div class="product-card">
<img src="${product.image}" alt="${product.title}">
<h3>${product.title}</h3>
<p class="price">$${product.price.toFixed(2)}</p>
<p class="rating">Rating: ${product.rating.rate}/5 (${product.rating.count} reviews)</p>
</div>
`;
}).join("");
productGrid.innerHTML = cardsHTML;
}
async function init() {
allProducts = await fetchProducts();
renderProducts(allProducts);
}
init();
Your catalog is now pulling live data and rendering it beautifully. Next up, you will add category filter buttons so users can browse products by type.
Add Category Filtering
Your product catalog is pulling live data and displaying 20 products in a responsive grid. Right now, though, browsing means scrolling through everything at once.
In this step, you'll add interactive category filter buttons that let users narrow the display to a specific product type. The Fake Store API provides a dedicated categories endpoint, so you'll fetch that data and use it to generate buttons dynamically with JavaScript.
In this step, get ready to:
- Fetch the list of product categories from the API.
- Build filter buttons dynamically using DOM manipulation.
- Wire up click handlers that filter products by category.
Fetch the list of categories from the API
The Fake Store API has a separate endpoint that returns all available product categories as an array of strings. You'll write an async function to fetch them, just like you did for products.
- In app.js, add the following function directly below your fetchProducts function:
async function fetchCategories() {
try {
const response = await fetch("https://fakestoreapi.com/products/categories");
const categories = await response.json();
return categories;
} catch (error) {
console.error("Failed to fetch categories:", error);
return [];
}
}
What does this code do?
This function follows the same pattern as fetchProducts. It calls the /products/categories endpoint, which returns a simple array of category name strings.
The try/catch block handles network errors gracefully. If the request fails, it logs the error and returns an empty array so the rest of your app doesn't crash.
Now you need to call this new function when the page loads. Your init function currently only fetches products.
- In app.js, scroll down to your init function at the bottom of the file.
- Find this line:
allProducts = await fetchProducts();
renderProducts(allProducts);
- Add const categories = await fetchCategories(); between those two lines, so the function now looks like this:
async function init() {
allProducts = await fetchProducts();
const categories = await fetchCategories();
renderProducts(allProducts);
}
- Save app.js.
- Check your browser. The page still shows all 20 product cards with no errors in the console. The categories data isn't visible yet because you haven't built the filter buttons.
Seeing a blank page or console errors?
Make sure you placed fetchCategories as a standalone function, not inside another function. It should sit between fetchProducts and renderProducts.
Also check that you typed await fetchCategories() with the parentheses and the await keyword.
Help me debug my fetchCategories function
Build the filter buttons and their click handlers
Instead of hardcoding buttons in HTML, you'll create them dynamically with JavaScript. This means the buttons automatically match whatever categories the API returns.
- In app.js, add the following function below renderProducts:
function renderFilters(categories) {
const filtersDiv = document.getElementById("category-filters");
// Create the "All" button first
const allButton = document.createElement("button");
allButton.textContent = "All";
allButton.classList.add("filter-btn", "active");
allButton.addEventListener("click", function() {
setActiveButton(allButton);
renderProducts(allProducts);
});
filtersDiv.appendChild(allButton);
// Create one button per category
categories.forEach(function(category) {
const button = document.createElement("button");
button.textContent = category;
button.classList.add("filter-btn");
button.addEventListener("click", function() {
setActiveButton(button);
const filtered = allProducts.filter(function(product) {
return product.category === category;
});
renderProducts(filtered);
});
filtersDiv.appendChild(button);
});
}
What does this code do?
- document.createElement("button") creates a new HTML button element in memory. You then set its text, add CSS classes, and append it to the page.
- The "All" button's click handler calls renderProducts(allProducts) to show every product.
- Each category button's click handler uses .filter() to create a new array containing only products whose category property matches the button's category, then passes that filtered array to renderProducts.
- filtersDiv.appendChild(button) adds each finished button to the #category-filters div so it appears on the page.
What is .filter()?
The .filter() array method creates a brand new array containing only the items that pass a test. It does not change the original array.
Here, the test is product.category === category. Only products whose category matches the clicked button's category make it into the new filtered array.
You won't see a change yet. The next chunk defines the setActiveButton helper and wires everything into your init function.
Highlight the active filter and wire everything together
Users need visual feedback showing which category is currently selected. A small helper function will manage the active CSS class across all filter buttons.
- In app.js, add the following function below renderFilters:
function setActiveButton(activeBtn) {
// Remove active class from all filter buttons
const buttons = document.querySelectorAll(".filter-btn");
buttons.forEach(function(btn) {
btn.classList.remove("active");
});
// Add active class to the clicked button
activeBtn.classList.add("active");
}
What does this code do?
document.querySelectorAll(".filter-btn") grabs every button with the filter-btn class. The function first strips the active class from all of them, then adds it back to just the one that was clicked.
This ensures only one button is highlighted at a time. The .filter-btn.active CSS rule in your stylesheet already handles the visual styling.
Now you need to call renderFilters from your init function so the buttons actually appear on the page.
- In your init function, find this line:
renderProducts(allProducts);
- Add renderFilters(categories); directly below it, so your init function now looks like this:
async function init() {
allProducts = await fetchProducts();
const categories = await fetchCategories();
renderProducts(allProducts);
renderFilters(categories);
}
- Save app.js and check your browser.
- You should see a row of rounded buttons above the product grid. There should be an "All" button plus one button for each category.
- Click a category button (like "electronics"). Only products in that category should appear.
- Click the "All" button. All 20 products should return.
- Notice how the clicked button stays highlighted with a dark background. That is your active state working.
Buttons not appearing or filtering not working?
- If no buttons appear, make sure you added renderFilters(categories); inside init and that categories is defined with const categories = await fetchCategories(); before the call.
- If clicking a button does nothing, check that your addEventListener calls are inside renderFilters and that you spelled allProducts correctly.
- If you see "setActiveButton is not defined" in the console, make sure the setActiveButton function is defined as a standalone function in your file (not inside another function).
Help me fix my category filter buttons
✔️ Awesome, I've got everything!
Great. Double check you've saved app.js and all your filters are working.
ⓧ I'd like to double check the full code
Here is the complete app.js at this point. Compare yours against it:
let allProducts = [];
async function fetchProducts() {
try {
const response = await fetch("https://fakestoreapi.com/products");
const products = await response.json();
return products;
} catch (error) {
console.error("Failed to fetch products:", error);
return [];
}
}
async function fetchCategories() {
try {
const response = await fetch("https://fakestoreapi.com/products/categories");
const categories = await response.json();
return categories;
} catch (error) {
console.error("Failed to fetch categories:", error);
return [];
}
}
function renderProducts(products) {
const productGrid = document.getElementById("product-grid");
if (products.length === 0) {
productGrid.innerHTML = "<p>No products found.</p>";
return;
}
const cardsHTML = products.map(function(product) {
return `
<div class="product-card">
<img src="${product.image}" alt="${product.title}">
<h3>${product.title}</h3>
<p class="price">$${product.price.toFixed(2)}</p>
<p class="rating">Rating: ${product.rating.rate}/5 (${product.rating.count} reviews)</p>
</div>
`;
}).join("");
productGrid.innerHTML = cardsHTML;
}
function renderFilters(categories) {
const filtersDiv = document.getElementById("category-filters");
const allButton = document.createElement("button");
allButton.textContent = "All";
allButton.classList.add("filter-btn", "active");
allButton.addEventListener("click", function() {
setActiveButton(allButton);
renderProducts(allProducts);
});
filtersDiv.appendChild(allButton);
categories.forEach(function(category) {
const button = document.createElement("button");
button.textContent = category;
button.classList.add("filter-btn");
button.addEventListener("click", function() {
setActiveButton(button);
const filtered = allProducts.filter(function(product) {
return product.category === category;
});
renderProducts(filtered);
});
filtersDiv.appendChild(button);
});
}
function setActiveButton(activeBtn) {
const buttons = document.querySelectorAll(".filter-btn");
buttons.forEach(function(btn) {
btn.classList.remove("active");
});
activeBtn.classList.add("active");
}
async function init() {
allProducts = await fetchProducts();
const categories = await fetchCategories();
renderProducts(allProducts);
renderFilters(categories);
}
init();
Your catalog now has real interactivity. Users can browse all products or drill into a specific category with a single click. Next up, the Secret Mission challenges you to add search and sorting on top of this foundation.
Secret mission
Add Search and Sort
Your catalog works great for browsing by category. But what if someone is looking for a specific product? Or wants to find the cheapest option? Time to add a search bar and price sorting that work together with your existing category filters.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally and uses a free public API, so there are no ongoing costs.
Resources you used:
- The product-catalog folder on your Desktop containing index.html, style.css, and app.js.
- The Live Server extension installed in VS Code.
Keep everything running
No action needed. Choose this if you're still actively building or want to experiment more with JavaScript concepts.
- Your product-catalog folder stays on your Desktop, ready to open any time.
- The Live Server extension stays installed in VS Code for future projects.
Pause - I'll come back to this later
Stop the Live Server process to free up the port, but keep your files for later.
- In VS Code, click the Go Live button in the bottom status bar (it shows the port number when active) to stop the server.
- Your project files remain on your Desktop. Reopen the folder in VS Code any time to pick up where you left off.
Delete - I don't want to use this again
Remove all project files and the Live Server extension to start fresh.
- Stop Live Server by clicking the port number in the VS Code status bar.
- Delete the project folder by running this command in your terminal (macOS/Linux):
rm -rf ~/Desktop/product-catalog
On Windows, run this in PowerShell:
Remove-Item -Recurse -Force ~\Desktop\product-catalog
- To uninstall the Live Server extension, press Ctrl+Shift+X to open the Extensions panel in VS Code.
- Search for Live Server, click on it, then click Uninstall.
Nice Work!
Nice Work!
You just built a complete product catalog web app using only HTML, CSS, and vanilla JavaScript. Your catalog fetches real product data from a live API, renders it dynamically, and lets users filter by category.
You've learned how to:
- Fetch live data from the Fake Store API using async/await and the fetch API.
- Render dynamic content using DOM manipulation, template literals, and array methods like .map().
- Implement interactive category filtering using event listeners and .filter().
- Secret Mission: Added search functionality and price sorting with combined filter state management.
Ready to quiz yourself?