docs/components/lightbox

Lightboxpreview

A full-screen viewer that opens from its triggers, with mobile gestures and zoom.

import { Lightbox } from "@logic-ui/react/lightbox"
sourcemarkdownedit

Usage guidelines#

  • The images fly: the image grows out of its trigger when the viewer opens and flies back when it closes, to its own trigger or to the one closeTarget picks, such as a "+5" tile. Hide the trigger while it has data-popup-open.
  • One gesture engine: swipe, pinch, double tap, pan and pull to dismiss are handled together, so they never fight over a touch. A long drag can cross multiple slides and settles at the position reached, rather than advancing only one. The viewer needs no carousel or zoom library.
  • Your layout, your items: the lightbox renders no thumbnails of its own. Lay out your page however you like and place a Lightbox.Trigger per item. Items can have any shape; the lightbox only needs a stable value for each.
  • No dependencies: the lightbox needs no carousel, zoom, animation or image library, and no framework. Lightbox.Media wraps your own <img>, <video> or <iframe>; give it any src or srcSet.

Anatomy#

Import the component and assemble its parts:

Anatomy

Examples#

Getting started#

The smallest viewer: triggers, the images and a close button. Swipe, pinch, double tap, pull to dismiss and the keyboard all work.

Lightbox.Root takes the items in items, with any shape. Each item needs a value that identifies it and stays the same when items are added, removed or reordered: by default its id, or whatever itemToValue returns. Lightbox.Trigger opens the viewer on the item with its value. Lightbox.Viewport calls its function child for the item shown and its neighbours; the parts take what they need as props. Your <img> gets its src and alt, and Lightbox.Media gets the natural width and height, which give it the right shape before the image loads.

Items

A single image#

One trigger opens one image, here a profile photo with the zoom only. The image grows out of the circle: the flight takes each corner radius of its trigger. Round the triggers themselves (first:rounded-l-xl), not a container that clips them (overflow-hidden rounded-xl): the flight can't see a parent's clip. The gallery at the top of the page does the same with its corner tiles.

Maya Lindqvist

Photographer

Chat attachments#

One lightbox for the whole conversation: swipe from one message's images to the next. The last tile of a message counts the images it hides, which the viewer still shows; Dots pick the image, with a bright dot gliding over the active one.

The messages scroll. As you move through the images, the list scrolls behind the viewer to the message of the image shown (scrollToTrigger, on by default), so closing always flies back to a tile you can see. With closeTarget="nearest", an image hidden behind "+1" flies back to the "+1" tile, which stays empty while such an image is shown, and turns into it on the way.

Lightbox.createHandle() links a lightbox to code and to triggers outside Lightbox.Root, like Base UI's detached triggers: pass the same handle to the root, and to each Lightbox.Trigger placed elsewhere. Here "Shared media" opens the first image with sharedMedia.open(value). Its tile is scrolled out of view, so the image fades in rather than flying from a place you can't see.

# weekend-trip

3 members

A
Ana

Photos from the trip. The valley at sunrise was unreal.

S
Sam

That jellyfish though.

L
Lee

Wait, where was the waterfall?

A
Ana

Day two. The rest of the set.

S
Sam

Saving all of these.

Where the image lands#

When the viewer closes, the image flies back to a trigger on the page, and focus returns to it. closeTarget on Lightbox.Root picks which:

  • "active" (the default): the trigger of the item shown. Without one on the page, the image fades out where it is.
  • "opener": the trigger that opened the viewer, whichever item is shown. For a cover that opens a whole album, like the ones below.
  • "nearest": the trigger of the item shown, or else the trigger of the nearest item that has one. For a grid that shows a few items and a "+5" tile, like the gallery at the top of the page.

A trigger that shows something other than its media takes crossfade: an avatar for a story, or a tile with a "+5" count or a play badge, which then fade out as the image leaves and back in as it lands. With it, the image grows out of its look as the viewer opens, and turns back into it on the way home, like the stories below.

The trigger the image will fly back to has data-popup-open the whole time the viewer is open, following the item shown: hide it (data-popup-open:invisible) and its slot waits empty, for the drop after a drag too. When that trigger shows another image (the cover, the "+5" tile), a copy of it flies along with the image and fades in over it, so the image turns into the trigger on the way, and lands looking exactly like it. A trigger that can't be seen, scrolled out of the window or out of a list that clips it, gets the image faded out instead; scrollToTrigger scrolls the page to it beforehand, as in the chat above.

Iceland

4 photos

Faces and places

4 photos

An album cover

Instagram-style stories#

The people stay in one horizontally scrollable row on every screen size. Their rings change color and opacity on hover without lifting the avatars. The demo uses select-none to avoid accidental selection, except for the reply field, which keeps select-text.

Each ring is a person, and the items of the lightbox are the people: swipe, or drag past the edge of the card, to go from one person to the next. A person's own stories play inside their slide, like Instagram:

  • They play by themselves. One bar per story fills for five seconds, or for the length of a video, then the next story shows, then the next person. After the last one, the viewer closes, and the ring turns grey.
  • Tap the left third of the card to go back, anywhere else to go on; ← → do the same. Hold to pause; the controls fade while you hold. The pause button and typing a reply pause too, as WCAG asks of content that moves on by itself.
  • Controlled. The stories move on to the next person by changing value, and the viewer slides there as it does for Next; after the last person, open turns false. onOpenChangeComplete starts the bars once the card has landed. The viewer loads the story the person is on before the card flies out of their ring.
  • Keys of your own. The popup's onKeyDown runs before its shortcuts: it takes ← → for the stories and prevents the default, so the library doesn't also change the person.

The rings take crossfade, since they show a face rather than the story: the card grows out of the face as it opens, and turns back into it as it flies home.

Every control sits on the story card, inside Lightbox.Media: the bars, the author, pause, Lightbox.Close, the story's text in a Lightbox.Caption, and the reply field. So they slide with the person during a swipe, follow the pull to dismiss, and fly back into the ring with the card. Controls inside a slide get their clicks: a press that starts on a button, a link or a field isn't a gesture. maxScale={1} keeps the stories at their resting scale.

Every story is the same shape: Lightbox.Media takes fill, so the card fills its slide whatever the shape of the photo or video, and object-cover on the media covers it. The viewport sets the size in CSS: 9:16 on a large screen, the whole screen on a phone.

The stories turn like the faces of a cube, in CSS only. Each Lightbox.Slide has --lightbox-slide-progress, its distance from the centre in slides (0 shown, -1 one to the left, 1 one to the right), on every frame of a swipe, its settle and Next. The library places slides with translate, so their transform is free: here each person turns by the progress times 90° around the edge they share with the next one, with a perspective on Lightbox.Viewport. The same variable makes a fade (opacity), a stack, or a coverflow.

Cube transition

Stories

From the people you follow.

6 new
  • Maya
  • Noah
  • Sofia
  • Leo
  • Ava
  • Kai

3D coverflow#

The same variable makes a coverflow. neighbors={3} on Lightbox.Viewport renders three covers on each side, instead of one. The covers sit closer together than the viewport is wide, so --lightbox-swipe-distance on the viewport sets how far the finger travels for one cover, as any CSS length ([--lightbox-swipe-distance:calc(var(--cover-size)*2.2)]), and the covers keep up with the finger instead of lagging behind it. Slides placed side by side don't need it, even when the media is smaller than the viewport: the media moves with its slide. While you hold a cover, the viewport has data-swiping; raising it (data-swiping:z-10) draws the cover above its reflection and the controls.

The structure separates four jobs:

  • Lightbox.Slide places the cover in the fan and sorts it by depth. Placement stays outside perspective, so depth does not stretch the spacing.
  • An inner pose turns the cover in its own perspective(), flattened into the slide. A smoothstep curve eases the turn into each wing. The slides stay parallel, so covers overlap without cutting through each other, including halfway through a swipe.
  • A surface inside the pose handles the pull and the exit. While the cover shown is pulled to dismiss, the others follow it a little (--lightbox-dismiss-movement-x and -y), shrink slightly and dim with --lightbox-dismiss-progress. On data-ending-style, while the active Lightbox.Media flies back to its trigger, they fly there too: each cancels its own distance from the centre, moves by --lightbox-flight-movement-x and -y, and scales down to almost nothing from --lightbox-flight-scale, fading out in 60% of --lightbox-flight-duration. The ambient background, captions and controls leave on the same timing. Do not transform or fade the active surface or the whole popup: that would move or fade its return flight too.
  • A decorative, masked image below the media supplies the reflection without a browser-specific property. It fades on close and zoom. It is outside Lightbox.Media, so it never becomes part of the flying image.

The cover size, perspective and depth scale together on smaller screens. The viewport fills the viewer; slide padding sizes and positions the resting covers. This keeps zoom and pan constrained by the viewer edges rather than the small cover box, avoiding empty strips when the enlarged image can fill that axis. While zoomed, neighboring covers and reflections fade away; keep the media's overflow visible so the enlarged image is not clipped. Put the ring and shadow on the image, not Lightbox.Media: zoom and pan transform only the image, so a frame on the media box would stay behind. The image inherits the media's rounded corners, including during zoom and the opening and closing flights. Reduced motion uses a flat carousel without reflections or entrance/exit movement.

Northern Lights

Aurora Field

Low Tide

The Undertow

Long Exposure

Night Shift

Ember

Saltwater Kids

Red Rock

Desert Lines

Above the Fog

Morning Choir

Open Water

Blue Hour

Base Camp

High Country

Coast Road

The Wanderers

Back Alley

Brick & Mortar

Coverflow

3D models#

An armory of live 3D weapons, rendered with Google's <model-viewer> and grouped by class. A weapon turns on its tile under the pointer, and they all open in one viewer: drag to rotate the weapon shown, and move between them with the arrows, the cards or ← →. The cards are grouped by class too, and the indicator, corner brackets with a lit bar, glides along with the slides. Escape or the close button flies the weapon back to its tile, turned the way you left it.

Lightbox.Media takes any content, so the model's element goes inside it with a width and height for its shape. Only images zoom, so the zoom gestures leave it alone. Its drags would normally swipe or pull the viewer closed: mark the element with Base UI's data-base-ui-swipe-ignore and the viewer leaves every press on it to the model. Set it to "x" or "y" to keep only the drags along that axis, so a drag along the other one still swipes or pulls, as in a Base UI drawer.

The viewer carries on from the tile, mid-turn and mid-animation, because it shows the same <model-viewer> element: the demo creates one per model outside React and moves it between its tile and its slide. Nothing is copied or loaded twice.

  • Ready before it flies. Before it opens, the viewer is mounted out of sight. A ref callback on each slide moves its model in, and onOpenChange returns a Promise that waits until the model shown has drawn itself at the viewer's size. Only then does it fly, so its first frame is already sharp and the right size.
  • Never an empty tile. A weapon that leaves its tile leaves a still of itself, from toDataURL(), in its place. When the viewer closes, the still is taken again, so the tile shows the model as it was turned. Once the model is back and has drawn, the still goes. The thumbnails are stills too.
  • Smooth flights. Every model holds still during the flights, so it isn't redrawn on every frame of them. Once the viewer has landed, only the model shown moves.
  • The angle you left it at. On its own, a moved <model-viewer> goes back to its camera-orbit. The demo writes the current orbit into it before each move.

Armory

11 weapons

Primary · Assault rifle

MDR

Assault rifle

Primary · Submachine gun

MPX

Submachine gun

MPSD

Submachine gun

Primary · Marksman rifle

MK14

Marksman rifle

M21 EBR

Marksman rifle

Primary · Sniper rifle

SRSA1

Sniper rifle

Precision rifle

Sniper rifle

Secondary · Sidearm

P320

Sidearm

P226

Sidearm

Revolver

Sidearm

Secondary · Launcher

RPG

Launcher

Models by austincford on Poly Pizza, CC BY 3.0.

A 3D model

The models are by austincford on Poly Pizza, under CC BY 3.0.

Controlled and changing items#

The viewer's state is two values, each of which you can control: whether it is open, with open and onOpenChange, and the item it shows, with value and onValueChange. Here they are shown in an address bar, as an app would put them in the URL. The value stays on the last item shown after the viewer closes, and is the first item until one is set. Changing value while the viewer is open slides to the new item, as Next does. A trigger calls onValueChange when it shows another item, then onOpenChange; handle.open(value) does the same.

items can change at any time, even while the viewer is open: it stays on the same item. Delete the open photo from the viewer and it moves to the neighbour; delete the last one and it closes. Add photos and the counter follows.

/album

Album · 5 photos

Controlled lightbox

Both callbacks also receive eventDetails, built like Base UI's: reason tells what made the change ("swipe", "trigger-press", "thumbnail-press", "escape-key", "close-press" …), event is the native event and trigger the element it came from. Call eventDetails.cancel() to stop it: a cancelled swipe springs back, and a cancelled pull to dismiss puts the image back. onOpenChangeComplete is called once the image has landed, in the viewer (true) or back on the page (false).

Change reasons

Loading and errors#

Before it opens, the viewer is mounted out of sight until the <img> or <video> in Lightbox.Media has loaded (a video, its size), so the image shows at once and flies at the right size. The trigger has data-pending meanwhile. The viewer opens anyway after LIGHTBOX_OPEN_TIMEOUT (3 s), still loading.

onOpenChange can return a Promise, and the viewer waits for it as well: here the slow photo waits 1.5 seconds more, as on a slow network. It waits up to 3 s before opening and 300 ms before closing (LIGHTBOX_CLOSE_TIMEOUT), then goes on. eventDetails.cancel() works until the wait ends, after an await too, and a cancelled change never happened: no onOpenChangeComplete, nothing to undo. A rejected Promise is logged, and the viewer goes on.

Inside the viewer, Lightbox.Media and its Lightbox.Slide have data-loading while the media loads and data-error if it fails, from the media's own events. Put a spinner or a message next to the media, inside Lightbox.Media: everything in it flies and follows the pull to dismiss. When the image fails, hide it (group-data-error:invisible): the box keeps its shape and the message takes its place. Give the <img> a new src to load it again. A failed image doesn't zoom, and the viewer keeps working.

  • Unknown size. Without width and height, the shape is only known once the image loads: fade it in with group-data-loading:opacity-0.
  • Retry. The fourth photo fails once and loads on retry. The message has role="alert", so screen readers announce it.
  • Broken thumbnails. When a photo is gone, its thumbnail fails too. Base UI's Avatar shows an icon in its place; its delay keeps a thumbnail that loads quickly from flashing the icon.

Instant

Slow network

Unknown size

Fails once

Missing

Loading and errors

Lightbox.Media only reads its direct <img>, <video> or <iframe> child (or the <img> of a <picture>), so an icon inside the message doesn't count. Content without one counts as loaded at once.

To show smaller images as thumbnails and larger ones in the viewer, give each <img> its own src or srcSet: the viewer waits for the one it shows.

Video and other media#

Lightbox.Media takes any media as its direct child: an <img>, a <video>, an <iframe>. It fits the media's shape inside the slide, flies it from the trigger and back, and follows the pull to dismiss. Only images zoom: on other media the zoom gestures do nothing and Lightbox.ZoomIn is disabled. A video pauses when its slide is no longer shown and when the viewer closes, and while a player with controls has focus, the arrow keys stay its own. Give the <video> preload="metadata": the viewer opens once it knows the video's size.

Give an embed its width and height (16 and 9). An iframe keeps every touch to itself, so swipe beside it or use Lightbox.Previous and Lightbox.Next.

Captions and animations#

Lightbox.Title and Lightbox.Description name and describe the viewer for screen readers. A function child receives the item shown, to build its title or caption; Lightbox.Description renders nothing when the result is empty. For text that belongs to a slide and moves with it, put a Lightbox.Caption in the slide: it describes its slide.

When the item shown changes, Lightbox.Title and Lightbox.Description (with a function child), Lightbox.Counter and Lightbox.Caption have data-starting-style for one frame, with data-activation-direction set to the side the new item came from: right after Next in a left-to-right layout, left after Previous. Style where the new text starts from, and a transition brings it in, as in the gallery at the top of the page. The element stays on screen while its text changes, so turn the transition off while the attribute is there (data-starting-style:transition-none): the text jumps to its start, then animates in.

Animated captions

Lightbox.Counter and Lightbox.ZoomValue take a function child, which receives their state, to format the position and the scale: {(state) => `${state.index + 1} of ${state.count}`}.

Toolbar#

The actions are plain buttons. To group them in a Base UI Toolbar, render each one as a Toolbar.Button. The lightbox part goes outside, so it keeps its disabled, and the toolbar keeps a disabled action focusable for its arrow keys. Set orientation="vertical" on Toolbar.Root for a column: ↑ and ↓ move between the actions.

Toolbar

Thumbnails#

Lightbox.ThumbnailList renders a Lightbox.Thumbnail per item inside the viewer, through a function child like Lightbox.Viewport. What's inside a thumbnail is yours: an image, a dot, a title. The list is a tab list and each thumbnail a tab, as in the WAI-ARIA tabbed carousel: the slides become their tab panels, and the thumbnail of the item shown has aria-selected and data-active.

  • Keyboard. The list is a single tab stop. The arrow keys move between thumbnails and show their item, and Home and End go to the first and last. With activateOnFocus={false}, the arrows only move focus and Enter shows the item.
  • Orientation. orientation sets which arrow keys move and which way the list scrolls: ← → for "horizontal" (the default), ↑ ↓ for "vertical". The list has data-orientation, so one class list can style both.
  • Scrolling. Give the list overflow: auto. The active thumbnail glides to its centre, and is centred again when a CSS transition changes its size. The mouse can drag the list (data-swiping meanwhile), and the wheel scrolls a horizontal list. Touch scrolls natively.
  • Edges. The list has data-overflow-start and data-overflow-end while thumbnails are hidden past an edge: fade that edge with a mask, data-overflow-end:mask-r-from-85%. Hide the browser's scrollbar with [scrollbar-width:none] [&::-webkit-scrollbar]:hidden.
  • Sliding indicator. Lightbox.ThumbnailIndicator, placed in the list after the function child, is one element that glides to the active thumbnail, like Base UI's Tabs.Indicator. Make the list relative: the indicator gets the active thumbnail's box in it as --active-thumbnail-left, --active-thumbnail-top, --active-thumbnail-width and --active-thumbnail-height, scrolls with the list, and follows thumbnails that change size. The variables move with the slides on every frame: towards the next or previous thumbnail while a swipe drags them, then along with them as they settle, at the speed of the flick, and the same for Previous, Next and the keys. So the indicator needs no transition: without one, it lands exactly when the slides do. A jump of several items (Home, End, a far thumbnail) starts the glide from the thumbnail next to the target.
Thumbnails

Portfolio#

A list with a title per project beside the image on wide screens, and a strip of squares below it on phones: orientation follows a media query so the arrow keys match the layout, and the classes switch on data-orientation. The indicator is a soft highlight behind the active row, or a ring around the active square. A vertical toolbar zooms; the panorama goes to 800% with maxScale={8} (4 by default).

Switch to right to left: the next image is on the left, and the arrow keys and swipes follow. The viewer is portaled to the end of <body>, so it takes its own dir, and Base UI's DirectionProvider sets the direction of the lightbox parts.

Selected work

River valley

River valley between cliffs

A portrait, taller than wide

Portrait of a woman

A square image

Waterfall in a forest

A wide panorama

Panorama of a valley

Untitled

Rocky coast

Jellyfish

Jellyfish in dark water

Cropping in the viewer#

To crop the photo shown, lay a Cropper over the viewer inside Lightbox.Popup: see its photo editor demo.

Phone viewer#

The viewer works like the photo app of a phone:

  • Swipe between images; the image follows the finger, carries on with the flick, and rubber-bands at the first and last image.
  • Pinch to zoom around the fingers, and double tap to zoom in at the tapped point or back out.
  • Pan a zoomed image; past its edge, the drag carries on to the next image.
  • Pull down (or up) to dismiss: the image follows the finger and flies back to its thumbnail on release.
  • Tap to hide or show the controls.

With a mouse or trackpad, drag swipes, double click zooms, and the wheel or a trackpad pinch zooms around the cursor.

Here the image fills the screen width under translucent bars. While the image is pulled to dismiss, --lightbox-dismiss-progress goes from 0 to 1 on the popup and the backdrop: the page fades in and un-blurs behind it, and the bars slide off the top and bottom of the screen. Try it on a phone, or with the browser's device toolbar.

Recents

8 photos
Controls that slide away

Styling#

Every part is unstyled. Style the viewer with Tailwind (or any CSS), using the parts' data attributes and CSS variables:

  • Lightbox.Popup has data-zoomed while the image is zoomed, data-controls-hidden after a tap hid the controls, and data-starting-style / data-ending-style while the image flies in and out. Fade the controls, not the popup: the image is inside it. From a control, read them with in-data-*: in-data-controls-hidden:opacity-0. Its className, style and render functions receive zoomed, controlsHidden and activationDirection with the dialog's state.
  • Lightbox.Popup, Lightbox.Viewport, Lightbox.Slide and the parts that show the item have data-activation-direction (left, right, or none when the viewer opens): the side the item shown came from, mirrored in a right-to-left layout.
  • Lightbox.Popup gets --lightbox-swipe-movement-x (px on screen, negative to the left) and --lightbox-swipe-progress (the same as a share of a slide, -1 to 1) while a swipe drags the slides and while they settle, like Base UI's drawer swipe variables. They are 0 at rest. Move anything with the swipe: translate-x-[calc(var(--lightbox-swipe-progress,0)*2rem)]. They are relative to the item shown, so they jump when it changes at the end of a swipe.
  • Lightbox.Popup gets --lightbox-swipe-strength when a swipe is released: 0.1 for a hard flick to 1 for a slow release, like Base UI's drawer. The slides settle in 300 ms times it, and go back to 1 once settled: scale a transition that follows them, duration-[calc(300ms*var(--lightbox-swipe-strength,1))].
  • Lightbox.Popup gets --lightbox-zoom-scale (1 at rest) and --lightbox-zoom-progress (0 at rest, 1 at the largest scale), to fade or move things as the image zooms.
  • Lightbox.Popup and Lightbox.Backdrop get --lightbox-flight-duration and --lightbox-flight-easing: the length and curve of the flight the image is on. After a pull to dismiss, the curve is a spring (a CSS linear()) that starts at the speed of the finger. Time the backdrop and the controls with them, duration-(--lightbox-flight-duration) ease-(--lightbox-flight-easing), and they arrive with the image.
  • Lightbox.Popup gets --lightbox-flight-movement-x and --lightbox-flight-movement-y (px) and --lightbox-flight-scale at the start of each flight: how far from where it rests in the viewer the image flies, to the trigger it lands on when the viewer closes, or from the trigger it leaves when it opens, and its size there as a share of its size in the viewer. They are 0px and 1 before any flight. Send other content to the same place: [translate:var(--lightbox-flight-movement-x)_var(--lightbox-flight-movement-y)] scale-(--lightbox-flight-scale) on data-ending-style, timed with --lightbox-flight-duration, as the coverflow does.
  • Lightbox.Popup and Lightbox.Backdrop get --lightbox-dismiss-movement-x and --lightbox-dismiss-movement-y (px) with --lightbox-dismiss-progress (0 to 1) while the image is pulled to dismiss, and have data-swiping during a drag and until the image settles back. Turn transitions off then (data-swiping:transition-none, or in-data-swiping:transition-none on a control), or whatever follows the progress lags behind the finger.
  • Lightbox.Viewport has data-swiping during a gesture. Give it a size (absolute inset-0) and set the space between images with gap-x-4. Nothing clips: the next images, the zoomed image and the image pulled to dismiss are drawn past its edges, off screen in a full-screen viewer. In a smaller one, add overflow-hidden; leave it off for the media to leave the viewport, like the stories. One slide of finger travel is its width plus that gap; when the CSS draws the slides closer together, set --lightbox-swipe-distance on it (any CSS length) so they keep up with the finger, as the coverflow does.
  • Lightbox.Slide has --lightbox-slide-progress: its distance from the centre in slides, on every frame the slides move. It is placed with translate, leaving transform free for a custom transition (a cube, a fade, a stack); put a perspective on Lightbox.Viewport for 3D.
  • Lightbox.Slide's padding is the space around the media: p-0 for edge to edge on phones, md:px-24 on larger screens. Add pt-[env(safe-area-inset-top)] and the like for notches (with viewport-fit=cover on the page). It has data-active, and data-loading and data-error for its media.
  • Lightbox.Media sets its own position and size, and the size of the media in it: style what's around the media, not its box.
  • Lightbox.Trigger has data-popup-open while the image will fly back to it, from the opening flight to the end of the closing one: the trigger of the item shown, or the one closeTarget picks. Hide it with data-popup-open:invisible: the thumbnail disappears from the page but keeps its place, and comes back when the image lands. It also has data-pending while the viewer waits to open from it, and data-flying while the image flies from it or to it.

Keyboard#

KeyAction
← →Previous / next image, or pan while zoomed
↑ ↓Pan while zoomed
Home / EndFirst / last image
+ −Zoom in / out
0Reset the zoom
EscClose; the image flies back to its thumbnail

Inside a widget that uses the arrow keys itself, they stay that widget's: a Base UI Toolbar, Lightbox.ThumbnailList, a slider, a text field, a video player.

API reference#

Lightbox.Root#

Holds the lightbox state: the items, the item shown, whether the viewer is open, its animation, and the zoom of the active image. Place the viewer parts inside it, and the triggers inside it or anywhere else with a handle. Before the viewer opens, it is mounted out of sight until its Lightbox.Media has loaded, so the image flies from its trigger at the right size; the trigger has data-pending meanwhile. Renders no element.

Props

items*
readonly Item[]

The items, in order. They can have any shape.

itemToValue
(item: Item) => LightboxItemValuedefault (item) => item.id

Returns the value that identifies an item. It must stay the same when items are added, removed or reordered.

children
React.ReactNode
open
boolean

Whether the viewer is open.

defaultOpen
booleandefault false

Whether the viewer is open at first, when open is not controlled.

onOpenChange
( open: boolean, eventDetails: LightboxRootChangeEventDetails, ) => void | Promise<void>

Called when the viewer is about to open or close, with why (eventDetails.reason). Call eventDetails.cancel() to keep it as it is: a cancelled pull to dismiss springs back. Return a Promise to make the viewer wait for it: up to LIGHTBOX_OPEN_TIMEOUT (3 s) before it opens, LIGHTBOX_CLOSE_TIMEOUT (300 ms) before it closes; it goes on after that. cancel() works until the wait ends, after an await too. A rejected Promise is logged, and the viewer goes on.

onOpenChangeComplete
(open: boolean) => void

Called when the viewer has finished opening or closing: the image has landed in the viewer, or back on the page.

value
LightboxItemValue

Value of the item shown in the viewer. It stays on the last item shown after the viewer closes.

defaultValue
LightboxItemValuedefault the first item's value

Value of the item shown at first, when value is not controlled.

onValueChange
( value: LightboxItemValue, eventDetails: LightboxRootChangeEventDetails, ) => void

Called when the viewer is about to show another item, with why (eventDetails.reason), and before it opens on an item that isn't the one shown. Call eventDetails.cancel() to keep the current item: a cancelled swipe springs back.

closeTarget
"active" | "opener" | "nearest"default "active"

Where the image flies back to when the viewer closes, and where focus returns. When that trigger shows another image, the image turns into it on the way, and the trigger stays empty while the viewer is open.

  • "active"default The trigger of the item shown. Without one on the page, the image fades out. For a grid with a trigger per item.
  • "opener" The trigger that opened the viewer, whichever item is shown. For one cover that opens a whole album.
  • "nearest" The trigger of the item shown, or else the trigger of the nearest item that has one. For a grid that shows a few items and a "+5" tile.
scrollToTrigger
booleandefault true

Scrolls the page, hidden behind the viewer, to keep the trigger the image flies back to in view as the items change. Without it, closing on an item whose trigger is off screen fades the image out instead.

handle
LightboxHandle

Links Lightbox.Triggers placed outside the root, from Lightbox.createHandle().

maxScale
numberdefault 4

Largest zoom scale.

Lightbox.Trigger#

Opens the viewer on one item. The image flies from the trigger to the viewer and back. Put the thumbnail inside it, or lay it over the thumbnail. It has data-popup-open while the image will fly back to it (the item shown is its own, or closeTarget picks it): hide it then, keeping its space, so the image always lands in an empty slot. When value matches no item, only the children render, without the button. Renders a <button> element.

Props

value*
LightboxItemValue

Value of the item it opens, as returned by itemToValue.

handle
LightboxHandle

Links a trigger placed outside Lightbox.Root to it, from Lightbox.createHandle().

crossfade
booleandefault false

Whether it shows something other than the media it opens: an avatar, a cover, an icon. The image then crossfades with a copy of it during the flights, growing out of its look and turning back into it on the way home. A trigger closeTarget picks for another item always crossfades.

className
string

CSS class applied to the element.

style
React.CSSProperties

Style applied to the element.

render
React.ReactElement | ((props, state) => React.ReactElement)

Replaces the element with another tag or component, or composes it with one. Takes an element, or a function that receives the props and the state.

Data attributes

data-pending
Present while its image loads before the viewer opens.
data-flying
Present while the image flies from it or back to it, even another item's image that closeTarget sends here. Hide the trigger meanwhile: the image lands in an empty slot.
data-crossfade
Present with crossfade: it shows something other than the media it opens, and the image crossfades with a copy of it during the flights.
data-disabled
Present while another trigger's image loads.
data-popup-open
Present on the trigger the image will fly back to: the one of the item shown, or the one closeTarget picks, such as a "+5" tile. From the start of the opening flight to the end of the closing one. Hide the thumbnail meanwhile and keep its space (visibility: hidden), like a phone's photo app: the image always lands in an empty slot.

Lightbox.Portal#

Moves the viewer to the end of <body> while it is open. Renders a <div> element.

Props

keepMounted
booleandefault false

Whether to keep the portal mounted in the DOM while the popup is hidden.

container
HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null

A parent element to render the portal element into.

className
string

CSS class applied to the element.

style
React.CSSProperties

Style applied to the element.

render
React.ReactElement | ((props, state) => React.ReactElement)

Replaces the element with another tag or component, or composes it with one. Takes an element, or a function that receives the props and the state.

Lightbox.Backdrop#

Covers the page behind the viewer. While the image is pulled to dismiss, it gets --lightbox-dismiss-progress (0 to 1), to fade it out: opacity: calc(1 - var(--lightbox-dismiss-progress, 0)). While the image flies back, it has data-ending-style like any Base UI popup. It has data-swiping during a drag: turn its transition off then, or the fade lags behind the finger. Renders a <div> element.

Props

forceRender
booleandefault false

Whether the backdrop is forced to render even when nested.

className
string

CSS class applied to the element.

style
React.CSSProperties

Style applied to the element.

render
React.ReactElement | ((props, state) => React.ReactElement)

Replaces the element with another tag or component, or composes it with one. Takes an element, or a function that receives the props and the state.

Data attributes

data-ending-style
Present while the image flies back or fades out.
data-swiping
Present while a finger or the mouse swipes or pulls the image, and until it settles back after a pull to close. Turn transitions off meanwhile, so what follows --lightbox-dismiss-progress keeps up with the finger.

CSS variables

--lightbox-flight-duration
Length of the flight the image is on, or was last on: 350ms. Fade the backdrop over it: transition-duration: var(--lightbox-flight-duration).CSS
--lightbox-flight-easing
Curve of that flight: a cubic-bezier(), or a linear() spring after a pull to dismiss. transition-timing-function: var(--lightbox-flight-easing).CSS
--lightbox-dismiss-progress
Progress of the pull to dismiss: 0 at rest, 1 at the closing distance. Fade with opacity: calc(1 - var(--lightbox-dismiss-progress, 0)).number
--lightbox-dismiss-movement-x
How far the image is pulled to dismiss, from where it rests.CSS
--lightbox-dismiss-movement-y
How far the image is pulled to dismiss, from where it rests.CSS

Lightbox.Popup#

The full-screen viewer. Place Lightbox.Viewport and the controls inside. It takes focus when it opens, so its keys work at once; Tab moves on to the controls. Pass initialFocus to focus another element. Keys: ← → previous / next (pan while zoomed), ↑ ↓ pan, Home / End first / last, + − zoom, 0 reset zoom, Esc close. Inside a widget that uses these keys itself (a Base UI Toolbar, Lightbox.ThumbnailList, a slider, a text field), they stay that widget's. Has data-zoomed while the image is zoomed, data-controls-hidden after a tap hides the controls, and data-ending-style while the image flies back (fade the controls then, not the popup: the image is inside it). Gets --lightbox-dismiss-progress while the image is pulled to dismiss, --lightbox-swipe-movement-x and --lightbox-swipe-progress while a swipe moves the slides, and has data-swiping during a drag: turn transitions off then. Renders a <div> element.

Props

initialFocus
boolean | React.RefObject<HTMLElement | null> | ((openType: InteractionType) => boolean | HTMLElement | null | void)

Determines the element to focus when the dialog is opened. By default, focus moves to the first tabbable element inside the popup, except when the dialog is opened by touch — then the popup itself is focused to avoid opening the virtual keyboard. - false: Do not move focus. - true: Move focus based on the default behavior (first tabbable element or popup). - RefObject: Move focus to the ref element. - function: Called with the interaction type (mouse, touch, pen, or keyboard). Return an element to focus, true to use the default behavior, null to fall back to the default behavior, or false/undefined to do nothing.

finalFocus
boolean | React.RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | HTMLElement | null | void)

Determines the element to focus when the dialog is closed. - false: Do not move focus. - true: Move focus based on the default behavior (trigger or previously focused element). - RefObject: Move focus to the ref element. - function: Called with the interaction type (mouse, touch, pen, or keyboard). Return an element to focus, true to use the default behavior, null to fall back to the default behavior, or false/undefined to do nothing.

className
string

CSS class applied to the element.

style
React.CSSProperties

Style applied to the element.

render
React.ReactElement | ((props, state) => React.ReactElement)

Replaces the element with another tag or component, or composes it with one. Takes an element, or a function that receives the props and the state.

Data attributes

data-zoomed
Present while the active image is zoomed in.
data-controls-hidden
Present after a tap hid the controls (toolbar, caption, close button).
data-ending-style
Present while the image flies back or fades out. Fade the controls then, not the popup: the image is inside it.
data-swiping
Present while a finger or the mouse swipes or pulls the image, and until it settles back after a pull to close. Turn transitions off meanwhile, so what follows --lightbox-dismiss-progress keeps up with the finger.
data-activation-direction
Side of the previous item the item shown came from, on screen: right after Next in a left-to-right layout. none when the viewer opens.'left' | 'right' | 'none'

CSS variables

--lightbox-flight-duration
Length of the flight the image is on, or was last on: 350ms. Time a transition that goes with it (the backdrop, the controls) with it: transition-duration: var(--lightbox-flight-duration).CSS
--lightbox-flight-easing
Curve of that flight: a cubic-bezier(), or a linear() spring that starts at the speed of the pull that dismissed the viewer. Use it with --lightbox-flight-duration so the transition and the image arrive together.CSS
--lightbox-flight-movement-x
How far the image flies from where it rests in the viewer, horizontally: to the trigger it lands on when the viewer closes, from the trigger it leaves when it opens. Set at the start of each flight. Move other content along with the image: translate: var(--lightbox-flight-movement-x) var(--lightbox-flight-movement-y), with --lightbox-flight-duration.CSS
--lightbox-flight-movement-y
The same, vertically.CSS
--lightbox-flight-scale
The image's size at that trigger, as a share of its size in the viewer: 0.2 when it lands on a tile a fifth as wide. 1 before any flight.number
--lightbox-dismiss-progress
Progress of the pull to dismiss: 0 at rest, 1 at the closing distance. Fade with opacity: calc(1 - var(--lightbox-dismiss-progress, 0)).number
--lightbox-swipe-movement-x
How far the slides have moved from the one shown, on screen: negative to the left. Set while a swipe drags them and while they settle, 0px at rest.CSS
--lightbox-swipe-progress
The same as a share of one slide: 0 at rest, 1 a whole slide to the right, -1 to the left. Move things with the swipe: translate: calc(var(--lightbox-swipe-progress, 0) * 2rem) 0.number
--lightbox-swipe-strength
How hard a released swipe was flicked, 0.1 to 1 (a slow release), like Base UI's drawer: the slides settle in 300ms times it. Scale a transition that follows them: transition-duration: calc(300ms * var(--lightbox-swipe-strength, 1)).number
--lightbox-dismiss-movement-x
How far the image is pulled to dismiss, from where it rests.CSS
--lightbox-dismiss-movement-y
How far the image is pulled to dismiss, from where it rests.CSS
--lightbox-zoom-scale
Scale of the active image: 1 at rest.number
--lightbox-zoom-progress
The zoom from rest to the largest scale: 0 to 1. Fade the controls as the image zooms in: opacity: calc(1 - var(--lightbox-zoom-progress, 0)).number

Lightbox.Title#

Names the viewer for screen readers. Hide it visually if the design has no visible title. With a function child, it names the item shown, and has data-starting-style for a frame when the item changes, with data-activation-direction, to animate the new title in. Renders an <h2> element.

Props

children
React.ReactNode | ((item: any) => React.ReactNode)

Content. A function receives the item shown in the viewer, as passed to items, to name it: {(photo) => photo.title}.

className
string

CSS class applied to the element.

style
React.CSSProperties

Style applied to the element.

render
React.ReactElement | ((props, state) => React.ReactElement)

Replaces the element with another tag or component, or composes it with one. Takes an element, or a function that receives the props and the state.

Data attributes

data-activation-direction
Side of the previous item the item shown came from, on screen: right after Next in a left-to-right layout. none when the viewer opens.'left' | 'right' | 'none'
data-starting-style
Present for one frame when the item shown changes, with a function child: style where the new content starts from, with transitions off (data-starting-style:transition-none), and a transition animates it in.

Lightbox.Description#

Describes the item shown in the viewer, such as its caption, linked to the viewer for screen readers. With a function child, it has data-starting-style for a frame when the item changes, with data-activation-direction, to animate the new caption in. Renders nothing when its content is empty. Renders a <p> element.

Props

children
React.ReactNode | ((item: any) => React.ReactNode)

Content. A function receives the item shown in the viewer, as passed to items, to show its caption: {(photo) => photo.caption}.

className
string

CSS class applied to the element.

style
React.CSSProperties

Style applied to the element.

render
React.ReactElement | ((props, state) => React.ReactElement)

Replaces the element with another tag or component, or composes it with one. Takes an element, or a function that receives the props and the state.

Data attributes

data-activation-direction
Side of the previous item the item shown came from, on screen: right after Next in a left-to-right layout. none when the viewer opens.'left' | 'right' | 'none'
data-starting-style
Present for one frame when the item shown changes, with a function child: style where the new content starts from, with transitions off (data-starting-style:transition-none), and a transition animates it in.

Lightbox.Viewport#

The window the slides swipe through: a carousel region that handles the gestures (swipe, pinch, double tap, pan, pull to dismiss, wheel zoom). Give it a size (usually position: absolute; inset: 0 in the popup), and set the space between slides with column-gap (gap-4). One slide of finger travel is the viewport's width plus that gap. When the CSS draws the slides closer together, as a coverflow does, set --lightbox-swipe-distance on it so they follow the finger. It doesn't clip: the slides next to the one shown, the zoomed image and the media pulled to dismiss are drawn past its edges. In a viewer smaller than the screen, clip them with overflow: hidden; leave it off for a pulled card or a 3D transition to leave the viewport. A press that starts on a control inside a slide (a button, a link, a player) is left to that control, and so is one on content marked with Base UI's data-base-ui-swipe-ignore, such as a 3D model that rotates with a drag: with no value, all its drags; with "x" or "y", only those along that axis. It has data-swiping while a finger or the mouse drags, and data-activation-direction after the item shown changes. Renders a <div> element.

Props

children*
(item: any, index: number) => React.ReactNode

Renders one item: a Lightbox.Slide with a Lightbox.Image or other content inside. Receives the item as passed to items. Called only for the active item and its neighbors, and the items a swipe brings into view.

neighbors
numberdefault 1

Items rendered on each side of the one shown. Raise it for a transition that shows more than the next item, like a coverflow; the others stay empty until they come near. While swiping, items around the visible part of the track are also rendered.

className
string

CSS class applied to the element.

style
React.CSSProperties

Style applied to the element.

render
React.ReactElement | ((props, state) => React.ReactElement)

Replaces the element with another tag or component, or composes it with one. Takes an element, or a function that receives the props and the state.

Data attributes

data-zoomed
Present while the active image is zoomed in.
data-swiping
Present while a finger or the mouse swipes, pans or pulls to dismiss.
data-activation-direction
Side of the previous item the item shown came from, on screen: right after Next in a left-to-right layout. none when the viewer opens.'left' | 'right' | 'none'

CSS variables

--lightbox-swipe-distance
How far the finger travels to move one slide: any CSS length. Set it yourself on the viewport; the viewport reads it. By default, one slide is the viewport's width plus its column-gap. Set it when the CSS draws the slides closer together than that, as a coverflow does, so they follow the finger instead of lagging behind it: [--lightbox-swipe-distance:calc(var(--cover-size)*0.68)].CSS

Lightbox.Slide#

One slide of the viewport, as large as the viewport. Return it from the function passed to Lightbox.Viewport. Its padding is the space kept around the media (p-6 md:p-24, or p-0 for edge to edge). Put a Lightbox.Media inside, and a Lightbox.Caption, or any other content. It has --lightbox-slide-progress, its distance from the centre, and it is placed with translate, so its transform is free for a custom transition between slides. With a Lightbox.ThumbnailList, it is the tab panel of its thumbnail. Renders a <div> element.

Props

className
string

CSS class applied to the element.

style
React.CSSProperties

Style applied to the element.

render
React.ReactElement | ((props, state) => React.ReactElement)

Replaces the element with another tag or component, or composes it with one. Takes an element, or a function that receives the props and the state.

Data attributes

data-active
Present on the slide shown in the viewer.
data-loading
Present while its Lightbox.Media loads.
data-error
Present when its Lightbox.Media failed to load.
data-activation-direction
Side of the previous item the item shown came from, on screen: right after Next in a left-to-right layout. none when the viewer opens.'left' | 'right' | 'none'

CSS variables

--lightbox-slide-progress
How far the slide is from the centre of the viewport, in slides, on screen: 0 when it is the one shown and at rest, -1 one slide to the left, 1 one to the right, in between while a swipe, its settle or Next moves the slides. The slide is placed with translate, so its transform is free for a custom transition, such as a cube: transform: rotateY(calc(var(--lightbox-slide-progress) * 90deg)) with a perspective on Lightbox.Viewport.number

Lightbox.Media#

The media of a Lightbox.Slide: put an <img>, a <video> or an <iframe> inside, as its direct child. It is a box with the media's shape, fitted inside the slide's padding, that the media fills. The box flies from the trigger and back and follows the pull to dismiss, with all of its content: put a spinner or an error message next to the media and show them with the slide's data-loading and data-error. An <img> also zooms and pans; other media don't. A <video> pauses when its slide is no longer shown. The loading state comes from the media's own events. Its position and size are set by the box, and its translate and scale while it is pulled to dismiss. Renders a <div> element.

Props

width
number | string

Width of the media, when known. With height, it sets the box's shape, before the media has loaded too, so the image flies in at the right size. Without both, the box takes the shape of the loaded media. A shape other than the media's keeps every slide the same: fit the media in it with object-fit.

height
number | string

Height of the media, when known. See width.

fill
booleandefault false

Whether the box fills the slide, inside its padding, whatever the shape of the media: width, height and the loaded size don't change it. Size the slide with CSS, and fit the media in it with object-fit.

className
string

CSS class applied to the element.

style
React.CSSProperties

Style applied to the element.

render
React.ReactElement | ((props, state) => React.ReactElement)

Replaces the element with another tag or component, or composes it with one. Takes an element, or a function that receives the props and the state.

Data attributes

data-active
Present on the media of the slide shown in the viewer.
data-loading
Present while the media loads.
data-error
Present when the media failed to load.

Lightbox.Caption#

The text of a Lightbox.Slide, such as its title or credits. Place it in the slide, next to Lightbox.Media: it moves with the slide, and describes it for screen readers. It has data-starting-style for a frame when its slide becomes the one shown, with data-activation-direction, to animate it in. Renders nothing when its content is empty. Renders a <div> element.

Props

className
string

CSS class applied to the element.

style
React.CSSProperties

Style applied to the element.

render
React.ReactElement | ((props, state) => React.ReactElement)

Replaces the element with another tag or component, or composes it with one. Takes an element, or a function that receives the props and the state.

Data attributes

data-active
Present when its slide is the one shown in the viewer.
data-activation-direction
Side of the previous item the item shown came from, on screen.'left' | 'right' | 'none'
data-starting-style
Present for one frame when its slide becomes the one shown: style where the caption starts from, with transitions off (data-starting-style:transition-none), and a transition animates it in.

Lightbox.Previous#

Shows the previous item. Disabled on the first one. Renders a <button> element.

Props

className
string

CSS class applied to the element.

style
React.CSSProperties

Style applied to the element.

render
React.ReactElement | ((props, state) => React.ReactElement)

Replaces the element with another tag or component, or composes it with one. Takes an element, or a function that receives the props and the state.

Data attributes

data-disabled
Present when the action can't run now.

Lightbox.Next#

Shows the next item. Disabled on the last one. Renders a <button> element.

Props

className
string

CSS class applied to the element.

style
React.CSSProperties

Style applied to the element.

render
React.ReactElement | ((props, state) => React.ReactElement)

Replaces the element with another tag or component, or composes it with one. Takes an element, or a function that receives the props and the state.

Data attributes

data-disabled
Present when the action can't run now.

Lightbox.Counter#

Position of the active item, as "2 / 5". Announced to screen readers when it changes. It has data-starting-style for a frame when the item changes, with data-activation-direction, to animate the new number in. Renders a <span> element.

Props

children
React.ReactNode | ((state: LightboxCounterState) => React.ReactNode)default `${index + 1} / ${count}`

Content. A function receives the state, to format it.

className
string

CSS class applied to the element.

style
React.CSSProperties

Style applied to the element.

render
React.ReactElement | ((props, state) => React.ReactElement)

Replaces the element with another tag or component, or composes it with one. Takes an element, or a function that receives the props and the state.

Data attributes

data-activation-direction
Side of the previous item the item shown came from, on screen: right after Next in a left-to-right layout. none when the viewer opens.'left' | 'right' | 'none'
data-starting-style
Present for one frame when the item shown changes: style where the new content starts from, with transitions off (data-starting-style:transition-none), and a transition animates it in.

Lightbox.ZoomIn#

Zooms the active image in by one step. Disabled at the largest scale. Renders a <button> element.

Props

className
string

CSS class applied to the element.

style
React.CSSProperties

Style applied to the element.

render
React.ReactElement | ((props, state) => React.ReactElement)

Replaces the element with another tag or component, or composes it with one. Takes an element, or a function that receives the props and the state.

Data attributes

data-disabled
Present when the action can't run now.

Lightbox.ZoomOut#

Zooms the active image out by one step. Disabled when it isn't zoomed. Renders a <button> element.

Props

className
string

CSS class applied to the element.

style
React.CSSProperties

Style applied to the element.

render
React.ReactElement | ((props, state) => React.ReactElement)

Replaces the element with another tag or component, or composes it with one. Takes an element, or a function that receives the props and the state.

Data attributes

data-disabled
Present when the action can't run now.

Lightbox.ZoomReset#

Puts the active image back at its original size. Disabled when it isn't zoomed. Renders a <button> element.

Props

className
string

CSS class applied to the element.

style
React.CSSProperties

Style applied to the element.

render
React.ReactElement | ((props, state) => React.ReactElement)

Replaces the element with another tag or component, or composes it with one. Takes an element, or a function that receives the props and the state.

Data attributes

data-disabled
Present when the action can't run now.

Lightbox.ZoomValue#

Zoom level of the active image, as a percentage. Renders a <span> element.

Props

children
React.ReactNode | ((state: LightboxZoomValueState) => React.ReactNode)default `${Math.round(scale * 100)}%`

Content. A function receives the state, to format the scale.

className
string

CSS class applied to the element.

style
React.CSSProperties

Style applied to the element.

render
React.ReactElement | ((props, state) => React.ReactElement)

Replaces the element with another tag or component, or composes it with one. Takes an element, or a function that receives the props and the state.

Lightbox.Close#

Closes the viewer; the image flies back to its trigger. Renders a <button> element.

Props

nativeButton
booleandefault true

Whether the component renders a native <button> element when replacing it via the render prop. Set to false if the rendered element is not a button (for example, <div>).

className
string

CSS class applied to the element.

style
React.CSSProperties

Style applied to the element.

render
React.ReactElement | ((props, state) => React.ReactElement)

Replaces the element with another tag or component, or composes it with one. Takes an element, or a function that receives the props and the state.

Lightbox.ThumbnailList#

A strip of thumbnails inside the viewer, one per item. It is a single tab stop: the arrow keys move between thumbnails and show their item, Home and End go to the first and last. It scrolls itself: the active thumbnail glides to its centre, the mouse can drag it, and the wheel scrolls a horizontal strip. Make it scroll with overflow: auto, and hide its scrollbar in CSS if you like. Follows the WAI-ARIA tabbed carousel pattern: it is a tab list, and the slides are its tab panels. Renders a <div> element.

Props

children*
| LightboxThumbnailRender | ReadonlyArray<LightboxThumbnailRender | React.ReactNode>

A function that renders one thumbnail: a Lightbox.Thumbnail with an image inside. Receives the item as passed to items, and is called for every item. Other children, such as a Lightbox.ThumbnailIndicator, render after the thumbnails.

orientation
"horizontal" | "vertical"default "horizontal"

Which arrow keys move between thumbnails, and which way the strip scrolls. Set as data-orientation.

  • "horizontal"default Left to right (right to left in a right-to-left layout); ← → move, and the wheel scrolls the strip sideways.
  • "vertical" Top to bottom; ↑ ↓ move.
activateOnFocus
booleandefault true

Whether the arrow keys also show the item of the thumbnail they move to. When false, they only move focus, and Enter shows the item.

className
string

CSS class applied to the element.

style
React.CSSProperties

Style applied to the element.

render
React.ReactElement | ((props, state) => React.ReactElement)

Replaces the element with another tag or component, or composes it with one. Takes an element, or a function that receives the props and the state.

Data attributes

data-orientation
Whether the thumbnails run left to right or top to bottom.'horizontal' | 'vertical'
data-overflow-start
Present while thumbnails are scrolled out of view before the start: fade that edge with a mask.
data-overflow-end
Present while thumbnails are scrolled out of view past the end.
data-swiping
Present while the mouse swipes the strip. Turn off snapping and transitions meanwhile.

Lightbox.Thumbnail#

Shows its item in the viewer. Return it from the function passed to Lightbox.ThumbnailList, with the thumbnail image inside, such as a Base UI Avatar that handles loading and errors, or anything else, such as a dot. It is a tab: it has aria-selected and data-active while its item is shown, and the list keeps it centred. Renders a <button> element.

Props

className
string

CSS class applied to the element.

style
React.CSSProperties

Style applied to the element.

render
React.ReactElement | ((props, state) => React.ReactElement)

Replaces the element with another tag or component, or composes it with one. Takes an element, or a function that receives the props and the state.

Data attributes

data-active
Present on the thumbnail of the item shown in the viewer.

Lightbox.ThumbnailIndicator#

Marks the active thumbnail with one element that moves from thumbnail to thumbnail, like Base UI's Tabs.Indicator. Place it in Lightbox.ThumbnailList, after the function child, and make the list position: relative: it gets the active thumbnail's box in the list as CSS variables, and scrolls with the list. They move with the slides on every frame (a swipe, its settle, Next), so style it with them and no transition: left: var(--active-thumbnail-left). Hidden from screen readers; renders nothing until it has measured a thumbnail. Renders a <span> element.

Props

className
string

CSS class applied to the element.

style
React.CSSProperties

Style applied to the element.

render
React.ReactElement | ((props, state) => React.ReactElement)

Replaces the element with another tag or component, or composes it with one. Takes an element, or a function that receives the props and the state.

Data attributes

data-activation-direction
Side of the previous item the item shown came from, on screen.'left' | 'right' | 'none'

CSS variables

--active-thumbnail-left
Distance from the left of the thumbnail list's content to the active thumbnail. While a swipe drags the slides, it moves towards the next or previous thumbnail with them.number
--active-thumbnail-top
Distance from the top of the thumbnail list's content to the active thumbnail.number
--active-thumbnail-width
Width of the active thumbnail.number
--active-thumbnail-height
Height of the active thumbnail.number

LightboxMediaStatusunion#

Loading state of the media in a Lightbox.Slide.

Values

"idle"

No Lightbox.Media in the slide.

"loading"

The media is loading; the slide and the media have data-loading.

"loaded"

The media has loaded, or has nothing to load.

"error"

The media failed to load; the slide and the media have data-error.

LightboxRootActivationDirectionunion#

Side of the previous item the item shown came from, on screen. Set as data-activation-direction, like Base UI's tabs.

Values

"none"

The viewer just opened, or the item shown didn't move.

"left"

It came from the left: after Previous in a left-to-right layout, Next in a right-to-left one.

"right"

It came from the right: after Next in a left-to-right layout, Previous in a right-to-left one.

LightboxRootChangeEventDetailstype#

Details of a change: its reason, the native event, and the element it came from (trigger). Call cancel() to stop it; a cancelled swipe or pull to dismiss springs back.

Properties

reason*
"trigger-press" | "swipe" | "keyboard" | "list-navigation" | "thumbnail-press" | "previous-press" | "next-press" | "close-press" | "escape-key" | "outside-press" | "focus-out" | "imperative-action" | "missing" | "none"

The reason for the event.

event*
ReasonToEvent<Reason>

The native event associated with the custom event.

cancel*
() => void

Cancels Base UI from handling the event.

allowPropagation*
() => void

Allows the event to propagate in cases where Base UI will stop the propagation.

isCanceled*
boolean

Indicates whether the event has been canceled.

isPropagationAllowed*
boolean

Indicates whether the event is allowed to propagate.

trigger*
Element | undefined

The element that triggered the event, if applicable.

LightboxRootChangeEventReasonunion#

Why the viewer opened or closed, in onOpenChange, or showed another item, in onValueChange. The values are Base UI's where it has one (escape-key, close-press), so a reason compares with the same string as in a Base UI dialog.

Values

"trigger-press"

A Lightbox.Trigger was pressed.

"swipe"

A swipe to the next or previous item, or a pull to dismiss.

"keyboard"

A viewer shortcut: ← → Home End.

"list-navigation"

The arrow keys in a Lightbox.ThumbnailList with activateOnFocus.

"thumbnail-press"

A Lightbox.Thumbnail was pressed.

"previous-press"

Lightbox.Previous was pressed.

"next-press"

Lightbox.Next was pressed.

"close-press"

Lightbox.Close was pressed.

"escape-key"

Escape was pressed.

"outside-press"

A press outside the popup.

"focus-out"

Focus left the popup.

"imperative-action"

A call on the handle: open() or close().

"missing"

The open item was removed from items: the viewer shows its neighbour, or closes.

"none"

Another cause, such as a Base UI dialog change without a reason.

LightboxRootCloseTargetunion#

Where the image flies back to when the viewer closes, and where focus returns.

Values

"active"

The trigger of the item shown. Without one on the page, the image fades out. For a grid with a trigger per item.

"opener"

The trigger that opened the viewer, whichever item is shown. For one cover that opens a whole album.

"nearest"

The trigger of the item shown, or else the trigger of the nearest item that has one. For a grid that shows a few items and a "+5" tile.

LightboxRootPhaseunion#

Where the viewer is in its open and close animation.

Values

"closed"

The viewer is not shown.

"opening"

The image flies from its trigger, or fades in.

"open"

The image has landed in the viewer.

"closing"

The image flies back to its trigger, or fades out.

LightboxThumbnailListOrientationunion#

Which way the thumbnails run, and which arrow keys move between them. Set as data-orientation.

Values

"horizontal"

Left to right (right to left in a right-to-left layout); ← → move, and the wheel scrolls the strip sideways.

"vertical"

Top to bottom; ↑ ↓ move.

LightboxItemValuetype#

Identifies an item, as returned by itemToValue: stable when items are added, removed or reordered.

Properties

toString*
valueOf*
toLocaleString*

LightboxMediaSizetype#

Natural size of a media element, in px.

Properties

width*
number
height*
number