Slides
Images, video files, YouTube, Vimeo and any page in an iframe.
Every item in slides is a Slide. Its src decides how it renders unless you set type.
| Kind | Detected from | Renders |
|---|---|---|
| Image | anything else, or type: "image" | <img> with srcset, sizes and explicit dimensions |
| Video file | .mp4, .webm, .ogv, .ogg, .mov, .m4v, or type: "video" | Native <video> with controls, playsInline, poster, sources and tracks; preload="metadata" on the current slide, "none" on neighbors |
| YouTube | youtube.com/watch, youtu.be, /embed/, /shorts/, /live/, youtube-nocookie.com | Iframe on https://www.youtube-nocookie.com/embed/ID, keeping a t or start time |
| Vimeo | vimeo.com/ID, vimeo.com/ID/HASH, player.vimeo.com/video/ID | Iframe on https://player.vimeo.com/video/ID with dnt=1 and the private hash |
| Page | type: "iframe" | Iframe on src |
The demo below mixes a native video with captions, images and an iframe page.
Live demo. Click a thumbnail to open it.
Images
{
src: "/photos/harbour-1600.jpg",
srcSet: "/photos/harbour-640.jpg 640w, /photos/harbour-1600.jpg 1600w",
width: 1600,
height: 1067,
alt: "Fishing boats in the harbour at dusk",
title: "Harbour",
description: "Porto, 2026",
thumbnail: "/photos/harbour-320.jpg",
}srcSettakes the same string as<img srcset>. Whensizesis not set, the lightbox fills it with the width the image is rendered at, so the browser picks the smallest file that is sharp enough.widthandheightare the natural size. With them, the image gets its final size before it loads. Without them, it is measured once it loads.altis the image's text alternative. The live region also reads it when there is no stringtitle.titlerenders as anh2anddescriptionas text under it. Both accept React nodes.
Modern formats and art direction
Give an image slide sources and it renders inside a <picture>. The browser takes the first source whose type it can decode and whose media matches, and falls back to src and srcSet otherwise:
{
src: "/photos/harbour-1600.jpg",
srcSet: "/photos/harbour-640.jpg 640w, /photos/harbour-1600.jpg 1600w",
sources: [
{
srcSet: "/photos/harbour-640.avif 640w, /photos/harbour-1600.avif 1600w",
type: "image/avif",
},
{ srcSet: "/photos/harbour-tall-900.webp 900w", media: "(orientation: portrait)" },
],
width: 1600,
height: 1067,
alt: "Fishing boats in the harbour at dusk",
}Sources get the image's sizes unless they set their own, and wait their turn like the image does: neighbors only start downloading once the current slide has loaded. The image box keeps the slide's width and height, so a source with another aspect ratio, like the portrait crop above, shows letterboxed inside it. When your crops change shape, leave width and height out and the slide is measured from whichever file the browser picked.
Caption position
By default the caption is a band across the bottom of the viewer. captionPosition="slide" lays each caption over the bottom of its own image instead, as wide as the image, so it moves with the slide when you swipe:
<Lightbox captionPosition="slide" slides={slides} open={open} onClose={close} />render.caption then runs for every mounted slide, not just the current one. Video and iframe slides keep their caption in the bar either way, below the player.
Under a video or iframe, the bar never covers the player: the slide is fitted into the room left above the caption, so the controls stay visible and clickable. A custom player from render.slide gets that smaller width and height too.
While an image loads, its slide has data-status="loading", shows the slide's thumbnail behind it as a placeholder and a spinner after --eg-spinner-delay. If it fails, the slide shows labels.loadError and gets data-status="error".
Video files
{
src: "/media/talk.mp4",
poster: "/media/talk.jpg",
width: 1920,
height: 1080,
alt: "Conference talk",
sources: [
{ src: "/media/talk.webm", type: "video/webm" },
{ src: "/media/talk.mp4", type: "video/mp4" },
],
tracks: [{ src: "/media/talk.en.vtt", srcLang: "en", label: "English", default: true }],
}sourceslists alternative files in order of preference.srcis used when it is missing.tracksadds<track>elements.kinddefaults tocaptions.- Leaving a video slide pauses its player.
YouTube and Vimeo
{ src: "https://www.youtube.com/watch?v=VIDEO_ID", title: "Product tour" }
{ src: "https://vimeo.com/VIDEO_ID", alt: "Short film" }No player script is loaded on your page. YouTube runs on youtube-nocookie.com and Vimeo gets dnt=1, each inside its own iframe. Embeds exist only on the current slide, so moving on stops them. Neighboring embed slides show poster; YouTube falls back to its own thumbnail, then to thumbnail.
Any page
{ src: "https://maps.example.com/embed?q=porto", type: "iframe", title: "Map", width: 960, height: 600 }Without width and height, an iframe slide fills the stage. Remember to allow its origin in your Content Security Policy.
Sizing
Videos and embeds use width and height to keep their shape. When those are unknown they are 16:9, and a native video switches to its real size once its metadata loads.
Accessible names
Players are named from alt, then a string title, then labels.video. Pass tracks so people who cannot hear the audio can follow along.
Autoplay
<Lightbox video={{ autoplay: true }} {...props} />Starts the current video or embed muted, which is what browsers allow without a click. It is off by default.
Detecting the kind yourself
getMedia(slide) returns the same decision the lightbox makes, if your grid needs a play badge on video thumbnails:
import { getMedia } from "liftbox";
getMedia({ src: "https://youtu.be/VIDEO_ID" });
// {
// kind: "youtube",
// embed: "https://www.youtube-nocookie.com/embed/VIDEO_ID?rel=0&playsinline=1",
// poster: "https://i.ytimg.com/vi/VIDEO_ID/hqdefault.jpg",
// }previewOf(slide) returns the still the grids and the thumbnail strip show: thumbnail, then poster, then the provider's poster, then src for images. Videos and embeds without a still get undefined, so you never put a video file in an <img>:
import { previewOf } from "liftbox";
previewOf({ src: "/clip.mp4", poster: "/clip.jpg" }); // "/clip.jpg"
previewOf({ src: "/clip.mp4" }); // undefined
previewOf({ src: "/a.jpg" }); // "/a.jpg"