Vue Lightbox

Full-screen image & video gallery lightbox for Vue 3, built on @reelkit/vue-lightbox.

View live demo →

Features

Images & Video
Built-in video slide support
Touch Gestures
Swipe to navigate
Swipe to Close
Swipe up to dismiss
Keyboard Nav
Arrow keys + Escape
Fullscreen
Cross-browser API
Transitions
Slide, fade, flip, zoom-in
Preloading
±2 neighbours prefetched
Sound Toggle
Per-slide mute/unmute
Loading States
Spinner + custom slot
Error Handling
Error icon + custom slot
Scoped Slots
6 customisable slot zones
v-model
v-model:is-open two-way binding
URL State
Shareable, bookmarkable links

Installation

bash

Don't forget to import the styles:

typescript
Icons
The default controls use lucide-vue-next for icons. If you prefer a different icon library, use the #controls and #navigation scoped slots to provide your own.

Basic Usage

Import the stylesheet and the LightboxOverlay component, then drive open/close with v-model:is-open.

App.vue

Scoped Slots

Six named scoped slots allow full customisation of the overlay surfaces. Omit the slot to keep the built-in default; provide nothing inside the slot (e.g. via v-if="false") to hide that section entirely.

SlotScopeDescription
#slideSlideSlotScopeReplace individual slide content (required for video slides)
#controlsControlsSlotScopeReplace the top controls bar (close, counter, fullscreen)
#navigationNavigationSlotScopeReplace the prev/next navigation arrows
#infoInfoSlotScopeReplace the bottom title/description gradient overlay
#loadingLoadingSlotScopeCustom loading indicator
#errorErrorSlotScopeCustom error indicator
vue

Video Support

Video slides are opt-in so the default bundle stays free of audio/video wiring. Call useVideoSlideRenderer(items) and forward the returned VideoSlideRenderer / VideoControlsRenderer into the overlay's #slide and #controls slots. Wrap the overlay in the returned SoundProvider so the built-in sound toggle has a context.

vue
The shared <video> element powering video slides uses the same pattern as the vue reel-player — playback continues across slide changes on iOS without requiring a per-slide user gesture.

Fullscreen

Use useFullscreen from @reelkit/vue to observe or toggle fullscreen state on a referenced element. The lightbox drives its built-in fullscreen button through the same composable.

vue

URL State

Build a controller with useOverlayUrlState from @reelkit/vue and hand it to LightboxUrlOverlay as controller, and the address bar owns the gallery: it opens itself when the parameter names a slide and closes when the parameter goes away. Links are shareable, and the back button closes the gallery. It is a separate component from LightboxOverlay, so each carries exactly one open-state driver — the is-open model or the url controller, never both.

Built-in keys
You can address slides with a built-in key — spread urlIndexKey (by position) or urlStableIdKey (by a stable id) into the controller — both re-exported from @reelkit/vue. See the URL State guide and Core API.

Back closes only when you opened from within the app — the link pushed an entry, so back pops to the gallery. A shared link opened directly in a fresh tab has no history behind it, so browser-back leaves the site; the close button or Escape removes the parameter in place and keeps you on the gallery.

vue

The composable takes one options object and returns a UrlStateController (with set, index, value). Keep it for programmatic control: set is the low-level write the overlay uses internally (slide change, and set(null) to close). It drives the overlay programmatically too — set(index) opens it, the same as navigating to the parameter. Prefer a link for opening though: the href is shareable, opens in a new tab, and the back button closes it — all for free, with no handler.

Full useOverlayUrlState options (param, adapter, codec, locator): see the Vue API reference.

LightboxUrlOverlay itself takes just :controller (required), a @close emit, plus every visual and behavior prop LightboxOverlay forwards (items, transition-fn, the scoped slots, and so on) — but no is-open.

  • Opening costs one history entry; paging slides replaces it, so a hundred swipes add none — one back step always leaves the gallery.
  • A shared link like ?photo=3 opens the gallery at that slide. A parameter naming no slide is dropped from the URL instead of asserting a slide that cannot open.

In a routed app, pass an adapter. Writing history directly leaves the router's own location stale, and its next navigation drops the parameter.

vue

Stable links. The index is positional, so a bookmark opens a different image once the list is reordered. urlStableIdKey keys by each item's stable id, scanning the live list — one call covers the common case.

vue

Pass hashCodec: base64UrlCodec to base64url-encode the id in the URL — reversible obfuscation, not a cryptographic hash.

Key by a different field (a slug), or page an infinite feed with locateAsync, and build the codec/locator yourself: codec spells the identity into the URL, locator finds where it now sits.

vue

Infinite / paginated galleries. locate is synchronous, so it can only answer for items already loaded — a shared link to image 400 of a feed that has loaded 20 comes up empty. locateAsync is the fallback, called only when it misses: load the pages you need, then return the index the identity turned out to have.

Shortcut
Keying by the item’s id? Skip the hand-rolled codec and locator — pass locateAsync straight to urlStableIdKey({ items, locateAsync }) (it fetches on a miss, then returns the index). The fuller version below is for keying by another field, or for full control.
vue

While it is pending the lightbox stays closed and the parameter is left alone, so the deep link survives the fetch. null or a rejection drops the parameter. An answer arriving after the URL moved on, after a close, or after unmount is discarded, so a slow fetch cannot open a slide nobody asked for. Whatever it returns is authoritative — it reports the index of data it just fetched, and the lightbox takes it as-is rather than re-reading items, which Vue has not re-rendered yet.

API Reference

LightboxOverlay Props

LightboxOverlayProps

PropTypeDefaultDescription
isOpenbooleanrequiredControls visibility; when false the overlay is removed from the DOM. Bindable via v-model:is-open.
itemsLightboxItem[]requiredArray of items (images or videos)
initialIndexnumber0Zero-based index of the initially visible item
transitionFnTransitionTransformFnslideTransitionSlide transition function. Import a built-in (slideTransition, flipTransition, lightboxFadeTransition, lightboxZoomTransition) or pass a custom one. Defaults to slideTransition when omitted.
showInfobooleantrueWhether to render the title/description info overlay
showControlsbooleantrueWhether to render the top controls bar (close, counter, fullscreen)
showNavigationbooleantrueWhether to render the prev/next navigation arrows (desktop only)
transitionDurationnumber300Slide animation duration in ms
swipeDistanceFactornumber0.12Minimum swipe distance fraction (0–1) to trigger slide change
swipeToCloseDirection'up' | 'down''up'Direction of the swipe-to-close gesture on mobile
loopbooleanfalseWhether the slider wraps from the last slide back to the first
enableNavKeysbooleantrueEnable keyboard arrow-key navigation
enableWheelbooleantrueEnable mouse-wheel navigation
wheelDebounceMsnumber200Debounce duration for wheel events in ms
ariaLabelstring'Image gallery'Accessible label for the dialog region

LightboxUrlOverlay Props

LightboxUrlOverlayProps

Takes every visual and behaviour prop above except is-open, and replaces it with a controller. It emits close, slide-change and api-ready, but no update:is-open. initial-index is ignored here — the controller's position picks the slide, so a value passed alongside it would be overwritten on every open.

PropTypeDefaultDescription
controllerUrlStateControllerrequiredController from useOverlayUrlState. Its position decides whether the overlay is open and which slide it shows; the overlay writes back through it on slide change and on close.

LightboxOverlay Events

EventPayloadDescription
closevoidEmitted when the user closes the lightbox
slide-changenumberEmitted with the new active slide index after a change
api-readyLightboxApiEmitted once the slider is ready, exposing the imperative API
update:is-openbooleanEmitted on close; enables v-model:is-open

LightboxItem Interface

FieldTypeRequiredDescription
srcstringyesURL of the image or video
type'image' | 'video'noItem type. Defaults to 'image'
posterstringnoThumbnail image for video items
titlestringnoTitle shown in the info overlay
descriptionstringnoDescription shown below the title
widthnumbernoIntrinsic image width in pixels
heightnumbernoIntrinsic image height in pixels

Slot Scope Types

TypeFields
SlideSlotScope{ item, index, size: [number, number], isActive, onReady, onWaiting, onError }
ControlsSlotScope{ item, activeIndex, count, isFullscreen, onClose, onToggleFullscreen }
NavigationSlotScope{ item, activeIndex, count, onPrev, onNext }
InfoSlotScope{ item, index }
LoadingSlotScope{ item, activeIndex }
ErrorSlotScope{ item, activeIndex }

Transitions

Pass any TransitionTransformFn via the transition-fn prop. Importing only the transition you use lets the bundler tree-shake the rest. Defaults to slideTransition when omitted.

FunctionDescription
slideTransitionDefault. Horizontal translate between slides; re-exported from @reelkit/vue.
lightboxFadeTransitionCrossfade with a subtle horizontal nudge. Local to @reelkit/vue-lightbox.
flipTransition3D flip around the Y-axis; re-exported from @reelkit/vue.
lightboxZoomTransitionIncoming slide scales 70% → 100% with fade. Local to @reelkit/vue-lightbox.
vue

Content Loading & Error Handling

When you take over rendering via the #slide slot, three lifecycle callbacks are available on the slot scope to report loading state. The lightbox tracks per-slide state and shows a spinner or error icon accordingly. A content preloader caches broken URLs so revisiting a failed slide skips the retry.

Lifecycle callbacks

CallbackTypeDescription
onReady() => voidNotify that the slide content has loaded successfully (e.g. image decoded)
onWaiting() => voidNotify that the slide content is loading/buffering (shows spinner)
onError() => voidNotify that the slide content failed to load (shows error icon)

Wiring callbacks in #slide

vue

Custom loading slot

Use the #loading slot to replace the default spinner.

vue

Custom error slot

Use the #error slot to replace the default broken-image icon.

vue

CSS Classes

All CSS classes are plain (not scoped), so they can be targeted with higher-specificity selectors in a stylesheet loaded after @reelkit/vue-lightbox/styles.css. For color, size, and z-index changes, prefer the CSS custom properties documented in the Theming section below.

ClassComponentDescription
.rk-lightbox-overlayOverlayRoot container (full-screen backdrop)
.rk-lightbox-top-shadeOverlayTop gradient scrim behind controls
.rk-lightbox-spinnerOverlayDefault loading spinner
.rk-lightbox-errorOverlayError state container (broken image)
.rk-lightbox-error-textOverlayError state text label
.rk-lightbox-controls-leftControlsTop-left controls container
.rk-lightbox-btnControlsControl button (fullscreen, sound, etc.)
.rk-lightbox-closeControlsClose button
.rk-lightbox-counterControlsImage counter chip
.rk-lightbox-navNavigationNavigation arrow (both prev and next)
.rk-lightbox-nav-prevNavigationPrevious arrow
.rk-lightbox-nav-nextNavigationNext arrow
.rk-lightbox-infoInfoTitle / description container
.rk-lightbox-info-titleInfoImage title
.rk-lightbox-info-descriptionInfoImage description
.rk-lightbox-slideSlideSlide container
.rk-lightbox-imgSlideImage element
.rk-lightbox-video-containerVideoSlideVideo slide container (opt-in)
.rk-lightbox-video-elementVideoSlideVideo element (opt-in)
.rk-lightbox-video-posterVideoSlideVideo poster image (opt-in)

Theming

Override any --rk-lightbox-* CSS custom property on :root (or any ancestor of .rk-lightbox-overlay) to retheme. Direct declarations on .rk-lightbox-overlay would shadow inherited values, so keep overrides on an ancestor selector.

TokenDefaultControls
--rk-lightbox-overlay-bg#000Backdrop color
--rk-lightbox-overlay-z9999Overlay z-index
--rk-lightbox-top-shade-height80pxTop scrim height
--rk-lightbox-top-shade-bglinear-gradient(rgba(0,0,0,0.6), transparent)Top scrim gradient
--rk-lightbox-edge-padding16pxEdge inset for close / nav / controls
--rk-lightbox-btn-bgrgba(0, 0, 0, 0.5)Default background for close / nav / small buttons
--rk-lightbox-btn-bg-hoverrgba(255, 255, 255, 0.2)Hover background for close / nav / small buttons
--rk-lightbox-btn-fg#fffIcon color for close / nav / small buttons
--rk-lightbox-btn-size36pxSmall button size (fullscreen toggle, etc.)
--rk-lightbox-close-size40pxClose button size
--rk-lightbox-nav-size48pxPrev / next arrow size
--rk-lightbox-nav-opacity0.7Idle opacity of prev / next arrows
--rk-lightbox-counter-bgrgba(0, 0, 0, 0.5)Counter chip background
--rk-lightbox-counter-fg#fffCounter text color
--rk-lightbox-info-bglinear-gradient(transparent, rgba(0,0,0,0.8))Caption scrim gradient
--rk-lightbox-title-size18pxTitle font size
--rk-lightbox-description-size14pxDescription font size
--rk-lightbox-video-bg#000Letterbox background behind <video>
css

Accessibility

The overlay root is a modal dialog (role="dialog", aria-modal="true"). Set the aria-label prop to change the screen-reader announcement; it defaults to "Image gallery". Each slide carries role="group", aria-roledescription="slide", and an aria-label derived from the position (e.g. "Image 2 of 5").

The lightbox captures focus on open and returns it to the trigger on close. Tab and Shift+Tab cycle through focusable elements inside; focus that escapes (click outside, programmatic focus) gets pulled back. Implemented with captureFocusForReturn and createFocusTrap from @reelkit/vue.

Keyboard Shortcuts

KeyAction
ArrowLeftPrevious image
ArrowRightNext image
EscapeClose lightbox (or exit fullscreen if active)