Shop — Cart / Bag
A polished e-commerce cart and bag page with line items showing product silhouette thumbnails, variant, quantity steppers, and per-line pricing. A promo-code field with live validation, an order summary breaking down subtotal, discount, shipping, and estimated tax, plus a free-shipping progress bar and a sticky secure-checkout panel with trust signals. Quantity edits recompute every total, removing an item offers an undo toast, and an empty-bag state ships built in.
MCP
Code
:root {
--bg: #ffffff;
--panel: #ffffff;
--ink: #16181d;
--muted: #6b7280;
--faint: #9aa1ad;
--brand: #3457ff;
--brand-d: #2742d6;
--sale: #e0245e;
--ok: #1f9d55;
--line: rgba(16, 18, 29, .1);
--line-2: rgba(16, 18, 29, .06);
--soft: #f5f6f9;
--soft-2: #eef0f5;
--ring: 0 0 0 3px rgba(52, 87, 255, .35);
--radius: 16px;
--shadow: 0 1px 2px rgba(16, 18, 29, .05), 0 12px 32px rgba(16, 18, 29, .07);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background:
radial-gradient(1200px 480px at 85% -10%, rgba(52, 87, 255, .06), transparent 60%),
var(--bg);
color: var(--ink);
font-family: "Inter", system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3 { margin: 0; letter-spacing: -.02em; }
a { color: inherit; }
button { font-family: inherit; }
.skip {
position: absolute;
left: -999px;
top: 8px;
background: var(--ink);
color: #fff;
padding: 10px 14px;
border-radius: 10px;
z-index: 50;
text-decoration: none;
}
.skip:focus { left: 12px; }
:focus-visible {
outline: none;
box-shadow: var(--ring);
border-radius: 10px;
}
/* ---------- top bar ---------- */
.topbar {
position: sticky;
top: 0;
z-index: 20;
background: rgba(255, 255, 255, .82);
backdrop-filter: saturate(140%) blur(10px);
border-bottom: 1px solid var(--line);
}
.topbar__in {
max-width: 1120px;
margin: 0 auto;
padding: 14px 20px;
display: flex;
align-items: center;
gap: 20px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 9px;
font-weight: 800;
font-size: 1.05rem;
text-decoration: none;
color: var(--ink);
}
.brand svg { color: var(--brand); }
.topnav {
display: flex;
gap: 22px;
margin-left: 8px;
}
.topnav a {
text-decoration: none;
color: var(--muted);
font-weight: 500;
font-size: .94rem;
padding: 4px 2px;
border-radius: 8px;
}
.topnav a:hover { color: var(--ink); }
.topbar__cart {
margin-left: auto;
position: relative;
display: inline-flex;
color: var(--ink);
}
.topbar__count {
position: absolute;
top: -8px;
right: -10px;
min-width: 18px;
height: 18px;
padding: 0 5px;
border-radius: 999px;
background: var(--brand);
color: #fff;
font-size: .68rem;
font-weight: 700;
display: grid;
place-items: center;
}
/* ---------- layout ---------- */
.wrap {
max-width: 1120px;
margin: 0 auto;
padding: 30px 20px 64px;
}
.head { margin-bottom: 22px; }
.head h1 { font-size: clamp(1.6rem, 4vw, 2.1rem); font-weight: 800; }
.head__sub { margin: 6px 0 0; color: var(--muted); font-size: .95rem; }
.head__sub a { color: var(--brand); font-weight: 600; text-decoration: none; }
.head__sub a:hover { text-decoration: underline; }
.layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 360px;
gap: 28px;
align-items: start;
}
/* ---------- free shipping banner ---------- */
.ship {
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 14px 16px;
margin-bottom: 16px;
background:
linear-gradient(120deg, rgba(31, 157, 85, .08), rgba(52, 87, 255, .05));
display: grid;
grid-template-columns: 1fr auto;
grid-template-areas: "msg icons" "track track";
gap: 8px 12px;
align-items: center;
}
.ship.is-free {
background: linear-gradient(120deg, rgba(31, 157, 85, .14), rgba(31, 157, 85, .06));
border-color: rgba(31, 157, 85, .35);
}
.ship__msg { grid-area: msg; margin: 0; font-size: .92rem; color: var(--ink); }
.ship__msg strong { color: var(--ink); }
.ship.is-free .ship__msg strong { color: var(--ok); }
.ship__icons { grid-area: icons; display: inline-flex; gap: 6px; font-size: 1rem; }
.ship__dot { opacity: .35; transition: opacity .2s, transform .25s; }
.ship__dot.is-on { opacity: 1; transform: scale(1.05); }
.ship__track {
grid-area: track;
height: 8px;
border-radius: 999px;
background: rgba(16, 18, 29, .1);
overflow: hidden;
}
.ship__fill {
height: 100%;
width: 0;
border-radius: 999px;
background: linear-gradient(90deg, var(--brand), #5b78ff);
transition: width .4s cubic-bezier(.2, .8, .2, 1);
}
.ship.is-free .ship__fill { background: linear-gradient(90deg, var(--ok), #36c97c); }
/* ---------- line items ---------- */
.list { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
.item {
display: grid;
grid-template-columns: 88px 1fr auto;
gap: 16px;
padding: 16px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--panel);
box-shadow: var(--shadow);
overflow: hidden;
animation: pop .28s ease;
}
.item.is-removing {
animation: collapse .32s ease forwards;
}
@keyframes pop { from { opacity: 0; transform: translateY(6px); } }
@keyframes collapse {
to { opacity: 0; transform: translateX(28px); max-height: 0; padding-top: 0; padding-bottom: 0; margin: 0; }
}
.thumb {
width: 88px;
height: 88px;
border-radius: 12px;
display: grid;
place-items: center;
position: relative;
overflow: hidden;
}
.thumb svg { width: 56%; height: 56%; }
.item__main { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.item__title { font-weight: 700; font-size: 1rem; }
.item__variant { color: var(--muted); font-size: .85rem; }
.item__stock {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: .76rem;
font-weight: 600;
color: var(--ok);
margin-top: 2px;
}
.item__stock::before {
content: "";
width: 7px; height: 7px;
border-radius: 50%;
background: var(--ok);
}
.item__stock.is-low { color: var(--sale); }
.item__stock.is-low::before { background: var(--sale); }
.item__controls {
margin-top: auto;
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
}
.stepper {
display: inline-flex;
align-items: center;
border: 1px solid var(--line);
border-radius: 10px;
overflow: hidden;
}
.stepper button {
width: 32px;
height: 32px;
border: 0;
background: var(--soft);
color: var(--ink);
font-size: 1.05rem;
cursor: pointer;
display: grid;
place-items: center;
transition: background .15s;
}
.stepper button:hover:not(:disabled) { background: var(--soft-2); }
.stepper button:active:not(:disabled) { background: #e2e5ec; }
.stepper button:disabled { color: var(--faint); cursor: not-allowed; }
.stepper output {
min-width: 34px;
text-align: center;
font-weight: 600;
font-variant-numeric: tabular-nums;
font-size: .95rem;
}
.link-btn {
border: 0;
background: none;
color: var(--muted);
font-size: .84rem;
font-weight: 600;
cursor: pointer;
padding: 4px 2px;
border-radius: 8px;
text-decoration: underline;
text-underline-offset: 2px;
}
.link-btn:hover { color: var(--sale); }
.item__price { text-align: right; display: flex; flex-direction: column; align-items: flex-end; gap: 3px; }
.item__line {
font-weight: 800;
font-size: 1.05rem;
font-variant-numeric: tabular-nums;
}
.item__unit { color: var(--faint); font-size: .78rem; }
.item__was {
color: var(--faint);
font-size: .78rem;
text-decoration: line-through;
}
/* ---------- empty ---------- */
.empty {
text-align: center;
padding: 56px 20px;
border: 1px dashed var(--line);
border-radius: var(--radius);
background: var(--panel);
}
.empty__art { font-size: 3rem; }
.empty h2 { margin: 12px 0 6px; font-size: 1.3rem; }
.empty p { color: var(--muted); margin: 0 0 18px; }
/* ---------- summary ---------- */
.summary { position: sticky; top: 92px; }
.summary__card {
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--panel);
padding: 20px;
box-shadow: var(--shadow);
}
.summary__card h2 { font-size: 1.1rem; margin-bottom: 14px; }
.promo { margin-bottom: 16px; }
.promo__label {
display: block;
font-size: .8rem;
font-weight: 600;
color: var(--muted);
margin-bottom: 6px;
}
.promo__row { display: flex; gap: 8px; }
.promo__input {
flex: 1;
min-width: 0;
border: 1px solid var(--line);
border-radius: 10px;
padding: 10px 12px;
font-size: .92rem;
font-family: inherit;
text-transform: uppercase;
letter-spacing: .04em;
color: var(--ink);
background: var(--bg);
}
.promo__input::placeholder { text-transform: none; letter-spacing: normal; color: var(--faint); }
.promo__input:focus-visible { border-color: var(--brand); }
.promo__msg { margin: 8px 0 0; font-size: .82rem; min-height: 1.1em; }
.promo__msg.is-ok { color: var(--ok); }
.promo__msg.is-err { color: var(--sale); }
.promo__chips { display: flex; gap: 7px; margin-top: 10px; }
.chip {
border: 1px dashed var(--line);
background: var(--soft);
color: var(--muted);
font-size: .72rem;
font-weight: 700;
letter-spacing: .03em;
padding: 4px 9px;
border-radius: 999px;
cursor: pointer;
}
.chip:hover { border-color: var(--brand); color: var(--brand); }
.totals {
margin: 0;
padding: 16px 0 4px;
border-top: 1px solid var(--line);
}
.totals__row {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 12px;
padding: 6px 0;
font-size: .94rem;
}
.totals__row dt { color: var(--muted); margin: 0; }
.totals__row dd { margin: 0; font-weight: 600; font-variant-numeric: tabular-nums; }
.totals__row--discount dd { color: var(--ok); }
.totals__tag {
display: inline-block;
margin-left: 6px;
font-size: .68rem;
font-weight: 700;
color: var(--ok);
background: rgba(31, 157, 85, .12);
padding: 1px 7px;
border-radius: 999px;
letter-spacing: .03em;
}
.totals__row--grand {
border-top: 1px solid var(--line);
margin-top: 6px;
padding-top: 12px;
font-size: 1.05rem;
}
.totals__row--grand dt { color: var(--ink); font-weight: 700; }
.totals__row--grand dd { font-size: 1.25rem; font-weight: 800; }
/* ---------- buttons ---------- */
.btn {
border: 1px solid transparent;
border-radius: 12px;
font-weight: 700;
font-size: .95rem;
cursor: pointer;
padding: 11px 16px;
transition: transform .06s, background .15s, box-shadow .15s, border-color .15s;
}
.btn:active { transform: translateY(1px); }
.btn--brand {
background: var(--brand);
color: #fff;
box-shadow: 0 8px 20px rgba(52, 87, 255, .28);
}
.btn--brand:hover { background: var(--brand-d); }
.btn--ghost {
background: var(--soft);
color: var(--ink);
border-color: var(--line);
}
.btn--ghost:hover { background: var(--soft-2); }
.btn--lg {
width: 100%;
margin-top: 18px;
padding: 14px 18px;
font-size: 1rem;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.btn--lg.is-loading { opacity: .8; pointer-events: none; }
.btn__total {
font-variant-numeric: tabular-nums;
padding-left: 10px;
border-left: 1px solid rgba(255, 255, 255, .35);
}
.trust {
list-style: none;
margin: 16px 0 0;
padding: 14px 0 0;
border-top: 1px solid var(--line);
display: grid;
gap: 8px;
}
.trust li {
display: flex;
align-items: center;
gap: 9px;
font-size: .82rem;
color: var(--muted);
}
/* ---------- toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translate(-50%, 16px);
background: var(--ink);
color: #fff;
padding: 12px 14px 12px 18px;
border-radius: 12px;
display: flex;
align-items: center;
gap: 14px;
box-shadow: 0 16px 40px rgba(16, 18, 29, .3);
z-index: 40;
opacity: 0;
transition: opacity .2s, transform .2s;
max-width: calc(100vw - 32px);
}
.toast.is-show { opacity: 1; transform: translate(-50%, 0); }
.toast__msg { font-size: .9rem; }
.toast__btn {
border: 0;
background: rgba(255, 255, 255, .14);
color: #fff;
font-weight: 700;
font-size: .85rem;
padding: 7px 12px;
border-radius: 9px;
cursor: pointer;
}
.toast__btn:hover { background: rgba(255, 255, 255, .24); }
/* ---------- responsive ---------- */
@media (max-width: 900px) {
.layout { grid-template-columns: 1fr; }
.summary { position: static; }
.summary__card { order: 2; }
}
@media (max-width: 560px) {
.topnav { display: none; }
.item {
grid-template-columns: 64px 1fr;
grid-template-areas: "thumb main" "price price";
gap: 12px 14px;
}
.thumb { width: 64px; height: 64px; grid-area: thumb; }
.item__main { grid-area: main; }
.item__price {
grid-area: price;
flex-direction: row;
align-items: center;
justify-content: space-between;
border-top: 1px solid var(--line-2);
padding-top: 12px;
}
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}(function () {
"use strict";
/* ---------- config ---------- */
var TAX_RATE = 0.0825; // 8.25% estimated tax
var SHIP_FLAT = 6.95; // standard shipping
var FREE_SHIP_THRESHOLD = 75; // free shipping over this subtotal
var PROMOS = {
SAVE15: { kind: "percent", value: 0.15, label: "15% off" },
FREESHIP: { kind: "freeship", value: 0, label: "Free shipping" }
};
/* ---------- product silhouette art (inline SVG, no images) ---------- */
function art(kind, c1, c2) {
var shapes = {
headphones:
'<path d="M5 13a7 7 0 0 1 14 0" fill="none" stroke="#fff" stroke-width="1.8" stroke-linecap="round"/>' +
'<rect x="3.5" y="12.5" width="3.6" height="6.5" rx="1.6" fill="#fff"/>' +
'<rect x="16.9" y="12.5" width="3.6" height="6.5" rx="1.6" fill="#fff"/>',
speaker:
'<rect x="7.5" y="3.5" width="9" height="17" rx="3" fill="none" stroke="#fff" stroke-width="1.8"/>' +
'<circle cx="12" cy="14" r="3" fill="#fff"/>' +
'<circle cx="12" cy="7.5" r="1.1" fill="#fff"/>',
watch:
'<rect x="8" y="8" width="8" height="8" rx="2.4" fill="#fff"/>' +
'<path d="M9.5 8l.6-3h3.8l.6 3M9.5 16l.6 3h3.8l.6-3" fill="none" stroke="#fff" stroke-width="1.7" stroke-linecap="round"/>',
lamp:
'<path d="M12 4l5 7H7l5-7z" fill="#fff"/>' +
'<rect x="11.2" y="11" width="1.6" height="7" fill="#fff"/>' +
'<rect x="8.5" y="18" width="7" height="1.8" rx=".9" fill="#fff"/>'
};
return (
'<svg viewBox="0 0 24 24" role="img" aria-hidden="true" focusable="false">' +
(shapes[kind] || shapes.speaker) +
"</svg>"
);
}
/* ---------- state ---------- */
var cart = [
{ id: "p1", name: "Aero Wireless Headphones", variant: "Midnight · Over-ear", price: 189.00, was: 219.00, qty: 1, stock: 12, art: "headphones", g1: "#3457ff", g2: "#7b5bff" },
{ id: "p2", name: "Pebble Portable Speaker", variant: "Sage · 12h battery", price: 64.50, was: null, qty: 2, stock: 3, art: "speaker", g1: "#1f9d55", g2: "#36c97c" },
{ id: "p3", name: "Lumen Desk Lamp", variant: "Warm white · USB-C", price: 42.00, was: 52.00, qty: 1, stock: 24, art: "lamp", g1: "#e0245e", g2: "#ff6f91" }
];
var promo = null; // { code, kind, value, label }
var undoStash = null;
var undoTimer = null;
var toastTimer = null;
/* ---------- helpers ---------- */
function $(id) { return document.getElementById(id); }
function money(n) {
return "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function plural(n, word) { return n + " " + word + (n === 1 ? "" : "s"); }
function compute() {
var subtotal = cart.reduce(function (s, it) { return s + it.price * it.qty; }, 0);
var discount = 0;
var freeShipPromo = false;
if (promo) {
if (promo.kind === "percent") discount = subtotal * promo.value;
else if (promo.kind === "freeship") freeShipPromo = true;
}
var taxedBase = Math.max(0, subtotal - discount);
var qualifiesFree = subtotal >= FREE_SHIP_THRESHOLD || freeShipPromo;
var shipping = cart.length === 0 ? 0 : (qualifiesFree ? 0 : SHIP_FLAT);
var tax = taxedBase * TAX_RATE;
var total = taxedBase + shipping + tax;
return {
subtotal: subtotal,
discount: discount,
shipping: shipping,
tax: tax,
total: total,
qualifiesFree: qualifiesFree,
freeShipPromo: freeShipPromo
};
}
/* ---------- render line items ---------- */
function renderItems() {
var list = $("list");
var empty = $("empty");
if (cart.length === 0) {
list.innerHTML = "";
empty.hidden = false;
$("shipBanner").hidden = true;
return;
}
empty.hidden = true;
$("shipBanner").hidden = false;
list.innerHTML = cart.map(function (it) {
var line = it.price * it.qty;
var lowStock = it.stock <= 5;
var stockTxt = lowStock ? ("Only " + it.stock + " left") : "In stock";
var was = it.was
? '<span class="item__was">' + money(it.was * it.qty) + "</span>"
: "";
return (
'<li class="item" data-id="' + it.id + '">' +
'<span class="thumb" style="background:linear-gradient(135deg,' + it.g1 + ',' + it.g2 + ')">' +
art(it.art) +
"</span>" +
'<div class="item__main">' +
'<span class="item__title">' + it.name + "</span>" +
'<span class="item__variant">' + it.variant + "</span>" +
'<span class="item__stock' + (lowStock ? " is-low" : "") + '">' + stockTxt + "</span>" +
'<div class="item__controls">' +
'<div class="stepper" role="group" aria-label="Quantity for ' + it.name + '">' +
'<button type="button" class="dec" data-id="' + it.id + '" aria-label="Decrease quantity"' +
(it.qty <= 1 ? " disabled" : "") + ">−</button>" +
'<output aria-live="polite">' + it.qty + "</output>" +
'<button type="button" class="inc" data-id="' + it.id + '" aria-label="Increase quantity"' +
(it.qty >= it.stock ? " disabled" : "") + ">+</button>" +
"</div>" +
'<button type="button" class="link-btn rm" data-id="' + it.id + '">Remove</button>' +
"</div>" +
"</div>" +
'<div class="item__price">' +
'<span class="item__line">' + money(line) + "</span>" +
was +
'<span class="item__unit">' + money(it.price) + " each</span>" +
"</div>" +
"</li>"
);
}).join("");
}
/* ---------- render totals + ship bar ---------- */
function renderSummary() {
var t = compute();
var count = cart.reduce(function (s, it) { return s + it.qty; }, 0);
$("navCount").textContent = count;
$("headCount").textContent = plural(count, "item");
$("subtotal").textContent = money(t.subtotal);
$("shipping").textContent = (cart.length && t.shipping === 0) ? "Free" : money(t.shipping);
$("tax").textContent = money(t.tax);
$("total").textContent = money(t.total);
$("ctaTotal").textContent = money(t.total);
var dRow = $("discountRow");
if (t.discount > 0) {
dRow.hidden = false;
$("discount").textContent = "−" + money(t.discount);
$("discountTag").textContent = promo ? promo.code : "";
} else {
dRow.hidden = true;
}
// free-shipping progress bar
var banner = $("shipBanner");
var fill = $("shipFill");
var msg = $("shipMsg");
var truck = $("shipTruck");
var pct, remaining;
if (t.freeShipPromo) {
pct = 100;
msg.innerHTML = "Promo applied — <strong>free shipping</strong> unlocked 🎉";
} else if (t.subtotal >= FREE_SHIP_THRESHOLD) {
pct = 100;
msg.innerHTML = "You've unlocked <strong>free shipping</strong> 🎉";
} else {
remaining = FREE_SHIP_THRESHOLD - t.subtotal;
pct = Math.min(100, (t.subtotal / FREE_SHIP_THRESHOLD) * 100);
msg.innerHTML = "Add <strong>" + money(remaining) + "</strong> more for <strong>free shipping</strong>";
}
fill.style.width = pct + "%";
banner.classList.toggle("is-free", pct >= 100);
truck.classList.toggle("is-on", pct >= 100);
banner.querySelector(".ship__track").setAttribute("aria-valuenow", Math.round(pct));
$("checkout").disabled = cart.length === 0;
}
function renderAll() {
renderItems();
renderSummary();
}
/* ---------- toast ---------- */
function toast(msg, withUndo) {
var el = $("toast");
$("toastMsg").textContent = msg;
$("toastBtn").hidden = !withUndo;
el.hidden = false;
// force reflow so transition runs
void el.offsetWidth;
el.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(hideToast, withUndo ? 5000 : 2600);
}
function hideToast() {
var el = $("toast");
el.classList.remove("is-show");
setTimeout(function () { el.hidden = true; }, 220);
}
/* ---------- quantity ---------- */
function changeQty(id, delta) {
var it = cart.find(function (x) { return x.id === id; });
if (!it) return;
var next = it.qty + delta;
if (next < 1 || next > it.stock) return;
it.qty = next;
renderAll();
}
/* ---------- remove + undo ---------- */
function removeItem(id) {
var li = document.querySelector('.item[data-id="' + id + '"]');
var idx = cart.findIndex(function (x) { return x.id === id; });
if (idx < 0) return;
undoStash = { item: cart[idx], index: idx };
function commit() {
cart.splice(idx, 1);
renderAll();
toast('"' + undoStash.item.name + '" removed', true);
clearTimeout(undoTimer);
undoTimer = setTimeout(function () { undoStash = null; }, 5000);
}
if (li) {
li.classList.add("is-removing");
li.addEventListener("animationend", commit, { once: true });
} else {
commit();
}
}
function undoRemove() {
if (!undoStash) return;
var idx = Math.min(undoStash.index, cart.length);
cart.splice(idx, 0, undoStash.item);
undoStash = null;
clearTimeout(undoTimer);
renderAll();
hideToast();
}
/* ---------- promo ---------- */
function applyPromo(raw) {
var code = (raw || "").trim().toUpperCase();
var msg = $("promoMsg");
msg.className = "promo__msg";
if (!code) {
msg.textContent = "Enter a promo code.";
msg.classList.add("is-err");
return;
}
if (promo && promo.code === code) {
msg.textContent = code + " is already applied.";
msg.classList.add("is-ok");
return;
}
var found = PROMOS[code];
if (!found) {
msg.textContent = "“" + code + "” isn't a valid code.";
msg.classList.add("is-err");
return;
}
promo = { code: code, kind: found.kind, value: found.value, label: found.label };
msg.textContent = "✓ " + found.label + " applied!";
msg.classList.add("is-ok");
renderSummary();
toast(found.label + " applied", false);
}
/* ---------- events ---------- */
function bind() {
// delegated clicks inside the items list
$("list").addEventListener("click", function (e) {
var inc = e.target.closest(".inc");
var dec = e.target.closest(".dec");
var rm = e.target.closest(".rm");
if (inc) changeQty(inc.dataset.id, +1);
else if (dec) changeQty(dec.dataset.id, -1);
else if (rm) removeItem(rm.dataset.id);
});
// promo form
$("promoForm").addEventListener("submit", function (e) {
e.preventDefault();
applyPromo($("promo").value);
});
Array.prototype.forEach.call(document.querySelectorAll(".chip"), function (chip) {
chip.addEventListener("click", function () {
$("promo").value = chip.dataset.fill;
applyPromo(chip.dataset.fill);
});
});
// toast undo
$("toastBtn").addEventListener("click", undoRemove);
// checkout (simulated)
$("checkout").addEventListener("click", function () {
var btn = this;
if (btn.disabled) return;
btn.classList.add("is-loading");
btn.querySelector("span").textContent = "Processing…";
setTimeout(function () {
btn.classList.remove("is-loading");
btn.querySelector("span").textContent = "Secure checkout";
toast("Demo only — no real checkout 🔒", false);
}, 1200);
});
// empty-state browse → reset demo cart
$("browse").addEventListener("click", function () {
location.reload();
});
}
/* ---------- init ---------- */
renderAll();
bind();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Northwind — Your Bag</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip" href="#bag">Skip to your bag</a>
<header class="topbar" role="banner">
<div class="topbar__in">
<a class="brand" href="#" aria-label="Northwind home">
<svg width="22" height="22" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M3 7l9-4 9 4-9 4-9-4z" fill="currentColor" opacity=".9"/>
<path d="M3 7v10l9 4 9-4V7l-9 4-9-4z" fill="currentColor" opacity=".45"/>
</svg>
<span>Northwind</span>
</a>
<nav class="topnav" aria-label="Primary">
<a href="#">New</a>
<a href="#">Audio</a>
<a href="#">Desk</a>
<a href="#">Sale</a>
</nav>
<div class="topbar__cart" aria-live="polite">
<svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M6 6h15l-1.5 9h-12L6 6z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/>
<path d="M6 6L5 3H2" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="9" cy="20" r="1.4" fill="currentColor"/>
<circle cx="18" cy="20" r="1.4" fill="currentColor"/>
</svg>
<span class="topbar__count" id="navCount">3</span>
</div>
</div>
</header>
<main id="bag" class="wrap" role="main">
<div class="head">
<h1>Your bag</h1>
<p class="head__sub"><span id="headCount">3 items</span> · <a href="#">Continue shopping</a></p>
</div>
<div class="layout">
<!-- LINE ITEMS -->
<section class="items" aria-label="Items in your bag">
<div class="ship" id="shipBanner" aria-live="polite">
<p class="ship__msg" id="shipMsg">Add <strong>$30.00</strong> more for <strong>free shipping</strong></p>
<div class="ship__track" role="progressbar" aria-label="Free shipping progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
<div class="ship__fill" id="shipFill"></div>
</div>
<div class="ship__icons" aria-hidden="true">
<span class="ship__dot is-on">🛍️</span>
<span class="ship__dot" id="shipTruck">🚚</span>
</div>
</div>
<ul class="list" id="list"><!-- rendered by JS --></ul>
<div class="empty" id="empty" hidden>
<div class="empty__art" aria-hidden="true">🛒</div>
<h2>Your bag is empty</h2>
<p>Looks like you haven't added anything yet.</p>
<button class="btn btn--brand" type="button" id="browse">Start shopping</button>
</div>
</section>
<!-- SUMMARY -->
<aside class="summary" aria-label="Order summary">
<div class="summary__card">
<h2>Order summary</h2>
<form class="promo" id="promoForm" novalidate>
<label class="promo__label" for="promo">Promo code</label>
<div class="promo__row">
<input class="promo__input" id="promo" name="promo" type="text" inputmode="text"
autocomplete="off" placeholder="e.g. SAVE15" aria-describedby="promoMsg" />
<button class="btn btn--ghost" type="submit" id="applyBtn">Apply</button>
</div>
<p class="promo__msg" id="promoMsg" role="status"></p>
<div class="promo__chips">
<button type="button" class="chip" data-fill="SAVE15">SAVE15</button>
<button type="button" class="chip" data-fill="FREESHIP">FREESHIP</button>
</div>
</form>
<dl class="totals">
<div class="totals__row"><dt>Subtotal</dt><dd id="subtotal">$0.00</dd></div>
<div class="totals__row totals__row--discount" id="discountRow" hidden>
<dt>Discount <span class="totals__tag" id="discountTag"></span></dt>
<dd id="discount">−$0.00</dd>
</div>
<div class="totals__row"><dt>Shipping</dt><dd id="shipping">$0.00</dd></div>
<div class="totals__row"><dt>Estimated tax</dt><dd id="tax">$0.00</dd></div>
<div class="totals__row totals__row--grand"><dt>Total</dt><dd id="total">$0.00</dd></div>
</dl>
<button class="btn btn--brand btn--lg" type="button" id="checkout">
<span>Secure checkout</span>
<span class="btn__total" id="ctaTotal">$0.00</span>
</button>
<ul class="trust" aria-label="Shopping benefits">
<li><span aria-hidden="true">🔒</span> 256-bit encrypted checkout</li>
<li><span aria-hidden="true">↩️</span> Free 30-day returns</li>
<li><span aria-hidden="true">⚡</span> Ships in 1–2 business days</li>
</ul>
</div>
</aside>
</div>
</main>
<div class="toast" id="toast" role="status" aria-live="polite" hidden>
<span class="toast__msg" id="toastMsg"></span>
<button class="toast__btn" type="button" id="toastBtn">Undo</button>
</div>
<script src="script.js"></script>
</body>
</html>Cart / Bag
A clean white, ink, and single-accent shopping bag page. Each line item pairs a gradient product tile with an inline-SVG silhouette, the product title and variant, a low-stock chip, a quantity stepper, a remove link, and right-aligned per-line pricing with strike-through original prices on sale items. A free-shipping progress bar at the top fills as the subtotal climbs toward the threshold and flips to a celebratory state once unlocked.
The sticky summary panel holds a promo-code input with quick-fill chips, an itemized totals list (subtotal, discount, shipping, estimated tax, total), and a prominent secure-checkout button that mirrors the running total. Trust signals — encrypted checkout, free returns, fast shipping — sit beneath the call to action.
Every interaction is real and self-contained in vanilla JS. Changing a quantity recomputes the
subtotal, tax, shipping, and total instantly; the stepper enforces a minimum of one and respects
per-item stock. Removing an item animates it out and surfaces an undo toast for a few seconds.
Valid codes SAVE15 (15% off) and FREESHIP (free shipping) apply discounts with inline
validation, and emptying the bag reveals a dedicated empty state. All money is formatted as
$1,299.00.
Illustrative storefront UI only — fictional products, prices, and reviews. No real checkout.