Lightbox
A click-to-enlarge media viewer: a thumbnail strip whose items open in an accessible, focus-trapped dialog. Multiple images and videos page through an embedded Carousel.
Import
import { Lightbox } from "@hyzer-labs/ui"Demo
Image + Lightbox
lightboxGroup attachment on the "Group attachment" tab to add click-to-view over an Image grid you
already render.Click the thumbnail and note the zoom cursor. Esc, the backdrop, and the close button all dismiss the viewer.
<Lightbox
src="/photos/hole-7.jpg"
thumbSrc="/photos/hole-7-thumb.jpg"
alt="Hole 7 fairway at sunset"
caption="Hole 7, sunset round"
/>With items, one component renders the whole strip. Each thumbnail opens the
viewer at its own item. ArrowLeft and ArrowRight, or the controls, page through with
wrap-around, and videos play through the Video component. Focus returns to the thumbnail that opened the viewer.
const items: LightboxItem[] = [
{ src: '/photos/hole-7.jpg', thumbSrc: '/photos/hole-7-thumb.jpg', alt: 'Hole 7 fairway' },
{ src: '/photos/putt.jpg', alt: 'Winning putt' },
{
type: 'video',
src: 'https://www.youtube.com/watch?v=…',
label: 'Final round highlights',
thumbSrc: '/photos/video-thumb.jpg'
}
];
<Lightbox {items} />The trigger snippet swaps each strip button's face. Lightbox still owns the <button>, its aria-haspopup="dialog", its accessible name, and the click wiring. So a
face like this Image should pass alt="": the button is already
named, and an empty alt keeps the decorative face out of the accessibility tree rather
than announcing it twice.
<Lightbox items={items} class="trigger-strip">
{#snippet trigger(item)}
<Image src={item.thumbSrc ?? item.src} alt="" aspectRatio="1/1" rounded="md" fit="cover" />
{/snippet}
</Lightbox>lightboxGroup() enhances a container's own media in place: no thumbnail
strip, and no Lightbox component at all. Click any photo below, or Tab to one
and press Enter or Space, and every qualifying image in the grid opens in one shared viewer,
starting at the photo you activated.
Two escape hatches: data-lightbox-ignore opts an element out, so the grey chart below gets no zoom cursor and never joins the group; data-lightbox-src opens a full-resolution source instead of the rendered (often scaled-down) src, which is what the second photo does.
Reach for the attachment to enhance media you already render, and for Lightbox when you control the markup and want the strongest accessibility
guarantees. The product detail pattern shows it composed
with Carousel: the active slide is the trigger, and
the viewer pages across every item once open.
data-lightbox-ignore: not part of the groupimport { lightboxGroup, Image } from '@hyzer-labs/ui';
<div class="gallery-grid" {@attach lightboxGroup()}>
<!-- Image spreads ...rest onto its inner img, so data-lightbox-src / -ignore -->
<!-- pass through: the attachment sees the real <img> it enhances. -->
<Image
src={photo.thumb}
alt={photo.alt}
aspectRatio="1/1"
rounded="md"
data-lightbox-src={photo.fullRes}
/>
<Image src={photo.src} alt={photo.alt} aspectRatio="1/1" rounded="md" />
<!-- opted out: no enhancement, not part of the shared viewer -->
<Image src={chart} alt="Static chart" aspectRatio="1/1" rounded="md" data-lightbox-ignore />
</div>Props
| Name | Type | Default | Note |
|---|---|---|---|
items | LightboxItem[] | — | Media entries (images and videos) — see LightboxItem below. Renders one thumbnail trigger per item; the viewer pages through them with a Carousel. |
src / alt / thumbSrc / caption | string | — | Single-image sugar — equivalent to a one-item items array. |
open | boolean (bindable) | false | |
dialogLabel | string | 'Media viewer' | Dialog name in multi-item mode (single items are labeled by their own name). |
triggerLabel | string | 'View larger: {name}' | Single trigger only. |
closeLabel | string | 'Close media viewer' | |
prevLabel | string | 'Previous item' | |
nextLabel | string | 'Next item' | |
onclose | () => void | — | Fires once per dismissal. |
children | Snippet | — | Custom trigger content — replaces the thumbnail strip; opens the first item. |
trigger | Snippet<[LightboxItem, number]> | — | Per-item trigger face, e.g. an Image — replaces the default thumb/badge for every item. Ignored (with a DEV warning) when children is also passed. |
class | string | — | Merged after the hz-lightbox-triggers class (the inline strip). |
LightboxItem (image)
| Name | Type | Default | Note |
|---|---|---|---|
type | 'image' | 'image' | Optional — image is the default. |
src | string | — | Required. Full-size image. |
alt | string | — | Required. |
thumbSrc | string | — | Strip thumbnail; defaults to src. |
caption | string | — |
LightboxItem (video)
| Name | Type | Default | Note |
|---|---|---|---|
type | 'video' | — | Required for videos. |
src | string | — | Required. Native file or YouTube/Vimeo URL — plays via the Video component. |
label | string | — | Required. Accessible name (the Video title). |
poster | string | — | |
thumbSrc | string | — | Strip thumbnail; defaults to poster. |
caption | string | — |
Theme hooks
What this component promises your CSS. The reference theme styles exactly these — from @layer hz-theme, so your unlayered rules win. See Styling Components for the how.
Root class: .hz-lightbox-triggers
Data attributes
| Hook | Values | Styles |
|---|---|---|
data-state | 'open' | 'closed' | On the viewer dialog, mirroring the bindable open prop. Visibility itself runs off the native [open] attribute — this is the hook to animate against. |
data-gallery | present with more than one item | On the viewer dialog. Drives the fixed gallery stage sizing that a single item skips (it hugs its media instead). |
data-lightbox-trigger | present on a grouped trigger | Set by the lightboxGroup attachment rather than the component. Its only styling is a zoom-in cursor. |
Part classes
| Hook | Values | Styles |
|---|---|---|
.hz-lightbox-trigger | child element | A thumbnail button. Also the root class when you supply your own children. |
.hz-lightbox-thumb | child element | The thumbnail image. |
.hz-lightbox-thumb-label | child element | Fallback chip when no thumbnail resolves. |
.hz-lightbox-badge | child element | The play marker on video items. |
.hz-lightbox | the viewer dialog | The overlay itself. It takes no class prop, so this name is unconditionally stable. |
.hz-lightbox-close | child element | The close control. |
.hz-lightbox-carousel | on the embedded Carousel | Multi-item viewers only — see Carousel for its own parts underneath. |
.hz-lightbox-figure | child element | The figure wrapping each item. |
.hz-lightbox-caption | child element | The caption. |
.hz-lightbox-img | on an Image's wrapper | Rides through Image’s class prop — so it names a div, not an img. Reach the picture itself through .hz-lightbox-img .hz-image__img. |
.hz-lightbox-video | child element | The wrapper around a video item’s Video. |
Accessibility
Each thumbnail is a real <button> with aria-haspopup="dialog", named by its item.
The viewer is a native <dialog> opened with showModal(). Focus is trapped. Escape and the backdrop both close it, and focus returns to the thumbnail that opened it.
Multi-item viewers embed the Carousel (labeled slides, live announcements), and ArrowLeft/ArrowRight page through it from anywhere in the dialog. Body scroll is locked while open.
References: APG Dialog (Modal) pattern · APG Carousel pattern