
The <LightboxImage /> custom element transforms static images into an interactive lightbox modal with smooth transitions. Instead of navigating away, users can tap an image for an enlarged modal.
This implementation combines the native <dialog> element with the View Transition API to create fluid animations when opening and closing images.
How to use it:
1. Enclose each image within the <lightbox-image /> Custom Element.
<lightbox-image dialog-id="lightbox"> <img src='1.jpg' alt=''> </lightbox-image> <lightbox-image dialog-id="lightbox"> <img src='2.jpg' alt=''> </lightbox-image> ...
2. Create a <dialog> element that will serve as your lightbox container. The id should match the dialog-id you used earlier.
<dialog id="lightbox">
<form method="dialog">
<button>Close</button>
</form>
</dialog>3. Apply these CSS styles to control the lightbox appearance:
html:has(dialog[open]) {
overflow: hidden;
scrollbar-gutter: stable;
}
lightbox-image:defined {
display: block;
cursor: zoom-in;
}
lightbox-image {
max-width: 250px;
}
dialog {
--_gutter: 2rem;
padding: 1rem;
outline: unset;
border: unset;
background: unset;
max-height: 100vh;
cursor: zoom-out;
&::-webkit-backdrop {
background: blue;
opacity: 0.75;
}
&::backdrop {
background: blue;
opacity: 0.75;
}
form {
position: absolute;
opacity: 0;
}
img {
max-height: calc(100vh - var(--_gutter));
}
}
::view-transition-group(active-lightbox-image) {
-webkit-animation-duration: 300ms;
animation-duration: 300ms;
-webkit-animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}4. Add the following script to your webpage. This defines the LightBoxImage class and registers the custom element.
- The constructor creates a shadow DOM for encapsulation
- The connectedCallback method sets up event listeners and initial DOM structure
- The moveImage method handles transition logic using the View Transition API
- Click handlers manage image movement between thumbnail and lightbox views
- The View Transition API creates smooth animations between states
class LightBoxImage extends HTMLElement {
get dialog() {
const attr = this.getAttribute("dialog-id");
return document.getElementById(attr);
}
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
this.image = this.querySelector("img");
this.shadowRoot.innerHTML = this.setupToggle();
this.toggle = this.shadowRoot.querySelector("button");
this.toggle.addEventListener("click", this);
this.dialog.addEventListener("click", this);
this.dialog.addEventListener("cancel", this);
}
setupToggle() {
return `
<style>
button {
all: unset;
outline: revert;
display: grid;
grid-template-areas: "box";
}
button > * {
grid-area: box;
}
img {
max-width: 100%;
height: auto;
visibility: hidden;
}
</style>
<button aria-label="Open lightbox">
${this.image.outerHTML}
<div>
<slot></slot>
</div>
</button>
`;
}
handleEvent(e) {
this[`on${e.type}`](e);
}
onclick(e) {
if (e.currentTarget === this.toggle) {
this.moveImage(() => this.moveImageToTarget());
}
if (e.currentTarget === this.dialog) {
this.dialogCallback(e);
}
}
// Handle "escape" key dialog event
oncancel(e) {
this.dialogCallback(e);
}
dialogCallback(e) {
if (this.dialog.contains(this.image)) {
e.preventDefault();
this.moveImage(() => this.moveImageBack());
}
}
moveImage(fn) {
if (!document.startViewTransition) {
fn();
} else {
this.handleViewTransition(fn);
}
}
async handleViewTransition(fn) {
this.image.style.viewTransitionName = "active-lightbox-image";
const transition = document.startViewTransition(() => fn());
try {
await transition.finished;
} finally {
this.image.style.removeProperty("view-transition-name");
}
}
moveImageToTarget() {
this.dialog.append(this.image);
this.dialog.showModal();
}
moveImageBack(e) {
this.append(this.image);
this.dialog.close();
}
}
customElements.define("lightbox-image", LightBoxImage);





