Tutorials 5 min read

How to add items to your cart with Tailwind CSS: JavaScript and Alpine.js

Build an add to cart component with Tailwind CSS, two ways: vanilla JavaScript or Alpine.js. Add items, manage quantities, and watch the total update.

Today we are building an add to cart component: a product list, a cart with quantities, and a running total. We’ll do it twice, first with vanilla JavaScript, then with Alpine.js, so you can pick whichever fits your stack.

What is an add to cart button?

An add to cart button does exactly what it says: when clicked, it adds the product next to it to the cart. It usually carries an “Add to Cart” label or icon, and the cart responds by showing the new item, its quantity, and the updated total price.

Use cases

  • Shopping carts: collect items and quantities before heading to checkout.
  • Product listings: let customers add products straight from the grid without opening each product page.
  • Online stores: keep a visible running total so shoppers always know what they are spending.
  • E-commerce product pages: pair the button with quantity controls for a faster purchase flow.

The markup

  • id="app": the wrapper we hang the whole component on.
  • id="product-list": the list of products you can add from.
  • data-id="1": the product’s id, used by the script to find the right product.
  • <button class="add-to-cart ...">: adds its product to the cart when clicked.
  • id="cart-list": where the cart items are rendered.
  • id="total-price": where the total price is displayed.

Classes are removed for brevity, but I’ll keep those classes relevant to the tutorial.

html
<div id="app">
  <!-- Product List -->
  <ul id="product-list">
    <li data-id="1">
      <div><span>Tomatoes</span> - <span>$10</span></div>
      <button class="add-to-cart ...">Add to Cart</button>
    </li>
    <!-- Add more products here -->
  </ul>
  <!-- Cart -->
  <div>
    <h4>Your items</h4>
    <ul id="cart-list">
      <!-- Cart items will be hardcoded here -->
    </ul>
    <div>Total Price: $ <span id="total-price">0</span></div>
  </div>
</div>

The script

  • document.addEventListener("DOMContentLoaded", function () {: waits until the page is loaded.
  • const products = [...]: the product catalog, each product with an id, a name and a price.
  • const cart = [];: the array that holds whatever the user adds.
  • renderCart(): clears the cart list, rebuilds it from the cart array, wires up the increase, decrease and remove buttons, and updates the total.
  • addToCart(product): bumps the quantity if the item is already in the cart, otherwise pushes it with quantity: 1.
  • removeFromCart(index), increaseQuantity(index), decreaseQuantity(index): manage quantities, removing the item when it would drop below one.
  • totalPrice(): reduces the cart to a single number by multiplying each item’s price by its quantity.
  • The .add-to-cart listeners read data-id from the clicked row, find the matching product, and add it to the cart.
js
document.addEventListener("DOMContentLoaded", function () {
  const products = [
    {
      id: 1,
      name: "Tomatoes",
      price: 10,
    },
    // Add more products here
  ];

  const cart = [];

  function renderCart() {
    const cartList = document.getElementById("cart-list");
    cartList.innerHTML = "";

    cart.forEach((item, index) => {
      const li = document.createElement("li");
      li.innerHTML = `
            <span>${item.name} x${item.quantity}</span> - $<span>${
        item.price * item.quantity
      }</span>
            <button class="increase-quantity ...">+</button>
            <button class="decrease-quantity ...">-</button>
            <button class="remove-from-cart ...">Remove</button>
          `;

      li.querySelector(".increase-quantity").addEventListener("click", () =>
        increaseQuantity(index)
      );
      li.querySelector(".decrease-quantity").addEventListener("click", () =>
        decreaseQuantity(index)
      );
      li.querySelector(".remove-from-cart").addEventListener("click", () =>
        removeFromCart(index)
      );

      cartList.appendChild(li);
    });

    document.getElementById("total-price").textContent = totalPrice();
  }

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

  function removeFromCart(index) {
    cart.splice(index, 1);
    renderCart();
  }

  function increaseQuantity(index) {
    cart[index].quantity++;
    renderCart();
  }

  function decreaseQuantity(index) {
    if (cart[index].quantity > 1) {
      cart[index].quantity--;
    } else {
      removeFromCart(index);
    }
    renderCart();
  }

  function totalPrice() {
    return cart.reduce((total, item) => total + item.price * item.quantity, 0);
  }

  document.querySelectorAll(".add-to-cart").forEach((button) => {
    button.addEventListener("click", (event) => {
      const productId = parseInt(event.target.closest("li").dataset.id);
      const product = products.find((p) => p.id === productId);
      addToCart(product);
    });
  });

  renderCart();
});

The Alpine.js version

Same component, no separate script: the products, the cart and all the methods live in x-data.

  • x-data="{ cart: [], products: [...] }": holds the products, the cart items and their quantities in one place.
  • addToCart(product), removeFromCart(index), increaseQuantity(index), decreaseQuantity(index), totalPrice(): the same cart logic as the vanilla version, defined inside x-data.
  • x-for="product in products": iterates over the products and displays them in a list.
  • x-text="product.name" and x-text="'$' + product.price": display each product’s name and price.
  • @click="addToCart(product)": adds that product to the cart.
  • x-for="(item, index) in cart": iterates over the items in the cart.
  • @click="increaseQuantity(index)", @click="decreaseQuantity(index)", @click="removeFromCart(index)": the quantity and remove buttons.
  • x-text="totalPrice()": keeps the total price in sync.

Classes are removed for brevity, but I’ll keep those classes relevant to the tutorial.

html
<div
  x-data="{
        cart: [],
        products: [
            { id: 1, name: 'Tomatoes', price: 10 },
            // Add more products here
        ],
        addToCart(product) {
            let existingItem = this.cart.find(item => item.id === product.id);
            if (existingItem) {
                existingItem.quantity++;
            } else {
                this.cart.push({ ...product, quantity: 1 });
            }
        },
        removeFromCart(index) {
            this.cart.splice(index, 1);
        },
        increaseQuantity(index) {
            this.cart[index].quantity++;
        },
        decreaseQuantity(index) {
            if (this.cart[index].quantity > 1) {
                this.cart[index].quantity--;
            } else {
                this.removeFromCart(index);
            }
        },
        totalPrice() {
            return this.cart.reduce((total, item) => total + item.price * item.quantity, 0);
        }
    }"
>
  <!-- Product List -->
  <ul>
    <template x-for="product in products" :key="product.id">
      <li>
        <div>
          <span x-text="product.name"></span> -
          <span x-text="'$' + product.price"></span>
        </div>
        <button @click="addToCart(product)">Add to Cart</button>
      </li>
    </template>
  </ul>

  <!-- Cart -->
  <div>
    <h4>Your items</h4>
    <ul>
      <template x-for="(item, index) in cart" :key="index">
        <li>
          <span x-text="`${item.name} x${item.quantity}`"></span> - $<span
            x-text="item.price * item.quantity"
          ></span>
          <button @click="increaseQuantity(index)">+</button>
          <button @click="decreaseQuantity(index)">-</button>
          <button @click="removeFromCart(index)">Remove</button>
        </li>
      </template>
    </ul>
    <div>Total Price: $<span x-text="totalPrice()"></span></div>
  </div>
</div>

You can try the Alpine version separately: live demo and source code.

Which one should you use?

If Alpine is already part of your stack, keeping the cart logic in x-data means there’s no separate script to maintain. If not, the vanilla version does the same job without adding a dependency.

Conclusion

A product list, a handful of functions and a running total: that’s a working cart in whichever flavor your project prefers. It’s a great starting point for more complex carts and checkout flows.

Hope you enjoyed this tutorial and have a great day!

/Michael Andreuzza