Skip to content

Lightbox ​

WxLightbox is a gallery over the whole screen: one picture at a time, arrows and keys between them, a strip of the rest, zoom and pan, and a player behind a video's poster.

A group: every picture opens the same gallery
Picture 1
Picture 2
Picture 3
Picture 4
Picture 5
Arrows or ← →, a swipe on a phone; a double click, the wheel or a pinch to zoom.
A cover with a gallery behind it, videos too
A product

Three ways to open it ​

A group of pictures. Put <wx-image preview> inside a <wx-image-group>, and any of them opens one gallery of all of them, in the order they stand on the page:

vue
<template>
  <wx-image-group class="thumbs">
    <wx-image v-for="image in images" :key="image.id" :src="image.url" :alt="image.alt" preview />
  </wx-image-group>
</template>

The group is a component, not a shared name on each picture: two cards on one screen that both called their pictures "photos" would otherwise become one gallery. The group renders a div (tag changes that) and keeps class and style; every other attribute — loop, thumbnails, the labels — goes to its lightbox.

A cover with a gallery behind it. One picture shows, all of them open:

vue
<template>
  <wx-image :src="product.cover" preview :preview-list="product.images" />
</template>

It opens on the item whose address is the cover's src, or on preview-start.

From code, the way openModal and openImageEditor work:

ts
import { openLightbox } from '@webx-ui/core'

openLightbox(product.images, 2)
openLightbox(product.images, { start: 2, loop: true, nextLabel: t('Next') })

It answers nothing: the promise settles with undefined when the gallery is closed, and carries close() for one that should not outlive a route.

And the component itself, for a screen that holds the state:

vue
<template>
  <wx-lightbox v-model:open="open" v-model:index="index" :items="images" />
</template>

In the admin panel ​

Every picture the panel shows small opens large the same way: a click on it opens this gallery over the whole set it stands in — all the pictures of a product, all the files of a gallery field, all the covers of a page of a list — on the full picture, never the thumbnail, and in the panel's language. @webx-ui/module-admin keeps that in one place.

WxPreviewThumb makes whatever is in its slot a button that opens the gallery. It keeps its click to itself, so a thumbnail in a row of a table or a tree opens the picture rather than the record, and a drag that starts on it is still the list's:

vue
<script setup lang="ts">
import { WxPreviewThumb } from '@webx-ui/module-admin'

const covered = computed(() => rows.value.filter((row) => row.cover))
const covers = computed(() =>
  covered.value.map((row) => ({ src: row.cover.url, thumb: row.cover.thumb, alt: row.title })),
)
</script>

<template>
  <wx-preview-thumb v-if="row.cover" fill :items="covers" :start="covered.indexOf(row)">
    <img :src="row.cover.thumb" alt="" />
  </wx-preview-thumb>
</template>
PropTypeDefaultDescription
itemsLightboxSource[]—The set the gallery pages through, full pictures
startnumber0Which of them this thumbnail is
labelstring"View full size"Accessible name, in the panel's language
fillbooleanfalseTakes the whole box it stands in — a cover cell

useLightbox() is the same from code, for a thumbnail that is already a link or a button of its own: open(items, start), labels() for the words, and click(event, items, start), which opens on a plain click and leaves Ctrl, Shift or the middle button to the browser — a thumbnail that links to the file still opens it in a tab. The words are webx-admin::lightbox.*.

Items ​

An item is the address of a picture, or an object:

ts
interface LightboxItem {
  src?: string // the picture; for a video, its poster
  alt?: string // and the caption, when there is no caption
  caption?: string
  original?: string // where "Open the original" goes; default: src
  thumb?: string // for the strip, and for the moment before src arrives
  video?: string | { src?: string; embed?: string }
}

A small thumb matters twice: the strip loads it instead of every full picture, and the stage shows it blurred underneath while the full one is on its way.

Video ​

An item with a video is shown as its picture with a play button on it. Nothing is fetched from the video's host until that button is pressed — a gallery of ten YouTube videos is ten pictures, not ten players.

  • A string is a YouTube address in any of its forms (watch?v=, youtu.be/, shorts/, embed/) or a file — MP4 or WebM, played by the browser's own <video>.
  • { embed } is any address for an iframe; a YouTube one is still recognised.
  • { src } is a file, whatever its address looks like.

YouTube plays from youtube-nocookie.com. Moving to another item stops the video, and zoom is not offered on one.

Getting around ​

On a computerOn a phone
← → between items, Home / Endswipe left or right
the arrows at the sides—
the strip of thumbnails—
double click: fit ↔ close updouble tap
the wheel, + and −, 0 to fit againpinch
drag a picture that is zoomed indrag
Esc, or the ✕swipe down, or the ✕

A double click goes to actual pixels — or to twice the fit, for a picture smaller than the screen — and back. max-zoom is how far the rest of the ways go, as a multiple of the size on screen; a picture whose actual pixels are further than that can still be seen at them. Zoom stays where it was put under the cursor or between the fingers.

The arrows and the strip are left out on a narrow screen: a phone swipes, and they would only cover the picture. loop makes the last item lead to the first.

Looks ​

The dimming is its own token, --wx-bg-lightbox: nearly opaque, and dark in both themes, because a photograph judged on a page that shows through it is judged on the wrong colours. Everything with words or buttons on it — the counter, the tools, the caption, the arrows — sits on --wx-bg-surface. The layer is --wx-z-index-overlay and the gallery --wx-z-index-dialog, like a Dialog.

Props ​

PropTypeDefaultDescription
items(string | LightboxItem)[]—What is shown
openbooleanfalsev-model:open
indexnumber0v-model:index — the item on screen
loopbooleanfalsePast the last comes the first
thumbnailsbooleanmore than one itemThe strip along the bottom
maxZoomnumber4How far zoom goes, × the size on screen
originalbooleantrueThe "Open the original" link
ariaLabelstring'Gallery'Name of the gallery
counterTextstring | ((index: number, total: number) => string)'{index} of {total}'The counter, and the strip's names
prevLabelstring'Previous'
nextLabelstring'Next'
closeLabelstring'Close'
zoomInLabelstring'Zoom in'
zoomOutLabelstring'Zoom out'
originalLabelstring'Open the original'
playLabelstring'Play'

Events: update:open, update:index, change (index) — the item changed, by any means.

On WxImage: preview, preview-list, preview-start; see Image. On WxImageGroup: tag ('div'), and everything above but items, open and index.

Accessibility ​

The gallery is a modal dialog: focus is held inside while it is open and goes back to the picture that opened it. Focus starts on the gallery itself rather than on a button, so the arrow keys work at once. The counter is announced as it changes, every button has a name, and the strip's buttons say which item they are — with aria-current on the one on screen. The fades are off under prefers-reduced-motion.

Released under the MIT License.