Shop — Price + Discount Display
A precise money UI primitive for storefronts that renders every pricing variant: a bold current price, compare-at strikethrough with an auto-calculated percent-off badge, per-unit math, interest-free installments, member pricing, and low-stock or price-drop notes. Four product cards demonstrate regular, on-sale, sold-out, and pre-order states. A currency switcher reformats and re-rounds every figure live, a member-price toggle swaps rates, and a flash-sale button flips a card between sale and regular pricing.
MCP
Code
:root {
--bg: #ffffff;
--surface: #ffffff;
--ink: #16181d;
--muted: #6b7280;
--brand: #1f7a5a;
--brand-d: #155c43;
--brand-tint: #eaf6f0;
--sale: #e0245e;
--sale-tint: #fdeaf1;
--ok: #1f9d55;
--warn: #b45309;
--warn-tint: #fef3e2;
--member: #6d4ad6;
--member-tint: #efeaff;
--line: rgba(16, 18, 29, .1);
--line-2: rgba(16, 18, 29, .06);
--shadow: 0 1px 2px rgba(16, 18, 29, .05), 0 8px 24px rgba(16, 18, 29, .06);
--radius: 16px;
--radius-s: 10px;
}
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
font-size: 15px;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(1200px 600px at 100% -10%, var(--brand-tint), transparent 60%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
.skip-link {
position: absolute;
left: -999px;
top: 8px;
background: var(--ink);
color: #fff;
padding: 8px 14px;
border-radius: 8px;
z-index: 50;
}
.skip-link:focus { left: 12px; }
:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
border-radius: 6px;
}
/* ---------- Topbar ---------- */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
padding: 16px clamp(16px, 4vw, 40px);
background: rgba(255, 255, 255, .82);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--line);
position: sticky;
top: 0;
z-index: 20;
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand-mark {
display: grid;
place-items: center;
width: 38px;
height: 38px;
border-radius: 11px;
background: var(--brand);
color: #fff;
}
.brand-name { font-weight: 800; letter-spacing: -.01em; font-size: 17px; }
.topbar-controls { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.control { display: flex; align-items: center; gap: 8px; }
.control-label {
font-size: 12px;
font-weight: 600;
color: var(--muted);
text-transform: uppercase;
letter-spacing: .04em;
}
.select-wrap { position: relative; display: inline-flex; }
.select-wrap select {
appearance: none;
-webkit-appearance: none;
font: inherit;
font-weight: 600;
font-size: 14px;
color: var(--ink);
background: #fff;
border: 1px solid var(--line);
border-radius: var(--radius-s);
padding: 8px 32px 8px 12px;
cursor: pointer;
transition: border-color .15s, box-shadow .15s;
}
.select-wrap select:hover { border-color: var(--brand); }
.select-caret {
position: absolute;
right: 11px;
top: 50%;
transform: translateY(-50%);
pointer-events: none;
color: var(--muted);
font-size: 11px;
}
.member-toggle {
display: inline-flex;
align-items: center;
gap: 9px;
font: inherit;
font-weight: 600;
font-size: 14px;
color: var(--ink);
background: #fff;
border: 1px solid var(--line);
border-radius: 999px;
padding: 6px 14px 6px 8px;
cursor: pointer;
transition: border-color .15s, background .15s;
}
.member-toggle:hover { border-color: var(--member); }
.switch-track {
width: 38px;
height: 22px;
border-radius: 999px;
background: #d6d8de;
position: relative;
transition: background .18s;
flex: 0 0 auto;
}
.switch-thumb {
position: absolute;
top: 2px;
left: 2px;
width: 18px;
height: 18px;
border-radius: 50%;
background: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, .25);
transition: transform .18s;
}
.member-toggle[aria-checked="true"] { border-color: var(--member); background: var(--member-tint); }
.member-toggle[aria-checked="true"] .switch-track { background: var(--member); }
.member-toggle[aria-checked="true"] .switch-thumb { transform: translateX(16px); }
/* ---------- Layout ---------- */
.wrap {
max-width: 1120px;
margin: 0 auto;
padding: clamp(28px, 5vw, 56px) clamp(16px, 4vw, 40px) 72px;
}
.page-head { max-width: 660px; }
.page-head h1 {
margin: 0 0 10px;
font-size: clamp(26px, 4.5vw, 38px);
font-weight: 800;
letter-spacing: -.025em;
line-height: 1.12;
}
.lede { margin: 0; color: var(--muted); font-size: clamp(15px, 2vw, 17px); }
.legend {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 14px 20px;
margin: 22px 0 26px;
padding: 0;
font-size: 13px;
font-weight: 600;
color: var(--muted);
}
.legend li { display: inline-flex; align-items: center; gap: 7px; }
.dot { width: 10px; height: 10px; border-radius: 50%; }
.dot-sale { background: var(--sale); }
.dot-member { background: var(--member); }
.dot-soldout { background: #9aa0ab; }
.dot-preorder { background: var(--brand); }
/* ---------- Grid ---------- */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(248px, 1fr));
gap: clamp(16px, 2.5vw, 22px);
}
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: var(--shadow);
transition: transform .16s ease, box-shadow .16s ease, border-color .16s;
}
.card:not(.is-disabled):hover {
transform: translateY(-3px);
box-shadow: 0 2px 4px rgba(16, 18, 29, .06), 0 16px 36px rgba(16, 18, 29, .1);
border-color: rgba(16, 18, 29, .14);
}
.thumb {
aspect-ratio: 4 / 3;
display: grid;
place-items: center;
position: relative;
}
.thumb svg { color: rgba(255, 255, 255, .92); filter: drop-shadow(0 6px 14px rgba(0, 0, 0, .18)); }
.thumb-a { background: linear-gradient(135deg, #2f9b73, #166049); }
.thumb-b { background: linear-gradient(135deg, #f0708f, #c4244e); }
.thumb-c { background: linear-gradient(135deg, #8b93a3, #5a6172); }
.thumb-d { background: linear-gradient(135deg, #4a63d8, #2a3a96); }
.card-body { padding: 16px 18px 18px; display: flex; flex-direction: column; gap: 6px; flex: 1; }
.chip {
align-self: flex-start;
font-size: 11px;
font-weight: 700;
letter-spacing: .02em;
text-transform: uppercase;
padding: 4px 9px;
border-radius: 999px;
margin-bottom: 2px;
}
.chip-stock { color: var(--ok); background: rgba(31, 157, 85, .12); }
.chip-low { color: var(--warn); background: var(--warn-tint); }
.chip-out { color: #5a6172; background: rgba(154, 160, 171, .18); }
.chip-pre { color: var(--brand-d); background: var(--brand-tint); }
.title {
margin: 0;
font-size: 16px;
font-weight: 700;
letter-spacing: -.01em;
line-height: 1.3;
}
.rating { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--muted); }
.stars { color: #f5a623; letter-spacing: 1px; position: relative; }
.stars-empty { color: #d6d8de; }
.star-half {
position: relative;
display: inline-block;
color: #d6d8de;
}
.star-half::before {
content: "★";
position: absolute;
left: 0;
width: 50%;
overflow: hidden;
color: #f5a623;
}
.rating-num { font-weight: 700; color: var(--ink); }
/* ---------- Price block ---------- */
.price-row {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 8px;
margin-top: 4px;
}
.price {
font-size: 24px;
font-weight: 800;
letter-spacing: -.02em;
font-variant-numeric: tabular-nums;
}
.price-sale { color: var(--sale); }
.price-muted { color: var(--muted); }
.compare {
color: var(--muted);
font-size: 15px;
font-weight: 500;
text-decoration: line-through;
font-variant-numeric: tabular-nums;
}
.badge-off {
font-size: 12px;
font-weight: 800;
color: #fff;
background: var(--sale);
padding: 3px 8px;
border-radius: 6px;
letter-spacing: .01em;
}
.price.is-member { color: var(--member); }
.member-tag {
font-size: 11px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: .03em;
color: var(--member);
background: var(--member-tint);
padding: 3px 8px;
border-radius: 6px;
}
.unit { margin: 0; font-size: 13px; color: var(--muted); font-variant-numeric: tabular-nums; }
.muted-note { color: var(--muted); }
.installment {
font-size: 13px;
color: var(--ink);
background: var(--brand-tint);
border-radius: 8px;
padding: 7px 10px;
margin-top: 4px;
}
.installment strong { font-weight: 800; font-variant-numeric: tabular-nums; }
.drop-note {
margin: 2px 0 0;
font-size: 12.5px;
font-weight: 600;
color: var(--sale);
}
.cta {
margin-top: auto;
font: inherit;
font-weight: 700;
font-size: 14px;
color: #fff;
background: var(--brand);
border: 1px solid var(--brand);
border-radius: var(--radius-s);
padding: 11px 14px;
cursor: pointer;
transition: background .15s, transform .08s, box-shadow .15s;
margin-top: 12px;
}
.cta:hover { background: var(--brand-d); border-color: var(--brand-d); }
.cta:active { transform: translateY(1px); }
.cta-ghost {
color: var(--ink);
background: #fff;
border-color: var(--line);
}
.cta-ghost:hover { background: #f6f7f9; border-color: var(--ink); }
/* ---------- Sold out state ---------- */
.card.is-disabled .thumb { filter: saturate(.55); }
.card.is-disabled .thumb::after {
content: "";
position: absolute;
inset: 0;
background: rgba(255, 255, 255, .42);
}
/* ---------- Demo bar ---------- */
.demo-bar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px;
margin-top: 34px;
padding: 16px 18px;
border: 1px dashed var(--line);
border-radius: var(--radius);
background: rgba(16, 18, 29, .02);
}
.demo-label {
margin: 0;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .05em;
color: var(--muted);
}
.demo-btn {
font: inherit;
font-weight: 700;
font-size: 14px;
color: var(--ink);
background: #fff;
border: 1px solid var(--ink);
border-radius: var(--radius-s);
padding: 9px 16px;
cursor: pointer;
transition: background .15s, color .15s;
}
.demo-btn:hover { background: var(--ink); color: #fff; }
.demo-btn[aria-pressed="false"] { border-color: var(--sale); color: var(--sale); }
.demo-btn[aria-pressed="false"]:hover { background: var(--sale); color: #fff; }
.demo-hint { font-size: 13px; color: var(--muted); }
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translate(-50%, 140%);
background: var(--ink);
color: #fff;
font-size: 14px;
font-weight: 600;
padding: 12px 18px;
border-radius: 12px;
box-shadow: 0 12px 34px rgba(0, 0, 0, .28);
opacity: 0;
pointer-events: none;
transition: transform .28s cubic-bezier(.2, .8, .2, 1), opacity .28s;
z-index: 60;
max-width: calc(100vw - 32px);
}
.toast.show { transform: translate(-50%, 0); opacity: 1; }
@media (max-width: 560px) {
.topbar { gap: 12px; }
.topbar-controls { width: 100%; justify-content: space-between; }
.grid { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) {
* { transition-duration: .01ms !important; animation-duration: .01ms !important; }
}(function () {
"use strict";
// --- Currency model. rate is relative to USD (fictional, fixed demo rates). ---
var CURRENCIES = {
USD: { symbol: "$", rate: 1, decimals: 2, prefix: true },
EUR: { symbol: "€", rate: 0.92, decimals: 2, prefix: true },
GBP: { symbol: "£", rate: 0.79, decimals: 2, prefix: true },
JPY: { symbol: "¥", rate: 156, decimals: 0, prefix: true }
};
var state = {
currency: "USD",
member: false,
saleOn: true
};
// ---------- Money formatting ----------
function formatMoney(usd) {
var c = CURRENCIES[state.currency];
var value = usd * c.rate;
var parts = value.toFixed(c.decimals);
var intPart = parts.split(".")[0];
var frac = c.decimals > 0 ? "." + parts.split(".")[1] : "";
// group thousands
intPart = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
return c.symbol + intPart + frac;
}
// ---------- Toast ----------
var toastEl = document.getElementById("toast");
var toastTimer = null;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2600);
}
// ---------- Render all prices ----------
function effectiveBase(el) {
// returns the USD price honoring member toggle when a member value exists
var base = parseFloat(el.getAttribute("data-base"));
var member = el.getAttribute("data-member");
if (state.member && member !== null && member !== "") {
return { value: parseFloat(member), isMember: true };
}
return { value: base, isMember: false };
}
function renderPrice(el) {
var eff = effectiveBase(el);
var compareAttr = el.getAttribute("data-compare");
var hasSale = el.classList.contains("price-sale") && state.saleOn && compareAttr;
el.textContent = formatMoney(eff.value);
// member visual + tag (only on active, non-sold-out prices)
el.classList.toggle("is-member", eff.isMember && !el.classList.contains("price-muted"));
var row = el.closest(".price-row");
if (row) {
var existingTag = row.querySelector(".member-tag");
if (eff.isMember && !el.classList.contains("price-muted")) {
if (!existingTag) {
var tag = document.createElement("span");
tag.className = "member-tag";
tag.textContent = "Member";
row.appendChild(tag);
}
} else if (existingTag) {
existingTag.remove();
}
}
}
function renderCompare() {
document.querySelectorAll(".compare").forEach(function (el) {
var usd = parseFloat(el.getAttribute("data-compare"));
el.textContent = formatMoney(usd);
});
}
function renderOffBadges() {
document.querySelectorAll("[data-off]").forEach(function (badge) {
var card = badge.closest(".card");
var priceEl = card.querySelector(".price");
var compareEl = card.querySelector(".compare");
if (!priceEl || !compareEl) return;
var current = effectiveBase(priceEl).value;
var compare = parseFloat(compareEl.getAttribute("data-compare"));
var pct = Math.round((1 - current / compare) * 100);
badge.textContent = pct + "% off";
});
}
function renderUnits() {
document.querySelectorAll("[data-unit]").forEach(function (el) {
var baseUnit = parseFloat(el.getAttribute("data-base-unit"));
var memberUnit = el.getAttribute("data-member-unit");
var total = baseUnit;
if (state.member && memberUnit) total = parseFloat(memberUnit);
var qty = parseFloat(el.getAttribute("data-qty")) || 1;
var per = total / qty;
var suffix = el.textContent.indexOf("each") > -1 ? " each" : (el.textContent.indexOf("/ L") > -1 ? " / L" : "");
// keep the unit label after the price
if (suffix === " each") {
el.textContent = formatMoney(per) + " each";
} else if (suffix === " / L") {
el.textContent = formatMoney(per) + " / L";
} else {
el.textContent = formatMoney(per);
}
});
}
function renderInstallments() {
document.querySelectorAll("[data-installment]").forEach(function (el) {
var base = parseFloat(el.getAttribute("data-base"));
var member = el.getAttribute("data-member");
var n = parseInt(el.getAttribute("data-n"), 10) || 4;
var total = (state.member && member) ? parseFloat(member) : base;
var per = total / n;
var strong = el.querySelector("strong");
if (strong) strong.textContent = formatMoney(per);
// update the count prefix text node ("or 4× ")
el.childNodes[0].nodeValue = "or " + n + "× ";
});
}
function renderDropNotes() {
document.querySelectorAll("[data-drop]").forEach(function (el) {
var card = el.closest(".card");
var priceEl = card.querySelector(".price");
var compareEl = card.querySelector(".compare");
if (!priceEl || !compareEl) return;
var diff = parseFloat(compareEl.getAttribute("data-compare")) - effectiveBase(priceEl).value;
el.textContent = "↓ Price dropped " + formatMoney(diff) + " — lowest in 30 days";
});
}
function renderAll() {
document.querySelectorAll(".price").forEach(renderPrice);
renderCompare();
renderOffBadges();
renderUnits();
renderInstallments();
renderDropNotes();
}
// ---------- Currency switcher ----------
var currencySelect = document.getElementById("currency");
currencySelect.addEventListener("change", function () {
state.currency = currencySelect.value;
renderAll();
toast("Prices shown in " + state.currency);
});
// ---------- Member toggle ----------
var memberToggle = document.getElementById("member-toggle");
function setMember(on) {
state.member = on;
memberToggle.setAttribute("aria-checked", on ? "true" : "false");
renderAll();
}
memberToggle.addEventListener("click", function () {
setMember(!state.member);
toast(state.member ? "Member pricing applied" : "Member pricing off");
});
memberToggle.addEventListener("keydown", function (e) {
if (e.key === " " || e.key === "Enter") {
e.preventDefault();
memberToggle.click();
}
});
// ---------- Toggle sale demo ----------
var saleBtn = document.getElementById("toggle-sale");
var saleCard = document.getElementById("sale-card");
function applySaleState() {
var priceEl = saleCard.querySelector(".price");
var compareEl = saleCard.querySelector(".compare");
var badgeEl = saleCard.querySelector("[data-off]");
var dropEl = saleCard.querySelector("[data-drop]");
var chipEl = document.getElementById("low-stock");
if (state.saleOn) {
saleCard.setAttribute("data-state", "sale");
priceEl.classList.add("price-sale");
compareEl.style.display = "";
badgeEl.style.display = "";
if (dropEl) dropEl.style.display = "";
chipEl.textContent = "Only 4 left";
chipEl.className = "chip chip-low";
saleBtn.setAttribute("aria-pressed", "true");
saleBtn.textContent = "End flash sale";
} else {
saleCard.setAttribute("data-state", "regular");
priceEl.classList.remove("price-sale");
compareEl.style.display = "none";
badgeEl.style.display = "none";
if (dropEl) dropEl.style.display = "none";
chipEl.textContent = "In stock";
chipEl.className = "chip chip-stock";
saleBtn.setAttribute("aria-pressed", "false");
saleBtn.textContent = "Start flash sale";
}
renderAll();
}
saleBtn.addEventListener("click", function () {
state.saleOn = !state.saleOn;
applySaleState();
toast(state.saleOn ? "Flash sale started — 33% off" : "Sale ended — regular price restored");
});
// ---------- Add to cart / notify ----------
document.querySelectorAll("[data-add]").forEach(function (btn) {
btn.addEventListener("click", function () {
var card = btn.closest(".card");
var title = card.querySelector(".title").textContent;
var price = card.querySelector(".price").textContent;
var verb = card.getAttribute("data-state") === "preorder" ? "Reserved" : "Added";
toast(verb + " · " + title + " — " + price);
});
});
document.querySelectorAll("[data-notify]").forEach(function (btn) {
btn.addEventListener("click", function () {
var card = btn.closest(".card");
var title = card.querySelector(".title").textContent;
toast("We'll email you when " + title + " is back");
});
});
// ---------- Init ----------
applySaleState(); // also calls renderAll()
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Price + Discount Display</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-link" href="#main">Skip to content</a>
<header class="topbar" role="banner">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M6 2 3 6v14a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V6l-3-4Z" />
<path d="M3 6h18" />
<path d="M16 10a4 4 0 0 1-8 0" />
</svg>
</span>
<span class="brand-name">Verdant Goods</span>
</div>
<div class="topbar-controls">
<div class="control">
<label for="currency" class="control-label">Currency</label>
<div class="select-wrap">
<select id="currency" aria-label="Display currency">
<option value="USD" selected>USD · $</option>
<option value="EUR">EUR · €</option>
<option value="GBP">GBP · £</option>
<option value="JPY">JPY · ¥</option>
</select>
<span class="select-caret" aria-hidden="true">▾</span>
</div>
</div>
<button id="member-toggle" class="member-toggle" type="button" role="switch" aria-checked="false">
<span class="switch-track" aria-hidden="true"><span class="switch-thumb"></span></span>
<span class="switch-text">Member price</span>
</button>
</div>
</header>
<main id="main" class="wrap">
<div class="page-head">
<h1>Price + Discount Display</h1>
<p class="lede">A precise money UI primitive — strikethrough compare-at, percent-off badges, per-unit math, installments, and stock states. Switch the currency or flip on member pricing to watch every figure reformat live.</p>
</div>
<ul class="legend" aria-hidden="true">
<li><span class="dot dot-sale"></span>On sale</li>
<li><span class="dot dot-member"></span>Member</li>
<li><span class="dot dot-soldout"></span>Sold out</li>
<li><span class="dot dot-preorder"></span>Pre-order</li>
</ul>
<section class="grid" aria-label="Product price displays">
<!-- REGULAR -->
<article class="card" data-state="regular">
<div class="thumb thumb-a" role="img" aria-label="Walnut pour-over kettle">
<svg viewBox="0 0 120 120" width="96" height="96" fill="none" aria-hidden="true">
<path d="M28 78c0-16 14-26 32-26s32 10 32 26v4a8 8 0 0 1-8 8H36a8 8 0 0 1-8-8Z" fill="currentColor" opacity=".9"/>
<path d="M60 52c0-8 4-14 14-16 12-2 22 4 26 14" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
<rect x="50" y="36" width="20" height="12" rx="4" fill="currentColor"/>
</svg>
</div>
<div class="card-body">
<span class="chip chip-stock">In stock</span>
<h2 class="title">Gooseneck Pour-Over Kettle</h2>
<div class="rating" aria-label="Rated 4.8 out of 5 from 642 reviews">
<span class="stars" aria-hidden="true">★★★★★</span>
<span class="rating-num">4.8</span>
<span class="rating-count">(642)</span>
</div>
<div class="price-row">
<span class="price"
data-base="89"
data-member="79">$89.00</span>
</div>
<p class="unit" data-unit data-base-unit="89" data-member-unit="79" data-qty="1.2">$74.17 / L</p>
<div class="installment" data-installment data-base="89" data-member="79" data-n="4">
or 4× <strong>$22.25</strong> interest-free
</div>
<button class="cta" type="button" data-add>Add to cart</button>
</div>
</article>
<!-- ON SALE -->
<article class="card" data-state="sale" id="sale-card">
<div class="thumb thumb-b" role="img" aria-label="Ceramic mug set">
<svg viewBox="0 0 120 120" width="96" height="96" fill="none" aria-hidden="true">
<path d="M34 44h44v30a18 18 0 0 1-18 18H52a18 18 0 0 1-18-18Z" fill="currentColor"/>
<path d="M78 52h8a10 10 0 0 1 0 20h-8" stroke="currentColor" stroke-width="6" fill="none"/>
<path d="M44 30c0 6-4 6-4 12M58 28c0 6-4 6-4 12" stroke="currentColor" stroke-width="5" stroke-linecap="round" opacity=".55"/>
</svg>
</div>
<div class="card-body">
<span class="chip chip-low" id="low-stock">Only 4 left</span>
<h2 class="title">Stoneware Mug Set (4)</h2>
<div class="rating" aria-label="Rated 4.6 out of 5 from 318 reviews">
<span class="stars" aria-hidden="true">★★★★<span class="star-half">★</span></span>
<span class="rating-num">4.6</span>
<span class="rating-count">(318)</span>
</div>
<div class="price-row">
<span class="price price-sale"
data-base="48"
data-member="42"
data-compare="72">$48.00</span>
<s class="compare" data-compare="72">$72.00</s>
<span class="badge-off" data-off>33% off</span>
</div>
<p class="unit" data-unit data-base-unit="48" data-member-unit="42" data-qty="4">$12.00 each</p>
<div class="installment" data-installment data-base="48" data-member="42" data-n="4">
or 4× <strong>$12.00</strong> interest-free
</div>
<p class="drop-note" data-drop>↓ Price dropped $24.00 — lowest in 30 days</p>
<button class="cta" type="button" data-add>Add to cart</button>
</div>
</article>
<!-- SOLD OUT -->
<article class="card is-disabled" data-state="soldout">
<div class="thumb thumb-c" role="img" aria-label="Burr coffee grinder">
<svg viewBox="0 0 120 120" width="96" height="96" fill="none" aria-hidden="true">
<rect x="40" y="30" width="40" height="22" rx="6" fill="currentColor"/>
<path d="M44 52h32l-4 36a8 8 0 0 1-8 7H56a8 8 0 0 1-8-7Z" fill="currentColor" opacity=".85"/>
<circle cx="60" cy="26" r="6" fill="currentColor"/>
</svg>
</div>
<div class="card-body">
<span class="chip chip-out">Sold out</span>
<h2 class="title">Conical Burr Grinder</h2>
<div class="rating" aria-label="Rated 4.9 out of 5 from 1204 reviews">
<span class="stars" aria-hidden="true">★★★★★</span>
<span class="rating-num">4.9</span>
<span class="rating-count">(1,204)</span>
</div>
<div class="price-row">
<span class="price price-muted" data-base="129" data-member="115">$129.00</span>
</div>
<p class="unit muted-note">Restock expected mid-July</p>
<button class="cta cta-ghost" type="button" data-notify>Notify me</button>
</div>
</article>
<!-- PRE-ORDER -->
<article class="card" data-state="preorder">
<div class="thumb thumb-d" role="img" aria-label="Espresso machine">
<svg viewBox="0 0 120 120" width="96" height="96" fill="none" aria-hidden="true">
<rect x="30" y="36" width="60" height="48" rx="8" fill="currentColor"/>
<rect x="52" y="84" width="16" height="10" rx="3" fill="currentColor" opacity=".8"/>
<rect x="40" y="44" width="40" height="14" rx="4" fill="#fff" opacity=".7"/>
<circle cx="78" cy="70" r="5" fill="#fff" opacity=".7"/>
</svg>
</div>
<div class="card-body">
<span class="chip chip-pre">Pre-order</span>
<h2 class="title">Dual-Boiler Espresso Machine</h2>
<div class="rating" aria-label="New release, no reviews yet">
<span class="stars stars-empty" aria-hidden="true">★★★★★</span>
<span class="rating-count">New release</span>
</div>
<div class="price-row">
<span class="price"
data-base="1299"
data-member="1199">$1,299.00</span>
</div>
<p class="unit muted-note">Ships Aug 2 · charged when it ships</p>
<div class="installment" data-installment data-base="1299" data-member="1199" data-n="12">
or 12× <strong>$108.25</strong> interest-free
</div>
<button class="cta" type="button" data-add>Reserve now</button>
</div>
</article>
</section>
<div class="demo-bar" role="region" aria-label="Demo controls">
<p class="demo-label">Live demo</p>
<button id="toggle-sale" class="demo-btn" type="button" aria-pressed="true">End flash sale</button>
<span class="demo-hint">Flips the mug set between sale and regular pricing.</span>
</div>
</main>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Price + Discount Display
A compact, self-contained price primitive built for clean white storefronts with a single green accent. Four product cards cover the pricing states you actually ship: a regular in-stock item, an on-sale item with a compare-at strikethrough and a calculated percent-off badge, a sold-out item with a muted price and notify action, and a pre-order with an installment plan and ship date. Each card layers the supporting money copy — per-unit price, interest-free installments, member price, and a price-drop note — on top of a gradient product tile with an inline-SVG silhouette, star rating, and stock chip.
Every figure is computed in vanilla JS from a single USD source value, so the display stays consistent. The currency switcher re-rates and re-rounds all prices, units, installments, badges, and drop notes to USD, EUR, GBP, or JPY (yen drops the decimals). The member-price toggle swaps in member rates, tags the affected prices, and recalculates the percent-off badge from the new effective price. The flash-sale demo button flips the mug set between its sale and regular pricing, showing and hiding the strikethrough, badge, drop note, and low-stock chip in sync.
All interactions are real: thousands grouping, decimal handling per currency, badge math derived from compare-at versus current price, and toasts confirming add-to-cart, reserve, and notify actions. The layout collapses to a single column under 360px and every control is keyboard-usable with visible focus rings.
Illustrative storefront UI only — fictional products, prices, and reviews. No real checkout.