Carrito de Compra con JavaScript y LocalStorage: Guía Paso a Paso

Crea un carrito de compra funcional desde cero: añade productos, calcula totales, persiste datos y aprende las mejores prácticas sin usar librerías

Cuando empecé con JavaScript, los carritos de compra me parecían magia negra. Hay mil tutoriales pero la mayoría asumen que ya sabes cosas que nadie te enseñó. Este es el tutorial que me hubiera gustado encontrar.

¿Qué vamos a construir?

  • Lista de productos con botón "Añadir al carrito"
  • Carrito que muestra productos añadidos con cantidades
  • Cálculo automático de subtotal, impuestos y total
  • Botones para aumentar/disminuir cantidad
  • Eliminar productos del carrito
  • Persistencia de datos con localStorage
  • Contador visual de items en el carrito

Lo que necesitas entender antes de empezar

Tres conceptos que vas a usar constantemente

LocalStorage: Almacenamiento del navegador que guarda datos como texto. Perfecto para carritos porque los datos persisten incluso si cierras la pestaña.

JSON: Formato para convertir objetos JavaScript a texto (y viceversa). Usaremos JSON.stringify() y JSON.parse().

Array de objetos: Nuestro carrito será un array donde cada elemento es un objeto con información del producto (id, nombre, precio, cantidad).

⚠️ Importante sobre seguridad:

NUNCA uses localStorage para manejar pagos reales. Este tutorial es para el frontend del carrito. Para procesar pagos necesitas backend seguro, HTTPS, y servicios como Stripe o PayPal.

El usuario puede modificar los datos de localStorage desde DevTools. Esto está bien para practicar, pero en producción siempre valida precios y cantidades en el servidor.

Estructura del proyecto

Vamos a crear tres archivos simples:

// Estructura de carpetas
carrito/
  |-- index.html
  |-- style.css
  |-- cart.js

Paso 1: el HTML

Primero creamos el HTML con dos secciones principales: productos disponibles y el carrito.

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Carrito de Compra</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <!-- Header con contador de carrito -->
        <header>
            <h1>Mi Tienda</h1>
            <div class="cart-icon">
                🛒 <span id="cart-count">0</span>
            </div>
        </header>

        <!-- Sección de productos -->
        <section class="products">
            <h2>Productos Disponibles</h2>
            <div id="products-list" class="products-grid">
                <!-- Los productos se generarán con JavaScript -->
            </div>
        </section>

        <!-- Carrito de compras -->
        <section class="cart">
            <h2>Tu Carrito</h2>
            <div id="cart-items">
                <p id="empty-cart">Tu carrito está vacío</p>
            </div>

            <div id="cart-summary" class="cart-summary hidden">
                <div class="summary-row">
                    <span>Subtotal:</span>
                    <span id="subtotal">$0.00</span>
                </div>
                <div class="summary-row">
                    <span>IVA (21%):</span>
                    <span id="tax">$0.00</span>
                </div>
                <div class="summary-row total">
                    <span>Total:</span>
                    <span id="total">$0.00</span>
                </div>
                <button id="clear-cart" class="btn-secondary">Vaciar Carrito</button>
            </div>
        </section>
    </div>

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

Los IDs que vas a usar en JavaScript:

Paso 2: el CSS

Estilos básicos para que la interfaz sea usable en móvil y escritorio:

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Arial', sans-serif;
    background: #f5f5f5;
    padding: 20px;
}

.container {
    max-width: 1200px;
    margin: 0 auto;
}

/* Header */
header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    background: white;
    padding: 20px;
    border-radius: 8px;
    margin-bottom: 30px;
    box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}

.cart-icon {
    font-size: 24px;
    display: flex;
    align-items: center;
    gap: 8px;
}

#cart-count {
    background: #ff6b6b;
    color: white;
    padding: 2px 8px;
    border-radius: 12px;
    font-size: 14px;
    font-weight: bold;
}

/* Grid de productos */
.products-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    gap: 20px;
    margin-bottom: 40px;
}

.product-card {
    background: white;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 2px 5px rgba(0,0,0,0.1);
    text-align: center;
}

.product-card img {
    width: 100%;
    height: 200px;
    object-fit: cover;
    border-radius: 4px;
    margin-bottom: 15px;
}

.product-price {
    color: #2ecc71;
    font-size: 24px;
    font-weight: bold;
    margin: 10px 0;
}

.btn-primary {
    background: #3498db;
    color: white;
    border: none;
    padding: 10px 20px;
    border-radius: 5px;
    cursor: pointer;
    font-size: 16px;
    transition: background 0.3s;
}

.btn-primary:hover {
    background: #2980b9;
}

/* Carrito */
.cart {
    background: white;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}

.cart-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 15px;
    border-bottom: 1px solid #eee;
}

.cart-item-info {
    flex: 1;
}

.quantity-controls {
    display: flex;
    align-items: center;
    gap: 10px;
}

.btn-quantity {
    background: #ecf0f1;
    border: none;
    width: 30px;
    height: 30px;
    border-radius: 50%;
    cursor: pointer;
    font-size: 18px;
}

.btn-remove {
    background: #e74c3c;
    color: white;
    border: none;
    padding: 5px 10px;
    border-radius: 3px;
    cursor: pointer;
}

.cart-summary {
    margin-top: 20px;
    padding-top: 20px;
    border-top: 2px solid #3498db;
}

.summary-row {
    display: flex;
    justify-content: space-between;
    margin-bottom: 10px;
}

.total {
    font-size: 20px;
    font-weight: bold;
    color: #2ecc71;
}

.hidden {
    display: none;
}

.btn-secondary {
    width: 100%;
    margin-top: 15px;
    background: #95a5a6;
    color: white;
    border: none;
    padding: 10px;
    border-radius: 5px;
    cursor: pointer;
}

⚡ Paso 3: JavaScript - La lógica del carrito

Ahora viene la parte más importante. Vamos a crear el archivo cart.js paso a paso:

3.1 - Base de datos de productos

// Base de datos de productos (en la vida real vendría de una API)
const products = [
    {
        id: 1,
        name: "Laptop Gaming",
        price: 899.99,
        image: "https://via.placeholder.com/250x200/3498db/ffffff?text=Laptop"
    },
    {
        id: 2,
        name: "Mouse Inalámbrico",
        price: 29.99,
        image: "https://via.placeholder.com/250x200/2ecc71/ffffff?text=Mouse"
    },
    {
        id: 3,
        name: "Teclado Mecánico",
        price: 79.99,
        image: "https://via.placeholder.com/250x200/e74c3c/ffffff?text=Teclado"
    },
    {
        id: 4,
        name: "Monitor 27 pulgadas",
        price: 249.99,
        image: "https://via.placeholder.com/250x200/9b59b6/ffffff?text=Monitor"
    }
];

// Array del carrito (se sincroniza con localStorage)
let cart = [];

3.2 - Funciones de localStorage

// ✅ BIEN: Guardar carrito en localStorage
function saveCart() {
    localStorage.setItem('cart', JSON.stringify(cart));
}

// ✅ BIEN: Cargar carrito desde localStorage
function loadCart() {
    const savedCart = localStorage.getItem('cart');
    if (savedCart) {
        cart = JSON.parse(savedCart);
    }
}

💡 ¿Por qué JSON.stringify y JSON.parse?

localStorage solo guarda texto. Si intentas guardar un array directamente, se convierte en texto raro tipo "[object Object]".

  • JSON.stringify(cart) - Convierte el array a texto JSON: "[{id:1,qty:2}]"
  • JSON.parse(savedCart) - Convierte el texto JSON de vuelta a array JavaScript

3.3 - Renderizar productos

// Mostrar productos en la página
function renderProducts() {
    const productsContainer = document.getElementById('products-list');
    productsContainer.replaceChildren(); // Limpiar contenedor

    products.forEach(product => {
        const productCard = document.createElement('div');
        productCard.className = 'product-card';

        const img = document.createElement('img');
        img.src = product.image;
        img.alt = product.name;

        const name = document.createElement('h3');
        name.textContent = product.name;

        const price = document.createElement('div');
        price.className = 'product-price';
        price.textContent = `${product.price.toFixed(2)}`;

        const button = document.createElement('button');
        button.className = 'btn-primary';
        button.textContent = 'Añadir al Carrito';
        button.addEventListener('click', () => addToCart(product.id));

        productCard.appendChild(img);
        productCard.appendChild(name);
        productCard.appendChild(price);
        productCard.appendChild(button);

        productsContainer.appendChild(productCard);
    });
}

⚠️ Nota importante sobre createElement:

Usamos createElement en vez de innerHTML por seguridad. Si los datos vienen de usuarios, innerHTML puede permitir ataques XSS.

3.4 - Añadir al carrito

// Añadir producto al carrito
function addToCart(productId) {
    // Buscar si el producto ya está en el carrito
    const existingItem = cart.find(item => item.id === productId);

    if (existingItem) {
        // Si ya existe, incrementar cantidad
        existingItem.quantity++;
    } else {
        // Si no existe, añadir nuevo item
        const product = products.find(p => p.id === productId);
        cart.push({
            id: product.id,
            name: product.name,
            price: product.price,
            quantity: 1
        });
    }

    saveCart();
    renderCart();
    updateCartCount();
}

🔍 Explicación de addToCart:

  1. find(): Busca si el producto ya está en el carrito
  2. Si existe: Solo incrementa la cantidad
  3. Si no existe: Añade un nuevo objeto al array con cantidad = 1
  4. saveCart(): Guarda en localStorage
  5. renderCart(): Actualiza la vista del carrito
  6. updateCartCount(): Actualiza el número en el icono del carrito

3.5 - Renderizar el carrito

// Mostrar items del carrito
function renderCart() {
    const cartContainer = document.getElementById('cart-items');
    const emptyMessage = document.getElementById('empty-cart');
    const summary = document.getElementById('cart-summary');

    cartContainer.replaceChildren();

    if (cart.length === 0) {
        cartContainer.appendChild(emptyMessage);
        summary.classList.add('hidden');
        return;
    }

    summary.classList.remove('hidden');

    cart.forEach(item => {
        const cartItem = document.createElement('div');
        cartItem.className = 'cart-item';

        const info = document.createElement('div');
        info.className = 'cart-item-info';

        const name = document.createElement('strong');
        name.textContent = item.name;

        const price = document.createElement('div');
        price.textContent = `${item.price.toFixed(2)}`;

        info.appendChild(name);
        info.appendChild(price);

        // Controles de cantidad
        const controls = document.createElement('div');
        controls.className = 'quantity-controls';

        const decreaseBtn = document.createElement('button');
        decreaseBtn.className = 'btn-quantity';
        decreaseBtn.textContent = '-';
        decreaseBtn.addEventListener('click', () => updateQuantity(item.id, -1));

        const quantity = document.createElement('span');
        quantity.textContent = item.quantity;

        const increaseBtn = document.createElement('button');
        increaseBtn.className = 'btn-quantity';
        increaseBtn.textContent = '+';
        increaseBtn.addEventListener('click', () => updateQuantity(item.id, 1));

        const removeBtn = document.createElement('button');
        removeBtn.className = 'btn-remove';
        removeBtn.textContent = 'Eliminar';
        removeBtn.addEventListener('click', () => removeFromCart(item.id));

        controls.appendChild(decreaseBtn);
        controls.appendChild(quantity);
        controls.appendChild(increaseBtn);
        controls.appendChild(removeBtn);

        cartItem.appendChild(info);
        cartItem.appendChild(controls);

        cartContainer.appendChild(cartItem);
    });

    updateCartSummary();
}

3.6 - Actualizar cantidad y eliminar

// Actualizar cantidad de un producto
function updateQuantity(productId, change) {
    const item = cart.find(item => item.id === productId);

    if (item) {
        item.quantity += change;

        // Si la cantidad llega a 0, eliminar del carrito
        if (item.quantity <= 0) {
            removeFromCart(productId);
            return;
        }

        saveCart();
        renderCart();
        updateCartCount();
    }
}

// Eliminar producto del carrito
function removeFromCart(productId) {
    cart = cart.filter(item => item.id !== productId);
    saveCart();
    renderCart();
    updateCartCount();
}

// Vaciar todo el carrito
function clearCart() {
    cart = [];
    saveCart();
    renderCart();
    updateCartCount();
}

3.7 - Cálculos y contador

// Actualizar resumen con totales
function updateCartSummary() {
    const subtotal = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
    const tax = subtotal * 0.21; // IVA 21%
    const total = subtotal + tax;

    document.getElementById('subtotal').textContent = `${subtotal.toFixed(2)}`;
    document.getElementById('tax').textContent = `${tax.toFixed(2)}`;
    document.getElementById('total').textContent = `${total.toFixed(2)}`;
}

// Actualizar contador del icono de carrito
function updateCartCount() {
    const totalItems = cart.reduce((sum, item) => sum + item.quantity, 0);
    document.getElementById('cart-count').textContent = totalItems;
}

¿Qué hace reduce()?

reduce() es un método de arrays que acumula valores. Aquí lo usamos para sumar:

  • Subtotal: precio × cantidad de cada item
  • Total items: suma de todas las cantidades

3.8 - Inicialización

// Inicializar la aplicación cuando carga la página
function init() {
    loadCart();
    renderProducts();
    renderCart();
    updateCartCount();

    // Event listener para el botón "Vaciar Carrito"
    document.getElementById('clear-cart').addEventListener('click', clearCart);
}

// Ejecutar cuando el DOM esté listo
document.addEventListener('DOMContentLoaded', init);

El flujo completo: cómo encajan las piezas

Cuando cargas la página:

  1. DOMContentLoaded dispara init()
  2. loadCart() lee localStorage y restaura carrito guardado
  3. renderProducts() muestra productos disponibles
  4. renderCart() muestra items del carrito (si hay)
  5. updateCartCount() actualiza el contador

Cuando añades un producto:

  1. Click en "Añadir al Carrito" → addToCart(productId)
  2. Busca si ya existe en el carrito
  3. Si existe: incrementa cantidad. Si no: añade nuevo item
  4. saveCart() guarda en localStorage
  5. renderCart() actualiza la vista
  6. updateCartCount() actualiza contador

Mejoras que puedes añadir

El carrito base funciona, pero en proyectos reales siempre necesitas más. Aquí van tres extensiones comunes:

1. Límite de stock

// Añadir stock a los productos
const products = [
    { id: 1, name: "Laptop", price: 899.99, stock: 5 }
];

// Validar stock antes de añadir
function addToCart(productId) {
    const product = products.find(p => p.id === productId);
    const existingItem = cart.find(item => item.id === productId);
    const currentQty = existingItem ? existingItem.quantity : 0;

    if (currentQty >= product.stock) {
        alert('No hay más stock disponible');
        return;
    }

    // ... resto del código
}

2. Animaciones al añadir

// En el CSS
@keyframes bounce {
    0%, 100% { transform: scale(1); }
    50% { transform: scale(1.3); }
}

.cart-icon.bounce {
    animation: bounce 0.3s;
}

// En el JavaScript
function addToCart(productId) {
    // ... código existente ...

    // Añadir animación al icono del carrito
    const cartIcon = document.querySelector('.cart-icon');
    cartIcon.classList.add('bounce');
    setTimeout(() => cartIcon.classList.remove('bounce'), 300);
}

3. Descuentos y cupones

let discount = 0;

function applyCoupon(code) {
    const coupons = {
        'DESCUENTO10': 0.10,
        'VERANO25': 0.25
    };

    if (coupons[code]) {
        discount = coupons[code];
        updateCartSummary();
        alert('¡Cupón aplicado!');
    } else {
        alert('Cupón inválido');
    }
}

function updateCartSummary() {
    const subtotal = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
    const discountAmount = subtotal * discount;
    const afterDiscount = subtotal - discountAmount;
    const tax = afterDiscount * 0.21;
    const total = afterDiscount + tax;

    // Actualizar DOM...
}

Los cuatro errores que vas a cometer seguro

🐛 Error #1: "Cannot read property 'push' of null"

Causa: Intentar hacer cart.push() cuando cart no se inicializó correctamente.

Solución: Siempre inicializar: let cart = JSON.parse(localStorage.getItem('cart')) || [];

🐛 Error #2: Los datos no persisten

Causa: Olvidaste llamar saveCart() después de modificar el array.

Solución: Siempre que modifiques cart, llama saveCart() inmediatamente después.

🐛 Error #3: Precios con muchos decimales (19.9999999)

Causa: Operaciones con decimales en JavaScript tienen precisión limitada.

Solución: Usa .toFixed(2) para mostrar siempre 2 decimales: price.toFixed(2)

🐛 Error #4: El carrito se duplica

Causa: No limpiar el contenedor antes de renderizar.

Solución: Usar container.replaceChildren() al inicio de cada render.

Por dónde seguir

El siguiente salto natural es conectar este carrito a un backend real: los productos vendrían de una API, los precios se validarían en el servidor y localStorage solo serviría para el estado temporal de sesión. Si dejas los precios solo en el frontend, cualquiera puede modificarlos desde DevTools antes de llegar al pago.

Para la UI, cambia los alert() por toast notifications — los alert bloquean el hilo y quedan fatal en producción.

Recuerda:

  • localStorage es solo para frontend: Nunca confíes en él para datos críticos
  • Valida en el servidor: Siempre verifica precios y stock en backend
  • HTTPS es obligatorio: Para manejar pagos reales necesitas conexión segura
  • Testing: Prueba con diferentes cantidades, productos, y escenarios extremos

🎯 Si quieres conectar esto con una API real

El siguiente paso concreto es aprender a hacer peticiones HTTP desde el carrito para cargar productos desde un servidor:

👉 JavaScript Fetch API: tu primera llamada a una API

Cómo cargar productos desde un servidor y enviar pedidos con fetch