UI Components Medium
Shopping Cart
Slide-out shopping cart drawer with item list, quantity controls, remove items, coupon code input, order summary, and checkout CTA. No dependencies.
Open in Lab
MCP
css vanilla-js
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg: #0f1117;
--surface: #16181f;
--surface2: #1e2130;
--border: #2a2d3a;
--text: #e2e8f0;
--text-muted: #64748b;
--accent: #818cf8;
--accent-hover: #a5b4fc;
--green: #34d399;
--red: #f87171;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
/* Overlay */
.cart-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.55);
z-index: 100;
}
.cart-overlay.visible {
display: block;
}
/* Shop page */
.shop-page {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
}
.shop-header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 60px;
background: var(--surface);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
z-index: 50;
}
.shop-logo {
font-size: 1rem;
font-weight: 700;
color: var(--accent);
text-decoration: none;
}
.shop-hint {
color: var(--text-muted);
font-size: 0.875rem;
margin-top: 60px;
}
.cart-trigger {
position: relative;
width: 42px;
height: 42px;
border-radius: 10px;
background: var(--surface2);
border: 1px solid var(--border);
color: var(--text);
cursor: pointer;
display: grid;
place-items: center;
transition: border-color .15s;
}
.cart-trigger:hover {
border-color: var(--accent);
color: var(--accent);
}
.cart-trigger-badge {
position: absolute;
top: -6px;
right: -6px;
min-width: 18px;
height: 18px;
background: var(--accent);
border-radius: 999px;
font-size: 0.62rem;
font-weight: 700;
color: #fff;
display: grid;
place-items: center;
padding: 0 4px;
border: 2px solid var(--bg);
}
/* Drawer */
.cart-drawer {
position: fixed;
top: 0;
right: 0;
height: 100vh;
width: 400px;
max-width: 95vw;
background: var(--surface);
border-left: 1px solid var(--border);
z-index: 200;
display: flex;
flex-direction: column;
transform: translateX(100%);
transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1);
}
.cart-drawer.open {
transform: none;
}
.cart-header {
padding: 18px 20px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
.cart-title {
font-size: 1rem;
font-weight: 700;
display: flex;
align-items: center;
gap: 8px;
}
.cart-count {
font-size: 0.72rem;
font-weight: 700;
background: var(--accent);
color: #fff;
border-radius: 999px;
padding: 1px 7px;
}
.cart-close {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: 4px;
border-radius: 6px;
display: grid;
place-items: center;
transition: color .15s;
}
.cart-close:hover {
color: var(--text);
}
.cart-items {
flex: 1;
overflow-y: auto;
padding: 16px 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
.cart-items::-webkit-scrollbar {
width: 4px;
}
.cart-items::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 2px;
}
/* Item */
.cart-item {
display: flex;
gap: 12px;
animation: itemIn .2s ease;
}
@keyframes itemIn {
from {
opacity: 0;
transform: translateX(8px);
}
to {
opacity: 1;
transform: none;
}
}
.item-thumb {
width: 60px;
height: 60px;
border-radius: 8px;
display: grid;
place-items: center;
font-size: 1.6rem;
flex-shrink: 0;
}
.item-info {
flex: 1;
min-width: 0;
}
.item-name {
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 3px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item-variant {
font-size: 0.72rem;
color: var(--text-muted);
margin-bottom: 8px;
}
.item-controls {
display: flex;
align-items: center;
gap: 10px;
}
.qty-btn {
width: 24px;
height: 24px;
border-radius: 6px;
border: 1px solid var(--border);
background: var(--surface2);
color: var(--text);
font-size: 1rem;
cursor: pointer;
display: grid;
place-items: center;
transition: border-color .15s;
line-height: 1;
}
.qty-btn:hover {
border-color: var(--accent);
color: var(--accent);
}
.qty-num {
font-size: 0.875rem;
font-weight: 600;
min-width: 20px;
text-align: center;
}
.item-remove {
background: none;
border: none;
color: var(--text-muted);
font-size: 0.78rem;
cursor: pointer;
font-family: inherit;
transition: color .15s;
margin-left: auto;
}
.item-remove:hover {
color: var(--red);
}
.item-price {
font-size: 0.875rem;
font-weight: 700;
color: var(--text);
align-self: center;
white-space: nowrap;
}
/* Empty */
.cart-empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 24px;
}
.cart-empty-icon {
font-size: 3rem;
}
.cart-empty p {
color: var(--text-muted);
font-size: 0.875rem;
}
.cart-empty-btn {
background: var(--accent);
border: none;
border-radius: 9px;
color: #fff;
font-size: 0.875rem;
font-weight: 600;
padding: 10px 20px;
cursor: pointer;
font-family: inherit;
}
/* Footer summary */
.cart-footer {
padding: 16px 20px;
border-top: 1px solid var(--border);
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 12px;
}
.coupon-row {
display: flex;
gap: 8px;
}
.coupon-input {
flex: 1;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
font-size: 0.82rem;
padding: 8px 12px;
outline: none;
font-family: inherit;
transition: border-color .15s;
text-transform: uppercase;
}
.coupon-input::placeholder {
text-transform: none;
color: var(--text-muted);
}
.coupon-input:focus {
border-color: var(--accent);
}
.coupon-btn {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
font-size: 0.82rem;
font-weight: 600;
padding: 0 14px;
cursor: pointer;
font-family: inherit;
white-space: nowrap;
transition: border-color .15s;
}
.coupon-btn:hover {
border-color: var(--accent);
color: var(--accent);
}
.coupon-msg {
font-size: 0.78rem;
margin-top: -4px;
}
.coupon-msg.ok {
color: var(--green);
}
.coupon-msg.err {
color: var(--red);
}
.summary {
display: flex;
flex-direction: column;
gap: 6px;
}
.summary-row {
display: flex;
justify-content: space-between;
font-size: 0.82rem;
color: var(--text-muted);
}
.summary-row--total {
font-size: 1rem;
font-weight: 700;
color: var(--text);
margin-top: 4px;
padding-top: 8px;
border-top: 1px solid var(--border);
}
.discount-label {
color: var(--green);
}
.discount-val {
color: var(--green);
}
.checkout-btn {
width: 100%;
background: var(--accent);
border: none;
border-radius: 9px;
color: #fff;
font-size: 0.9rem;
font-weight: 700;
padding: 12px;
cursor: pointer;
font-family: inherit;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: background .15s;
}
.checkout-btn:hover {
background: var(--accent-hover);
}
/* Undo toast */
.undo-toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 10px;
padding: 10px 16px;
display: flex;
align-items: center;
gap: 12px;
font-size: 0.82rem;
z-index: 300;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
animation: toastIn .2s ease;
}
@keyframes toastIn {
from {
opacity: 0;
transform: translateX(-50%) translateY(10px);
}
to {
opacity: 1;
transform: translateX(-50%);
}
}
.undo-btn {
background: none;
border: none;
color: var(--accent);
font-size: 0.82rem;
font-weight: 700;
cursor: pointer;
font-family: inherit;
}let items = [
{
id: 1,
name: "Pro UI Component Pack",
variant: "License · Indigo",
price: 29,
qty: 1,
bg: "linear-gradient(135deg,#818cf8,#6366f1)",
icon: "PRO",
},
{
id: 2,
name: "GSAP Animation Bundle",
variant: "Full Access",
price: 49,
qty: 1,
bg: "linear-gradient(135deg,#f59e0b,#fb923c)",
icon: "⚡",
},
{
id: 3,
name: "React Starter Kit",
variant: "Standard · Green",
price: 19,
qty: 2,
bg: "linear-gradient(135deg,#34d399,#059669)",
icon: "🚀",
},
];
let discount = 0,
freeShipping = false,
removedItem = null,
undoTimer = null;
const SHIPPING = 9;
const drawer = document.getElementById("cartDrawer");
const overlay = document.getElementById("cartOverlay");
const trigger = document.getElementById("cartTrigger");
const closeBtn = document.getElementById("cartClose");
const closeBtn2 = document.getElementById("cartClose2");
const couponInput = document.getElementById("couponInput");
const couponBtn = document.getElementById("couponBtn");
const couponMsg = document.getElementById("couponMsg");
function openCart() {
drawer.classList.add("open");
overlay.classList.add("visible");
}
function closeCart() {
drawer.classList.remove("open");
overlay.classList.remove("visible");
}
trigger?.addEventListener("click", openCart);
closeBtn?.addEventListener("click", closeCart);
closeBtn2?.addEventListener("click", closeCart);
overlay?.addEventListener("click", closeCart);
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") closeCart();
});
function updateBadge() {
const total = items.reduce((a, i) => a + i.qty, 0);
document.getElementById("cartCount").textContent = total;
document.getElementById("triggerBadge").textContent = total;
}
function recalc() {
const subtotal = items.reduce((a, i) => a + i.price * i.qty, 0);
const discountAmt = Math.round(subtotal * discount);
const shipping = freeShipping || subtotal === 0 ? 0 : SHIPPING;
const total = subtotal - discountAmt + shipping;
document.getElementById("subtotal").textContent = "$" + subtotal.toFixed(2);
document.getElementById("shipping").textContent =
shipping === 0 ? (subtotal ? "FREE" : "—") : "$" + shipping.toFixed(2);
document.getElementById("total").textContent = "$" + (total > 0 ? total : 0).toFixed(2);
const discRow = document.getElementById("discountRow");
discRow.hidden = !discountAmt;
document.getElementById("discount").textContent = "-$" + discountAmt.toFixed(2);
}
function renderItems() {
const list = document.getElementById("cartItems");
const empty = document.getElementById("cartEmpty");
const footer = document.getElementById("cartFooter");
list.innerHTML = "";
if (!items.length) {
list.hidden = true;
empty.hidden = false;
footer.hidden = true;
updateBadge();
return;
}
list.hidden = false;
empty.hidden = true;
footer.hidden = false;
items.forEach((item) => {
const el = document.createElement("div");
el.className = "cart-item";
el.dataset.id = item.id;
el.innerHTML = `
<div class="item-thumb" style="background:${item.bg}">${item.icon}</div>
<div class="item-info">
<div class="item-name">${item.name}</div>
<div class="item-variant">${item.variant}</div>
<div class="item-controls">
<button class="qty-btn" data-action="dec" data-id="${item.id}">−</button>
<span class="qty-num">${item.qty}</span>
<button class="qty-btn" data-action="inc" data-id="${item.id}">+</button>
<button class="item-remove" data-id="${item.id}">Remove</button>
</div>
</div>
<div class="item-price">$${(item.price * item.qty).toFixed(2)}</div>
`;
list.appendChild(el);
});
// Events
list.querySelectorAll(".qty-btn").forEach((btn) => {
btn.addEventListener("click", () => {
const item = items.find((i) => i.id === +btn.dataset.id);
if (!item) return;
if (btn.dataset.action === "inc") item.qty++;
else if (btn.dataset.action === "dec" && item.qty > 1) item.qty--;
renderItems();
recalc();
});
});
list.querySelectorAll(".item-remove").forEach((btn) => {
btn.addEventListener("click", () => removeItem(+btn.dataset.id));
});
updateBadge();
recalc();
}
function removeItem(id) {
const idx = items.findIndex((i) => i.id === id);
if (idx === -1) return;
removedItem = { item: { ...items[idx] }, idx };
items.splice(idx, 1);
renderItems();
clearTimeout(undoTimer);
const toast = document.getElementById("undoToast");
toast.hidden = false;
undoTimer = setTimeout(() => {
toast.hidden = true;
removedItem = null;
}, 4000);
}
document.getElementById("undoBtn")?.addEventListener("click", () => {
if (!removedItem) return;
items.splice(removedItem.idx, 0, removedItem.item);
removedItem = null;
clearTimeout(undoTimer);
document.getElementById("undoToast").hidden = true;
renderItems();
});
// Coupon
couponBtn?.addEventListener("click", () => {
const code = couponInput.value.trim().toUpperCase();
couponMsg.hidden = false;
if (code === "SAVE10") {
discount = 0.1;
freeShipping = false;
couponMsg.className = "coupon-msg ok";
couponMsg.textContent = "✓ 10% discount applied!";
} else if (code === "FREE") {
freeShipping = true;
discount = 0;
couponMsg.className = "coupon-msg ok";
couponMsg.textContent = "✓ Free shipping applied!";
} else {
couponMsg.className = "coupon-msg err";
couponMsg.textContent = "✗ Invalid coupon code.";
discount = 0;
freeShipping = false;
}
recalc();
});
// Checkout
document.getElementById("checkoutBtn")?.addEventListener("click", async () => {
const btn = document.getElementById("checkoutBtn");
const txt = document.getElementById("checkoutText");
btn.disabled = true;
txt.textContent = "Processing…";
await new Promise((r) => setTimeout(r, 1500));
txt.textContent = "✓ Order placed! (demo)";
setTimeout(() => {
txt.textContent = "Proceed to Checkout";
btn.disabled = false;
}, 2000);
});
renderItems();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Shopping Cart</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!-- Cart overlay -->
<div class="cart-overlay" id="cartOverlay"></div>
<!-- ── Cart Drawer ── -->
<div class="cart-drawer" id="cartDrawer" role="dialog" aria-label="Shopping cart" aria-modal="true">
<div class="cart-header">
<h2 class="cart-title">
Shopping Cart
<span class="cart-count" id="cartCount">3</span>
</h2>
<button class="cart-close" id="cartClose" aria-label="Close cart">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
<!-- Items -->
<div class="cart-items" id="cartItems"></div>
<!-- Empty state -->
<div class="cart-empty" id="cartEmpty" hidden>
<div class="cart-empty-icon">🛒</div>
<p>Your cart is empty.</p>
<button class="cart-empty-btn" id="cartClose2">Browse products</button>
</div>
<!-- Summary -->
<div class="cart-footer" id="cartFooter">
<!-- Coupon -->
<div class="coupon-row">
<input type="text" class="coupon-input" id="couponInput" placeholder="Coupon code (SAVE10, FREE)" />
<button class="coupon-btn" id="couponBtn">Apply</button>
</div>
<p class="coupon-msg" id="couponMsg" hidden></p>
<!-- Summary lines -->
<div class="summary">
<div class="summary-row"><span>Subtotal</span><span id="subtotal">—</span></div>
<div class="summary-row" id="discountRow" hidden><span class="discount-label">Discount</span><span
id="discount" class="discount-val">—</span></div>
<div class="summary-row"><span>Shipping</span><span id="shipping">—</span></div>
<div class="summary-row summary-row--total"><span>Total</span><span id="total">—</span></div>
</div>
<button class="checkout-btn" id="checkoutBtn">
<span id="checkoutText">Proceed to Checkout</span>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M5 12h14M12 5l7 7-7 7" />
</svg>
</button>
</div>
</div>
<!-- Page content -->
<main class="shop-page">
<header class="shop-header">
<a href="#" class="shop-logo">✦ StealthShop</a>
<button class="cart-trigger" id="cartTrigger" aria-label="Open cart">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z" />
<line x1="3" y1="6" x2="21" y2="6" />
<path d="M16 10a4 4 0 0 1-8 0" />
</svg>
<span class="cart-trigger-badge" id="triggerBadge">3</span>
</button>
</header>
<p class="shop-hint">Click the cart icon to open the drawer.</p>
</main>
<!-- Undo toast -->
<div class="undo-toast" id="undoToast" hidden>
<span>Item removed</span>
<button class="undo-btn" id="undoBtn">Undo</button>
</div>
<script src="script.js"></script>
</body>
</html>Shopping Cart
A slide-out cart drawer with product item list, +/− quantity controls, item removal with undo toast, coupon code input with validation, live order summary (subtotal, discount, shipping, total), and a checkout CTA button.
Features
- Slide-in drawer from the right with backdrop overlay
- Product items with thumbnail, name, variant, unit price, and quantity stepper
- +/− quantity controls with min 1 enforcement; cart badge updates live
- Remove item with animated slide-out and “Undo” toast (3 s)
- Coupon code input:
SAVE10→ 10% off,FREE→ free shipping - Live order summary: subtotal, discount line, shipping, grand total
- Empty cart state with browse button
- Proceed to Checkout button with simulated loading
How it works
- Cart state is a JS array of
{ id, name, price, qty, image }objects renderCart()rebuilds the item list from state on every changerecalcTotals()sums items, applies coupon discount, calculates shipping, and updates DOM- Remove uses
element.style.maxHeightanimation thensplice()on state array