Lightbox
<x-wirekit::lightbox> is a reusable, accessible zoom overlay for media —
photos, video, and embeds. It is a focus-trapped role="dialog" that opens over
the page, steps through its items with the arrow keys, closes on Escape, and
returns focus to whatever opened it. It is the same overlay
<x-wirekit::image-gallery> uses internally, exposed
on its own so you can drive it from any trigger and any layout — a single
thumbnail, a "play" button, a link in prose.
Pass an array of items, each with a src, an alt, an optional caption, and
an optional type (image — the default — video, or embed). Then open it two
ways: a trigger inside the component calls openAt(index) directly, or any
control anywhere on the page dispatches a wirekit-lightbox-open event with a
matching name.
Usage
Put a trigger in the default slot — it shares the lightbox's Alpine scope, so it
can call openAt(0) to open at the first item:
A thumbnail as the trigger
Any element in the slot works as the trigger. Here a clickable
<x-wirekit::image> thumbnail opens the full-size view:
Multiple items
Give the lightbox several items and it gains prev / next controls plus arrow-key navigation. Any control in the slot can jump straight to a specific index:
Large images scale to fit
Whatever the source dimensions, the media fills most of the screen with its aspect ratio intact — it never overflows or forces the page to scroll:
- A very wide panorama is capped to ~90% of the viewport width
(
max-w-[90vw]); its height scales down to match. - A very tall image is capped to ~85% of the viewport height
(
max-h-[85vh]); its width scales down to match.
object-contain keeps both from stretching. Open each below — a 21:9 panorama
and a 9:21 column both land fully inside the viewport, using the available space:
Lazy loading and the loading spinner
Off-screen slides are not downloaded up front. Each image is
loading="lazy", so only the slide you are viewing (and one you navigate to)
fetches its full-size source — a gallery of large photos does not pay for all of
them on open. Embeds are lazy too; videos preload only their metadata.
While a large image is still downloading, the viewer shows a loading spinner in its place, then fades the image in once it has loaded. There is nothing to configure — it is automatic for every image item. (Open the wide or tall image above on a throttled connection to see the spinner before the image resolves.)
Video and embeds
Set type to video for a native <video> player, or embed for an
<iframe> (an embedded map, a video host, a document viewer). A video item
also takes an optional poster still, shown before the clip paints its first
frame — so the surface is not blank while it buffers. Here a poster image with a
play affordance opens the clip in the lightbox:
Mix types freely in one lightbox — the viewer, focus trap, and keyboard model are identical:
<x-wirekit::lightbox name="product-media" :items="[
['src' => $photo->url, 'alt' => 'Product photo', 'caption' => 'Front view'],
['src' => $clip->url, 'type' => 'video', 'poster' => $clip->posterUrl, 'alt' => 'Product in use', 'caption' => 'In use'],
['src' => $tourUrl, 'type' => 'embed', 'alt' => '360° tour', 'caption' => '360° tour'],
]">
<x-wirekit::button x-on:click="openAt(1)">Watch the clip</x-wirekit::button>
</x-wirekit::lightbox>
Loop on or off
By default prev / next wrap around at the ends. Set :loop="false" and the
controls stop — the prev control is disabled on the first item, next on the last:
Captions off
Captions render under the media by default. Turn them off with
:show-captions="false" for a clean, chrome-free viewer:
A longer caption
A caption is not limited to a short label. A full sentence wraps onto multiple lines, stays centered under the media, and never runs wider than the image — good for a credit line, a description, or a note about what the reader is looking at:
Backdrop color per instance
The backdrop uses the themeable --color-wk-overlay token by default. Override it
per instance with overlay — any CSS color, so you can dim harder for photos or
tint the surround to match a brand:
Open from anywhere (event-driven)
A control does not have to live inside the lightbox. Give the lightbox a stable
name, then dispatch a wirekit-lightbox-open event with that name and the
target index from any element on the page — a table row, a card, a menu item:
{{-- The lightbox lives once, near the end of the page --}}
<x-wirekit::lightbox name="product-media" :items="$media" />
{{-- Any control, anywhere, opens it at a chosen index --}}
<button
type="button"
x-on:click="$dispatch('wirekit-lightbox-open', { name: 'product-media', index: 3 })"
>
Open the fourth image
</button>
The event carries { name, index }; only the lightbox whose name matches
responds. This keeps one overlay instance for many triggers instead of nesting a
lightbox per thumbnail.
Design tokens
The lightbox styles itself entirely from design tokens, so it re-themes with the rest of WireKit. Retint these to restyle it (see theming):
| Token | Drives |
|---|---|
--color-wk-overlay |
The backdrop color (default — override per instance with overlay). |
--z-wk-modal |
The stacking level, above page content. |
--radius-wk-md |
The corner radius on the media and controls. |
--shadow-wk-lg |
The elevation of the media surface. |
--shadow-wk-md |
The elevation of the prev / next / close controls. |
--color-wk-bg-elevated |
The control button background. |
--color-wk-text |
The control icon color. |
--color-wk-ring + --ring-wk-width |
The keyboard focus ring on the controls. |
--color-wk-bg |
The caption text color (light-on-dark). |
--text-wk-sm |
The caption text size. |
--space-wk-md / --space-wk-sm |
Overlay padding, control insets, and the media-to-caption gap. |
--opacity-wk-disabled |
The dimmed prev / next control when loop is off at an end. |
Props
| Prop | Type | Default | Description |
|---|---|---|---|
name |
string|null |
auto | Identifier this instance answers to for the wirekit-lightbox-open event. Auto-generated when omitted. |
items |
array |
[] |
The slides — each an array with src, alt, optional caption, optional type (image / video / embed), and an optional poster (video-only still shown before the clip paints). A plain string is a decorative image src. |
loop |
bool |
true |
Whether prev / next wraps around at the ends. false disables the control at each end. |
showCaptions |
bool |
true |
Render each item's caption under the media. |
overlay |
string|null |
null |
Backdrop color / opacity for this instance (any CSS color). Null → the --color-wk-overlay token. |
scope |
string|null |
null |
Scoped personalization key. |
Accessibility
- The overlay is a
role="dialog"aria-modal="true"surface, focus-trapped with the same helper Modal and Drawer use — focus cannot reach the page behind it, and it returns to the trigger on close. - Every prev / next / close control has an accessible name; the media carries its
alt(decorative items passalt=""). - Motion (the fade / scale entrance) respects
prefers-reduced-motion— users who ask for less motion get an instant open. - Provide a real, keyboard-focusable trigger (a
<button>or<x-wirekit::button>) so the lightbox is reachable without a pointer.
Keyboard Interaction
| Key | Action |
|---|---|
Enter / Space |
Open the lightbox from the focused trigger |
Escape |
Close the lightbox (focus returns to the trigger) |
← |
Previous item |
→ |
Next item |
Tab |
Move between the lightbox controls (focus stays trapped inside) |