Museum — Gift-Shop Product Card
A museum gift-shop product gallery of six fictional editions and objects — Vandermeer prints, a Goryeo celadon monograph, a Salviati map tote, an enamel pin and more. Each card frames a CSS-drawn artwork in a soft mat, then sets an italic serif title above its catalog number, star rating, list price and a gold member-price badge. Colour swatches recolour the art, a quantity stepper bounds the count, and add-to-bag increments a live masthead counter with confirmation toasts and a hover quick-view.
MCP
Code
:root {
--paper: #f6f4ef;
--wall: #ffffff;
--charcoal: #1c1b19;
--ink: #2a2825;
--ink-2: #4a4640;
--muted: #8c857a;
--gold: #a98140;
--gold-d: #876631;
--gold-50: #f3ecdd;
--line: rgba(28, 27, 25, 0.12);
--line-2: rgba(28, 27, 25, 0.2);
--ok: #3f7d56;
--warn: #b8842c;
--danger: #b4493a;
--r-sm: 6px;
--r-md: 12px;
--r-lg: 18px;
--serif: "Cormorant Garamond", Georgia, serif;
--sans: "Inter", system-ui, -apple-system, sans-serif;
--shadow-sm: 0 1px 2px rgba(28, 27, 25, 0.05), 0 2px 8px rgba(28, 27, 25, 0.04);
--shadow-md: 0 6px 22px rgba(28, 27, 25, 0.1), 0 2px 6px rgba(28, 27, 25, 0.06);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
background: var(--paper);
color: var(--ink);
font-family: var(--sans);
line-height: 1.55;
}
.wrap {
width: min(1140px, 92vw);
margin-inline: auto;
}
.skip {
position: absolute;
left: -999px;
top: 0;
background: var(--charcoal);
color: #fff;
padding: 10px 16px;
border-radius: var(--r-sm);
z-index: 50;
}
.skip:focus {
left: 12px;
top: 12px;
}
/* ---------- masthead ---------- */
.masthead {
background: var(--wall);
border-bottom: 1px solid var(--line);
position: sticky;
top: 0;
z-index: 20;
}
.masthead-in {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 18px 0;
}
.brand {
display: flex;
align-items: center;
gap: 14px;
}
.brand-mark {
width: 42px;
height: 42px;
display: grid;
place-items: center;
border: 1px solid var(--line-2);
border-radius: 50%;
color: var(--gold);
font-size: 20px;
}
.brand-kicker {
margin: 0;
font-size: 11px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--muted);
}
.brand-title {
margin: 0;
font-family: var(--serif);
font-weight: 600;
font-size: 26px;
line-height: 1.1;
color: var(--charcoal);
}
.cart-btn {
display: inline-flex;
align-items: center;
gap: 9px;
font-family: var(--sans);
font-size: 14px;
font-weight: 500;
color: var(--ink);
background: var(--wall);
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 9px 16px;
cursor: pointer;
transition: border-color 0.18s, box-shadow 0.18s, transform 0.18s;
}
.cart-btn:hover {
border-color: var(--gold);
box-shadow: var(--shadow-sm);
}
.cart-btn:active {
transform: translateY(1px);
}
.cart-ico {
font-size: 17px;
}
.cart-count {
min-width: 22px;
height: 22px;
padding: 0 6px;
display: inline-grid;
place-items: center;
background: var(--charcoal);
color: #fff;
font-size: 12px;
font-weight: 600;
border-radius: 999px;
transition: transform 0.2s;
}
.cart-count.bump {
transform: scale(1.35);
}
/* ---------- intro ---------- */
.intro {
padding: 56px 0 30px;
max-width: 640px;
}
.eyebrow {
margin: 0 0 10px;
font-size: 12px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--gold-d);
font-weight: 600;
}
.intro-title {
margin: 0 0 14px;
font-family: var(--serif);
font-weight: 600;
font-size: clamp(34px, 5vw, 48px);
line-height: 1.05;
color: var(--charcoal);
}
.intro-lede {
margin: 0;
color: var(--ink-2);
font-size: 16px;
}
/* ---------- grid ---------- */
.grid {
list-style: none;
margin: 0;
padding: 8px 0 64px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 28px;
}
.card {
position: relative;
background: var(--wall);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
display: flex;
flex-direction: column;
transition: box-shadow 0.22s, transform 0.22s, border-color 0.22s;
}
.card:hover,
.card:focus-within {
box-shadow: var(--shadow-md);
border-color: var(--line-2);
transform: translateY(-3px);
}
/* artwork / image */
.thumb {
position: relative;
aspect-ratio: 4 / 3;
display: grid;
place-items: center;
padding: 22px;
background: var(--gold-50);
border-bottom: 1px solid var(--line);
}
.mat {
width: 100%;
height: 100%;
border-radius: var(--r-sm);
border: 1px solid rgba(28, 27, 25, 0.18);
box-shadow: inset 0 0 0 7px rgba(255, 255, 255, 0.92), var(--shadow-sm);
overflow: hidden;
}
.art {
width: 100%;
height: 100%;
display: block;
}
.tag {
position: absolute;
top: 14px;
left: 14px;
font-size: 10px;
letter-spacing: 0.12em;
text-transform: uppercase;
font-weight: 600;
color: var(--charcoal);
background: rgba(255, 255, 255, 0.92);
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 5px 10px;
}
/* quick view */
.quick {
position: absolute;
inset: auto 14px 14px 14px;
display: flex;
justify-content: center;
opacity: 0;
transform: translateY(8px);
transition: opacity 0.2s, transform 0.2s;
}
.card:hover .quick,
.card:focus-within .quick {
opacity: 1;
transform: translateY(0);
}
.quick-btn {
font-family: var(--sans);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.04em;
color: var(--charcoal);
background: rgba(255, 255, 255, 0.95);
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 8px 16px;
cursor: pointer;
backdrop-filter: blur(4px);
transition: background 0.16s, color 0.16s;
}
.quick-btn:hover {
background: var(--charcoal);
color: #fff;
}
/* body */
.body {
padding: 18px 18px 20px;
display: flex;
flex-direction: column;
gap: 12px;
flex: 1;
}
.meta {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
}
.cat-no {
font-size: 11px;
letter-spacing: 0.08em;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.rating {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 12px;
color: var(--ink-2);
}
.stars {
color: var(--gold);
letter-spacing: 1px;
}
.title {
margin: 0;
font-family: var(--serif);
font-weight: 600;
font-size: 21px;
line-height: 1.15;
color: var(--charcoal);
}
.byline {
margin: -4px 0 0;
font-size: 13px;
color: var(--muted);
}
.prices {
display: flex;
align-items: center;
gap: 12px;
}
.price {
font-size: 19px;
font-weight: 600;
color: var(--ink);
font-variant-numeric: tabular-nums;
}
.member {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 12px;
font-weight: 600;
color: var(--gold-d);
background: var(--gold-50);
border: 1px solid rgba(169, 129, 64, 0.35);
border-radius: 999px;
padding: 4px 10px;
}
/* swatches */
.swatches {
display: flex;
align-items: center;
gap: 8px;
}
.swatch {
width: 22px;
height: 22px;
border-radius: 50%;
border: 1px solid var(--line-2);
cursor: pointer;
padding: 0;
position: relative;
transition: transform 0.14s;
}
.swatch:hover {
transform: scale(1.12);
}
.swatch[aria-pressed="true"] {
box-shadow: 0 0 0 2px var(--wall), 0 0 0 4px var(--gold);
}
.swatch-name {
font-size: 12px;
color: var(--ink-2);
margin-left: 2px;
}
/* controls */
.controls {
display: flex;
align-items: center;
gap: 12px;
margin-top: auto;
}
.stepper {
display: inline-flex;
align-items: center;
border: 1px solid var(--line-2);
border-radius: 999px;
overflow: hidden;
}
.step {
width: 34px;
height: 38px;
border: 0;
background: transparent;
font-size: 18px;
color: var(--ink);
cursor: pointer;
transition: background 0.14s;
}
.step:hover {
background: var(--gold-50);
}
.step:disabled {
color: var(--line-2);
cursor: not-allowed;
}
.qty {
min-width: 30px;
text-align: center;
font-size: 14px;
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.add {
flex: 1;
height: 40px;
border: 0;
border-radius: 999px;
background: var(--charcoal);
color: #fff;
font-family: var(--sans);
font-size: 13px;
font-weight: 600;
letter-spacing: 0.04em;
cursor: pointer;
transition: background 0.18s, transform 0.12s;
}
.add:hover {
background: var(--gold-d);
}
.add:active {
transform: translateY(1px);
}
/* focus visibility */
:where(a, button):focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
/* ---------- footer ---------- */
.foot {
border-top: 1px solid var(--line);
background: var(--wall);
}
.foot-in {
display: flex;
justify-content: space-between;
gap: 14px;
flex-wrap: wrap;
padding: 22px 0;
font-size: 13px;
color: var(--muted);
}
.foot-in p {
margin: 0;
}
/* ---------- toast ---------- */
.toast-host {
position: fixed;
left: 50%;
bottom: 26px;
transform: translateX(-50%);
display: flex;
flex-direction: column;
gap: 10px;
z-index: 60;
pointer-events: none;
}
.toast {
background: var(--charcoal);
color: #fff;
font-size: 13.5px;
padding: 11px 18px;
border-radius: var(--r-md);
box-shadow: var(--shadow-md);
opacity: 0;
transform: translateY(10px);
transition: opacity 0.25s, transform 0.25s;
}
.toast.in {
opacity: 1;
transform: translateY(0);
}
.toast b {
color: var(--gold-50);
}
/* ---------- responsive ---------- */
@media (max-width: 880px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 520px) {
.grid {
grid-template-columns: 1fr;
gap: 22px;
}
.intro {
padding: 40px 0 22px;
}
.masthead-in {
padding: 14px 0;
}
.brand-title {
font-size: 22px;
}
.quick {
opacity: 1;
transform: none;
}
}
@media (prefers-reduced-motion: reduce) {
* {
transition: none !important;
}
}(function () {
"use strict";
/* ---------- demo catalogue (fictional) ---------- */
var PRODUCTS = [
{
id: "PR-0418",
tag: "Limited print",
title: "Harbour at Dusk",
byline: "after E. Vandermeer, 1871 · Giclée, 50×70cm",
price: 68,
member: 58,
rating: 4.8,
reviews: 124,
art: ["#2c3e57", "#7d96ad", "#e6c98a"],
variants: [
{ name: "Slate", c: "#2c3e57" },
{ name: "Linen", c: "#e7e1d4" },
{ name: "Sienna", c: "#9a5a36" }
]
},
{
id: "BK-2207",
tag: "Monograph",
title: "Goryeo Celadon",
byline: "ed. M. Reyes · 248pp, clothbound",
price: 45,
member: 38,
rating: 4.9,
reviews: 86,
art: ["#3f6f5e", "#8fae9c", "#d7e3d6"],
variants: [
{ name: "Jade", c: "#3f6f5e" },
{ name: "Bone", c: "#ece7da" }
]
},
{
id: "TT-1190",
tag: "Canvas tote",
title: "Salviati Map Tote",
byline: "Heavyweight cotton · 38×42cm",
price: 24,
member: 20,
rating: 4.6,
reviews: 203,
art: ["#caa15a", "#efe2c2", "#5e4a2a"],
variants: [
{ name: "Ochre", c: "#caa15a" },
{ name: "Charcoal", c: "#34322e" },
{ name: "Natural", c: "#e9e2cf" }
]
},
{
id: "PR-0533",
tag: "Limited print",
title: "Réquard, Light Study II",
byline: "after C. Réquard, 1908 · Giclée, 40×40cm",
price: 54,
member: 46,
rating: 4.7,
reviews: 67,
art: ["#b4493a", "#e7b38a", "#f3ecdd"],
variants: [
{ name: "Vermilion", c: "#b4493a" },
{ name: "Cream", c: "#f3ecdd" }
]
},
{
id: "BK-2318",
tag: "Exhibition catalogue",
title: "Aubin: Bronze & Air",
byline: "Arden Museum Press · 176pp",
price: 39,
member: 33,
rating: 4.5,
reviews: 41,
art: ["#5a534a", "#9a8f7e", "#c9bfa8"],
variants: [
{ name: "Patina", c: "#6b7a64" },
{ name: "Bronze", c: "#8a6a38" }
]
},
{
id: "OB-0902",
tag: "Enamel pin",
title: "Cole Garden Pin",
byline: "Hard enamel · 28mm, gold finish",
price: 14,
member: 12,
rating: 4.9,
reviews: 318,
art: ["#3f7d56", "#a98140", "#f3ecdd"],
variants: [
{ name: "Garden", c: "#3f7d56" },
{ name: "Gold", c: "#a98140" }
]
}
];
/* ---------- helpers ---------- */
function esc(s) {
return String(s).replace(/[&<>"]/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """ }[c];
});
}
function artGradient(p) {
var a = p.art;
return (
"background:" +
"radial-gradient(120% 90% at 22% 18%, " +
a[2] +
" 0%, transparent 46%)," +
"linear-gradient(150deg, " +
a[0] +
" 0%, " +
a[1] +
" 100%);"
);
}
function starRow(r) {
var full = Math.round(r);
var s = "";
for (var i = 0; i < 5; i++) s += i < full ? "★" : "☆";
return s;
}
/* ---------- toast ---------- */
var host = document.getElementById("toastHost");
function toast(msg) {
var t = document.createElement("div");
t.className = "toast";
t.innerHTML = msg;
host.appendChild(t);
requestAnimationFrame(function () {
t.classList.add("in");
});
setTimeout(function () {
t.classList.remove("in");
setTimeout(function () {
t.remove();
}, 280);
}, 2200);
}
/* ---------- render ---------- */
var grid = document.getElementById("grid");
PRODUCTS.forEach(function (p) {
var li = document.createElement("li");
li.className = "card";
li.dataset.id = p.id;
var swatches = p.variants
.map(function (v, i) {
return (
'<button class="swatch" type="button" data-c="' +
v.c +
'" data-name="' +
esc(v.name) +
'" aria-pressed="' +
(i === 0 ? "true" : "false") +
'" aria-label="Colour ' +
esc(v.name) +
'" style="background:' +
v.c +
'"></button>'
);
})
.join("");
li.innerHTML =
'<div class="thumb">' +
'<span class="tag">' +
esc(p.tag) +
"</span>" +
'<div class="mat"><div class="art" style="' +
artGradient(p) +
'"></div></div>' +
'<div class="quick"><button class="quick-btn" type="button" data-quick>Quick view</button></div>' +
"</div>" +
'<div class="body">' +
'<div class="meta">' +
'<span class="cat-no">' +
esc(p.id) +
"</span>" +
'<span class="rating"><span class="stars" aria-hidden="true">' +
starRow(p.rating) +
'</span><span>' +
p.rating.toFixed(1) +
" (" +
p.reviews +
")</span></span>" +
"</div>" +
'<h3 class="title">' +
esc(p.title) +
"</h3>" +
'<p class="byline">' +
esc(p.byline) +
"</p>" +
'<div class="prices">' +
'<span class="price">$' +
p.price +
"</span>" +
'<span class="member">◆ Member $' +
p.member +
"</span>" +
"</div>" +
'<div class="swatches">' +
swatches +
'<span class="swatch-name">' +
esc(p.variants[0].name) +
"</span></div>" +
'<div class="controls">' +
'<div class="stepper" role="group" aria-label="Quantity">' +
'<button class="step" type="button" data-step="-1" aria-label="Decrease quantity" disabled>−</button>' +
'<span class="qty" aria-live="polite">1</span>' +
'<button class="step" type="button" data-step="1" aria-label="Increase quantity">+</button>' +
"</div>" +
'<button class="add" type="button" data-add>Add to bag</button>' +
"</div>" +
"</div>";
grid.appendChild(li);
});
/* ---------- cart count ---------- */
var count = 0;
var countEl = document.getElementById("cartCount");
function addToCart(n) {
count += n;
countEl.textContent = count;
countEl.classList.remove("bump");
void countEl.offsetWidth;
countEl.classList.add("bump");
}
/* ---------- delegated interactions ---------- */
grid.addEventListener("click", function (e) {
var card = e.target.closest(".card");
if (!card) return;
var id = card.dataset.id;
var product = PRODUCTS.find(function (p) {
return p.id === id;
});
// swatch
var sw = e.target.closest(".swatch");
if (sw) {
card.querySelectorAll(".swatch").forEach(function (s) {
s.setAttribute("aria-pressed", "false");
});
sw.setAttribute("aria-pressed", "true");
card.querySelector(".swatch-name").textContent = sw.dataset.name;
card.querySelector(".art").style.background = sw.dataset.c;
return;
}
// stepper
var step = e.target.closest(".step");
if (step) {
var qtyEl = card.querySelector(".qty");
var q = parseInt(qtyEl.textContent, 10) + parseInt(step.dataset.step, 10);
q = Math.max(1, Math.min(99, q));
qtyEl.textContent = q;
card.querySelector('[data-step="-1"]').disabled = q <= 1;
return;
}
// quick view
if (e.target.closest("[data-quick]")) {
toast(
"<b>" +
esc(product.title) +
"</b> · " +
esc(product.byline) +
" — $" +
product.price
);
return;
}
// add to bag
if (e.target.closest("[data-add]")) {
var qty = parseInt(card.querySelector(".qty").textContent, 10);
var variant = card.querySelector('.swatch[aria-pressed="true"]').dataset
.name;
addToCart(qty);
toast(
"Added <b>" +
qty +
"× " +
esc(product.title) +
"</b> (" +
esc(variant) +
") to bag"
);
}
});
/* ---------- masthead bag ---------- */
document.getElementById("cartBtn").addEventListener("click", function () {
if (count === 0) {
toast("Your bag is empty — add an edition to begin.");
} else {
toast("Bag: <b>" + count + "</b> item" + (count === 1 ? "" : "s") + " ready for checkout.");
}
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Museum — Gift-Shop Product Card</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=Cormorant+Garamond:wght@500;600;700&family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip" href="#shop">Skip to shop</a>
<header class="masthead">
<div class="wrap masthead-in">
<div class="brand">
<span class="brand-mark" aria-hidden="true">◈</span>
<div>
<p class="brand-kicker">The Arden Museum</p>
<h1 class="brand-title">Museum Shop</h1>
</div>
</div>
<button class="cart-btn" id="cartBtn" type="button" aria-label="Shopping bag">
<span class="cart-ico" aria-hidden="true">🛍</span>
<span>Bag</span>
<span class="cart-count" id="cartCount" aria-live="polite">0</span>
</button>
</div>
</header>
<main class="wrap" id="shop">
<section class="intro">
<p class="eyebrow">Editions & Objects</p>
<h2 class="intro-title">Take the collection home</h2>
<p class="intro-lede">Limited prints, monographs and everyday objects produced with our curatorial team. Members save on every purchase — sign in at checkout.</p>
</section>
<ul class="grid" id="grid" role="list" aria-label="Gift-shop products"></ul>
</main>
<footer class="foot">
<div class="wrap foot-in">
<p>© 2026 The Arden Museum · 14 Linden Court</p>
<p>Free shipping over $75 · Members save 15%</p>
</div>
</footer>
<div class="toast-host" id="toastHost" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Gift-Shop Product Card
A retail card for a museum shop, shown as a gallery of six fictional products — a Harbour at Dusk giclée after Vandermeer, a Goryeo celadon monograph, the Salviati map tote, a Réquard light-study print, the Aubin exhibition catalogue and a Cole garden enamel pin. Every product image is drawn entirely in CSS as a matted, framed block, so the component stays self-contained with no external images. Each card pairs an italic serif title with its byline, a gold catalog number, a star rating with review count, the list price and a gold Member price badge.
Cards lift on hover and on keyboard focus, revealing a quick-view button tucked into the mat that surfaces the title, medium and price in a toast. Colour and variant swatches sit below the price: selecting one updates the pressed state, names the choice and recolours the framed artwork live. A pill stepper bounds quantity between one and ninety-nine, disabling the minus control at one.
Adding to bag reads the chosen quantity and variant, increments a live counter in the masthead bag button with a small bump animation, and confirms the change with a toast. The masthead bag itself reports how many items are ready for checkout. The grid uses generous wall space, retiles from three columns to two to one as the viewport narrows, keeps quick-view visible on touch, exposes accessible labels and live regions, and respects reduced-motion and visible-focus preferences.
Illustrative UI only — demo data; not a real museum system.