Lightbox
A media lightbox web component with an inline gallery, fullscreen dialog, and thumbs.
The lightbox pairs an inline <embla-carousel> gallery with a fullscreen <dialog>
carousel and thumb strip, all wrapped in <media-lightbox>. Slides open the dialog via
command="show-modal", so opening and closing still comes from HTML. The web component
only coordinates state: it opens the dialog carousel at the gallery's current slide,
focuses the viewport for keyboard navigation, and syncs the inline gallery to the last
viewed slide when the dialog closes.
The elements use light DOM, so the .lightbox classes and data-embla-* attributes style
and configure the same markup you author. The preview loads the required scripts
automatically; open the JS tab to inspect the custom elements.
Default
<media-lightbox class="lightbox max-w-screen-xs mx-auto"> <div class="lightbox__gallery"> <embla-carousel class="lightbox__stage" data-embla-loop="true" data-embla-classnames> <div data-embla="viewport"> <div data-embla="container"> <button class="lightbox__slide" data-embla="slide" type="button" command="show-modal" commandfor="lightbox-example-1" > <figure class="lightbox__content"> <img sizes="(max-width: 40rem) 100vw, 800px" src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&h=600&fit=crop" srcset=" https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=456&h=342&fit=crop 456w, https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&h=600&fit=crop 800w " alt="Mountain landscape" loading="lazy" width="800" height="600" /> </figure> </button> <button class="lightbox__slide" data-embla="slide" type="button" command="show-modal" commandfor="lightbox-example-1" > <figure class="lightbox__content"> <img sizes="(max-width: 40rem) 100vw, 800px" src="https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=800&h=600&fit=crop" srcset=" https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=456&h=342&fit=crop 456w, https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=800&h=600&fit=crop 800w " alt="Valley with sunlight" loading="lazy" width="800" height="600" /> </figure> </button> <button class="lightbox__slide" data-embla="slide" type="button" command="show-modal" commandfor="lightbox-example-1" > <figure class="lightbox__content"> <img sizes="(max-width: 40rem) 100vw, 800px" src="https://images.unsplash.com/photo-1447752875215-b2761acb3c5d?w=800&h=600&fit=crop" srcset=" https://images.unsplash.com/photo-1447752875215-b2761acb3c5d?w=456&h=342&fit=crop 456w, https://images.unsplash.com/photo-1447752875215-b2761acb3c5d?w=800&h=600&fit=crop 800w " alt="Forest path" loading="lazy" width="800" height="600" /> </figure> </button> <button class="lightbox__slide" data-embla="slide" type="button" command="show-modal" commandfor="lightbox-example-1" > <figure class="lightbox__content"> <img sizes="(max-width: 40rem) 100vw, 800px" src="https://images.unsplash.com/photo-1433086966358-54859d0ed716?w=800&h=600&fit=crop" srcset=" https://images.unsplash.com/photo-1433086966358-54859d0ed716?w=456&h=342&fit=crop 456w, https://images.unsplash.com/photo-1433086966358-54859d0ed716?w=800&h=600&fit=crop 800w " alt="Waterfall in forest" loading="lazy" width="800" height="600" /> </figure> </button> <button class="lightbox__slide" data-embla="slide" type="button" command="show-modal" commandfor="lightbox-example-1" > <figure class="lightbox__content"> <img sizes="(max-width: 40rem) 100vw, 800px" src="https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=800&h=600&fit=crop" srcset=" https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=456&h=342&fit=crop 456w, https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=800&h=600&fit=crop 800w " alt="Lake at sunset" loading="lazy" width="800" height="600" /> </figure> </button> <button class="lightbox__slide" data-embla="slide" type="button" command="show-modal" commandfor="lightbox-example-1" > <figure class="lightbox__content"> <img sizes="(max-width: 40rem) 100vw, 800px" src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=800&h=600&fit=crop" srcset=" https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=456&h=342&fit=crop 456w, https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=800&h=600&fit=crop 800w " alt="Foggy forest" loading="lazy" width="800" height="600" /> </figure> </button> </div> </div> <div class="lightbox__thumbs" data-embla="thumbs" data-embla-loop="true"> <button class="lightbox__thumbs-prev button text-muted-foreground hover:text-foreground" data-size="icon-sm" data-variant="ghost" data-embla="prev" type="button" aria-label="Previous image" > <svg class="size-sm" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" aria-hidden="true" > <rect width="256" height="256" fill="none" /> <polyline points="160 208 80 128 160 48" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="14" /> </svg> </button> <div data-embla="viewport"> <div data-embla="container"> <button type="button" class="lightbox__thumb" data-embla="slide" aria-label="Show image 1" > <figure class="lightbox__thumb-content"> <img sizes="auto" loading="lazy" width="228" height="171" src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=228&h=171&fit=crop" srcset=" https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=228&h=171&fit=crop 228w, https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=456&h=342&fit=crop 456w " alt="Mountain landscape" loading="lazy" width="800" height="600" /> </figure> </button> <button type="button" class="lightbox__thumb" data-embla="slide" aria-label="Show image 2" > <figure class="lightbox__thumb-content"> <img sizes="auto" loading="lazy" width="228" height="171" src="https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=228&h=171&fit=crop" srcset=" https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=228&h=171&fit=crop 228w, https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=456&h=342&fit=crop 456w " alt="Valley with sunlight" loading="lazy" width="800" height="600" /> </figure> </button> <button type="button" class="lightbox__thumb" data-embla="slide" aria-label="Show image 3" > <figure class="lightbox__thumb-content"> <img sizes="auto" loading="lazy" width="228" height="171" src="https://images.unsplash.com/photo-1447752875215-b2761acb3c5d?w=228&h=171&fit=crop" srcset=" https://images.unsplash.com/photo-1447752875215-b2761acb3c5d?w=228&h=171&fit=crop 228w, https://images.unsplash.com/photo-1447752875215-b2761acb3c5d?w=456&h=342&fit=crop 456w " alt="Forest path" loading="lazy" width="800" height="600" /> </figure> </button> <button type="button" class="lightbox__thumb" data-embla="slide" aria-label="Show image 4" > <figure class="lightbox__thumb-content"> <img sizes="auto" loading="lazy" width="228" height="171" src="https://images.unsplash.com/photo-1433086966358-54859d0ed716?w=228&h=171&fit=crop" srcset=" https://images.unsplash.com/photo-1433086966358-54859d0ed716?w=228&h=171&fit=crop 228w, https://images.unsplash.com/photo-1433086966358-54859d0ed716?w=456&h=342&fit=crop 456w " alt="Waterfall in forest" loading="lazy" width="800" height="600" /> </figure> </button> <button type="button" class="lightbox__thumb" data-embla="slide" aria-label="Show image 5" > <figure class="lightbox__thumb-content"> <img sizes="auto" loading="lazy" width="228" height="171" src="https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=228&h=171&fit=crop" srcset=" https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=228&h=171&fit=crop 228w, https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=456&h=342&fit=crop 456w " alt="Lake at sunset" loading="lazy" width="800" height="600" /> </figure> </button> <button type="button" class="lightbox__thumb" data-embla="slide" aria-label="Show image 6" > <figure class="lightbox__thumb-content"> <img sizes="auto" loading="lazy" width="228" height="171" src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=228&h=171&fit=crop" srcset=" https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=228&h=171&fit=crop 228w, https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=456&h=342&fit=crop 456w " alt="Foggy forest" loading="lazy" width="800" height="600" /> </figure> </button> </div> </div> <button class="lightbox__thumbs-next button text-muted-foreground hover:text-foreground" data-size="icon-sm" data-variant="ghost" data-embla="next" type="button" aria-label="Next image" > <svg class="size-sm" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" aria-hidden="true" > <rect width="256" height="256" fill="none" /> <polyline points="96 48 176 128 96 208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="14" /> </svg> </button> </div> </embla-carousel> </div> <dialog id="lightbox-example-1" class="lightbox__dialog dialog dark" data-size="screen" closedby="any" > <div class="dialog__content"> <embla-carousel data-embla-loop="true" data-embla-classnames> <div data-embla="viewport"> <div data-embla="container"> <div class="lightbox__slide" data-embla="slide"> <figure class="lightbox__content"> <img sizes="(max-width: 40rem) 100vw, 800px" src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&h=600&fit=crop" srcset=" https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=456&h=342&fit=crop 456w, https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&h=600&fit=crop 800w " alt="Mountain landscape" loading="lazy" width="800" height="600" /> </figure> </div> <div class="lightbox__slide" data-embla="slide"> <figure class="lightbox__content"> <img sizes="(max-width: 40rem) 100vw, 800px" src="https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=800&h=600&fit=crop" srcset=" https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=456&h=342&fit=crop 456w, https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=800&h=600&fit=crop 800w " alt="Valley with sunlight" loading="lazy" width="800" height="600" /> </figure> </div> <div class="lightbox__slide" data-embla="slide"> <figure class="lightbox__content"> <img sizes="(max-width: 40rem) 100vw, 800px" src="https://images.unsplash.com/photo-1447752875215-b2761acb3c5d?w=800&h=600&fit=crop" srcset=" https://images.unsplash.com/photo-1447752875215-b2761acb3c5d?w=456&h=342&fit=crop 456w, https://images.unsplash.com/photo-1447752875215-b2761acb3c5d?w=800&h=600&fit=crop 800w " alt="Forest path" loading="lazy" width="800" height="600" /> </figure> </div> <div class="lightbox__slide" data-embla="slide"> <figure class="lightbox__content"> <img sizes="(max-width: 40rem) 100vw, 800px" src="https://images.unsplash.com/photo-1433086966358-54859d0ed716?w=800&h=600&fit=crop" srcset=" https://images.unsplash.com/photo-1433086966358-54859d0ed716?w=456&h=342&fit=crop 456w, https://images.unsplash.com/photo-1433086966358-54859d0ed716?w=800&h=600&fit=crop 800w " alt="Waterfall in forest" loading="lazy" width="800" height="600" /> </figure> </div> <div class="lightbox__slide" data-embla="slide"> <figure class="lightbox__content"> <img sizes="(max-width: 40rem) 100vw, 800px" src="https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=800&h=600&fit=crop" srcset=" https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=456&h=342&fit=crop 456w, https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=800&h=600&fit=crop 800w " alt="Lake at sunset" loading="lazy" width="800" height="600" /> </figure> </div> <div class="lightbox__slide" data-embla="slide"> <figure class="lightbox__content"> <img sizes="(max-width: 40rem) 100vw, 800px" src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=800&h=600&fit=crop" srcset=" https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=456&h=342&fit=crop 456w, https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=800&h=600&fit=crop 800w " alt="Foggy forest" loading="lazy" width="800" height="600" /> </figure> </div> </div> </div> <button class="lightbox__prev button size-auto text-muted-foreground hover:text-foreground" data-size="icon" data-variant="ghost" data-embla="prev" type="button" aria-label="Previous image" > <svg class="size-md" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"> <rect width="256" height="256" fill="none" /> <polyline points="160 208 80 128 160 48" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="14" /> </svg> </button> <button class="lightbox__next button size-auto text-muted-foreground hover:text-foreground" data-size="icon" data-variant="ghost" data-embla="next" type="button" aria-label="Next image" > <svg class="size-md" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"> <rect width="256" height="256" fill="none" /> <polyline points="96 48 176 128 96 208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="14" /> </svg> </button> <div class="lightbox__thumbs" data-embla="thumbs"> <div data-embla="viewport"> <div data-embla="container"> <button type="button" class="lightbox__thumb" data-embla="slide" aria-label="Show image 1" > <figure class="lightbox__thumb-content"> <img sizes="auto" loading="lazy" width="228" height="171" src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=228&h=171&fit=crop" srcset=" https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=228&h=171&fit=crop 228w, https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=456&h=342&fit=crop 456w " alt="Mountain landscape" loading="lazy" width="800" height="600" /> </figure> </button> <button type="button" class="lightbox__thumb" data-embla="slide" aria-label="Show image 2" > <figure class="lightbox__thumb-content"> <img sizes="auto" loading="lazy" width="228" height="171" src="https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=228&h=171&fit=crop" srcset=" https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=228&h=171&fit=crop 228w, https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=456&h=342&fit=crop 456w " alt="Valley with sunlight" loading="lazy" width="800" height="600" /> </figure> </button> <button type="button" class="lightbox__thumb" data-embla="slide" aria-label="Show image 3" > <figure class="lightbox__thumb-content"> <img sizes="auto" loading="lazy" width="228" height="171" src="https://images.unsplash.com/photo-1447752875215-b2761acb3c5d?w=228&h=171&fit=crop" srcset=" https://images.unsplash.com/photo-1447752875215-b2761acb3c5d?w=228&h=171&fit=crop 228w, https://images.unsplash.com/photo-1447752875215-b2761acb3c5d?w=456&h=342&fit=crop 456w " alt="Forest path" loading="lazy" width="800" height="600" /> </figure> </button> <button type="button" class="lightbox__thumb" data-embla="slide" aria-label="Show image 4" > <figure class="lightbox__thumb-content"> <img sizes="auto" loading="lazy" width="228" height="171" src="https://images.unsplash.com/photo-1433086966358-54859d0ed716?w=228&h=171&fit=crop" srcset=" https://images.unsplash.com/photo-1433086966358-54859d0ed716?w=228&h=171&fit=crop 228w, https://images.unsplash.com/photo-1433086966358-54859d0ed716?w=456&h=342&fit=crop 456w " alt="Waterfall in forest" loading="lazy" width="800" height="600" /> </figure> </button> <button type="button" class="lightbox__thumb" data-embla="slide" aria-label="Show image 5" > <figure class="lightbox__thumb-content"> <img sizes="auto" loading="lazy" width="228" height="171" src="https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=228&h=171&fit=crop" srcset=" https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=228&h=171&fit=crop 228w, https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=456&h=342&fit=crop 456w " alt="Lake at sunset" loading="lazy" width="800" height="600" /> </figure> </button> <button type="button" class="lightbox__thumb" data-embla="slide" aria-label="Show image 6" > <figure class="lightbox__thumb-content"> <img sizes="auto" loading="lazy" width="228" height="171" src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=228&h=171&fit=crop" srcset=" https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=228&h=171&fit=crop 228w, https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=456&h=342&fit=crop 456w " alt="Foggy forest" loading="lazy" width="800" height="600" /> </figure> </button> </div> </div> </div> </embla-carousel> </div> <button class="lightbox__close button" data-size="icon" data-variant="ghost" type="button" commandfor="lightbox-example-1" command="close" aria-label="Close lightbox" > <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"> <rect width="256" height="256" fill="none" /> <line x1="200" y1="56" x2="56" y2="200" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" /> <line x1="200" y1="200" x2="56" y2="56" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" /> </svg> </button> </dialog></media-lightbox>// carousel.js"use strict";/** * @fileoverview `<embla-carousel>` — HTML web component for Embla carousels. * @description Light-DOM custom element that wraps standard carousel markup * and owns the Embla lifecycle: it initializes on connect (via * `EmblaInit.initRoot`) and destroys its instances on disconnect, so * dynamically inserted or SPA-swapped carousels need no manual wiring. * * The element *is* the carousel root — it applies `data-embla="root"` to * itself on connect, so all existing CSS hooks and `data-embla-*` * configuration attributes work unchanged (see embla.js for the full * attribute reference). No shadow DOM; children are regular markup. * * Carousels inside a closed `<dialog>` defer initialization until the dialog * first opens (a closed dialog is `display: none`, so Embla cannot measure * the viewport). * * Load order: Embla CDN bundles → utils.js → embla.js → carousel.js. * * @example * <embla-carousel data-embla-loop="true"> * <div data-embla="viewport"> * <div data-embla="container"> * <div data-embla="slide">Slide 1</div> * <div data-embla="slide">Slide 2</div> * </div> * </div> * <button type="button" data-embla="prev">Prev</button> * <button type="button" data-embla="next">Next</button> * </embla-carousel> */import { EmblaInit } from "./embla.js";class EmblaCarouselElement extends HTMLElement { /** @type {MutationObserver|null} */ #dialogObserver = null; connectedCallback() { // The element is the carousel root — expose the CSS/config hook. this.setAttribute("data-embla", "root"); const dialog = this.closest("dialog"); if (dialog && !dialog.open) { // Closed dialogs are display:none — Embla can't measure the viewport. // Initialize on the dialog's first open instead. this.#dialogObserver = new MutationObserver(() => { if (dialog.open) this.init(); }); this.#dialogObserver.observe(dialog, { attributes: true, attributeFilter: ["open"] }); return; } this.init(); } disconnectedCallback() { this.#dialogObserver?.disconnect(); this.#dialogObserver = null; // Abort first so the per-carousel DOM listeners (prev/next, dots, thumbs, // drag-click suppression) are removed before the Embla instances are torn down. this._emblaController?.abort(); delete this._emblaController; this._emblaApi?.destroy(); this._emblaApiThumb?.destroy(); delete this._emblaApi; delete this._emblaApiThumb; // Allow re-initialization if the element is re-inserted. this.removeAttribute("data-embla-init"); } /** * @description Initializes the carousel. Idempotent — already-initialized * roots and roots inside closed dialogs are skipped by `initRoot`. * * @returns {void} */ init() { EmblaInit.initRoot(this); } /** * @returns {EmblaCarouselType|null} The Embla API, or null before initialization. */ get api() { return this._emblaApi ?? null; }}// Register the element (guarded against double script loads)if (typeof window !== "undefined" && !customElements.get("embla-carousel")) { customElements.define("embla-carousel", EmblaCarouselElement);}// Attach to window so embla.js's lightbox sync can feature-detect the element type,// and export for module consumers (lightbox.js imports it via the main.js bundle).if (typeof window !== "undefined") { window.EmblaCarouselElement = EmblaCarouselElement;}export { EmblaCarouselElement };// lightbox.js"use strict";/** * @fileoverview `<media-lightbox>` — HTML web component for lightbox galleries. * @description Light-DOM custom element that coordinates the two carousels in * a lightbox: the inline gallery and the fullscreen `<dialog>` slideshow. * * Responsibilities: * - On dialog open: initializes the dialog's `<embla-carousel>` (deferred * while the dialog was closed), jumps it to the gallery's current slide, * and focuses the viewport so keyboard navigation works immediately. * - On dialog close: scrolls the inline gallery to the last viewed slide. * * Opening and closing the dialog itself needs no JavaScript — slides carry * `command="show-modal"` / `command="close"` (Invoker Commands). Drag-aware * click suppression on the stage and thumbs is wired by `initRoot` in * embla.js (keyed on `.lightbox__stage` / thumbs markup). * * Load order: Embla CDN bundles → utils.js → embla.js → carousel.js → lightbox.js. * * @example * <media-lightbox class="lightbox"> * <div class="lightbox__gallery"> * <embla-carousel class="lightbox__stage" data-embla-loop="true">…</embla-carousel> * </div> * <dialog class="lightbox__dialog dialog" closedby="any"> * <embla-carousel data-embla-loop="true">…</embla-carousel> * </dialog> * </media-lightbox> */import { EmblaCarouselElement } from "./carousel.js";class MediaLightbox extends HTMLElement { /** @type {AbortController|null} */ #controller = null; /** @type {MutationObserver|null} */ #dialogObserver = null; connectedCallback() { if (this.#controller) return; const dialog = this.querySelector("dialog"); if (!(dialog instanceof HTMLDialogElement)) return; this.#controller = new AbortController(); dialog.addEventListener("close", () => this.#syncGalleryToDialog(dialog), { signal: this.#controller.signal, }); this.#dialogObserver = new MutationObserver(() => { if (dialog.open) this.#onDialogOpen(dialog); }); this.#dialogObserver.observe(dialog, { attributes: true, attributeFilter: ["open"] }); } disconnectedCallback() { this.#controller?.abort(); this.#controller = null; this.#dialogObserver?.disconnect(); this.#dialogObserver = null; } /** * @returns {Element|null} The inline gallery's carousel root. */ #galleryRoot() { return this.querySelector('.lightbox__gallery [data-embla="root"]'); } /** * @description Initializes the dialog carousel, opens it at the gallery's * current slide, and moves focus to the slideshow viewport. * * @param {HTMLDialogElement} dialog - The lightbox dialog. * @returns {void} */ #onDialogOpen(dialog) { const dialogRoot = dialog.querySelector('[data-embla="root"]'); if (!dialogRoot) return; // <embla-carousel> defers init while its dialog is closed — init now. // (This element connects before its children, so its observer fires first.) if (dialogRoot instanceof EmblaCarouselElement) { dialogRoot.init(); } const galleryApi = this.#galleryRoot()?._emblaApi; if (dialogRoot._emblaApi && galleryApi) { dialogRoot._emblaApi.scrollTo(galleryApi.selectedScrollSnap(), true); } const viewport = dialogRoot.querySelector('[data-embla="viewport"]'); if (viewport instanceof HTMLElement) { viewport.focus({ preventScroll: true }); } } /** * @description Scrolls the inline gallery to the slide last viewed in the dialog. * * @param {HTMLDialogElement} dialog - The lightbox dialog. * @returns {void} */ #syncGalleryToDialog(dialog) { const dialogApi = dialog.querySelector('[data-embla="root"]')?._emblaApi; const galleryApi = this.#galleryRoot()?._emblaApi; if (dialogApi && galleryApi) { galleryApi.scrollTo(dialogApi.selectedScrollSnap()); } }}// Register the element (guarded against double script loads)if (typeof window !== "undefined" && !customElements.get("media-lightbox")) { customElements.define("media-lightbox", MediaLightbox);}// Attach to window for parity with the other component scripts, and export for// module consumers (loaded for its side effect — the custom-element registration).if (typeof window !== "undefined") { window.MediaLightbox = MediaLightbox;}export { MediaLightbox };API
| Markup | Where | Purpose |
|---|---|---|
<media-lightbox class="lightbox"> | root | Coordinates the inline gallery and dialog carousel |
<embla-carousel class="lightbox__stage"> | gallery | Inline carousel; owns drag, thumbs, and stage navigation |
<dialog class="lightbox__dialog"> | root | Fullscreen modal surface |
<embla-carousel> | dialog | Fullscreen carousel; initialized when the dialog opens |
command="show-modal" / command="close" | triggers | Opens and closes the dialog through Invoker Commands |
data-embla="viewport" / "container" / "slide" / "thumbs" | children | Embla roles for stage, slides, and thumb carousel |
Put Embla options (data-embla-loop, data-embla-classnames, etc.) on each
<embla-carousel>.