Bootstrap 5 Waterwheel Card Carousel
A cascading waterwheel carousel where the active card is enlarged at the centre and neighbouring cards fall away on both sides, with previous, next and autoplay controls. Built with Bootstrap 5 utilities and a short vanilla script, with keyboard support.
<section class="wdw-stage">
<div class="container" style="max-width:1100px">
<div class="text-center mb-4 mb-md-5">
<span class="wdw-eyebrow">Workspace</span>
<h1 class="wdw-heading display-6 mt-1 mb-0">Everything in one place</h1>
</div>
<div class="ww" role="region" aria-roledescription="carousel" aria-label="Workspace sections" tabindex="0">
<div class="ww-stage">
<article class="ww-item" data-i="0" role="group" aria-roledescription="slide" aria-label="1 of 7"><div class="ww-card wdw-card"><span class="ww-badge"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20V3H6.5A2.5 2.5 0 0 0 4 5.5ZM4 19.5V21h16"/></svg></span><h2 class="h5 fw-bold mb-1">Library</h2><p class="small wdw-muted mb-0">Browse every document and guide</p></div></article>
<article class="ww-item" data-i="1" role="group" aria-roledescription="slide" aria-label="2 of 7"><div class="ww-card wdw-card"><span class="ww-badge"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 18h6M10 22h4M12 2a7 7 0 0 0-4 12.7c.6.5 1 1.3 1 2.3h6c0-1 .4-1.8 1-2.3A7 7 0 0 0 12 2Z"/></svg></span><h2 class="h5 fw-bold mb-1">Know-how</h2><p class="small wdw-muted mb-0">Playbooks from the team</p></div></article>
<article class="ww-item" data-i="2" role="group" aria-roledescription="slide" aria-label="3 of 7"><div class="ww-card wdw-card"><span class="ww-badge"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.6 13.4 13.4 20.6a2 2 0 0 1-2.8 0L2 12V2h10l8.6 8.6a2 2 0 0 1 0 2.8ZM7 7h.01"/></svg></span><h2 class="h5 fw-bold mb-1">Add taxonomy</h2><p class="small wdw-muted mb-0">Organise content with tags</p></div></article>
<article class="ww-item" data-i="3" role="group" aria-roledescription="slide" aria-label="4 of 7"><div class="ww-card wdw-card"><span class="ww-badge"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 8v13H3V8M1 3h22v5H1zM10 12h4"/></svg></span><h2 class="h5 fw-bold mb-1">Archive</h2><p class="small wdw-muted mb-0">Everything we retired</p></div></article>
<article class="ww-item" data-i="4" role="group" aria-roledescription="slide" aria-label="5 of 7"><div class="ww-card wdw-card"><span class="ww-badge"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8ZM23 21v-2a4 4 0 0 0-3-3.9M16 3.1a4 4 0 0 1 0 7.8"/></svg></span><h2 class="h5 fw-bold mb-1">Community</h2><p class="small wdw-muted mb-0">Ask, answer, share</p></div></article>
<article class="ww-item" data-i="5" role="group" aria-roledescription="slide" aria-label="6 of 7"><div class="ww-card wdw-card"><span class="ww-badge"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3v18h18M7 15l4-4 3 3 5-6"/></svg></span><h2 class="h5 fw-bold mb-1">Reports</h2><p class="small wdw-muted mb-0">Weekly numbers at a glance</p></div></article>
<article class="ww-item" data-i="6" role="group" aria-roledescription="slide" aria-label="7 of 7"><div class="ww-card wdw-card"><span class="ww-badge"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1Z"/></svg></span><h2 class="h5 fw-bold mb-1">Settings</h2><p class="small wdw-muted mb-0">Tune the workspace</p></div></article>
</div>
<div class="d-flex justify-content-center align-items-center gap-3 mt-4">
<button type="button" class="btn btn-outline-primary rounded-circle p-0" style="width:44px" data-dir="-1" aria-label="Previous"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg></button>
<button type="button" class="btn btn-primary rounded-pill px-4" data-play aria-pressed="false">Play</button>
<button type="button" class="btn btn-outline-primary rounded-circle p-0" style="width:44px" data-dir="1" aria-label="Next"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg></button>
</div>
</div>
</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 */
.ww { outline: none; }
.ww-stage { overflow-x: clip; overflow-y: visible; position: relative; height: 230px; perspective: 1200px; }
.ww-item { position: absolute; top: 0; left: 50%; width: min(240px, 52vw); margin-left: calc(min(240px, 52vw) / -2); height: 100%; display: flex; align-items: center; transition: transform .5s cubic-bezier(.2,.7,.3,1), opacity .4s; will-change: transform; cursor: pointer; }
.ww-card { width: 100%; padding: 1.5rem 1.25rem; text-align: center; transition: box-shadow .4s; }
.ww-badge { display: inline-grid; place-items: center; width: 3rem; height: 3rem; margin-bottom: .875rem; border-radius: 50%; background: var(--bs-primary-bg-subtle); color: var(--bs-primary); }
.ww-item[data-pos="0"] .ww-card { background: var(--bs-primary); border-color: transparent; color: #fff; box-shadow: 0 24px 48px -16px rgba(var(--bs-primary-rgb), .55); }
.ww-item[data-pos="0"] .ww-card .wdw-muted { color: rgba(255,255,255,.8); }
.ww-item[data-pos="0"] .ww-badge { background: rgba(255,255,255,.2); color: #fff; }
[data-bs-theme="dark"] .ww-item[data-pos="0"] .ww-card { color: #0f1117; }
[data-bs-theme="dark"] .ww-item[data-pos="0"] .ww-card .wdw-muted { color: rgba(15,17,23,.75); }
[data-bs-theme="dark"] .ww-item[data-pos="0"] .ww-badge { background: rgba(15,17,23,.15); color: #0f1117; }
.ww-item[data-pos="0"] { z-index: 3; }
.ww-item[data-pos="1"], .ww-item[data-pos="-1"] { z-index: 2; }
.ww-item[data-pos="2"], .ww-item[data-pos="-2"] { z-index: 1; }
.ww-item[data-pos="far"] { opacity: 0; pointer-events: none; z-index: 0; }
@media (prefers-reduced-motion: reduce) { .ww-item { transition: none; } }
function initWaterwheel() {
const root = document.querySelector('.ww');
if (!root) return;
const items = [...root.querySelectorAll('.ww-item')];
const playBtn = root.querySelector('[data-play]');
const n = items.length;
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let current = 2;
let timer = null;
const layout = () => {
const narrow = root.clientWidth < 600;
items.forEach((el, i) => {
let d = ((i - current) % n + n) % n;
if (d > n / 2) d -= n;
const abs = Math.abs(d);
const far = abs > 2;
el.dataset.pos = far ? 'far' : String(d);
const w = el.offsetWidth;
const x = d * w * (narrow ? 0.62 : 0.9);
const scale = 1 - abs * 0.16;
const y = abs * 14;
el.style.transform = 'translateX(' + x + 'px) translateY(' + y + 'px) scale(' + scale + ')';
el.setAttribute('aria-hidden', far ? 'true' : 'false');
el.tabIndex = -1;
});
};
const go = (dir) => { current = (current + dir + n) % n; layout(); };
const stop = () => { clearInterval(timer); timer = null; playBtn.textContent = 'Play'; playBtn.setAttribute('aria-pressed', 'false'); };
const start = () => { timer = setInterval(() => go(1), 2200); playBtn.textContent = 'Pause'; playBtn.setAttribute('aria-pressed', 'true'); };
root.querySelectorAll('[data-dir]').forEach((b) => b.addEventListener('click', () => { stop(); go(Number(b.dataset.dir)); }));
playBtn.addEventListener('click', () => (timer ? stop() : start()));
items.forEach((el, i) => el.addEventListener('click', () => { stop(); current = i; layout(); }));
root.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') { stop(); go(-1); }
if (e.key === 'ArrowRight') { stop(); go(1); }
});
root.addEventListener('mouseenter', () => { if (timer) stop(); });
window.addEventListener('resize', layout);
layout();
if (reduce) playBtn.disabled = true;
}
initWaterwheel();
Built on Bootstrap 5.3.8 with plain JavaScript — no jQuery. Supports light and dark mode via data-bs-theme.