How to Build an E-Commerce Store with Shopping Cart

Intermediate 16 views Jul 20, 2026

Introduction

This project moves past static markup into a store that actually behaves like one: products are rendered from a JavaScript data array instead of being hand-typed into the HTML, clicks are handled with event delegation instead of one listener per button, and the cart survives a page refresh using localStorage. If you're comfortable with basic DOM manipulation and want to practice thinking in data + render functions, this is the right next step.

What you'll build:

  • A product grid rendered dynamically from a JavaScript array
  • An "Add to Cart" flow using event delegation (one listener, not six)
  • A slide-in cart sidebar with quantity controls and a remove button
  • A running subtotal and item-count badge that update live
  • Cart state saved to localStorage, so it's still there after a refresh
  • A demo checkout step (a real one needs a payment provider on a server you control โ€” more on that in Part 3)

Part 1: HTML Structure

The markup itself stays intentionally sparse: a header with a cart toggle, an empty #product-grid container, and a cart sidebar with an empty #cart-items container. Both get filled in by script.js โ€” this is the core intermediate idea: the DOM is a rendering target, not something you write out by hand for every product.

index.html

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Ritebook Store</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>

  <header class="site-header">
    <h1>Ritebook Store</h1>
    <button id="cart-toggle" class="cart-toggle">
      ๐Ÿ›’ <span id="cart-count" class="cart-count">0</span>
    </button>
  </header>

  <main>
    <section id="product-grid" class="product-grid">
      <!-- Product cards are rendered here by script.js -->
    </section>
  </main>

  <div id="cart-overlay" class="cart-overlay"></div>

  <aside id="cart-sidebar" class="cart-sidebar">
    <div class="cart-header">
      <h2>Your Cart</h2>
      <button id="cart-close" class="cart-close">ร—</button>
    </div>

    <div id="cart-items" class="cart-items">
      <!-- Cart line items are rendered here by script.js -->
    </div>

    <div class="cart-footer">
      <div class="cart-subtotal">
        <span>Subtotal</span>
        <span id="cart-subtotal">$0.00</span>
      </div>
      <button id="checkout-btn" class="checkout-btn">Checkout</button>
    </div>
  </aside>

  <script src="script.js"></script>
</body>
</html>

Key Points:

  • Empty containers, not empty pages: #product-grid and #cart-items start blank in the markup โ€” script.js owns what goes inside them. Add a seventh product later and you only touch the data array, not the HTML.
  • #cart-overlay: a full-screen dimming layer that sits behind the cart sidebar. Clicking it closes the cart โ€” a common pattern for slide-in panels and modals.
  • data-id everywhere it matters: buttons and containers carry the product's ID as a data attribute, which is what makes event delegation possible in Part 3 โ€” JavaScript reads which product was clicked from the DOM itself instead of needing a separate listener per button.

Part 2: CSS Styling

Two layout techniques carry this file: grid-template-columns: repeat(auto-fill, minmax(...)) for a product grid that reflows itself at any screen width without a single media query, and a right: -380px / right: 0 transition for the cart sidebar's slide-in effect.

style.css

CSS
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: "Segoe UI", Arial, sans-serif;
}

body {
    background: #f7f8fa;
    color: #1e293b;
}

.site-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 18px 32px;
    background: #ffffff;
    border-bottom: 1px solid #e2e8f0;
    position: sticky;
    top: 0;
    z-index: 10;
}

.site-header h1 {
    font-size: 20px;
}

.cart-toggle {
    position: relative;
    background: #3498db;
    color: #ffffff;
    border: none;
    border-radius: 10px;
    padding: 10px 16px;
    font-size: 16px;
    cursor: pointer;
}

.cart-count {
    background: #e74c3c;
    color: #ffffff;
    font-size: 12px;
    font-weight: 700;
    border-radius: 100px;
    padding: 1px 7px;
    margin-left: 4px;
}

/* Product grid: auto-fill + minmax reflows columns without media queries */
.product-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 20px;
    padding: 32px;
    max-width: 1100px;
    margin: 0 auto;
}

.product-card {
    background: #ffffff;
    border-radius: 14px;
    padding: 20px;
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.06);
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.product-image {
    font-size: 48px;
    text-align: center;
    background: #f1f5f9;
    border-radius: 10px;
    padding: 24px 0;
}

.product-name {
    font-size: 16px;
}

.product-description {
    font-size: 13.5px;
    color: #64748b;
    flex-grow: 1;
}

.product-footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 8px;
}

.product-price {
    font-weight: 700;
}

.add-to-cart-btn {
    background: #3498db;
    color: #ffffff;
    border: none;
    border-radius: 8px;
    padding: 8px 14px;
    font-size: 13.5px;
    font-weight: 600;
    cursor: pointer;
    transition: background 0.15s ease;
}

.add-to-cart-btn:hover {
    background: #2980b9;
}

/* Dimmed backdrop behind the cart sidebar */
.cart-overlay {
    position: fixed;
    inset: 0;
    background: rgba(15, 23, 42, 0.4);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s ease;
    z-index: 20;
}

.cart-overlay.visible {
    opacity: 1;
    pointer-events: auto;
}

/* Cart sidebar slides in from off-screen */
.cart-sidebar {
    position: fixed;
    top: 0;
    right: -380px;
    width: 360px;
    max-width: 90vw;
    height: 100vh;
    background: #ffffff;
    box-shadow: -10px 0 30px rgba(0, 0, 0, 0.15);
    transition: right 0.25s ease;
    display: flex;
    flex-direction: column;
    z-index: 30;
}

.cart-sidebar.open {
    right: 0;
}

.cart-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 20px;
    border-bottom: 1px solid #e2e8f0;
}

.cart-close {
    background: none;
    border: none;
    font-size: 22px;
    cursor: pointer;
    color: #64748b;
}

.cart-items {
    flex-grow: 1;
    overflow-y: auto;
    padding: 16px 20px;
}

.cart-empty {
    color: #94a3b8;
    text-align: center;
    margin-top: 40px;
}

.cart-item {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 0;
    border-bottom: 1px solid #f1f5f9;
}

.cart-item-emoji {
    font-size: 28px;
}

.cart-item-details {
    flex-grow: 1;
}

.cart-item-name {
    font-size: 14px;
    font-weight: 600;
}

.cart-item-price {
    font-size: 12.5px;
    color: #64748b;
}

.cart-item-controls {
    display: flex;
    align-items: center;
    gap: 6px;
}

.cart-item-controls button {
    width: 26px;
    height: 26px;
    border: 1px solid #e2e8f0;
    background: #f8fafc;
    border-radius: 6px;
    cursor: pointer;
    font-size: 14px;
}

.remove-btn {
    color: #e74c3c;
    border: none !important;
    background: none !important;
    font-size: 16px !important;
}

.cart-footer {
    padding: 20px;
    border-top: 1px solid #e2e8f0;
}

.cart-subtotal {
    display: flex;
    justify-content: space-between;
    font-weight: 700;
    margin-bottom: 14px;
}

.checkout-btn {
    width: 100%;
    background: #27ae60;
    color: #ffffff;
    border: none;
    border-radius: 10px;
    padding: 12px;
    font-size: 15px;
    font-weight: 700;
    cursor: pointer;
}

.checkout-btn:hover {
    background: #219150;
}

Key Points:

  • repeat(auto-fill, minmax(220px, 1fr)): the grid decides on its own how many columns fit at the current width โ€” no @media breakpoints needed for basic reflow.
  • position: sticky on the header: keeps the cart button reachable while scrolling a long product list.
  • Two-state transitions: both the overlay (opacity) and sidebar (right) animate between a resting state and a .open/.visible class toggled by JavaScript โ€” no animation logic lives in the CSS itself, only the two endpoints.

Part 3: JavaScript Functionality

This is where the project earns "intermediate": products live in one data array, two render functions turn that data (plus the cart array) into DOM, and two click listeners โ€” using event delegation โ€” handle every button in the grid and every button in the cart, no matter how many products or cart lines exist.

script.js

JAVASCRIPT
const PRODUCTS = [
    { id: 1, name: "Wireless Headphones", price: 59.99, emoji: "๐ŸŽง", description: "Over-ear, 30-hour battery life." },
    { id: 2, name: "Canvas Backpack", price: 39.5, emoji: "๐ŸŽ’", description: "Water-resistant, laptop compartment." },
    { id: 3, name: "Stainless Water Bottle", price: 18.0, emoji: "๐Ÿงด", description: "Keeps drinks cold for 24 hours." },
    { id: 4, name: "Polarized Sunglasses", price: 24.99, emoji: "๐Ÿ•ถ", description: "UV400 protection, lightweight frame." },
    { id: 5, name: "Smart Watch", price: 89.0, emoji: "โŒš", description: "Heart-rate tracking, 7-day battery." },
    { id: 6, name: "Running Sneakers", price: 64.99, emoji: "๐Ÿ‘Ÿ", description: "Breathable mesh, cushioned sole." },
    { id: 7, name: "Yoga Mat", price: 28.5, emoji: "๐Ÿง˜", description: "Non-slip, extra thick for joint support." },
    { id: 8, name: "Ceramic Coffee Mug", price: 12.99, emoji: "โ˜•", description: "12oz, microwave and dishwasher safe." },
    { id: 9, name: "LED Desk Lamp", price: 34.0, emoji: "๐Ÿ’ก", description: "Adjustable brightness, USB charging port." },
    { id: 10, name: "Bluetooth Speaker", price: 45.99, emoji: "๐Ÿ”Š", description: "Waterproof, 12-hour playtime." },
    { id: 11, name: "Leather Wallet", price: 32.5, emoji: "๐Ÿ‘š", description: "RFID-blocking, slim bifold design." },
    { id: 12, name: "Travel Duffel Bag", price: 54.0, emoji: "๐Ÿงฒ", description: "Carry-on sized, weekend getaway essential." },
];

const CART_STORAGE_KEY = "myritebook_cart";

let cart = loadCart();

/**
 * Loads the saved cart from localStorage, or returns an empty cart
 * if nothing's saved yet (or the saved value is corrupted).
 */
function loadCart() {
    try {
        const raw = localStorage.getItem(CART_STORAGE_KEY);
        return raw ? JSON.parse(raw) : [];
    } catch (error) {
        return [];
    }
}

function saveCart() {
    localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(cart));
}

function formatCurrency(amount) {
    return "$" + amount.toFixed(2);
}

/**
 * Builds the product grid from the PRODUCTS array. Called once on load โ€”
 * the products themselves never change, so this only needs to run once.
 */
function renderProducts() {
    const grid = document.getElementById("product-grid");
    grid.innerHTML = PRODUCTS.map(product => `
        <article class="product-card" data-id="${product.id}">
            <div class="product-image">${product.emoji}</div>
            <h3 class="product-name">${product.name}</h3>
            <p class="product-description">${product.description}</p>
            <div class="product-footer">
                <span class="product-price">${formatCurrency(product.price)}</span>
                <button class="add-to-cart-btn" data-action="add" data-id="${product.id}">Add to Cart</button>
            </div>
        </article>
    `).join("");
}

function addToCart(productId) {
    const existingItem = cart.find(item => item.id === productId);
    if (existingItem) {
        existingItem.quantity += 1;
    } else {
        cart.push({ id: productId, quantity: 1 });
    }
    saveCart();
    renderCart();
}

function changeQuantity(productId, delta) {
    const item = cart.find(item => item.id === productId);
    if (!item) return;

    item.quantity += delta;
    if (item.quantity <= 0) {
        cart = cart.filter(cartItem => cartItem.id !== productId);
    }
    saveCart();
    renderCart();
}

function removeFromCart(productId) {
    cart = cart.filter(item => item.id !== productId);
    saveCart();
    renderCart();
}

/**
 * Re-renders the cart sidebar, subtotal, and item-count badge from the
 * current `cart` array. Called after every cart mutation โ€” this keeps
 * the DOM as a pure reflection of state, rather than being hand-edited
 * in multiple places.
 */
function renderCart() {
    const cartItemsEl = document.getElementById("cart-items");
    const subtotalEl = document.getElementById("cart-subtotal");
    const countEl = document.getElementById("cart-count");

    if (cart.length === 0) {
        cartItemsEl.innerHTML = `<p class="cart-empty">Your cart is empty.</p>`;
    } else {
        cartItemsEl.innerHTML = cart.map(item => {
            const product = PRODUCTS.find(p => p.id === item.id);
            const lineTotal = product.price * item.quantity;

            return `
                <div class="cart-item" data-id="${item.id}">
                    <span class="cart-item-emoji">${product.emoji}</span>
                    <div class="cart-item-details">
                        <p class="cart-item-name">${product.name}</p>
                        <p class="cart-item-price">${formatCurrency(product.price)} ร— ${item.quantity} = ${formatCurrency(lineTotal)}</p>
                    </div>
                    <div class="cart-item-controls">
                        <button data-action="decrease" data-id="${item.id}">โˆ’</button>
                        <span>${item.quantity}</span>
                        <button data-action="increase" data-id="${item.id}">+</button>
                        <button data-action="remove" data-id="${item.id}" class="remove-btn">ร—</button>
                    </div>
                </div>
            `;
        }).join("");
    }

    const subtotal = cart.reduce((sum, item) => {
        const product = PRODUCTS.find(p => p.id === item.id);
        return sum + product.price * item.quantity;
    }, 0);

    subtotalEl.textContent = formatCurrency(subtotal);

    const itemCount = cart.reduce((sum, item) => sum + item.quantity, 0);
    countEl.textContent = String(itemCount);
}

// One listener for the whole grid, instead of one per "Add to Cart" button.
document.getElementById("product-grid").addEventListener("click", (event) => {
    const button = event.target.closest("[data-action='add']");
    if (!button) return;
    addToCart(Number(button.dataset.id));
});

// Same idea for the cart: one listener handles increase, decrease, and remove
// for every line item, current or future.
document.getElementById("cart-items").addEventListener("click", (event) => {
    const button = event.target.closest("button[data-action]");
    if (!button) return;

    const productId = Number(button.dataset.id);
    const action = button.dataset.action;

    if (action === "increase") changeQuantity(productId, 1);
    if (action === "decrease") changeQuantity(productId, -1);
    if (action === "remove") removeFromCart(productId);
});

const cartSidebar = document.getElementById("cart-sidebar");
const cartOverlay = document.getElementById("cart-overlay");

document.getElementById("cart-toggle").addEventListener("click", () => {
    cartSidebar.classList.add("open");
    cartOverlay.classList.add("visible");
});

function closeCart() {
    cartSidebar.classList.remove("open");
    cartOverlay.classList.remove("visible");
}

document.getElementById("cart-close").addEventListener("click", closeCart);
cartOverlay.addEventListener("click", closeCart);

document.getElementById("checkout-btn").addEventListener("click", () => {
    if (cart.length === 0) {
        alert("Your cart is empty.");
        return;
    }

    const itemCount = cart.reduce((sum, item) => sum + item.quantity, 0);
    const subtotal = cart.reduce((sum, item) => {
        const product = PRODUCTS.find(p => p.id === item.id);
        return sum + product.price * item.quantity;
    }, 0);

    // Demo checkout only โ€” it doesn't process real payments. A real
    // checkout needs a payment provider (e.g. Stripe or Razorpay)
    // integrated on a server you control; never handle card details
    // directly in client-side JavaScript.
    alert(`Order summary:\n${itemCount} item(s)\nTotal: ${formatCurrency(subtotal)}\n\nThanks for your order!`);

    cart = [];
    saveCart();
    renderCart();
    closeCart();
});

renderProducts();
renderCart();

Key Points:

  • Data array + render function, not hand-written HTML: PRODUCTS is the single source of truth. Add, remove, or edit a product by editing the array โ€” renderProducts() handles the rest.
  • Event delegation: both listeners are attached to the container (#product-grid, #cart-items), not to individual buttons. event.target.closest(...) figures out which button was actually clicked, so newly rendered buttons work automatically โ€” no need to re-attach listeners after every re-render.
  • cart as the single source of truth: every mutation function (addToCart, changeQuantity, removeFromCart) edits the cart array, then calls saveCart() and renderCart(). The DOM never gets edited directly in response to a click โ€” it's always a re-render from state.
  • localStorage persistence: saveCart()/loadCart() mean the cart survives a page refresh. The try...catch in loadCart() protects against a corrupted or manually-edited value in storage.
  • The checkout button is a deliberate stub: it shows what a real checkout's data flow looks like (compute totals, confirm, clear cart) without pretending to process a real payment โ€” that requires a payment provider's SDK and a server-side component, which is its own project.

Complete Project Structure

TEXT
ecommerce-store/
โ”œโ”€โ”€ index.html
โ”œโ”€โ”€ style.css
โ””โ”€โ”€ script.js

How to Use

  1. Save the three files in one folder and open index.html in your browser.
  2. Click Add to Cart on any product โ€” the cart badge updates immediately.
  3. Click the cart icon to open the sidebar, then use +/โˆ’ to adjust quantities or the ร— to remove an item.
  4. Refresh the page โ€” your cart is still there, loaded from localStorage.
  5. Click Checkout to see the order summary and clear the cart.

Conclusion

The jump from the calculator projects to this one is really a jump in how state is managed: instead of one display value, you're now keeping an array (cart) in sync with what's on screen every time it changes. That pattern โ€” mutate the data, then re-render from the data โ€” is the same one every major front-end framework is built around, just without the framework. Once this feels natural, building the same store with React or Vue mostly becomes a matter of syntax, not concept.

Technologies Used

HTML5 CSS3 Vanilla JavaScript Web Storage API

Live Output

See the project in action below. This is the live preview of the code you just learned.

Output Preview