Skip to content

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.

More view transitions components

← Browse all NudaUI components