Creating a Product Card with Quantity Spinner using Bootstrap 5 and jQuery

Based on the FoodFarm HTML template from TemplatesJungle.com, this tutorial will guide you through building a fully functional product grid where each product card includes a quantity selector and an "Add to Cart" button.
Overview
The FoodFarm template demonstrates a clean e-commerce product grid with:
Responsive product cards (2 to 5 columns based on screen size)
Product images with optional sale badges
Rating stars display
Original price with strikethrough and discounted price
Quantity selector with plus/minus buttons
Add to Cart button with cart icon
Prerequisites
Include these dependencies in your HTML:
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- Bootstrap 5 JS Bundle -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
Step 1: Create the Product Grid Container
The product grid uses Bootstrap's responsive row-cols classes to automatically adjust the number of columns:
<div class="container">
<div class="row">
<div class="col-md-12">
<div class="product-grid row row-cols-2 row-cols-sm-2 row-cols-md-3 row-cols-lg-3 row-cols-xl-4 row-cols-xxl-5">
<!-- Product cards go here -->
</div>
</div>
</div>
</div>
Responsive Column Breakdown:
| Breakpoint | Class | Columns |
|---|---|---|
| Extra small | row-cols-2 |
2 columns |
| Small (sm) | row-cols-sm-2 |
2 columns |
| Medium (md) | row-cols-md-3 |
3 columns |
| Large (lg) | row-cols-lg-3 |
3 columns |
| Extra large (xl) | row-cols-xl-4 |
4 columns |
| Extra extra large (xxl) | row-cols-xxl-5 |
5 columns |
Step 2: Build a Single Product Card
Each product card contains the product image, badge, title, rating, pricing, quantity selector, and add to cart button.
<div class="col">
<div class="product-item">
<!-- Product Image with Badge -->
<figure class="position-relative">
<span class="badge bg-primary text-white fw-normal px-2 py-2 fs-7 position-absolute end-0 me-2">
10% OFF
</span>
<a href="single-product.html" title="Product Title">
<img src="images/product-thumbnail-1.png" alt="Product Thumbnail" class="tab-image img-fluid">
</a>
</figure>
<!-- Product Details -->
<div class="d-flex flex-column text-center">
<!-- Product Title -->
<h3 class="fs-6 fw-normal mb-0">Whole Wheat Sandwich Bread</h3>
<!-- Rating Stars -->
<div>
<span class="rating">
<svg width="18" height="18" class="text-warning">
<use xlink:href="#star-full"></use>
</svg>
<svg width="18" height="18" class="text-warning">
<use xlink:href="#star-full"></use>
</svg>
<svg width="18" height="18" class="text-warning">
<use xlink:href="#star-full"></use>
</svg>
<svg width="18" height="18" class="text-warning">
<use xlink:href="#star-full"></use>
</svg>
<svg width="18" height="18" class="text-warning">
<use xlink:href="#star-half"></use>
</svg>
</span>
<span>(222)</span>
</div>
<!-- Pricing -->
<div class="d-flex justify-content-center align-items-center gap-2">
<del>$24.00</del>
<span class="text-dark fw-semibold">$18.00</span>
</div>
<!-- Quantity and Add to Cart Area -->
<div class="button-area p-3 pt-0">
<div class="row g-1">
<!-- Quantity Selector -->
<div class="col-12 justify-content-center d-flex mt-0">
<div class="input-group product-qty" style="max-width: 150px;">
<span class="input-group-btn">
<button type="button" class="quantity-left-minus btn btn-light btn-number" data-type="minus">
<svg width="16" height="16">
<use xlink:href="#minus"></use>
</svg>
</button>
</span>
<input type="text" name="quantity" class="quantity form-control input-number text-center"
value="1" min="1" max="100">
<span class="input-group-btn">
<button type="button" class="quantity-right-plus btn btn-light btn-number" data-type="plus">
<svg width="16" height="16">
<use xlink:href="#plus"></use>
</svg>
</button>
</span>
</div>
</div>
<!-- Add to Cart Button -->
<div class="col-12">
<a href="#" class="btn btn-primary rounded-1 p-2 fs-7 btn-cart" data-product-id="1" data-product-name="Whole Wheat Sandwich Bread" data-product-price="18.00">
<svg width="18" height="18">
<use xlink:href="#cart"></use>
</svg>
Add to Cart
</a>
</div>
</div>
</div>
</div>
</div>
</div>
Step 3: Create the SVG Icon Definitions
Add these SVG icons at the beginning of your body (hidden from view):
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<defs>
<!-- Star Full Icon -->
<symbol id="star-full" viewBox="0 0 24 24">
<path fill="currentColor" d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/>
</symbol>
<!-- Star Half Icon -->
<symbol id="star-half" viewBox="0 0 24 24">
<path fill="currentColor" d="M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4V6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z"/>
</symbol>
<!-- Cart Icon -->
<symbol id="cart" viewBox="0 0 24 24">
<path fill="currentColor" d="M7 18c-1.1 0-1.99.9-1.99 2S5.9 22 7 22s2-.9 2-2-.9-2-2-2zM1 2v2h2l3.6 7.59-1.35 2.45c-.16.28-.25.61-.25.96 0 1.1.9 2 2 2h12v-2H7.42c-.14 0-.25-.11-.25-.25l.03-.12.9-1.63h7.45c.75 0 1.41-.41 1.75-1.03l3.58-6.49c.08-.14.12-.31.12-.48 0-.55-.45-1-1-1H5.21l-.94-2H1zm16 16c-1.1 0-1.99.9-1.99 2s.89 2 1.99 2 2-.9 2-2-.9-2-2-2z"/>
</symbol>
<!-- Minus Icon -->
<symbol id="minus" viewBox="0 0 24 24">
<path fill="currentColor" d="M19 13H5v-2h14v2z"/>
</symbol>
<!-- Plus Icon -->
<symbol id="plus" viewBox="0 0 24 24">
<path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</symbol>
</defs>
</svg>
Step 4: jQuery for Quantity Spinner
The quantity spinner allows users to increase or decrease the product quantity with plus/minus buttons:
(function($) {
"use strict";
// Initialize quantity spinner for all product cards
var initQuantitySpinner = function() {
$('.product-qty').each(function() {
var \(productQty = \)(this);
// Plus button click handler
$productQty.find('.quantity-right-plus').click(function(e) {
e.preventDefault();
var \(quantityInput = \)productQty.find('.quantity');
var currentVal = parseInt($quantityInput.val());
var maxVal = parseInt($quantityInput.attr('max'));
if (!isNaN(currentVal) && currentVal < maxVal) {
$quantityInput.val(currentVal + 1);
}
});
// Minus button click handler
$productQty.find('.quantity-left-minus').click(function(e) {
e.preventDefault();
var \(quantityInput = \)productQty.find('.quantity');
var currentVal = parseInt($quantityInput.val());
var minVal = parseInt($quantityInput.attr('min'));
if (!isNaN(currentVal) && currentVal > minVal) {
$quantityInput.val(currentVal - 1);
}
});
// Manual input validation
$productQty.find('.quantity').on('change', function() {
var value = parseInt($(this).val());
var min = parseInt($(this).attr('min'));
var max = parseInt($(this).attr('max'));
if (isNaN(value) || value < min) {
$(this).val(min);
} else if (value > max) {
$(this).val(max);
}
});
});
};
// Add to Cart functionality
var initAddToCart = function() {
$('.btn-cart').click(function(e) {
e.preventDefault();
// Get product details
var productId = $(this).data('product-id');
var productName = $(this).data('product-name');
var productPrice = $(this).data('product-price');
// Get quantity from the sibling quantity input
var quantity = $(this).closest('.button-area').find('.quantity').val();
// Calculate subtotal
var subtotal = (parseFloat(productPrice) * parseInt(quantity)).toFixed(2);
// Display toast notification (optional)
showAddToCartNotification(productName, quantity, subtotal);
// Store in localStorage or send to server
addToCartStorage(productId, productName, productPrice, quantity);
// Log to console for debugging
console.log(`Added to cart: \({quantity} x \){productName} = $${subtotal}`);
});
};
// Show toast notification
var showAddToCartNotification = function(productName, quantity, subtotal) {
// Check if toast container exists, create if not
if ($('#toast-container').length === 0) {
$('body').append('<div id="toast-container" class="position-fixed bottom-0 end-0 p-3" style="z-index: 1100;"></div>');
}
var toastHtml = `
<div class="toast align-items-center text-white bg-success border-0 mb-2" role="alert" aria-live="assertive" aria-atomic="true" data-bs-autohide="true" data-bs-delay="3000">
<div class="d-flex">
<div class="toast-body">
<strong>\({quantity}</strong> × \){productName} added to cart ($${subtotal})
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
</div>
`;
$('#toast-container').append(toastHtml);
var toastElement = $('#toast-container .toast').last();
var toast = new bootstrap.Toast(toastElement);
toast.show();
// Remove toast after hiding
toastElement.on('hidden.bs.toast', function() {
$(this).remove();
});
};
// Store cart items in localStorage
var addToCartStorage = function(productId, productName, productPrice, quantity) {
var cart = localStorage.getItem('shoppingCart');
cart = cart ? JSON.parse(cart) : [];
// Check if product already in cart
var existingItem = cart.find(item => item.id == productId);
if (existingItem) {
existingItem.quantity = parseInt(existingItem.quantity) + parseInt(quantity);
} else {
cart.push({
id: productId,
name: productName,
price: parseFloat(productPrice),
quantity: parseInt(quantity)
});
}
localStorage.setItem('shoppingCart', JSON.stringify(cart));
updateCartBadge();
};
// Update cart badge with total items count
var updateCartBadge = function() {
var cart = localStorage.getItem('shoppingCart');
cart = cart ? JSON.parse(cart) : [];
var totalItems = cart.reduce((sum, item) => sum + item.quantity, 0);
if (totalItems > 0) {
$('#cart-badge').text(totalItems).show();
} else {
$('#cart-badge').hide();
}
};
// Document ready
$(document).ready(function() {
initQuantitySpinner();
initAddToCart();
updateCartBadge();
});
})(jQuery);
Step 5: Complete HTML for Multiple Product Cards
Here's how to structure multiple product cards efficiently:
<div class="product-grid row row-cols-2 row-cols-sm-2 row-cols-md-3 row-cols-lg-3 row-cols-xl-4 row-cols-xxl-5">
<!-- Product Card 1 - With Discount Badge -->
<div class="col">
<div class="product-item">
<figure class="position-relative">
<span class="badge bg-primary text-white fw-normal px-2 py-2 fs-7 position-absolute end-0 me-2">10% OFF</span>
<a href="single-product.html">
<img src="images/product-thumbnail-1.png" alt="Product" class="img-fluid">
</a>
</figure>
<div class="d-flex flex-column text-center">
<h3 class="fs-6 fw-normal mb-0">Whole Wheat Sandwich Bread</h3>
<div>
<span class="rating"><!-- Stars here --></span>
<span>(222)</span>
</div>
<div class="d-flex justify-content-center align-items-center gap-2">
<del>$24.00</del>
<span class="text-dark fw-semibold">$18.00</span>
</div>
<!-- Quantity and Add to Cart section -->
</div>
</div>
</div>
<!-- Product Card 2 - No Discount -->
<div class="col">
<div class="product-item">
<figure>
<a href="single-product.html">
<img src="images/product-thumbnail-2.png" alt="Product" class="img-fluid">
</a>
</figure>
<div class="d-flex flex-column text-center">
<h3 class="fs-6 fw-normal mb-0">Whole Grain Oatmeal</h3>
<div>
<span class="rating"><!-- Stars here --></span>
<span>(41)</span>
</div>
<div class="d-flex justify-content-center align-items-center gap-2">
<del>$54.00</del>
<span class="text-dark fw-semibold">$50.00</span>
</div>
<!-- Quantity and Add to Cart section -->
</div>
</div>
</div>
<!-- Repeat for additional products -->
</div>
Step 6: Optional Cart Badge in Header
Add this to your header to show cart item count:
<button type="button" class="btn btn-primary position-relative">
<svg width="24" height="24"><use xlink:href="#cart"></use></svg>
<span id="cart-badge" class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" style="display: none;">
0
<span class="visually-hidden">items in cart</span>
</span>
</button>
Key Bootstrap Classes Used
| Class | Purpose |
|---|---|
row-cols-* |
Responsive column count without media queries |
input-group |
Groups quantity buttons and input field |
btn / btn-primary / btn-light |
Button styling |
badge |
Sale/discount indicator |
position-relative / position-absolute |
For badge positioning |
fs-6 / fs-7 |
Font size utilities |
fw-normal / fw-semibold |
Font weight utilities |
gap-2 |
Spacing between elements |
Summary
This tutorial covered building a complete product card system with:
Responsive product grid using Bootstrap's row-cols classes
Product cards with images, badges, ratings, and pricing
Quantity selector with plus/minus buttons and validation
Add to Cart functionality with localStorage persistence
Toast notifications for user feedback
Cart badge showing total item count
The pattern from FoodFarm Grocery Store template demonstrates a clean, maintainable approach to building e-commerce product listings that work seamlessly across all device sizes. You can find many other free eCommerce HTML templates at TemplatesJungle.com which you can use as a starter template for your projects.
