The CartPanel component is a full-height sliding panel that displays cart items, allows quantity management, and provides checkout functionality.
Overview
Key features:
- Sliding animation from the right side
- Dark overlay with click-to-close
- Product list with images and details
- Quantity controls (increment/decrement)
- Remove item functionality
- Dynamic total calculation
- GSAP-powered animations
- Responsive design
Props
The CartPanel component does not accept props. It uses the CartContext for all state management.
Cart Context API
The component requires these context values:
Array of cart items with id, nombre, precio, cantidad, imagen
Whether the cart panel is open
Function to open/close the cart panel
Function to remove a product by id
Function to adjust quantity (id, delta)
Total price of all items in cart
Implementation
import { useEffect, useRef } from "react";
import { useCart } from "../../context/CartContext/CartContext.jsx";
import gsap from "gsap";
function CartPanel() {
const { carrito, carritoAbierto, setCarritoAbierto, quitarProducto, cambiarCantidad, total } = useCart();
const panelRef = useRef(null);
const overlayRef = useRef(null);
useEffect(() => {
if (carritoAbierto) {
gsap.to(panelRef.current, { x: 0, duration: 0.4, ease: "power3.out" });
gsap.to(overlayRef.current, { opacity: 1, duration: 0.3, pointerEvents: "auto" });
} else {
gsap.to(panelRef.current, { x: "100%", duration: 0.4, ease: "power3.in" });
gsap.to(overlayRef.current, { opacity: 0, duration: 0.3, pointerEvents: "none" });
}
}, [carritoAbierto]);
return (
<>
{/* Overlay */}
<div
ref={overlayRef}
onClick={() => setCarritoAbierto(false)}
className="fixed inset-0 bg-black/60 z-50"
style={{ opacity: 0, pointerEvents: "none" }}
/>
{/* Panel */}
<div
ref={panelRef}
className="fixed top-0 right-0 h-full w-full max-w-md bg-[#0e0e0e] border-l border-white/10 z-50 flex flex-col"
style={{ transform: "translateX(100%)" }}
>
{/* Header */}
<div className="flex items-center justify-between px-6 py-5 border-b border-white/10">
<h2 className="text-white text-lg font-semibold">Carrito</h2>
<button onClick={() => setCarritoAbierto(false)} className="text-white/40 hover:text-white text-2xl">
✕
</button>
</div>
{/* Products */}
<div className="flex-1 overflow-y-auto px-6 py-4 space-y-4">
{carrito.length === 0 ? (
<p className="text-white/30 text-sm text-center mt-20">Tu carrito está vacío</p>
) : (
carrito.map(prod => (
<div key={prod.id} className="flex gap-4 bg-white/5 rounded-2xl p-4 border border-white/10">
<img src={prod.imagen} alt={prod.nombre} className="w-20 h-20 object-contain rounded-xl" />
<div className="flex-1">
<h3 className="text-white text-sm">{prod.nombre}</h3>
<p className="text-white/40 text-xs">${(prod.precio * prod.cantidad).toLocaleString()}</p>
<div className="flex items-center justify-between mt-2">
<div className="flex items-center gap-3 bg-white/10 rounded-full px-3 py-1">
<button onClick={() => cambiarCantidad(prod.id, -1)}>−</button>
<span>{prod.cantidad}</span>
<button onClick={() => cambiarCantidad(prod.id, 1)}>+</button>
</div>
<button onClick={() => quitarProducto(prod.id)}>Eliminar</button>
</div>
</div>
</div>
))
)}
</div>
{/* Footer */}
{carrito.length > 0 && (
<div className="px-6 py-5 border-t border-white/10">
<div className="flex justify-between">
<span className="text-white/50">Total</span>
<span className="text-white text-xl font-semibold">${total.toLocaleString()}</span>
</div>
<button className="w-full py-4 bg-white text-black font-semibold rounded-full mt-4">
Proceder al pago
</button>
</div>
)}
</div>
</>
);
}
export default CartPanel;
Usage
import CartPanel from './components/CartPanel/CartPanel';
import { CartProvider } from './context/CartContext/CartContext';
function App() {
return (
<CartProvider>
<CartPanel />
{/* Other components */}
</CartProvider>
);
}
Animation System
The panel uses GSAP for smooth slide-in/out animations:
Opening Animation
gsap.to(panelRef.current, { x: 0, duration: 0.4, ease: "power3.out" });
gsap.to(overlayRef.current, { opacity: 1, duration: 0.3, pointerEvents: "auto" });
Closing Animation
gsap.to(panelRef.current, { x: "100%", duration: 0.4, ease: "power3.in" });
gsap.to(overlayRef.current, { opacity: 0, duration: 0.3, pointerEvents: "none" });
The initial state is set with inline styles:
style={{ transform: "translateX(100%)" }} // Panel starts off-screen
style={{ opacity: 0, pointerEvents: "none" }} // Overlay starts invisible
Cart Item Structure
Each cart item should have this structure:
{
id: string | number,
nombre: string,
precio: number,
cantidad: number,
imagen: string
}
Quantity Controls
The quantity controls use the cambiarCantidad function:
<button onClick={() => cambiarCantidad(prod.id, -1)}>−</button>
<span>{prod.cantidad}</span>
<button onClick={() => cambiarCantidad(prod.id, 1)}>+</button>
The delta parameter can be:
1: Increment quantity
-1: Decrement quantity
Styling & Customization
Panel Layout
className="fixed top-0 right-0 h-full w-full max-w-md bg-[#0e0e0e] border-l border-white/10 z-50 flex flex-col"
- Fixed positioning on the right side
- Full height
- Max width of
md (28rem/448px)
- Dark background (#0e0e0e)
- Flexbox column layout
Product Cards
className="flex gap-4 bg-white/5 rounded-2xl p-4 border border-white/10"
- Semi-transparent white background
- Large border radius (rounded-2xl)
- Subtle border for depth
className="w-full py-4 bg-white text-black font-semibold rounded-full hover:bg-white/90 hover:-translate-y-1"
The button includes hover effects:
- Background opacity change
- Upward translation (-translate-y-1)
- Shadow enhancement
The panel is conditionally rendered based on carritoAbierto state from the CartContext.