liftbox

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.

KindDetected fromRenders
Imageanything 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
YouTubeyoutube.com/watch, youtu.be, /embed/, /shorts/, /live/, youtube-nocookie.comIframe on https://www.youtube-nocookie.com/embed/ID, keeping a t or start time
Vimeovimeo.com/ID, vimeo.com/ID/HASH, player.vimeo.com/video/IDIframe on https://player.vimeo.com/video/ID with dnt=1 and the private hash
Pagetype: "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",
}
  • srcSet takes the same string as <img srcset>. When sizes is 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.
  • width and height are the natural size. With them, the image gets its final size before it loads. Without them, it is measured once it loads.
  • alt is the image's text alternative. The live region also reads it when there is no string title.
  • title renders as an h2 and description as 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 }],
}
  • sources lists alternative files in order of preference. src is used when it is missing.
  • tracks adds <track> elements. kind defaults to captions.
  • 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"
Edit this page on GitHub

On this page