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:
- #cart-count: Mostrará el número total de productos en el carrito
- #products-list: Contenedor donde generaremos los productos dinámicamente
- #cart-items: Lista de productos añadidos al carrito
- #cart-summary: Resumen con subtotal, IVA y total (oculto si carrito vacío)
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:
- find(): Busca si el producto ya está en el carrito
- Si existe: Solo incrementa la cantidad
- Si no existe: Añade un nuevo objeto al array con cantidad = 1
- saveCart(): Guarda en localStorage
- renderCart(): Actualiza la vista del carrito
- 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:
DOMContentLoadeddisparainit()loadCart()lee localStorage y restaura carrito guardadorenderProducts()muestra productos disponiblesrenderCart()muestra items del carrito (si hay)updateCartCount()actualiza el contador
Cuando añades un producto:
- Click en "Añadir al Carrito" →
addToCart(productId) - Busca si ya existe en el carrito
- Si existe: incrementa cantidad. Si no: añade nuevo item
saveCart()guarda en localStoragerenderCart()actualiza la vistaupdateCartCount()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 APICómo cargar productos desde un servidor y enviar pedidos con fetch