Add to cart animation
Zenith Store: Animated Flux Cart with GSAP & Morphing Button
CSS
GSAP
HTML
JavaScript
A sophisticated e-commerce product page demonstration featuring a unique "Add to Cart" button that morphs into a mini-cart preview, displaying the item count and product thumbnail, before animating the thumbnail to a global cart icon. The interface utilizes a modern dark theme with vibrant cyan accents, glassmorphism effects, and smooth, elastic animations powered by GSAP. The cart panel slides in from the right, offering a clean, responsive, and interactive way to manage selected items, including quantity adjustments and item removal, all within a single-page application feel.
Zenith Store: Advanced E-commerce UI with Morphing Cart Animation
This project showcases an advanced and aesthetically pleasing user interface for an e-commerce platform, aptly named "Zenith Store." The core focus is on a highly interactive "Add to Cart" experience, which leverages the GreenSock Animation Platform (GSAP) to create a fluid and engaging morphing button effect. When a user decides to add an item, the button doesn't just confirm the action; it transforms. Initially, it displays a loading spinner, then subtly expands to reveal a miniature thumbnail of the product and an item count, effectively becoming a transient mini-cart preview directly on the button itself. This "flux" state provides immediate visual feedback before a cloned thumbnail gracefully animates, flying across the screen to the main navigation cart icon, which then provides its own elastic feedback.
The overall design aesthetic is a modern dark theme, employing deep grays and near-blacks contrasted with a vibrant cyan as the primary accent color. This creates a futuristic and premium feel. Glassmorphism is utilized in the sticky header and the slide-in cart panel, adding a layer of visual sophistication with blurred, semi-transparent backgrounds. Product cards feature a clean layout, rounded corners, subtle hover effects including a gentle lift and glow, and clear calls to action. Typography is carefully selected, with "Outfit" for headings and "Inter" for body text, ensuring both style and readability.
The cart panel itself is a key component, sliding in smoothly from the right side of the screen when activated. It provides a comprehensive overview of added items, complete with product images, titles, prices, and intuitive quantity adjustment controls (increase/decrease buttons). Users can also easily remove items from their cart. The panel includes a running subtotal and a prominent "Proceed to Checkout" button. An elegant empty state message encourages users to shop if their cart is empty. All transitions and interactions within the cart panel, such as item addition or removal, are designed to be smooth and responsive, enhancing the user experience. The entire implementation is contained within a single HTML file, utilizing CDNs for external libraries like Font Awesome and GSAP, making it easy to deploy and test.
The overall design aesthetic is a modern dark theme, employing deep grays and near-blacks contrasted with a vibrant cyan as the primary accent color. This creates a futuristic and premium feel. Glassmorphism is utilized in the sticky header and the slide-in cart panel, adding a layer of visual sophistication with blurred, semi-transparent backgrounds. Product cards feature a clean layout, rounded corners, subtle hover effects including a gentle lift and glow, and clear calls to action. Typography is carefully selected, with "Outfit" for headings and "Inter" for body text, ensuring both style and readability.
The cart panel itself is a key component, sliding in smoothly from the right side of the screen when activated. It provides a comprehensive overview of added items, complete with product images, titles, prices, and intuitive quantity adjustment controls (increase/decrease buttons). Users can also easily remove items from their cart. The panel includes a running subtotal and a prominent "Proceed to Checkout" button. An elegant empty state message encourages users to shop if their cart is empty. All transitions and interactions within the cart panel, such as item addition or removal, are designed to be smooth and responsive, enhancing the user experience. The entire implementation is contained within a single HTML file, utilizing CDNs for external libraries like Font Awesome and GSAP, making it easy to deploy and test.
HTML (html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Zenith Store - Flux Cart Experience</title>
<!-- Google Fonts: Outfit & Inter -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600&display=swap"
rel="stylesheet"
/>
<!-- Font Awesome for icons -->
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
/>
<!-- GSAP for animations -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<header>
<div class="container">
<nav>
<div class="logo">
<i class="fas fa-atom"></i>
<!-- Changed icon -->
<span>Zenith</span>
</div>
<div class="nav-cart-icon-wrapper" id="navCartIconWrapper">
<i class="fas fa-shopping-basket nav-cart-icon"></i>
<!-- Changed icon -->
<span class="nav-cart-count" id="navCartCount">0</span>
</div>
</nav>
</div>
</header>
<main class="container">
<div class="products-grid" id="productsGrid">
<!-- Products will be injected here by JavaScript -->
</div>
</main>
<div class="cart-overlay" id="cartOverlay"></div>
<div class="cart-panel" id="cartPanel">
<div class="cart-panel__header">
<div class="cart-panel__title">
<i class="fas fa-cart-arrow-down"></i>
<span>Your Haul</span>
</div>
<button class="cart-panel__close-btn" id="closeCartBtn">
<i class="fas fa-times-circle"></i>
</button>
</div>
<div class="cart-panel__items" id="cartItemsContainer">
<div class="cart-panel__empty-cart" id="emptyCartMessage">
<i class="fas fa-space-shuttle fa-rotate-90"></i>
<!-- Changed icon -->
<p>Your cart is lightspeed empty.</p>
<p>Discover your next favorite thing!</p>
</div>
</div>
<div class="cart-panel__footer">
<div class="cart-panel__total">
<span>Subtotal:</span>
<span class="cart-panel__total-amount" id="cartTotalAmount"
>$0.00</span
>
</div>
<button class="checkout-btn" id="checkoutBtn" disabled>
<i class="fas fa-shield-alt"></i>
<span>Secure Checkout</span>
</button>
</div>
</div>
<script src="./script.js"></script>
</body>
</html>
CSS (css)
:root {
--bg-deep-dark: #0d0d0d; /* Almost black */
--bg-dark: #1a1a1a; /* Dark grey for cards/panels */
--bg-dark-secondary: #2c2c2c; /* Slightly lighter dark */
--text-primary-light: #e0e0e0;
--text-secondary-light: #b0b0b0;
--text-muted-light: #757575;
--accent-cyan: #00e5ff;
--accent-cyan-dark: #00b8d4;
--accent-magenta: #f50057; /* Contrasting accent for specific actions if needed */
--success-green: #00c853;
--glow-cyan: 0 0 10px var(--accent-cyan), 0 0 20px var(--accent-cyan),
0 0 30px rgba(0, 229, 255, 0.5);
--glow-cyan-soft: 0 0 8px rgba(0, 229, 255, 0.3);
--shadow-card: 0px 8px 24px rgba(0, 0, 0, 0.3);
--shadow-interactive: 0px 4px 15px rgba(0, 0, 0, 0.5);
--radius-xl: 16px;
--radius-lg: 12px;
--radius-md: 8px;
--font-heading: "Outfit", sans-serif;
--font-body: "Inter", sans-serif;
--transition-fast: 0.2s ease-out;
--transition-smooth: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
--transition-elastic: 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-body);
background-color: var(--bg-deep-dark);
/* Subtle noise background for texture */
background-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%232c2c2c' fill-opacity='0.1'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
color: var(--text-primary-light);
line-height: 1.7;
min-height: 100vh;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.container {
max-width: 1320px; /* Slightly wider */
margin: 0 auto;
padding: 0 2rem;
}
/* Header */
header {
background-color: rgba(13, 13, 13, 0.8); /* Semi-transparent */
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
position: sticky;
top: 0;
z-index: 1000;
padding: 1.25rem 0;
}
nav {
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-family: var(--font-heading);
font-size: 2rem;
font-weight: 700;
display: flex;
align-items: center;
gap: 0.6rem;
color: var(--text-primary-light);
text-shadow: 0 0 5px var(--accent-cyan);
}
.logo i {
color: var(--accent-cyan);
filter: drop-shadow(0 0 8px var(--accent-cyan));
}
.nav-cart-icon-wrapper {
position: relative;
cursor: pointer;
}
.nav-cart-icon {
font-size: 1.8rem;
color: var(--text-secondary-light);
transition: color var(--transition-fast), transform var(--transition-fast),
filter var(--transition-fast);
}
.nav-cart-icon-wrapper:hover .nav-cart-icon {
color: var(--accent-cyan);
transform: scale(1.1);
filter: drop-shadow(0 0 8px var(--accent-cyan));
}
.nav-cart-count {
position: absolute;
top: -10px;
right: -14px;
background: linear-gradient(
135deg,
var(--accent-cyan),
var(--accent-cyan-dark)
);
color: var(--bg-deep-dark);
font-size: 0.75rem;
font-weight: 700;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 0 10px var(--accent-cyan);
border: 2px solid var(--bg-deep-dark);
}
/* Product Grid */
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 2.5rem;
padding: 4rem 0;
}
.product-card {
background-color: var(--bg-dark);
border-radius: var(--radius-xl);
overflow: hidden;
box-shadow: var(--shadow-card);
transition: transform 0.4s ease, box-shadow 0.4s ease;
display: flex;
flex-direction: column;
border: 1px solid rgba(255, 255, 255, 0.05); /* Subtle border */
}
.product-card:hover {
transform: translateY(-12px) scale(1.02);
box-shadow: 0px 16px 40px rgba(0, 0, 0, 0.5), var(--glow-cyan-soft);
}
.product-card__image-container {
position: relative;
height: 250px;
overflow: hidden;
background-color: var(--bg-dark-secondary);
}
.product-card__image-container img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.6s cubic-bezier(0.165, 0.84, 0.44, 1);
}
.product-card:hover .product-card__image-container img {
transform: scale(1.1);
}
.product-card__badge {
position: absolute;
top: 1rem;
right: 1rem;
background: linear-gradient(
45deg,
var(--accent-cyan),
var(--accent-cyan-dark)
);
color: var(--bg-deep-dark);
padding: 0.4rem 0.9rem;
border-radius: var(--radius-md);
font-size: 0.8rem;
font-weight: 600;
box-shadow: 0 2px 8px rgba(0, 229, 255, 0.4);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.product-card__info {
padding: 1.75rem;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.product-card__title {
font-family: var(--font-heading);
font-size: 1.35rem;
font-weight: 600;
margin-bottom: 0.6rem;
color: var(--text-primary-light);
line-height: 1.3;
}
.product-card__description {
font-size: 0.95rem;
color: var(--text-secondary-light);
margin-bottom: 1.25rem;
flex-grow: 1;
line-height: 1.6;
}
.product-card__footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.product-card__price {
font-size: 1.5rem;
font-weight: 700;
color: var(--accent-cyan);
text-shadow: 0 0 5px rgba(0, 229, 255, 0.5);
}
.add-to-cart-btn {
background: transparent;
border: 2px solid var(--accent-cyan);
color: var(--accent-cyan);
padding: 0.8rem 1.5rem; /* Adjusted for content changes */
border-radius: var(--radius-md);
font-weight: 600;
cursor: pointer;
transition: all var(--transition-smooth);
display: flex;
align-items: center;
justify-content: center; /* Center content */
position: relative;
overflow: hidden;
min-height: 50px; /* Ensure consistent height */
min-width: 160px; /* Initial width */
}
.add-to-cart-btn:hover:not(:disabled) {
background-color: var(--accent-cyan);
color: var(--bg-deep-dark);
box-shadow: var(--glow-cyan);
transform: translateY(-2px);
}
.add-to-cart-btn:disabled {
border-color: var(--text-muted-light);
color: var(--text-muted-light);
cursor: not-allowed;
}
.btn-content-initial,
.btn-content-loading,
.btn-content-added {
display: flex;
align-items: center;
gap: 0.6rem;
position: absolute; /* For smooth transitions */
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
white-space: nowrap;
}
.btn-content-loading img.btn-thumbnail-preview {
width: 28px;
height: 28px;
border-radius: 4px;
object-fit: cover;
margin-left: 0.5rem;
border: 1px solid rgba(0, 229, 255, 0.3);
}
.btn-item-count {
font-size: 0.9em;
font-weight: bold;
}
/* Cart Panel */
.cart-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
z-index: 1900;
opacity: 0;
visibility: hidden;
transition: opacity var(--transition-smooth),
visibility var(--transition-smooth);
}
.cart-overlay.active {
opacity: 1;
visibility: visible;
}
.cart-panel {
position: fixed;
top: 0;
right: -100%;
width: 100%;
max-width: 450px;
height: 100%;
background-color: rgba(26, 26, 26, 0.9); /* Dark with slight transparency */
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
z-index: 2000;
box-shadow: -10px 0 30px rgba(0, 0, 0, 0.5);
border-left: 1px solid rgba(0, 229, 255, 0.2);
display: flex;
flex-direction: column;
/* transition handled by GSAP for custom ease */
}
.cart-panel__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.75rem;
border-bottom: 1px solid rgba(0, 229, 255, 0.15);
}
.cart-panel__title {
font-family: var(--font-heading);
font-size: 1.6rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.8rem;
color: var(--text-primary-light);
}
.cart-panel__title i {
color: var(--accent-cyan);
}
.cart-panel__close-btn {
background: none;
border: none;
font-size: 2rem;
cursor: pointer;
color: var(--text-secondary-light);
transition: transform var(--transition-fast), color var(--transition-fast);
}
.cart-panel__close-btn:hover {
transform: rotate(90deg) scale(1.1);
color: var(--accent-cyan);
}
.cart-panel__items {
flex-grow: 1;
overflow-y: auto;
padding: 1.75rem;
}
/* Custom Scrollbar */
.cart-panel__items::-webkit-scrollbar {
width: 8px;
}
.cart-panel__items::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
border-radius: var(--radius-md);
}
.cart-panel__items::-webkit-scrollbar-thumb {
background: var(--accent-cyan-dark);
border-radius: var(--radius-md);
}
.cart-panel__items::-webkit-scrollbar-thumb:hover {
background: var(--accent-cyan);
}
.cart-panel__empty-cart {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-muted-light);
text-align: center;
padding: 2rem;
}
.cart-panel__empty-cart i {
font-size: 6rem;
margin-bottom: 1.5rem;
color: rgba(0, 229, 255, 0.2);
filter: drop-shadow(0 0 10px rgba(0, 229, 255, 0.1));
}
.cart-panel__empty-cart p {
font-size: 1.1rem;
font-family: var(--font-heading);
}
.cart-item {
display: flex;
gap: 1.25rem;
padding: 1.25rem;
border-radius: var(--radius-lg);
background-color: var(--bg-dark-secondary);
margin-bottom: 1.25rem;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
position: relative;
border: 1px solid rgba(255, 255, 255, 0.03);
}
.cart-item__img-container {
width: 90px;
height: 90px;
border-radius: var(--radius-md);
overflow: hidden;
flex-shrink: 0;
background-color: var(--bg-dark);
border: 1px solid rgba(0, 229, 255, 0.1);
}
.cart-item__img-container img {
width: 100%;
height: 100%;
object-fit: cover;
}
.cart-item__info {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.cart-item__title {
font-weight: 500;
font-size: 1.05rem;
margin-bottom: 0.3rem;
color: var(--text-primary-light);
}
.cart-item__price {
color: var(--accent-cyan);
font-weight: 600;
font-size: 0.95rem;
margin-bottom: 0.6rem;
}
.cart-item__quantity-controls {
display: flex;
align-items: center;
gap: 0.75rem;
}
.quantity-btn {
width: 30px;
height: 30px;
border-radius: 50%;
border: 1px solid var(--text-muted-light);
background-color: transparent;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
color: var(--text-secondary-light);
transition: all var(--transition-fast);
font-size: 0.9rem;
}
.quantity-btn:hover {
background-color: var(--accent-cyan);
color: var(--bg-deep-dark);
border-color: var(--accent-cyan);
transform: scale(1.1);
}
.cart-item__quantity {
font-weight: 600;
font-size: 1rem;
min-width: 20px;
text-align: center;
color: var(--text-primary-light);
}
.cart-item__remove-btn {
position: absolute;
top: 0.8rem;
right: 0.8rem;
color: var(--text-muted-light);
background: none;
border: none;
cursor: pointer;
font-size: 1.3rem;
padding: 0.3rem;
transition: color var(--transition-fast), transform var(--transition-fast);
}
.cart-item__remove-btn:hover {
color: var(--accent-magenta);
transform: scale(1.15);
}
.cart-panel__footer {
padding: 1.75rem;
border-top: 1px solid rgba(0, 229, 255, 0.15);
background-color: rgba(13, 13, 13, 0.7); /* Match header */
}
.cart-panel__total {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
font-weight: 600;
font-size: 1.15rem;
color: var(--text-secondary-light);
}
.cart-panel__total-amount {
font-size: 1.6rem;
font-weight: 700;
color: var(--accent-cyan);
text-shadow: 0 0 5px rgba(0, 229, 255, 0.5);
}
.checkout-btn {
width: 100%;
padding: 1rem;
border-radius: var(--radius-md);
background: linear-gradient(
45deg,
var(--accent-cyan),
var(--accent-cyan-dark)
);
color: var(--bg-deep-dark);
border: none;
font-weight: 700;
font-size: 1.1rem;
font-family: var(--font-heading);
cursor: pointer;
transition: transform var(--transition-fast),
box-shadow var(--transition-fast);
display: flex;
justify-content: center;
align-items: center;
gap: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.checkout-btn:hover:not(:disabled) {
transform: translateY(-3px) scale(1.02);
box-shadow: var(--glow-cyan);
}
.checkout-btn:disabled {
background: var(--bg-dark-secondary);
color: var(--text-muted-light);
cursor: not-allowed;
box-shadow: none;
transform: none;
}
/* Flying Image Animation */
.flying-product-thumbnail {
position: fixed;
z-index: 2500;
pointer-events: none;
border-radius: var(--radius-md);
box-shadow: var(--glow-cyan);
overflow: hidden;
border: 2px solid var(--accent-cyan);
}
.flying-product-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* Responsive */
@media (max-width: 992px) {
.products-grid {
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 2rem;
}
.container {
padding: 0 1.5rem;
}
}
@media (max-width: 768px) {
.cart-panel {
max-width: 85%;
}
.product-card__title {
font-size: 1.2rem;
}
.product-card__price {
font-size: 1.3rem;
}
.add-to-cart-btn {
padding: 0.7rem 1.2rem;
min-width: 140px;
}
}
@media (max-width: 480px) {
.container {
padding: 0 1rem;
}
.logo {
font-size: 1.6rem;
}
.nav-cart-icon {
font-size: 1.6rem;
}
.nav-cart-count {
width: 22px;
height: 22px;
font-size: 0.7rem;
}
.products-grid {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.product-card__image-container {
height: 220px;
}
.product-card__info {
padding: 1.25rem;
}
.cart-panel {
max-width: 100%;
border-radius: 0;
border-left: none;
}
.cart-item {
flex-direction: column;
align-items: stretch;
padding: 1rem;
}
.cart-item__img-container {
width: 100%;
height: 150px;
margin-bottom: 1rem;
}
.cart-item__info {
width: 100%;
}
.cart-panel__title {
font-size: 1.4rem;
}
}
JAVASCRIPT (javascript)
document.addEventListener("DOMContentLoaded", () => {
const productData = [
{
id: "prod1",
name: "Cosmic Voyager VR Headset",
price: 349.99,
image:
"https://images.unsplash.com/photo-1593508512255-86ab42a8e620?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8dnIlMjBoZWFkc2V0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60",
description:
"Explore new dimensions with ultra-HD visuals and immersive audio.",
isNew: true,
},
{
id: "prod2",
name: "NovaBeat Wireless Earbuds",
price: 129.5,
image:
"https://images.unsplash.com/photo-1618384887929-16ec33fab9ef?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTB8fHdpcmVsZXNzJTIwZWFyYnVkc3xlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=500&q=60",
description:
"Crystal-clear sound with adaptive noise cancellation technology.",
isNew: true,
},
{
id: "prod3",
name: "Orion Pro Gaming Mouse",
price: 79.0,
image:
"https://images.unsplash.com/photo-1623820919239-0d0ff10797a1?fm=jpg&q=60&w=3000&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjB8fGdhbWluZyUyMG1vdXNlfGVufDB8fDB8fHww",
description:
"Precision tracking and customizable buttons for elite gaming.",
isNew: false,
},
{
id: "prod4",
name: "Aether Smart Water Bottle",
price: 55.25,
image:
"https://images.unsplash.com/photo-1602143407151-7111542de6e8?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8d2F0ZXIlMjBib3R0bGV8ZW58MHx8MHx8fDA%3D&auto=format&fit=crop&w=500&q=60",
description: "Tracks hydration and glows to remind you to drink.",
isNew: true,
},
{
id: "prod5",
name: "Helios Portable Solar Charger",
price: 99.99,
image:
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR1KwLSj4W7k3wIBk5MFEFvpx3JEt24MrAyMw&s",
description:
"Charge your devices on the go with sustainable solar power.",
isNew: false,
},
{
id: "prod6",
name: "Nebula Ambient Desk Light",
price: 65.0,
image:
"https://m.media-amazon.com/images/I/71UHTy82nbL._AC_UF1000,1000_QL80_.jpg",
description: "Create the perfect mood with customizable RGB lighting.",
isNew: true,
},
];
let cart = [];
const productsGrid = document.getElementById("productsGrid");
const navCartIconWrapper = document.getElementById("navCartIconWrapper");
const navCartCount = document.getElementById("navCartCount");
const cartOverlay = document.getElementById("cartOverlay");
const cartPanel = document.getElementById("cartPanel");
const closeCartBtn = document.getElementById("closeCartBtn");
const cartItemsContainer = document.getElementById("cartItemsContainer");
const emptyCartMessage = document.getElementById("emptyCartMessage");
const cartTotalAmount = document.getElementById("cartTotalAmount");
const checkoutBtn = document.getElementById("checkoutBtn");
function renderProducts() {
productsGrid.innerHTML = "";
productData.forEach((product) => {
const productCard = document.createElement("div");
productCard.className = "product-card";
productCard.innerHTML = `
<div class="product-card__image-container">
${
product.isNew
? '<span class="product-card__badge">Flux Pick</span>'
: ""
}
<img src="${product.image}" alt="${
product.name
}" id="product-img-${product.id}">
</div>
<div class="product-card__info">
<h3 class="product-card__title">${product.name}</h3>
<p class="product-card__description">${
product.description
}</p>
<div class="product-card__footer">
<span class="product-card__price">$${product.price.toFixed(
2
)}</span>
<button class="add-to-cart-btn" data-id="${
product.id
}" data-img-src="${product.image}">
<span class="btn-content-initial">
<i class="fas fa-cart-plus btn-icon-initial"></i>
<span class="btn-text-initial">Add to Cart</span>
</span>
<span class="btn-content-loading" style="opacity:0; pointer-events:none;">
<i class="fas fa-spinner fa-spin"></i>
<img src="" alt="preview" class="btn-thumbnail-preview" style="display:none;">
<span class="btn-item-count" style="display:none;"></span>
</span>
<span class="btn-content-added" style="opacity:0; pointer-events:none;">
<i class="fas fa-check-circle"></i>
<span>Added!</span>
</span>
</button>
</div>
</div>
`;
productsGrid.appendChild(productCard);
});
}
function animateButtonMorph(button, productId, productImgSrc) {
const initialContent = button.querySelector(".btn-content-initial");
const loadingContent = button.querySelector(".btn-content-loading");
const addedContent = button.querySelector(".btn-content-added");
const previewImg = loadingContent.querySelector(".btn-thumbnail-preview");
const itemCountSpan = loadingContent.querySelector(".btn-item-count");
previewImg.src = productImgSrc;
button.disabled = true;
const tl = gsap.timeline({
onComplete: () => {
// Re-enable button after full sequence
gsap.delayedCall(1, () => {
// Keep "Added!" for 1s
gsap.to(addedContent, {
opacity: 0,
y: 10,
duration: 0.2,
ease: "power2.in",
onComplete: () => (addedContent.style.pointerEvents = "none"),
});
gsap.fromTo(
initialContent,
{ y: -10, opacity: 0 },
{
y: 0,
opacity: 1,
duration: 0.3,
ease: "power2.out",
delay: 0.1,
onComplete: () => (button.disabled = false),
}
);
});
},
});
// 1. Initial to Loading (Spinner only)
tl.to(initialContent, {
opacity: 0,
y: 10,
duration: 0.2,
ease: "power2.in",
})
.set(loadingContent, { opacity: 1, y: -10, pointerEvents: "auto" })
.set(previewImg, { display: "none", opacity: 0 })
.set(itemCountSpan, { display: "none", opacity: 0 })
.to(loadingContent, { y: 0, duration: 0.2, ease: "power2.out" });
// 2. Show Thumbnail and Count in Button (Morph part)
tl.to(
button,
{
minWidth: button.offsetWidth + 40,
duration: 0.3,
ease: "power2.out",
},
"-=0.1"
) // Expand button
.set(previewImg, { display: "inline-block" })
.to(
previewImg,
{
opacity: 1,
scale: 1,
x: 0,
duration: 0.3,
ease: "back.out(1.7)",
},
"-=0.2"
)
.set(itemCountSpan, { display: "inline-block", innerText: "+1" })
.to(itemCountSpan, { opacity: 1, duration: 0.2 }, "-=0.1");
// 3. Fly thumbnail to main cart
tl.add(() => flyProductToCart(productId, button), "+=0.3"); // Delay slightly before flying
// 4. Loading to Added
tl.to(loadingContent, {
opacity: 0,
y: 10,
duration: 0.2,
ease: "power2.in",
delay: 0.5,
}) // Wait for fly animation to be significant
.set(button, { minWidth: "160px" }) // Reset button width
.set(addedContent, { opacity: 1, y: -10, pointerEvents: "auto" })
.to(addedContent, {
y: 0,
duration: 0.3,
ease: "elastic.out(1, 0.7)",
});
return tl;
}
function flyProductToCart(productId, buttonElement) {
const productCardImg = document.getElementById(`product-img-${productId}`);
if (!productCardImg) return;
const btnPreviewImg = buttonElement.querySelector(".btn-thumbnail-preview");
const btnPreviewRect = btnPreviewImg.getBoundingClientRect();
const cartIconRect = navCartIconWrapper.getBoundingClientRect();
const flyingThumb = document.createElement("div");
flyingThumb.className = "flying-product-thumbnail";
flyingThumb.innerHTML = `<img src="${btnPreviewImg.src}" alt="flying item">`;
document.body.appendChild(flyingThumb);
gsap.set(flyingThumb, {
left: btnPreviewRect.left,
top: btnPreviewRect.top,
width: btnPreviewRect.width,
height: btnPreviewRect.height,
});
gsap.to(flyingThumb, {
left: cartIconRect.left + cartIconRect.width / 2 - 15, // 15 is half of target 30px
top: cartIconRect.top + cartIconRect.height / 2 - 15,
width: 30,
height: 30,
opacity: 0.5,
scale: 0.5,
rotation: gsap.utils.random(-180, 180),
duration: 0.8,
ease: "power3.inOut",
onComplete: () => {
flyingThumb.remove();
gsap.fromTo(
navCartIconWrapper.querySelector(".nav-cart-icon"),
{ scale: 1 },
{
scale: 1.5,
duration: 0.4,
ease: "elastic.out(1.2, 0.5)",
yoyo: true,
repeat: 1,
}
);
},
});
}
function handleAddToCart(event) {
const button = event.target.closest(".add-to-cart-btn");
if (!button || button.disabled) return;
const productId = button.dataset.id;
const productImgSrc = button.dataset.imgSrc; // Get img from data attribute for consistency
const product = productData.find((p) => p.id === productId);
if (!product) return;
animateButtonMorph(button, productId, productImgSrc);
const existingItem = cart.find((item) => item.id === productId);
if (existingItem) {
existingItem.quantity++;
} else {
cart.push({ ...product, quantity: 1 });
}
// Update cart UI elements (count, total) slightly delayed to sync with animations
gsap.delayedCall(0.8, updateCart); // Delay to sync with flying animation
}
function updateCart() {
renderCartItems();
updateCartCount();
updateCartTotal();
updateCheckoutButtonState();
}
function renderCartItems() {
if (cart.length === 0) {
emptyCartMessage.style.display = "flex";
cartItemsContainer.innerHTML = "";
if (!cartItemsContainer.contains(emptyCartMessage)) {
cartItemsContainer.appendChild(emptyCartMessage);
}
} else {
emptyCartMessage.style.display = "none";
cartItemsContainer.innerHTML = "";
cart.forEach((item) => {
const cartItemDiv = document.createElement("div");
cartItemDiv.className = "cart-item";
cartItemDiv.style.opacity = 0; // For entry animation
cartItemDiv.innerHTML = `
<div class="cart-item__img-container">
<img src="${item.image}" alt="${item.name}">
</div>
<div class="cart-item__info">
<div>
<h4 class="cart-item__title">${item.name}</h4>
<p class="cart-item__price">$${(
item.price * item.quantity
).toFixed(2)}</p>
</div>
<div class="cart-item__quantity-controls">
<button class="quantity-btn decrease-qty" data-id="${
item.id
}"><i class="fas fa-minus"></i></button>
<span class="cart-item__quantity">${
item.quantity
}</span>
<button class="quantity-btn increase-qty" data-id="${
item.id
}"><i class="fas fa-plus"></i></button>
</div>
</div>
<button class="cart-item__remove-btn" data-id="${
item.id
}"><i class="fas fa-times"></i></button>
`;
cartItemsContainer.appendChild(cartItemDiv);
gsap.to(cartItemDiv, {
opacity: 1,
y: 0,
duration: 0.4,
delay: cart.indexOf(item) * 0.05,
ease: "power2.out",
});
});
}
}
function updateCartCount() {
const totalItems = cart.reduce((sum, item) => sum + item.quantity, 0);
navCartCount.textContent = totalItems;
if (totalItems > 0 || navCartCount.textContent !== "0") {
// Animate only if changed
gsap.fromTo(
navCartCount,
{ scale: 1.8, opacity: 0 },
{
scale: 1,
opacity: 1,
duration: 0.5,
ease: "elastic.out(1, 0.6)",
}
);
}
}
function updateCartTotal() {
const total = cart.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
cartTotalAmount.textContent = `$${total.toFixed(2)}`;
}
function updateCheckoutButtonState() {
checkoutBtn.disabled = cart.length === 0;
}
function handleCartActions(event) {
const target = event.target.closest("button");
if (!target) return;
const productId = target.dataset.id;
let itemChanged = false;
if (target.classList.contains("increase-qty")) {
const item = cart.find((i) => i.id === productId);
if (item) {
item.quantity++;
itemChanged = true;
}
} else if (target.classList.contains("decrease-qty")) {
const itemIndex = cart.findIndex((i) => i.id === productId);
if (itemIndex > -1) {
cart[itemIndex].quantity--;
itemChanged = true;
if (cart[itemIndex].quantity <= 0) {
// Animate removal
const itemDiv = target.closest(".cart-item");
gsap.to(itemDiv, {
opacity: 0,
x: -50,
duration: 0.3,
ease: "power2.in",
onComplete: () => {
cart.splice(itemIndex, 1);
updateCart(); // Update after animation
},
});
return; // Avoid immediate update
}
}
} else if (target.classList.contains("cart-item__remove-btn")) {
const itemIndex = cart.findIndex((i) => i.id === productId);
if (itemIndex > -1) {
const itemDiv = target.closest(".cart-item");
gsap.to(itemDiv, {
opacity: 0,
scale: 0.8,
duration: 0.3,
ease: "power2.in",
onComplete: () => {
cart.splice(itemIndex, 1);
updateCart();
},
});
return;
}
}
if (itemChanged) updateCart();
}
function openCartPanel() {
cartOverlay.classList.add("active");
gsap.to(cartPanel, { right: 0, duration: 0.7, ease: "expo.out" });
gsap.to(cartOverlay, {
opacity: 1,
duration: 0.5,
ease: "power2.out",
});
}
function closeCartPanel() {
gsap.to(cartPanel, {
right: "-100%",
duration: 0.6,
ease: "expo.in",
onComplete: () => {},
});
gsap.to(cartOverlay, {
opacity: 0,
duration: 0.4,
ease: "power2.in",
onComplete: () => cartOverlay.classList.remove("active"),
});
}
// Event Listeners
productsGrid.addEventListener("click", handleAddToCart);
navCartIconWrapper.addEventListener("click", openCartPanel);
closeCartBtn.addEventListener("click", closeCartPanel);
cartOverlay.addEventListener("click", closeCartPanel);
cartItemsContainer.addEventListener("click", handleCartActions);
checkoutBtn.addEventListener("click", () => {
if (cart.length > 0) {
alert(
`Initiating checkout for $${cartTotalAmount.textContent}.\n(Zenith Store: Demo only)`
);
cart = [];
updateCart();
closeCartPanel();
}
});
renderProducts();
updateCart();
});
Download Source Code
Get the complete source code for this tutorial to use in your projects.
Comments (0)
No comments yet. Be the first to comment!