Lightbox Zoom
A copy-paste view transitions component in pure HTML, CSS & vanilla JS. Zero dependencies, framework-agnostic, MIT-licensed.
View TransitionsHTMLJavaScriptCSSany framework
Copy into your project
HTML
<div class="nuda-vt2-lightbox-zoom" data-state="closed">
<div class="nuda-vt2-lightbox-zoom__scrim" aria-hidden="true"></div>
<button type="button" class="nuda-vt2-lightbox-zoom__thumb" aria-label="Open photo"></button>
<button type="button" class="nuda-vt2-lightbox-zoom__full" aria-label="Close photo"></button>
</div>JavaScript
/* Lightbox Zoom — vanilla JS
Two elements share one view-transition-name (only one is visible/in-flow
at a time via display), so the API morphs the small thumbnail into the
full frame. A separate scrim fades in/out as its own named transition. */
(function () {
document.querySelectorAll(".nuda-vt2-lightbox-zoom").forEach(function (root) {
var thumb = root.querySelector(".nuda-vt2-lightbox-zoom__thumb");
var full = root.querySelector(".nuda-vt2-lightbox-zoom__full");
function open() {
function doSwap() { root.dataset.state = "open"; }
if (!document.startViewTransition) { doSwap(); return; }
document.startViewTransition(doSwap);
}
function close() {
function doSwap() { root.dataset.state = "closed"; }
if (!document.startViewTransition) { doSwap(); return; }
document.startViewTransition(doSwap);
}
thumb.addEventListener("click", open);
full.addEventListener("click", close);
});
})();CSS
.nuda-vt2-lightbox-zoom-demo {
padding: .5rem;
}
.nuda-vt2-lightbox-zoom {
position: relative;
width: 150px;
height: 96px;
}
.nuda-vt2-lightbox-zoom__scrim {
position: absolute;
inset: 0;
background: rgba(0,0,0,.6);
display: none;
border-radius: 12px;
}
.nuda-vt2-lightbox-zoom[data-state="open"] .nuda-vt2-lightbox-zoom__scrim {
display: block;
view-transition-name: vt2-lightbox-scrim;
}
.nuda-vt2-lightbox-zoom__thumb {
position: absolute;
left: 0;
top: 0;
width: 56px;
height: 56px;
border: none;
border-radius: 8px;
background: linear-gradient(135deg,#e4ff54,#a3e635);
cursor: pointer;
padding: 0;
}
.nuda-vt2-lightbox-zoom__thumb:focus-visible {
outline: 2px solid #e4ff54;
outline-offset: 2px;
}
.nuda-vt2-lightbox-zoom[data-state="closed"] .nuda-vt2-lightbox-zoom__thumb {
display: block;
view-transition-name: vt2-lightbox-photo;
}
.nuda-vt2-lightbox-zoom[data-state="open"] .nuda-vt2-lightbox-zoom__thumb {
display: none;
}
.nuda-vt2-lightbox-zoom__full {
position: absolute;
inset: 8px;
width: auto;
height: auto;
border: none;
border-radius: 10px;
background: linear-gradient(135deg,#e4ff54,#a3e635);
cursor: pointer;
padding: 0;
display: none;
}
.nuda-vt2-lightbox-zoom[data-state="open"] .nuda-vt2-lightbox-zoom__full {
display: block;
view-transition-name: vt2-lightbox-photo;
}
.nuda-vt2-lightbox-zoom__full:focus-visible {
outline: 2px solid #e4ff54;
outline-offset: 2px;
}
@keyframes _nuda-vt2scrimin {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes _nuda-vt2scrimout {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
::view-transition-new(vt2-lightbox-scrim) {
animation: _nuda-vt2scrimin .3s ease both;
}
::view-transition-old(vt2-lightbox-scrim) {
animation: _nuda-vt2scrimout .3s ease both;
}
::view-transition-group(vt2-lightbox-photo) {
animation-duration: .4s;
animation-timing-function: cubic-bezier(.4,0,.2,1);
}
@media (prefers-reduced-motion:reduce) {
::view-transition-new(vt2-lightbox-scrim),::view-transition-old(vt2-lightbox-scrim),::view-transition-group(vt2-lightbox-photo) {
animation: none;
}
}
How to use Lightbox Zoom
Paste the HTML where you need it and the CSS into a global stylesheet (or a <style> tag). Every class is prefixed nuda- so it never collides with Tailwind or your own styles. Tweak the CSS custom properties to match your design system.
Works in React, Vue, Svelte, Astro, Next.js, Nuxt, Laravel Blade, Django, Rails — or a single .html file. No npm install, no build step.