In this post we will take an existing design from Frontend Mentor, and convert the design into a HTML
, CSS
and JavaScript
webpage.
The webpage will have some cool features like:
- Image gallery
- Image slide show
- Adding product to cart
- Removing product from cart
- Cart counter
- Mobile responsiveness
- and more...
Interested in a video tutorial guide? I got you sorted. The following is the video:
Subscribe to my channel for more.
Starter files
To get the designs and starter files, visit the following link which will take you to the front-end mentor challenge: E-commerce product page
After downloading the starter file, just unzip the zipped folder. Open the unzipped folder with your favorite code editor. I will be using vs-code. You should now have the following starter files.
- design folder - this folder have all the designs you need to complete the challenge
- images - these are the different images and icons you will need when working on the web page
- index.html - A html template with content to get started with.
- README - please read the README to get more details about the challenge and what is expected
- style-guide - a guide on fonts and colors to be used
Head
Open the index.html file. At our head tag we will link the different css and javascript files that we will need. Remove the already existing style tag, we will use an external css file for styles called main.css
. Add script tags for main.js
, slider.js
, and cart.js
.
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="icon"
type="image/png"
sizes="32x32"
href="./images/favicon-32x32.png"
/>
<title>Frontend Mentor | E-commerce product page</title>
<link rel="stylesheet" type="text/css" media="screen" href="main.css" />
<script src="main.js" defer></script>
<script src="slider.js" defer></script>
<script src="cart.js" defer></script>
</head>
Note how we have the defer attributes
at our scripts. When you include a script with the defer attribute, the browser will download the script in the background while continuing to parse and render the HTML. Once the HTML parsing is complete, the deferred scripts will be executed in the order they appear in the document.
Make sure to also create these files at the root. Note how the folder structure now have additional files. That is main.css
, main.js
, slider.js
, and cart.js
.
Nav
At index.html
file we will start by working on our navbar. Below will be our html markup for the navbar. We will make sure to place our nav inside the div .container
which will set a maximum width for all our content.
<body>
<div class="container">
<header>
<nav class="navbar">
<section class="nav-first">
<svg
class="menu-icon"
width="16"
height="15"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M16 12v3H0v-3h16Zm0-6v3H0V6h16Zm0-6v3H0V0h16Z"
fill="#69707D"
fill-rule="evenodd"
/>
</svg>
<div class="logo">
<img src="images/logo.svg" alt="sneakers logo" />
</div>
<div class="backdrop"></div>
<div class="nav-links">
<svg
class="close-icon"
width="14"
height="15"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m11.596.782 2.122 2.122L9.12 7.499l4.597 4.597-2.122 2.122L7 9.62l-4.595 4.597-2.122-2.122L4.878 7.5.282 2.904 2.404.782l4.595 4.596L11.596.782Z"
fill="#69707D"
fill-rule="evenodd"
/>
</svg>
<a href="#">Collections</a>
<a href="#">Men</a>
<a href="#">Women</a>
<a href="#">About</a>
<a href="#">Contact</a>
</div>
</section>
<section class="nav-second">
<div class="cart">
<img
class="cart-icon"
src="images/icon-cart.svg"
alt="cart icon"
/>
<div class="cart-container">
<div class="cart-title">Cart</div>
<div class="cart-items empty">
<p class="cart-empty">Your cart is empty</p>
</div>
<button class="checkout empty">Checkout</button>
</div>
<div class="cart-count">
<span class="qty">0</span>
</div>
</div>
<div class="avatar">
<img src="images/image-avatar.png" alt="avatar" />
</div>
</section>
</nav>
</header>
</div>
</body>
Initial CSS
At main.css
we will start by importing Kumbh Sans
font from google fonts. We then remove the default margin
and padding
from all the elements and set box-sizing
to border-box
. This will tell css to include the padding and border of the elements to their height and width. This way we will not have unexpected sizes of our elements.
We also target :root
to setup css variables for our colors from the style guide file. The following will be our initial css.
@import url("https://fonts.googleapis.com/css2?family=Kumbh+Sans:wght@400;700&display=swap");
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--orange: hsl(26, 100%, 55%);
--pale-orange: hsl(25, 100%, 94%);
--very-dark-blue: hsl(220, 13%, 13%);
--dark-grayish-blue: hsl(219, 9%, 45%);
--grayish-blue: hsl(220, 14%, 75%);
--light-grayish-blue: hsl(223, 64%, 98%);
--white: hsl(0, 0%, 100%);
--black: hsl(0, 0%, 0%);
--black-with-opacity: hsla(0, 0%, 0%, 0.75);
}
html {
font-family: "Kumbh Sans", sans-serif;
}
a {
text-decoration: none;
color: var(--dark-grayish-blue);
}
body {
min-height: 100vh;
min-width: 100vw;
position: relative;
}
.container {
max-width: 1120px;
min-height: 100vh;
padding: 0 5px;
margin: auto;
}
Nav CSS
To layout our navbar we will just use css flex box. Notice how we had divided the navbar into two sections. .nav-first
and .nav-second
. On the first section we style the logo and links while on the second section we will style our cart and profile. Since the cart styles are more detailed, we will have a separate section for it below.
/* Navbar */
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 26px;
border-bottom: 1px solid var(--grayish-blue);
margin-bottom: 85px;
position: relative;
}
.nav-first {
display: flex;
align-items: center;
gap: 50px;
padding-bottom: 30px;
}
.nav-first .menu-icon {
display: none;
}
.nav-first .backdrop {
display: none;
}
.nav-links .close-icon {
display: none;
}
.nav-links {
display: flex;
align-items: center;
gap: 30px;
}
.nav-links a {
position: relative;
}
.nav-links a:hover {
color: var(--black);
}
.nav-links a:hover::after {
content: "";
position: absolute;
background-color: var(--orange);
width: 100%;
height: 3px;
left: 0;
bottom: -47px;
}
.nav-second {
display: flex;
align-items: center;
gap: 45px;
padding-bottom: 30px;
}
.logo img {
height: 22px;
}
.avatar img {
height: 50px;
width: 50px;
}
Cart CSS
.cart-container
will hold our cart items. .empty
is a dynamic class which we will add or remove it depending on whether we have items in cart or not by making use of javascript. By default the cart will be empty. Note how .cart-container
has a display of none. We will add an active class with javascript whenever we click the cart icon in order to open the cart. If you check the html that we currently have, we don't have a .cart-item
. We will create the cart-item at our javascript file and make it a child of .cart-items
.
/* Cart */
.cart {
position: relative;
}
.cart-icon {
cursor: pointer;
}
.cart-container {
right: -95px;
top: 50px;
z-index: 9;
position: absolute;
width: 360px;
min-height: 260px;
background: white;
box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
display: none;
}
.cart-container.active {
display: flex;
flex-direction: column;
}
.cart-title {
padding: 25px 20px;
font-weight: 700;
border-bottom: 1px solid var(--grayish-blue);
}
.cart .cart-items {
padding: 25px 20px;
display: flex;
flex-direction: column;
gap: 25px;
}
.cart .cart-items.empty {
display: flex;
align-items: center;
justify-content: center;
height: 185px;
font-weight: 700;
}
.cart .cart-items.empty .cart-empty {
color: var(--grayish-blue);
display: inline-block;
}
.cart .cart-items .cart-empty {
display: none;
}
.cart-item {
display: flex;
align-items: center;
gap: 20px;
}
.cart-item img {
height: 50px;
border-radius: 5px;
}
.cart-item {
color: var(--dark-grayish-blue);
}
.cart-item .total-price {
color: var(--black);
font-weight: 700;
}
.checkout.empty {
display: none;
}
.checkout {
height: 56px;
margin: 27px 23px;
border: none;
color: var(--white);
background-color: var(--orange);
border-radius: 10px;
font-weight: 700;
}
.checkout:hover {
cursor: pointer;
}
.cart-count {
cursor: pointer;
position: absolute;
top: -8px;
right: -10px;
background-color: var(--orange);
color: var(--white);
min-width: 25px;
min-height: 17px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
}
.delete-item {
border: none;
background: none;
cursor: pointer;
}
Side Nav
To style the side navbar, add the following at the bottom of our css file. We have created a breakpoint at which the side nav will become active.
/* Mobile */
@media (max-width: 755px) {
.navbar {
margin-bottom: 0;
border-bottom: none;
}
.nav-first,
.nav-second {
gap: 30px;
padding-bottom: 10px;
}
.nav-first .menu-icon {
cursor: pointer;
display: inline-block;
}
.nav-links {
display: none;
}
.nav-links.active {
display: flex;
flex-direction: column;
position: absolute;
top: 0;
left: -5px;
max-width: 220px;
width: 100%;
height: 100vh;
background: var(--white);
align-items: start;
z-index: 15;
padding: 25px 30px;
}
.nav-first .backdrop.active {
background: var(--black-with-opacity);
width: 100vw;
height: 100vh;
display: block;
position: absolute;
top: 0;
left: -5px;
z-index: 11;
}
.nav-links.active .close-icon {
display: inline-block;
margin-bottom: 30px;
cursor: pointer;
}
.nav-links a {
font-weight: 700;
color: black;
}
.nav-links.active a:hover::after {
bottom: -5px;
}
}
To open and close the navbar, let's add some javascript. At main.js
, add the following code:
const menuIcon = document.querySelector(".menu-icon");
const backdrop = document.querySelector(".backdrop");
const navLinks = document.querySelector(".nav-links");
const closeIcon = document.querySelector(".close-icon");
menuIcon.addEventListener("click", () => {
backdrop.classList.add("active");
navLinks.classList.add("active");
});
closeIcon.addEventListener("click", () => {
backdrop.classList.remove("active");
navLinks.classList.remove("active");
});
backdrop.addEventListener("click", () => {
backdrop.classList.remove("active");
navLinks.classList.remove("active");
});
Image Gallery, Lightbox and Product Details
At our index.html
just right after our header, we will add a main section with the html markup for our gallery
, lightbox
and product description
.
<section class="main">
<div class="default gallery">
<div class="main-img">
<img
class="active"
src="images/image-product-1.jpg"
alt="product-img"
/>
<img src="images/image-product-2.jpg" alt="product-img" />
<img src="images/image-product-3.jpg" alt="product-img" />
<img src="images/image-product-4.jpg" alt="product-img" />
</div>
<div class="thumb-list">
<div class="active">
<img
src="images/image-product-1-thumbnail.jpg"
alt="product-img"
/>
</div>
<div>
<img
src="images/image-product-2-thumbnail.jpg"
alt="product-img"
/>
</div>
<div>
<img
src="images/image-product-3-thumbnail.jpg"
alt="product-img"
/>
</div>
<div>
<img
src="images/image-product-4-thumbnail.jpg"
alt="product-img"
/>
</div>
</div>
</div>
<div class="lightbox">
<div class="gallery">
<div class="main-img">
<!-- icons -->
<span class="icon-close">
<svg width="14" height="15" xmlns="http://www.w3.org/2000/svg">
<path
d="m11.596.782 2.122 2.122L9.12 7.499l4.597 4.597-2.122 2.122L7 9.62l-4.595 4.597-2.122-2.122L4.878 7.5.282 2.904 2.404.782l4.595 4.596L11.596.782Z"
fill="#69707D"
fill-rule="evenodd"
/>
</svg>
</span>
<span class="icon-prev">
<svg width="12" height="18" xmlns="http://www.w3.org/2000/svg">
<path
d="M11 1 3 9l8 8"
stroke="#1D2026"
stroke-width="3"
fill="none"
fill-rule="evenodd"
/>
</svg>
</span>
<span class="icon-next">
<svg width="13" height="18" xmlns="http://www.w3.org/2000/svg">
<path
d="m2 1 8 8-8 8"
stroke="#1D2026"
stroke-width="3"
fill="none"
fill-rule="evenodd"
/>
</svg>
</span>
<!-- main images -->
<img
class="active"
src="images/image-product-1.jpg"
alt="product-img"
/>
<img src="images/image-product-2.jpg" alt="product-img" />
<img src="images/image-product-3.jpg" alt="product-img" />
<img src="images/image-product-4.jpg" alt="product-img" />
</div>
<div class="thumb-list">
<div class="active">
<img
src="images/image-product-1-thumbnail.jpg"
alt="product-img"
/>
</div>
<div>
<img
src="images/image-product-2-thumbnail.jpg"
alt="product-img"
/>
</div>
<div>
<img
src="images/image-product-3-thumbnail.jpg"
alt="product-img"
/>
</div>
<div>
<img
src="images/image-product-4-thumbnail.jpg"
alt="product-img"
/>
</div>
</div>
</div>
</div>
<div class="content">
<h3>SNEAKER COMPANY</h3>
<h2 class="product-name">Fall Limited Edition Sneakers</h2>
<p class="product-desc">
These low-profile sneakers are your perfect casual wear companion.
Featuring a durable rubber outer sole, they’ll withstand everything
the weather can offer.
</p>
<div class="price-info">
<div class="price">
<span class="current-price">$125.00</span>
<span class="discount">50%</span>
</div>
<div class="prev-price">$250.00</div>
</div>
<div class="add-to-cart-container">
<div class="counter">
<button class="minus">
<svg
width="12"
height="4"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<defs>
<path
d="M11.357 3.332A.641.641 0 0 0 12 2.69V.643A.641.641 0 0 0 11.357 0H.643A.641.641 0 0 0 0 .643v2.046c0 .357.287.643.643.643h10.714Z"
id="a"
/>
</defs>
<use fill="#FF7E1B" fill-rule="nonzero" xlink:href="#a" />
</svg>
</button>
<span class="count">0</span>
<button class="plus">
<svg
width="12"
height="12"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<defs>
<path
d="M12 7.023V4.977a.641.641 0 0 0-.643-.643h-3.69V.643A.641.641 0 0 0 7.022 0H4.977a.641.641 0 0 0-.643.643v3.69H.643A.641.641 0 0 0 0 4.978v2.046c0 .356.287.643.643.643h3.69v3.691c0 .356.288.643.644.643h2.046a.641.641 0 0 0 .643-.643v-3.69h3.691A.641.641 0 0 0 12 7.022Z"
id="b"
/>
</defs>
<use fill="#FF7E1B" fill-rule="nonzero" xlink:href="#b" />
</svg>
</button>
</div>
<button class="add-to-cart">
<span>
<svg width="22" height="20" xmlns="http://www.w3.org/2000/svg">
<path
d="M20.925 3.641H3.863L3.61.816A.896.896 0 0 0 2.717 0H.897a.896.896 0 1 0 0 1.792h1l1.031 11.483c.073.828.52 1.726 1.291 2.336C2.83 17.385 4.099 20 6.359 20c1.875 0 3.197-1.87 2.554-3.642h4.905c-.642 1.77.677 3.642 2.555 3.642a2.72 2.72 0 0 0 2.717-2.717 2.72 2.72 0 0 0-2.717-2.717H6.365c-.681 0-1.274-.41-1.53-1.009l14.321-.842a.896.896 0 0 0 .817-.677l1.821-7.283a.897.897 0 0 0-.87-1.114ZM6.358 18.208a.926.926 0 0 1 0-1.85.926.926 0 0 1 0 1.85Zm10.015 0a.926.926 0 0 1 0-1.85.926.926 0 0 1 0 1.85Zm2.021-7.243-13.8.81-.57-6.341h15.753l-1.383 5.53Z"
fill="#69707D"
fill-rule="nonzero"
/>
</svg>
</span>
<span>Add to cart</span>
</button>
</div>
</div>
</section>
Before the mobile responsiveness, we will style the gallery and the product details. By default the lightbox(image slideshow) will be hidden, and we will add the active to it upon clicking the .main-img
.
/* Main */
.main {
display: flex;
gap: 125px;
min-height: 570px;
align-items: center;
padding: 0 50px;
}
/* Image gallery */
.gallery {
flex: 1;
display: flex;
flex-direction: column;
gap: 30px;
}
.gallery .main-img img {
display: none;
}
.gallery .main-img img.active {
display: inline-block;
max-width: 445px;
max-height: 445px;
width: 100%;
height: 100%;
border-radius: 20px;
cursor: pointer;
}
.gallery .thumb-list {
display: flex;
justify-content: space-between;
max-width: 445px;
width: 100%;
}
.gallery .thumb-list div {
max-width: 90px;
max-height: 90px;
margin: 0 2px;
}
.gallery .thumb-list img {
width: 100%;
height: 100%;
border-radius: 10px;
cursor: pointer;
}
.gallery .thumb-list img:hover {
opacity: 50%;
}
.gallery .thumb-list .active img {
opacity: 30%;
}
.gallery .thumb-list .active {
border: 2px solid var(--orange);
border-radius: 13px;
margin: 0;
}
/* lightbox */
.lightbox {
display: none;
position: absolute;
top: 0;
left: 0;
height: 100vh;
width: 100vw;
z-index: 10;
background: var(--black-with-opacity);
align-items: center;
justify-content: center;
}
.lightbox.active {
display: flex;
}
.lightbox.active .gallery {
max-width: 445px;
}
.lightbox .main-img {
position: relative;
}
.lightbox .icon-prev,
.lightbox .icon-next {
position: absolute;
height: 60px;
width: 60px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--white);
border-radius: 50%;
}
.icon-prev:hover,
.icon-next:hover {
cursor: pointer;
}
/* .icon-prev svg path {
fill: var(--orange);
} */
.icon-prev {
top: 50%;
transform: translate(-50%, -50%);
}
.icon-next {
top: 50%;
right: 0;
transform: translate(50%, -50%);
}
.icon-close svg path {
fill: var(--white);
}
.icon-close svg path:hover {
cursor: pointer;
fill: var(--orange);
}
.icon-close {
position: absolute;
right: 0;
top: -40px;
}
/* Content */
.content {
flex: 1;
}
.content h3 {
font-size: 16px;
color: var(--orange);
}
.content h2 {
font-size: 37px;
margin: 20px 0 40px 0;
}
.content p {
font-size: 16px;
color: var(--dark-grayish-blue);
margin-bottom: 30px;
}
.price {
display: flex;
align-items: center;
gap: 15px;
}
.current-price {
font-weight: 700;
font-size: 25px;
}
.discount {
display: flex;
align-items: center;
justify-content: center;
padding: 0 6px;
border-radius: 10%;
height: 25px;
background-color: var(--pale-orange);
font-weight: 700;
color: var(--orange);
}
.prev-price {
margin: 10px 0 35px 0;
font-size: 18px;
color: var(--grayish-blue);
font-weight: 700;
text-decoration: line-through;
}
.add-to-cart-container {
display: flex;
align-items: center;
gap: 15px;
}
.counter {
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 15px;
width: 150px;
height: 55px;
background: var(--light-grayish-blue);
}
.counter button {
width: 50px;
height: 100%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
}
.counter .count {
font-weight: 700;
}
.add-to-cart {
color: var(--white);
background-color: var(--orange);
border: 0px;
height: 55px;
width: 100%;
border-radius: 10px;
font-weight: 700;
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
cursor: pointer;
padding: 0 5px;
}
.add-to-cart svg path {
fill: var(--white);
}
Gallery and Slider Logic
Now let's add logic for our gallery and lightbox. At slider.js
add the following code. In the following code we have selected all the mainImages
and all the thumbnails
, both for gallery and lightbox. Their logic will be similar apart from the slider. The goal is to show a certain image upon clicking it's thumbnail.
const mainImages = document.querySelectorAll(".default .main-img img");
const thumbnails = document.querySelectorAll(".default .thumb-list div");
const lightboxMainImages = document.querySelectorAll(".lightbox .main-img img");
const lightboxThumbnails = document.querySelectorAll(
".lightbox .thumb-list div"
);
const lightbox = document.querySelector(".lightbox");
const iconClose = document.querySelector(".icon-close");
const iconPrev = document.querySelector(".icon-prev");
const iconNext = document.querySelector(".icon-next");
let currentImageIndex = 0;
const changeImage = (index, mainImages, thumbnails) => {
mainImages.forEach((img) => {
img.classList.remove("active");
});
thumbnails.forEach((thumb) => {
thumb.classList.remove("active");
});
mainImages[index].classList.add("active");
thumbnails[index].classList.add("active");
currentImageIndex = index;
};
thumbnails.forEach((thumb, index) => {
thumb.addEventListener("click", () => {
changeImage(index, mainImages, thumbnails);
});
});
lightboxThumbnails.forEach((thumb, index) => {
thumb.addEventListener("click", () => {
changeImage(index, lightboxMainImages, lightboxThumbnails);
});
});
mainImages.forEach((img, index) => {
img.addEventListener("click", () => {
lightbox.classList.add("active");
changeImage(index, lightboxMainImages, lightboxThumbnails);
});
});
iconPrev.addEventListener("click", () => {
if (currentImageIndex <= 0) {
changeImage(mainImages.length - 1, lightboxMainImages, lightboxThumbnails);
} else {
changeImage(currentImageIndex - 1, lightboxMainImages, lightboxThumbnails);
}
});
iconNext.addEventListener("click", () => {
if (currentImageIndex >= mainImages.length - 1) {
changeImage(0, lightboxMainImages, lightboxThumbnails);
} else {
changeImage(currentImageIndex + 1, lightboxMainImages, lightboxThumbnails);
}
});
iconClose.addEventListener("click", () => {
lightbox.classList.remove("active");
});
Let's break the above code down. We select the thumbnails and loop over each of them. This allows us to add click event listener to each of it, pass a callback function and then call changeImage
function.
thumbnails.forEach((thumb, index) => {
thumb.addEventListener("click", () => {
changeImage(index, mainImages, thumbnails);
});
});
The changeImage function uses the thumbnail index to change the active image. First we remove the .active
class from all mainImages and thumbnails and then we use the passed index to add the .active
class to only specific mainImage and it's thumbnail.
This logic will be similar for our lighbox when we change images by clicking on thumbnails.
const changeImage = (index, mainImages, thumbnails) => {
mainImages.forEach((img) => {
img.classList.remove("active");
});
thumbnails.forEach((thumb) => {
thumb.classList.remove("active");
});
mainImages[index].classList.add("active");
thumbnails[index].classList.add("active");
currentImageIndex = index;
};
Open Lightbox
To open the lightbox, we looped over the mainImages and added the click event to each of them. Upon clicking the main Image, we add an active
class to the lightbox. This will open the lightbox. Now to make sure that the clicked image is the one that show on the lightbox, we also call our changeImage
function and pass the index of the clicked image. This will update the lightbox to the current image.
mainImages.forEach((img, index) => {
img.addEventListener("click", () => {
lightbox.classList.add("active");
changeImage(index, lightboxMainImages, lightboxThumbnails);
});
});
Next and Prev Images
Here we reused our changeImage
function. The main thing to note is the correct calculation of the index. To simply explain it, for next
we simply add 1
to the index. But if we get to the end, we move the count of the index back to 0
. And for previous we decrease the index by 1
and if we get to the first image, we move the count of the index to the last image. We make use of the .length
property to know the last image.
iconPrev.addEventListener("click", () => {
if (currentImageIndex <= 0) {
changeImage(mainImages.length - 1, lightboxMainImages, lightboxThumbnails);
} else {
changeImage(currentImageIndex - 1, lightboxMainImages, lightboxThumbnails);
}
});
iconNext.addEventListener("click", () => {
if (currentImageIndex >= mainImages.length - 1) {
changeImage(0, lightboxMainImages, lightboxThumbnails);
} else {
changeImage(currentImageIndex + 1, lightboxMainImages, lightboxThumbnails);
}
});
Cart Logic
Now add the following code at cart.js
. At addToCartBtn
we will add a click
event listener. upon clicking the button we will extract the product details from the DOM including the name, price and image. The product quantity will already be available at our count
variable. We will then call addItemToCart
with the product details as parameters.
At addItemToCart
we will create a cart item, notice how the information is dynamic depending on the details we got at addItemToCart
as arguments, e.g the name, price etc
We also create a custom quantity attribute which will be helpful when calculating the total cart quantity at updateTotalCartQty
function.
cartItem.dataset.quantity = count;
After creating the carItem
, we then append it as a child of cartItems
. This is how the item will simply be added to cart.
const countEl = document.querySelector(".count");
const minus = document.querySelector(".minus");
const plus = document.querySelector(".plus");
const cartIcon = document.querySelector(".cart-icon");
const cartContainer = document.querySelector(".cart-container");
const addToCartBtn = document.querySelector(".add-to-cart");
const cartItems = document.querySelector(".cart-items");
const checkout = document.querySelector(".checkout");
const cartCount = document.querySelector(".cart-count");
let count = 0;
let totalCartQty = 0;
const updateCount = (newCount) => {
count = newCount;
countEl.textContent = count;
};
minus.addEventListener("click", () => {
if (count > 0) {
updateCount(count - 1);
}
});
plus.addEventListener("click", () => {
updateCount(count + 1);
});
cartCount.addEventListener("click", () => {
cartContainer.classList.toggle("active");
});
const updateTotalCartQty = () => {
const cartItemsList = document.querySelectorAll(".cart-item");
totalCartQty = 0;
cartItemsList.forEach((item) => {
totalCartQty += parseInt(item.dataset.quantity);
});
cartCount.innerHTML = `<span class="qty">${totalCartQty}</span>`;
};
// add item to cart
const addItemToCart = (name, price, imageSrc) => {
const totalPrice = count * price;
const cartItem = document.createElement("div");
cartItem.classList.add("cart-item");
cartItem.dataset.quantity = count;
cartItem.innerHTML = `
<img src="${imageSrc}" alt="${name}" />
<div class="item-details">
<div>${name}</div>
<div>
<p>
$${price.toFixed(2)} x ${count}
<span class='total-price'>$${totalPrice.toFixed(2)}</span>
</p>
</div>
</div>
<button class="delete-item">
<svg width="14" height="16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path d="M0 2.625V1.75C0 1.334.334 1 .75 1h3.5l.294-.584A.741.741 0 0 1 5.213 0h3.571a.75.75 0 0 1 .672.416L9.75 1h3.5c.416 0 .75.334.75.75v.875a.376.376 0 0 1-.375.375H.375A.376.376 0 0 1 0 2.625Zm13 1.75V14.5a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 1 14.5V4.375C1 4.169 1.169 4 1.375 4h11.25c.206 0 .375.169.375.375ZM4.5 6.5c0-.275-.225-.5-.5-.5s-.5.225-.5.5v7c0 .275.225.5.5.5s.5-.225.5-.5v-7Zm3 0c0-.275-.225-.5-.5-.5s-.5.225-.5.5v7c0 .275.225.5.5.5s.5-.225.5-.5v-7Zm3 0c0-.275-.225-.5-.5-.5s-.5.225-.5.5v7c0 .275.225.5.5.5s.5-.225.5-.5v-7Z" id="a"/></defs><use fill="#C3CAD9" fill-rule="nonzero" xlink:href="#a"/></svg>
</button>
`;
cartItems.appendChild(cartItem);
updateTotalCartQty();
if (cartItems.classList.contains("empty")) {
cartItems.classList.remove("empty");
checkout.classList.remove("empty");
}
// attach an event listener to the delete button
const deleteButton = cartItem.querySelector(".delete-item");
deleteButton.addEventListener("click", (event) => {
const cartItem = event.target.closest(".cart-item");
removeItemFromCart(cartItem);
});
};
addToCartBtn.addEventListener("click", () => {
if (count === 0) return;
const productName = document.querySelector(".main .product-name").textContent;
const productPriceEl = document.querySelector(".main .current-price");
const productPrice = parseFloat(productPriceEl.textContent.replace("$", ""));
const productImg = document
.querySelector(".default.gallery .main-img img")
.getAttribute("src");
addItemToCart(productName, productPrice, productImg);
cartContainer.classList.add("active");
updateCount(0);
});
// remove item from cart
const removeItemFromCart = (cartItem) => {
cartItem.remove();
updateTotalCartQty();
if (cartItems.children.length === 1) {
cartItems.classList.add("empty");
checkout.classList.add("empty");
}
};
Mobile responsiveness
Let's make the rest of the page responsive. At our breakpoint @media (max-width: 755px)
after .nav-links.active
, add the following.
/* main */
.main {
flex-direction: column;
gap: 20px;
padding: 0;
}
.main .default {
display: none;
}
.lightbox {
display: flex;
position: relative;
height: auto;
width: auto;
background: none;
}
.main .thumb-list {
display: none;
}
.main .icon-prev {
left: 50px;
height: 45px;
width: 45px;
}
.main .icon-next {
right: 50px;
height: 45px;
width: 45px;
}
.gallery .main-img img.active {
max-width: none;
max-height: none;
width: 100vw;
height: auto;
border-radius: 0;
}
.content {
padding: 0 20px;
}
.content h2 {
margin: 10px 0;
font-size: 30px;
}
.price-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.prev-price {
margin: 0;
}
.add-to-cart-container {
flex-direction: column;
}
.counter {
width: 100%;
}
.counter button {
width: 40%;
}
.cart-container {
z-index: 20;
right: -85px;
top: 40px;
}
That's it devs, incase of any confusion you can watch the video tutorial I pinned above and the source code is also available for free on my github: https://github.com/chaoocharles/ecommerce-product-page-html-css-javascript
Do me a favor now and subscribe to my channel 😊: https://www.youtube.com/c/chaoocharles
Top comments (0)