Bootstrap 5 Card Gallery with View Transitions
A filterable photo card gallery where cards glide into their new positions and a chosen photo morphs into a full detail view, powered by the View Transitions API. It falls back to instant updates when the API is unsupported or reduced motion is preferred, and works fully from the keyboard.
<section class="wdw-stage vt-gallery">
<div class="container" style="max-width:1040px">
<div class="vt-view-grid">
<div class="d-flex flex-column flex-md-row align-items-md-end justify-content-between gap-3 mb-4">
<div>
<span class="wdw-eyebrow">Field notes</span>
<h2 class="wdw-heading display-6 mb-1">Wild places</h2>
<p class="wdw-muted mb-0" id="vt-count" role="status" aria-live="polite">Showing 9 of 9 photos</p>
</div>
<div class="d-flex flex-wrap gap-2" role="group" aria-label="Filter photos by category">
<button type="button" class="btn rounded-pill btn-primary px-4" data-filter="all" aria-pressed="true">All</button>
<button type="button" class="btn rounded-pill btn-outline-primary px-4" data-filter="mountains" aria-pressed="false">Mountains</button>
<button type="button" class="btn rounded-pill btn-outline-primary px-4" data-filter="water" aria-pressed="false">Water</button>
<button type="button" class="btn rounded-pill btn-outline-primary px-4" data-filter="wild" aria-pressed="false">Wilderness</button>
</div>
</div>
<ul class="list-unstyled row row-cols-1 row-cols-sm-2 row-cols-lg-3 g-3 g-lg-4 mb-0" id="vt-grid">
<li class="col" data-id="0" data-category="mountains">
<article class="wdw-card-sm vt-card h-100 position-relative overflow-hidden">
<img class="vt-photo w-100" src="https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?w=640&h=480&fit=crop&auto=format&q=70" width="640" height="480" alt="Pine ridge: evergreens climb a ridge while snow holds on to the far peaks">
<div class="p-3 d-flex align-items-center justify-content-between gap-2">
<h3 class="h6 fw-bold mb-0"><button type="button" class="btn btn-link stretched-link p-0 text-body-emphasis text-decoration-none vt-open" style="min-height:0">Pine ridge</button></h3>
<span class="badge rounded-pill text-bg-light border">Mountains</span>
</div>
</article>
</li>
<li class="col" data-id="1" data-category="water">
<article class="wdw-card-sm vt-card h-100 position-relative overflow-hidden">
<img class="vt-photo w-100" src="https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=640&h=480&fit=crop&auto=format&q=70" width="640" height="480" alt="Alpine lake: glacier-green water, a single rowing boat and cliffs on both sides">
<div class="p-3 d-flex align-items-center justify-content-between gap-2">
<h3 class="h6 fw-bold mb-0"><button type="button" class="btn btn-link stretched-link p-0 text-body-emphasis text-decoration-none vt-open" style="min-height:0">Alpine lake</button></h3>
<span class="badge rounded-pill text-bg-light border">Water</span>
</div>
</article>
</li>
<li class="col" data-id="2" data-category="wild">
<article class="wdw-card-sm vt-card h-100 position-relative overflow-hidden">
<img class="vt-photo w-100" src="https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=640&h=480&fit=crop&auto=format&q=70" width="640" height="480" alt="Forest trail: tall trunks, low sun and a path that disappears around the bend">
<div class="p-3 d-flex align-items-center justify-content-between gap-2">
<h3 class="h6 fw-bold mb-0"><button type="button" class="btn btn-link stretched-link p-0 text-body-emphasis text-decoration-none vt-open" style="min-height:0">Forest trail</button></h3>
<span class="badge rounded-pill text-bg-light border">Wilderness</span>
</div>
</article>
</li>
<li class="col" data-id="3" data-category="mountains">
<article class="wdw-card-sm vt-card h-100 position-relative overflow-hidden">
<img class="vt-photo w-100" src="https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=640&h=480&fit=crop&auto=format&q=70" width="640" height="480" alt="Golden ridge: haze softens the layers of a valley while a lone hiker stands on the rocks above it">
<div class="p-3 d-flex align-items-center justify-content-between gap-2">
<h3 class="h6 fw-bold mb-0"><button type="button" class="btn btn-link stretched-link p-0 text-body-emphasis text-decoration-none vt-open" style="min-height:0">Golden ridge</button></h3>
<span class="badge rounded-pill text-bg-light border">Mountains</span>
</div>
</article>
</li>
<li class="col" data-id="4" data-category="water">
<article class="wdw-card-sm vt-card h-100 position-relative overflow-hidden">
<img class="vt-photo w-100" src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=640&h=480&fit=crop&auto=format&q=70" width="640" height="480" alt="River at dawn: a shallow river carries the first pink light between dark pines">
<div class="p-3 d-flex align-items-center justify-content-between gap-2">
<h3 class="h6 fw-bold mb-0"><button type="button" class="btn btn-link stretched-link p-0 text-body-emphasis text-decoration-none vt-open" style="min-height:0">River at dawn</button></h3>
<span class="badge rounded-pill text-bg-light border">Water</span>
</div>
</article>
</li>
<li class="col" data-id="5" data-category="wild">
<article class="wdw-card-sm vt-card h-100 position-relative overflow-hidden">
<img class="vt-photo w-100" src="https://images.unsplash.com/photo-1519681393784-d120267933ba?w=640&h=480&fit=crop&auto=format&q=70" width="640" height="480" alt="Milky Way peaks: snowy summits under a dense band of stars">
<div class="p-3 d-flex align-items-center justify-content-between gap-2">
<h3 class="h6 fw-bold mb-0"><button type="button" class="btn btn-link stretched-link p-0 text-body-emphasis text-decoration-none vt-open" style="min-height:0">Milky Way peaks</button></h3>
<span class="badge rounded-pill text-bg-light border">Wilderness</span>
</div>
</article>
</li>
<li class="col" data-id="6" data-category="mountains">
<article class="wdw-card-sm vt-card h-100 position-relative overflow-hidden">
<img class="vt-photo w-100" src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=640&h=480&fit=crop&auto=format&q=70" width="640" height="480" alt="Valley mist: cloud spills over a green escarpment as the sun breaks through">
<div class="p-3 d-flex align-items-center justify-content-between gap-2">
<h3 class="h6 fw-bold mb-0"><button type="button" class="btn btn-link stretched-link p-0 text-body-emphasis text-decoration-none vt-open" style="min-height:0">Valley mist</button></h3>
<span class="badge rounded-pill text-bg-light border">Mountains</span>
</div>
</article>
</li>
<li class="col" data-id="7" data-category="water">
<article class="wdw-card-sm vt-card h-100 position-relative overflow-hidden">
<img class="vt-photo w-100" src="https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=640&h=480&fit=crop&auto=format&q=70" width="640" height="480" alt="Sunset shore: warm light on the foam and a long, flat beach">
<div class="p-3 d-flex align-items-center justify-content-between gap-2">
<h3 class="h6 fw-bold mb-0"><button type="button" class="btn btn-link stretched-link p-0 text-body-emphasis text-decoration-none vt-open" style="min-height:0">Sunset shore</button></h3>
<span class="badge rounded-pill text-bg-light border">Water</span>
</div>
</article>
</li>
<li class="col" data-id="8" data-category="wild">
<article class="wdw-card-sm vt-card h-100 position-relative overflow-hidden">
<img class="vt-photo w-100" src="https://images.unsplash.com/photo-1469975692758-66d107a536cb?w=640&h=480&fit=crop&auto=format&q=70" width="640" height="480" alt="Desert stars: sandstone towers glow against a night sky">
<div class="p-3 d-flex align-items-center justify-content-between gap-2">
<h3 class="h6 fw-bold mb-0"><button type="button" class="btn btn-link stretched-link p-0 text-body-emphasis text-decoration-none vt-open" style="min-height:0">Desert stars</button></h3>
<span class="badge rounded-pill text-bg-light border">Wilderness</span>
</div>
</article>
</li>
</ul>
</div>
<div class="vt-view-detail wdw-card p-3 p-lg-4" hidden>
<div class="d-flex align-items-center justify-content-between gap-2 mb-3">
<button type="button" class="btn btn-outline-primary rounded-pill px-3 vt-back"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg> Back to gallery</button>
<div class="d-flex gap-2">
<button type="button" class="btn btn-outline-secondary rounded-pill px-3 vt-step" data-dir="-1" aria-label="Previous photo"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg></button>
<button type="button" class="btn btn-outline-secondary rounded-pill px-3 vt-step" data-dir="1" aria-label="Next photo"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 18l6-6-6-6"/></svg></button>
</div>
</div>
<div class="vt-detail-body">
<div class="vt-detail-item row g-4 g-lg-5 align-items-center" data-detail="0" hidden>
<div class="col-lg-7"><img class="vt-photo w-100" src="https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?w=1200&h=900&fit=crop&auto=format&q=70" width="1200" height="900" loading="lazy" alt="Pine ridge: evergreens climb a ridge while snow holds on to the far peaks"></div>
<div class="col-lg-5">
<span class="badge rounded-pill bg-primary-subtle text-primary-emphasis mb-3">Mountains</span>
<h2 class="wdw-heading h2 mb-3" tabindex="-1">Pine ridge</h2>
<p class="wdw-muted mb-4">Evergreens climb a ridge while snow holds on to the far peaks. A wide, clear view that rewards the early start.</p>
<dl class="row g-0 small mb-4">
<dt class="col-5 wdw-muted fw-medium">Best light</dt><dd class="col-7 fw-semibold mb-2">Late morning</dd>
<dt class="col-5 wdw-muted fw-medium">Mood</dt><dd class="col-7 fw-semibold mb-0">Clear and crisp</dd>
</dl>
</div>
</div>
<div class="vt-detail-item row g-4 g-lg-5 align-items-center" data-detail="1" hidden>
<div class="col-lg-7"><img class="vt-photo w-100" src="https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=1200&h=900&fit=crop&auto=format&q=70" width="1200" height="900" loading="lazy" alt="Alpine lake: glacier-green water, a single rowing boat and cliffs on both sides"></div>
<div class="col-lg-5">
<span class="badge rounded-pill bg-primary-subtle text-primary-emphasis mb-3">Water</span>
<h2 class="wdw-heading h2 mb-3" tabindex="-1">Alpine lake</h2>
<p class="wdw-muted mb-4">Glacier-green water, a single rowing boat and cliffs on both sides. The kind of still you only get before the wind picks up.</p>
<dl class="row g-0 small mb-4">
<dt class="col-5 wdw-muted fw-medium">Best light</dt><dd class="col-7 fw-semibold mb-2">Early morning</dd>
<dt class="col-5 wdw-muted fw-medium">Mood</dt><dd class="col-7 fw-semibold mb-0">Calm and bright</dd>
</dl>
</div>
</div>
<div class="vt-detail-item row g-4 g-lg-5 align-items-center" data-detail="2" hidden>
<div class="col-lg-7"><img class="vt-photo w-100" src="https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=1200&h=900&fit=crop&auto=format&q=70" width="1200" height="900" loading="lazy" alt="Forest trail: tall trunks, low sun and a path that disappears around the bend"></div>
<div class="col-lg-5">
<span class="badge rounded-pill bg-primary-subtle text-primary-emphasis mb-3">Wilderness</span>
<h2 class="wdw-heading h2 mb-3" tabindex="-1">Forest trail</h2>
<p class="wdw-muted mb-4">Tall trunks, low sun and a path that disappears around the bend. Slow down; the light changes every few steps.</p>
<dl class="row g-0 small mb-4">
<dt class="col-5 wdw-muted fw-medium">Best light</dt><dd class="col-7 fw-semibold mb-2">Golden hour</dd>
<dt class="col-5 wdw-muted fw-medium">Mood</dt><dd class="col-7 fw-semibold mb-0">Hushed and warm</dd>
</dl>
</div>
</div>
<div class="vt-detail-item row g-4 g-lg-5 align-items-center" data-detail="3" hidden>
<div class="col-lg-7"><img class="vt-photo w-100" src="https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=1200&h=900&fit=crop&auto=format&q=70" width="1200" height="900" loading="lazy" alt="Golden ridge: haze softens the layers of a valley while a lone hiker stands on the rocks above it"></div>
<div class="col-lg-5">
<span class="badge rounded-pill bg-primary-subtle text-primary-emphasis mb-3">Mountains</span>
<h2 class="wdw-heading h2 mb-3" tabindex="-1">Golden ridge</h2>
<p class="wdw-muted mb-4">Haze softens the layers of a valley while a lone hiker stands on the rocks above it. Plan extra time for the descent.</p>
<dl class="row g-0 small mb-4">
<dt class="col-5 wdw-muted fw-medium">Best light</dt><dd class="col-7 fw-semibold mb-2">Sunrise</dd>
<dt class="col-5 wdw-muted fw-medium">Mood</dt><dd class="col-7 fw-semibold mb-0">Hazy and gold</dd>
</dl>
</div>
</div>
<div class="vt-detail-item row g-4 g-lg-5 align-items-center" data-detail="4" hidden>
<div class="col-lg-7"><img class="vt-photo w-100" src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=1200&h=900&fit=crop&auto=format&q=70" width="1200" height="900" loading="lazy" alt="River at dawn: a shallow river carries the first pink light between dark pines"></div>
<div class="col-lg-5">
<span class="badge rounded-pill bg-primary-subtle text-primary-emphasis mb-3">Water</span>
<h2 class="wdw-heading h2 mb-3" tabindex="-1">River at dawn</h2>
<p class="wdw-muted mb-4">A shallow river carries the first pink light between dark pines. Wet rocks make the best foreground.</p>
<dl class="row g-0 small mb-4">
<dt class="col-5 wdw-muted fw-medium">Best light</dt><dd class="col-7 fw-semibold mb-2">Dawn</dd>
<dt class="col-5 wdw-muted fw-medium">Mood</dt><dd class="col-7 fw-semibold mb-0">Cool and quiet</dd>
</dl>
</div>
</div>
<div class="vt-detail-item row g-4 g-lg-5 align-items-center" data-detail="5" hidden>
<div class="col-lg-7"><img class="vt-photo w-100" src="https://images.unsplash.com/photo-1519681393784-d120267933ba?w=1200&h=900&fit=crop&auto=format&q=70" width="1200" height="900" loading="lazy" alt="Milky Way peaks: snowy summits under a dense band of stars"></div>
<div class="col-lg-5">
<span class="badge rounded-pill bg-primary-subtle text-primary-emphasis mb-3">Wilderness</span>
<h2 class="wdw-heading h2 mb-3" tabindex="-1">Milky Way peaks</h2>
<p class="wdw-muted mb-4">Snowy summits under a dense band of stars. Far from town lights the sky does most of the work for you.</p>
<dl class="row g-0 small mb-4">
<dt class="col-5 wdw-muted fw-medium">Best light</dt><dd class="col-7 fw-semibold mb-2">New moon</dd>
<dt class="col-5 wdw-muted fw-medium">Mood</dt><dd class="col-7 fw-semibold mb-0">Silent and vast</dd>
</dl>
</div>
</div>
<div class="vt-detail-item row g-4 g-lg-5 align-items-center" data-detail="6" hidden>
<div class="col-lg-7"><img class="vt-photo w-100" src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=1200&h=900&fit=crop&auto=format&q=70" width="1200" height="900" loading="lazy" alt="Valley mist: cloud spills over a green escarpment as the sun breaks through"></div>
<div class="col-lg-5">
<span class="badge rounded-pill bg-primary-subtle text-primary-emphasis mb-3">Mountains</span>
<h2 class="wdw-heading h2 mb-3" tabindex="-1">Valley mist</h2>
<p class="wdw-muted mb-4">Cloud spills over a green escarpment as the sun breaks through. A narrow road is the only sign of people.</p>
<dl class="row g-0 small mb-4">
<dt class="col-5 wdw-muted fw-medium">Best light</dt><dd class="col-7 fw-semibold mb-2">Sunrise</dd>
<dt class="col-5 wdw-muted fw-medium">Mood</dt><dd class="col-7 fw-semibold mb-0">Moody and open</dd>
</dl>
</div>
</div>
<div class="vt-detail-item row g-4 g-lg-5 align-items-center" data-detail="7" hidden>
<div class="col-lg-7"><img class="vt-photo w-100" src="https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=1200&h=900&fit=crop&auto=format&q=70" width="1200" height="900" loading="lazy" alt="Sunset shore: warm light on the foam and a long, flat beach"></div>
<div class="col-lg-5">
<span class="badge rounded-pill bg-primary-subtle text-primary-emphasis mb-3">Water</span>
<h2 class="wdw-heading h2 mb-3" tabindex="-1">Sunset shore</h2>
<p class="wdw-muted mb-4">Warm light on the foam and a long, flat beach. Low tide gives you a mirror at your feet.</p>
<dl class="row g-0 small mb-4">
<dt class="col-5 wdw-muted fw-medium">Best light</dt><dd class="col-7 fw-semibold mb-2">Sunset</dd>
<dt class="col-5 wdw-muted fw-medium">Mood</dt><dd class="col-7 fw-semibold mb-0">Soft and golden</dd>
</dl>
</div>
</div>
<div class="vt-detail-item row g-4 g-lg-5 align-items-center" data-detail="8" hidden>
<div class="col-lg-7"><img class="vt-photo w-100" src="https://images.unsplash.com/photo-1469975692758-66d107a536cb?w=1200&h=900&fit=crop&auto=format&q=70" width="1200" height="900" loading="lazy" alt="Desert stars: sandstone towers glow against a night sky"></div>
<div class="col-lg-5">
<span class="badge rounded-pill bg-primary-subtle text-primary-emphasis mb-3">Wilderness</span>
<h2 class="wdw-heading h2 mb-3" tabindex="-1">Desert stars</h2>
<p class="wdw-muted mb-4">Sandstone towers glow against a night sky. A tripod, patience and a warm jacket are all you need.</p>
<dl class="row g-0 small mb-4">
<dt class="col-5 wdw-muted fw-medium">Best light</dt><dd class="col-7 fw-semibold mb-2">After dusk</dd>
<dt class="col-5 wdw-muted fw-medium">Mood</dt><dd class="col-7 fw-semibold mb-0">Still and dry</dd>
</dl>
</div>
</div>
</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 */
/* Card gallery with View Transitions.
JS adds view-transition-name only while a transition runs (see script.js). */
.vt-photo { height: auto; aspect-ratio: 3 / 2; object-fit: cover; display: block; border-radius: 0; background: var(--bs-secondary-bg); }
.vt-detail-item .vt-photo { border-radius: var(--bs-border-radius-xl); }
.vt-card { transition: box-shadow .2s ease, transform .2s ease; }
.vt-card:hover { transform: translateY(-3px); box-shadow: var(--wdw-shadow); }
.vt-card:has(.vt-open:focus-visible) { outline: 3px solid var(--bs-primary); outline-offset: 3px; }
.vt-open:focus-visible { outline: none; box-shadow: none; }
.vt-open { text-align: start; }
[data-bs-theme="dark"] .badge.text-bg-light { background-color: var(--bs-tertiary-bg) !important; color: var(--bs-body-color) !important; }
/* Named pieces share one class so a single rule styles them all. */
.vt-card { view-transition-class: vt-card; }
@media (prefers-reduced-motion: no-preference) {
/* Cards glide to their new grid slots; ones that appear or leave cross-fade. */
::view-transition-group(*.vt-card) { animation-duration: .45s; animation-timing-function: cubic-bezier(.2, .8, .2, 1); }
::view-transition-old(*.vt-card), ::view-transition-new(*.vt-card) { animation-duration: .3s; }
/* The page itself: a quick fade so the morphing photo does the talking. */
::view-transition-group(root) { animation-duration: .25s; }
/* The photo morphs between the card thumbnail and the detail image. */
::view-transition-group(hero) { animation-duration: .5s; animation-timing-function: cubic-bezier(.2, .8, .2, 1); }
::view-transition-old(hero), ::view-transition-new(hero) { height: 100%; object-fit: cover; }
/* Next / previous: the photo slides instead of cross-fading in place. */
html:active-view-transition-type(next, prev) {
&::view-transition-old(hero) { animation: vt-out .35s ease both; }
&::view-transition-new(hero) { animation: vt-in .35s ease both; }
}
html:active-view-transition-type(prev) {
&::view-transition-old(hero) { animation-name: vt-out-r; }
&::view-transition-new(hero) { animation-name: vt-in-r; }
}
}
@keyframes vt-out { to { opacity: 0; transform: translateX(-12%); } }
@keyframes vt-in { from { opacity: 0; transform: translateX(12%); } }
@keyframes vt-out-r { to { opacity: 0; transform: translateX(12%); } }
@keyframes vt-in-r { from { opacity: 0; transform: translateX(-12%); } }
/* Anchor to the top so the header does not jump when the grid gets shorter. */
.vt-gallery { align-items: flex-start; }
(function () {
var root = document.querySelector('.vt-gallery');
if (!root) return;
var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
var supportsTypes = typeof ViewTransition !== 'undefined' && 'types' in ViewTransition.prototype;
var gridView = root.querySelector('.vt-view-grid');
var detailView = root.querySelector('.vt-view-detail');
var items = Array.prototype.slice.call(root.querySelectorAll('#vt-grid > li'));
var details = Array.prototype.slice.call(root.querySelectorAll('.vt-detail-item'));
var current = 0;
var canAnimate = function () { return !!document.startViewTransition && !reduceMotion.matches; };
// Run a DOM update inside a view transition when the browser supports it and
// the visitor has not asked for less motion. Otherwise just run the update.
// The optional type lets the CSS style each kind of change differently.
function swap(update, type, cleanup) {
if (!canAnimate()) {
update();
return;
}
var vt = supportsTypes
? document.startViewTransition({ update: update, types: [type] })
: document.startViewTransition(update);
if (cleanup) vt.finished.finally(cleanup);
}
/* ---- Filtering ---- */
function initFilters() {
var buttons = root.querySelectorAll('[data-filter]');
var count = root.querySelector('#vt-count');
buttons.forEach(function (btn) {
btn.addEventListener('click', function () {
var filter = btn.dataset.filter;
// Name every card so each one can glide to its new slot. The names live
// only for the length of the transition: unique, and cheap when idle.
var named = canAnimate();
var setNames = function (on) {
if (!named) return;
items.forEach(function (li) { li.firstElementChild.style.viewTransitionName = on ? 'card-' + li.dataset.id : ''; });
};
setNames(true);
swap(function () {
var shown = 0;
items.forEach(function (li) {
var match = filter === 'all' || li.dataset.category === filter;
li.hidden = !match;
if (match) shown++;
});
buttons.forEach(function (b) {
var on = b === btn;
b.setAttribute('aria-pressed', on);
b.classList.toggle('btn-primary', on);
b.classList.toggle('btn-outline-primary', !on);
});
count.textContent = 'Showing ' + shown + ' of ' + items.length + ' photos';
}, 'filter', function () { setNames(false); });
});
});
}
/* ---- Detail view ---- */
function showDetail(index) {
current = index;
details.forEach(function (d, i) { d.hidden = i !== index; });
}
function cardPhoto(index) { return items[index].querySelector('.vt-photo'); }
function detailPhoto(index) { return details[index].querySelector('.vt-photo'); }
function open(index) {
var from = cardPhoto(index);
var run = canAnimate();
if (run) from.style.viewTransitionName = 'hero';
function update() {
from.style.viewTransitionName = '';
gridView.hidden = true;
detailView.hidden = false;
showDetail(index);
if (run) detailPhoto(index).style.viewTransitionName = 'hero';
details[index].querySelector('h2').focus({ preventScroll: true });
}
swap(update, 'open', function () { detailPhoto(index).style.viewTransitionName = ''; });
}
function close() {
var index = current;
var run = canAnimate();
if (run) detailPhoto(index).style.viewTransitionName = 'hero';
function update() {
detailPhoto(index).style.viewTransitionName = '';
detailView.hidden = true;
gridView.hidden = false;
if (run) cardPhoto(index).style.viewTransitionName = 'hero';
items[index].querySelector('.vt-open').focus({ preventScroll: true });
}
swap(update, 'close', function () { cardPhoto(index).style.viewTransitionName = ''; });
}
function step(dir) {
var next = (current + dir + details.length) % details.length;
var prev = current;
var run = canAnimate();
if (run) detailPhoto(prev).style.viewTransitionName = 'hero';
function update() {
detailPhoto(prev).style.viewTransitionName = '';
showDetail(next);
if (run) detailPhoto(next).style.viewTransitionName = 'hero';
details[next].querySelector('h2').focus({ preventScroll: true });
}
swap(update, dir > 0 ? 'next' : 'prev', function () { detailPhoto(next).style.viewTransitionName = ''; });
}
function initDetail() {
items.forEach(function (li) {
li.querySelector('.vt-open').addEventListener('click', function () { open(+li.dataset.id); });
});
detailView.querySelector('.vt-back').addEventListener('click', close);
detailView.querySelectorAll('.vt-step').forEach(function (b) {
b.addEventListener('click', function () { step(+b.dataset.dir); });
});
root.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && !detailView.hidden) close();
});
}
initFilters();
initDetail();
})();
Built on Bootstrap 5.3.8 with plain JavaScript — no jQuery. Supports light and dark mode via data-bs-theme.