Skip to main content

Lightbox primitives API

Advanced lightbox building blocks for custom markup.

The built-in lightbox is a small set of primitives composed together. Nuxt applications import them from @lupinum/nuxt-photo/app. Plain Vue applications import them from @lupinum/vue-photo.

Primitives give you control, not automatic accessibility. The default composition handles focus, labels, keyboard navigation, and announcements; custom compositions need their own accessibility review.

ts
import {
  LightboxProvider,
  LightboxRoot,
  LightboxOverlay,
  LightboxViewport,
  LightboxSlide,
  LightboxControls,
  LightboxCaption,
} from '@lupinum/nuxt-photo/app'
If you are new to the primitives, start with the primitives guide. It explains the smallest working example step by step.

Composition overview

The primitives have a strict nesting order:

<LightboxProvider :photos="photos">         ← provides context for triggers + primitives
  <PhotoTrigger />                          ← thumbnail trigger(s)

  <LightboxRoot>                            ← teleports + renders only while open
    <LightboxOverlay />                     ← backdrop + self-click-to-close
    <LightboxViewport>                      ← gestures, embla container
      <LightboxSlide>                       ← one photo's frame, zoom, pan
    </LightboxViewport>
    <LightboxControls>                      ← next/prev/close buttons, a11y live region
    <LightboxCaption>                       ← caption/description text
  </LightboxRoot>
</LightboxProvider>

Primitive root elements forward normal Vue attributes where the component has a single stable root. For components with internal wrappers, use the documented class props and slots instead of assuming every attribute lands on an outer element.

<LightboxProvider>

Provides the lightbox context to its descendants.

vue
<LightboxProvider :photos="photos">
  <!-- everything else -->
</LightboxProvider>

Props:

PropTypeDescription
photosPhotoItem | PhotoItem[]Photo collection for triggers and lightbox.
transitionLightboxTransitionOptionFLIP/fade transition mode.
minZoomnumberMinimum zoom scale for click/keyboard zoom.
imageAdapterImageAdapterPer-provider image source mapping.

Use <LightboxProvider> when you want component-only composition. Use provideLightbox() instead when your component needs the returned methods directly.

<LightboxRoot>

The dialog shell. It consumes the nearest lightbox context, teleports to <body>, renders only while open, owns focus trapping, and renders the internal FLIP transition layer.

Give custom roots an accessible name with aria-label or aria-labelledby. The included lightbox uses aria-label="Photo viewer".

vue
<LightboxRoot class="fixed inset-0" role="dialog" aria-modal="true" aria-label="Photo viewer">
  <!-- overlay, viewport, controls, caption -->
</LightboxRoot>

<LightboxOverlay>

The semi-transparent backdrop. Clicking the overlay itself closes the lightbox; clicks from slotted children are ignored so controls and custom content do not accidentally close it.

vue
<LightboxOverlay class="my-overlay">
  <!-- viewport, controls, etc. -->
</LightboxOverlay>

The primitive registers itself with the shared motion controller, so the backdrop follows open, close, and swipe-to-dismiss progress automatically.

<LightboxViewport>

The gesture area. Receives pointer/touch/wheel events and drives the Embla carousel underneath. Exposes a viewport-ref you wire to Embla's container element.

vue
<LightboxViewport v-slot="{ photos, viewportRef, imageLoadFailed }">
  <div :ref="viewportRef" class="embla">
    <div class="embla__container">
      <LightboxSlide
        v-for="(photo, i) in photos"
        :key="photo.id"
        :photo="photo"
        :index="i"
      />
    </div>
  </div>
  <div v-if="imageLoadFailed">Image could not be loaded.</div>
</LightboxViewport>

Slot props:

  • photos - the photos list.
  • viewportRef - ref you attach to the Embla container.
  • imageLoadFailed - true when the active slide could not be loaded.

<LightboxSlide>

Renders one slide. Handles zoom state, pan bounds, and the FLIP frame.

vue
<LightboxSlide :photo="photo" :index="i" />

Props:

PropTypeDescription
photoPhotoItemThe slide's photo.
indexnumberIndex within the photo list.
effectClassstringClasses for the effect wrapper.
frameClassstringClasses for the frame element.
zoomClassstringClasses for the pan/zoom wrapper.
imgClassstringClasses on the inner <img>.

If the context has a custom slide renderer (via provideLightbox's resolveSlide option or a <Photo> parent's #slide slot), <LightboxSlide> delegates to it.

<LightboxControls>

Render any UI you want - close button, prev/next arrows, zoom toggle, counter, share button. The primitive wires up the state and hands it to the default slot.

vue
<LightboxControls
  v-slot="{ activeIndex, count, close, next, prev, toggleZoom, isZoomedIn, zoomAllowed }"
>
  <button @click="close" aria-label="Close">×</button>
  <div>{{ activeIndex + 1 }} / {{ count }}</div>
  <button @click="prev" aria-label="Previous">←</button>
  <button @click="next" aria-label="Next">→</button>
  <button v-if="zoomAllowed" @click="toggleZoom">
    {{ isZoomedIn ? 'Zoom out' : 'Zoom in' }}
  </button>
</LightboxControls>

The primitive also renders a live region announcing Photo 3 of 12 for screen readers.

<LightboxCaption>

Renders caption/description text for the active photo. Default slot gives you photo and activeIndex so you can render whatever the photo's metadata supports.

vue
<LightboxCaption v-slot="{ photo }">
  <h2 v-if="photo?.caption">{{ photo.caption }}</h2>
  <p v-if="photo?.description">{{ photo.description }}</p>
</LightboxCaption>

<PhotoTrigger>

PhotoTrigger connects custom thumbnail markup to the nearest provider.

PropTypeDescription
photoPhotoItemPhoto represented by the trigger.
indexnumberPosition in the provider collection.

The default slot receives { photo, index, hidden }. Hide thumbnail overlays when hidden is true so they do not sit above the FLIP transition frame. Click, Enter, and Space open the matching photo.

vue
<PhotoTrigger
  v-for="(photo, index) in photos"
  :key="photo.id"
  :photo="photo"
  :index="index"
  v-slot="{ hidden }"
>
  <figure :style="{ opacity: hidden ? 0 : 1 }">
    <PhotoImage :photo="photo" context="thumb" />
  </figure>
</PhotoTrigger>

<PhotoImage>

PhotoImage resolves a source through the current image adapter and renders the final <img>.

PropTypeDefaultDescription
photoPhotoItemRequiredPhoto to render.
context'thumb' | 'slide''thumb'Delivery context passed to the adapter.
imageAdapterImageAdapterProvided adapterPer-instance override.
loading'lazy' | 'eager''lazy'Native image loading behavior.
sizesstringAdapter resultPer-instance sizes override.

Use it in custom thumbnails and slides when provider-backed image delivery should remain active.