Bootstrap 5 Product Card with Colour Swatches
A product card with a large photo, wishlist toggle, rating, sale price, colour swatches, quantity stepper and an add-to-cart button.
<section class="wdw-stage">
<div class="container" style="max-width:880px">
<article class="wdw-card overflow-hidden product">
<div class="row g-0">
<div class="col-md-6 product-media position-relative">
<img src="https://images.unsplash.com/photo-1546435770-a3e426bf472b?auto=format&fit=crop&w=900&q=75" alt="Black over-ear headphones on a white desk" data-product-image>
<span class="badge rounded-pill text-bg-light position-absolute top-0 start-0 m-3 px-3 py-2">New</span>
<button type="button" class="btn rounded-circle position-absolute top-0 end-0 m-3 p-0 fav" aria-label="Save to wishlist" aria-pressed="false">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20s-7-4.4-9-9.2C1.8 7.6 3.8 4.5 7 4.5c2 0 3.3 1 5 3 1.7-2 3-3 5-3 3.2 0 5.2 3.1 4 6.3-2 4.8-9 9.2-9 9.2Z"/></svg>
</button>
</div>
<div class="col-md-6 p-4 p-lg-5 d-flex flex-column">
<div class="d-flex justify-content-between align-items-start gap-3 mb-2">
<span class="wdw-eyebrow">Audio</span>
<span class="d-inline-flex align-items-center gap-1 small fw-semibold">
<svg width="16" height="16" viewBox="0 0 24 24" fill="#f59e0b" aria-hidden="true"><path d="m12 2.5 2.9 6 6.6.9-4.8 4.6 1.2 6.5L12 17.3 6.1 20.5l1.2-6.5L2.5 9.4l6.6-.9L12 2.5Z"/></svg>
<span class="visually-hidden">Rated 4.8 out of 5 from 214 reviews</span>
<span aria-hidden="true">4.8 <span class="wdw-muted fw-normal">(214)</span></span>
</span>
</div>
<h2 class="wdw-heading h3 mb-1">Aero ANC Headphones</h2>
<p class="wdw-muted mb-3">Active noise cancelling, 40-hour battery and a folding hard case.</p>
<p class="mb-4"><span class="h2 fw-bold me-2" data-price>$220</span><s class="wdw-muted">$269</s> <span class="badge rounded-pill bg-success text-white ms-1">Save 18%</span></p>
<fieldset class="mb-4">
<legend class="small fw-bold text-uppercase wdw-muted mb-2" style="font-size:.75rem;letter-spacing:.06em">Colour: <span class="text-body-emphasis" data-colour-name>Graphite</span></legend>
<div class="d-flex gap-2">
<input type="radio" class="btn-check" name="colour" id="c1" value="Graphite" data-src="https://images.unsplash.com/photo-1546435770-a3e426bf472b?auto=format&fit=crop&w=900&q=75" data-alt="Black over-ear headphones on a white desk" checked><label class="swatch" for="c1" style="--c:#1f2937;--ck:#fff"><span class="visually-hidden">Graphite</span></label>
<input type="radio" class="btn-check" name="colour" id="c2" value="Sand" data-src="https://images.unsplash.com/photo-1618366712010-f4ae9c647dcb?auto=format&fit=crop&w=900&q=75" data-alt="Dark over-ear headphones on a warm sand backdrop"><label class="swatch" for="c2" style="--c:#d6c7a8;--ck:#3b3322"><span class="visually-hidden">Sand</span></label>
<input type="radio" class="btn-check" name="colour" id="c3" value="Rose" data-src="https://images.unsplash.com/photo-1524678606370-a47ad25cb82a?auto=format&fit=crop&w=900&q=75" data-alt="Pink headphones against rose and mint panels"><label class="swatch" for="c3" style="--c:#d8a7a7;--ck:#5a2a2a"><span class="visually-hidden">Rose</span></label>
<input type="radio" class="btn-check" name="colour" id="c4" value="Lilac" data-src="https://images.unsplash.com/photo-1577174881658-0f30ed549adc?auto=format&fit=crop&w=900&q=75" data-alt="White headphones on a lilac backdrop"><label class="swatch" for="c4" style="--c:#a5b4fc;--ck:#1e1b4b"><span class="visually-hidden">Lilac</span></label>
</div>
</fieldset>
<div class="d-flex gap-2 mt-auto">
<div class="input-group qty" style="width:9rem">
<button class="btn btn-outline-secondary" type="button" data-qty="-1" aria-label="Decrease quantity"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M5 12h14"/></svg></button>
<input class="form-control text-center px-0" type="number" min="1" max="10" value="1" aria-label="Quantity" data-qty-input>
<button class="btn btn-outline-secondary" type="button" data-qty="1" aria-label="Increase quantity"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M5 12h14M12 5v14"/></svg></button>
</div>
<button type="button" class="btn btn-primary rounded-pill flex-grow-1" data-add><svg class="ck-icon d-none" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg><span data-add-label>Add to cart</span></button>
</div>
<p class="small text-center wdw-muted mt-3 mb-0" role="status" data-cart-status>Free shipping over $100</p>
</div>
</div>
</article>
</div>
</section>
/* Web Designer Wall library style — shared base for every Bootstrap 5 snippet.
Indigo accent, Plus Jakarta Sans, soft cards, pill buttons. Works in light and dark. */
@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap");
:root,
[data-bs-theme="light"] {
--wdw-font: "Plus Jakarta Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--bs-body-font-family: var(--wdw-font);
--bs-primary: #4f46e5;
--bs-primary-rgb: 79, 70, 229;
--bs-primary-text-emphasis: #3730a3;
--bs-primary-bg-subtle: #eef2ff;
--bs-primary-border-subtle: #c7d2fe;
--bs-link-color: #4f46e5;
--bs-link-color-rgb: 79, 70, 229;
--bs-link-hover-color: #4338ca;
--bs-link-hover-color-rgb: 67, 56, 202;
--bs-focus-ring-color: rgba(79, 70, 229, 0.25);
--bs-border-radius: 0.75rem;
--bs-border-radius-lg: 1rem;
--bs-border-radius-xl: 1.25rem;
--wdw-radius-card: 1.25rem;
--wdw-shadow: 0 1px 2px rgba(16, 24, 40, 0.05), 0 16px 40px -16px rgba(16, 24, 40, 0.22);
--wdw-shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06), 0 4px 12px -4px rgba(16, 24, 40, 0.12);
--wdw-stage: #f5f6fb;
}
[data-bs-theme="dark"] {
--bs-primary: #818cf8;
--bs-primary-rgb: 129, 140, 248;
--bs-primary-text-emphasis: #c7d2fe;
--bs-primary-bg-subtle: rgba(129, 140, 248, 0.14);
--bs-primary-border-subtle: rgba(129, 140, 248, 0.35);
--bs-link-color: #a5b4fc;
--bs-link-color-rgb: 165, 180, 252;
--bs-link-hover-color: #c7d2fe;
--bs-link-hover-color-rgb: 199, 210, 254;
--bs-focus-ring-color: rgba(129, 140, 248, 0.35);
--wdw-shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 16px 40px -16px rgba(0, 0, 0, 0.7);
--wdw-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4), 0 4px 12px -4px rgba(0, 0, 0, 0.5);
--wdw-stage: #0f1117;
}
body { font-family: var(--wdw-font); -webkit-font-smoothing: antialiased; }
/* Bootstrap compiles its blue into these components; point them at the accent. */
.btn-primary {
--bs-btn-bg: var(--bs-primary); --bs-btn-border-color: var(--bs-primary);
--bs-btn-hover-bg: #4338ca; --bs-btn-hover-border-color: #4338ca;
--bs-btn-active-bg: #3730a3; --bs-btn-active-border-color: #3730a3;
--bs-btn-disabled-bg: var(--bs-primary); --bs-btn-disabled-border-color: var(--bs-primary);
--bs-btn-focus-shadow-rgb: var(--bs-primary-rgb);
}
[data-bs-theme="dark"] .btn-primary { --bs-btn-color: #0f1117; --bs-btn-hover-bg: #a5b4fc; --bs-btn-hover-border-color: #a5b4fc; --bs-btn-hover-color: #0f1117; --bs-btn-active-color: #0f1117; }
.btn-outline-primary {
--bs-btn-color: var(--bs-primary); --bs-btn-border-color: var(--bs-primary);
--bs-btn-hover-bg: var(--bs-primary); --bs-btn-hover-border-color: var(--bs-primary);
--bs-btn-active-bg: var(--bs-primary); --bs-btn-active-border-color: var(--bs-primary);
--bs-btn-focus-shadow-rgb: var(--bs-primary-rgb);
}
.btn { --bs-btn-font-weight: 600; min-height: 44px; display: inline-flex; align-items: center; justify-content: center; gap: .5rem; }
.btn-sm { min-height: 36px; }
.form-control, .form-select { min-height: 44px; border-radius: .75rem; }
.form-control:focus, .form-select:focus, .form-check-input:focus { border-color: rgba(var(--bs-primary-rgb), .6); box-shadow: 0 0 0 .25rem var(--bs-focus-ring-color); }
.form-check-input:checked { background-color: var(--bs-primary); border-color: var(--bs-primary); }
.nav-pills { --bs-nav-pills-link-active-bg: var(--bs-primary); }
.nav { --bs-nav-link-color: var(--bs-secondary-color); --bs-nav-link-hover-color: var(--bs-emphasis-color); }
.pagination { --bs-pagination-active-bg: var(--bs-primary); --bs-pagination-active-border-color: var(--bs-primary); --bs-pagination-color: var(--bs-body-color); --bs-pagination-focus-box-shadow: 0 0 0 .25rem var(--bs-focus-ring-color); }
.progress, .progress-stacked { --bs-progress-bar-bg: var(--bs-primary); }
.list-group { --bs-list-group-active-bg: var(--bs-primary); --bs-list-group-active-border-color: var(--bs-primary); }
.dropdown-menu { --bs-dropdown-link-active-bg: var(--bs-primary); border-radius: .875rem; box-shadow: var(--wdw-shadow-sm); }
.accordion { --bs-accordion-active-bg: var(--bs-primary-bg-subtle); --bs-accordion-active-color: var(--bs-primary-text-emphasis); --bs-accordion-btn-focus-box-shadow: 0 0 0 .25rem var(--bs-focus-ring-color); }
.badge { font-weight: 600; }
/* Library building blocks — use these instead of re-inventing them per snippet. */
.wdw-stage { min-height: 100vh; display: flex; align-items: center; background: var(--wdw-stage); padding-block: 3rem; }
.wdw-card { background: var(--bs-body-bg); border: 1px solid var(--bs-border-color-translucent); border-radius: var(--wdw-radius-card); box-shadow: var(--wdw-shadow); }
.wdw-card-sm { background: var(--bs-body-bg); border: 1px solid var(--bs-border-color-translucent); border-radius: 1rem; box-shadow: var(--wdw-shadow-sm); }
.wdw-aurora { position: relative; color: #fff; background: radial-gradient(60% 55% at 100% 0, #818cf8 0, transparent 70%), radial-gradient(65% 60% at 0 100%, #ec4899 0, transparent 65%), #1e1b4b; }
.wdw-eyebrow { display: inline-block; font-size: .75rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--bs-primary); }
.wdw-heading { font-weight: 800; letter-spacing: -.02em; color: var(--bs-emphasis-color); }
.wdw-muted { color: var(--bs-secondary-color); }
.wdw-icon { width: 2.75rem; height: 2.75rem; flex: none; display: inline-flex; align-items: center; justify-content: center; border-radius: .875rem; background: var(--bs-primary-bg-subtle); color: var(--bs-primary); }
/* Component */
.product-media { min-height: 320px; background: var(--bs-tertiary-bg); }
.product-media img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; transition: transform .6s ease, opacity .25s ease; }
.product-media img.is-swapping { opacity: 0; }
.product:hover .product-media img { transform: scale(1.04); }
.fav { width: 2.5rem; height: 2.5rem; min-height: 0; background: var(--bs-body-bg); color: var(--bs-body-color); border: 1px solid var(--bs-border-color-translucent); }
.fav[aria-pressed="true"] svg { fill: #ef4444; stroke: #ef4444; }
.swatch { position: relative; width: 2.75rem; height: 2.75rem; border-radius: 50%; background: var(--c); cursor: pointer; border: 2px solid var(--bs-body-bg); box-shadow: 0 0 0 1px var(--bs-border-color); transition: box-shadow .15s; }
.swatch:hover { box-shadow: 0 0 0 2px var(--bs-secondary-color); }
.btn-check:checked + .swatch { box-shadow: 0 0 0 2px var(--bs-body-bg), 0 0 0 4px var(--bs-primary); }
.btn-check:checked + .swatch::after { content: ""; position: absolute; inset: 0; border-radius: 50%; }
.btn-check:checked + .swatch::before { content: ""; position: absolute; left: 50%; top: 50%; width: .5rem; height: .9rem; border: solid var(--ck); border-width: 0 2.5px 2.5px 0; transform: translate(-50%, -62%) rotate(45deg); }
.btn-check:focus-visible + .swatch { box-shadow: 0 0 0 2px var(--bs-body-bg), 0 0 0 4px var(--bs-primary), 0 0 0 7px var(--bs-focus-ring-color); }
.qty .btn { --bs-btn-color: var(--bs-body-color); --bs-btn-border-color: var(--bs-border-color); --bs-btn-hover-bg: var(--bs-tertiary-bg); --bs-btn-hover-color: var(--bs-emphasis-color); --bs-btn-hover-border-color: var(--bs-border-color); --bs-btn-active-bg: var(--bs-tertiary-bg); --bs-btn-active-color: var(--bs-emphasis-color); padding-inline: .75rem; }
.qty input[type=number] { -moz-appearance: textfield; appearance: textfield; }
.qty input[type=number]::-webkit-outer-spin-button, .qty input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
[data-add].is-added { --bs-btn-bg: var(--bs-success); --bs-btn-border-color: var(--bs-success); --bs-btn-color: #fff; --bs-btn-hover-bg: var(--bs-success); --bs-btn-hover-border-color: var(--bs-success); --bs-btn-hover-color: #fff; background: var(--bs-success); border-color: var(--bs-success); color: #fff; }
@media (prefers-reduced-motion: reduce) { .product-media img { transition: none; } }
(function () {
function initColour() {
var name = document.querySelector('[data-colour-name]');
var img = document.querySelector('[data-product-image]');
var inputs = document.querySelectorAll('input[name="colour"]');
if (!name || !inputs.length) return;
inputs.forEach(function (i) {
var pre = new Image(); pre.src = i.dataset.src;
i.addEventListener('change', function () {
name.textContent = i.value;
if (!img) return;
img.classList.add('is-swapping');
setTimeout(function () {
img.src = i.dataset.src;
img.alt = i.dataset.alt;
img.classList.remove('is-swapping');
}, 200);
});
});
}
function initQuantity() {
var input = document.querySelector('[data-qty-input]');
if (!input) return;
function clamp() { input.value = Math.min(10, Math.max(1, parseInt(input.value, 10) || 1)); }
document.querySelectorAll('[data-qty]').forEach(function (b) {
b.addEventListener('click', function () {
input.value = (parseInt(input.value, 10) || 1) + parseInt(b.dataset.qty, 10);
clamp();
});
});
input.addEventListener('change', clamp);
}
function initFavourite() {
var fav = document.querySelector('.fav');
if (!fav) return;
fav.addEventListener('click', function () {
fav.setAttribute('aria-pressed', String(fav.getAttribute('aria-pressed') !== 'true'));
});
}
function initAddToCart() {
var btn = document.querySelector('[data-add]');
var status = document.querySelector('[data-cart-status]');
if (!btn || !status) return;
var label = btn.querySelector('[data-add-label]');
var icon = btn.querySelector('.ck-icon');
var timer;
btn.addEventListener('click', function () {
var q = document.querySelector('[data-qty-input]').value;
var c = document.querySelector('input[name="colour"]:checked').value;
status.textContent = 'Added ' + q + ' x Aero ANC Headphones (' + c + ') to your cart.';
label.textContent = 'Added';
icon.classList.remove('d-none');
btn.classList.add('is-added');
clearTimeout(timer);
timer = setTimeout(function () {
label.textContent = 'Add to cart';
icon.classList.add('d-none');
btn.classList.remove('is-added');
}, 1800);
});
}
initColour();
initQuantity();
initFavourite();
initAddToCart();
})();
Built on Bootstrap 5.3.8 with plain JavaScript — no jQuery. Supports light and dark mode via data-bs-theme.