Media Viewer
Fullscreen photo lightbox with navigation, thumbnails and zoom.
kuiMediaViewer() is an imperative opener, the same shape as kuiDialog() and kuiConfirm(). It reuses KuiDialog for the portal, backdrop, focus trap, Escape handling and scroll lock.
import { kuiMediaViewer } from '@kikita-labs/ui';
import type { KuiMediaViewerItem } from '@kikita-labs/ui';Call the opener from a click handler with the photos and the index to open on. The grid, the trigger buttons and any selection are your own composition.
<div class="media-viewer-example">
@for (photo of photos; track photo.id) {
<button type="button" class="media-viewer-example__tile" (click)="openPhotoAt($index)">
<img [src]="photo.src" [alt]="photo.alt" />
</button>
}
<span>Last viewed index: {{ lastViewed() }}</span>
</div>onIndexChange fires once on open with the clamped index and again on every navigation while the lightbox stays open. Photos only: video is out of scope.
A one-item items array needs no separate API. The counter, Prev, Next and thumbnail strip disappear and only zoom and Close remain.
<div class="media-viewer-example">
<button kuiButton type="button" (click)="open()">Open a single photo</button>
</div>A per-tile checkbox next to, not inside, the cover button is your own composition and not a kuiMediaViewer() option.
<div class="media-viewer-example">
@for (photo of photos; track photo.id) {
<div class="media-viewer-example__cell">
<button
type="button"
class="media-viewer-example__tile"
[attr.aria-label]="'Open photo ' + ($index + 1)"
(click)="openPhotoAt($index)"
>
<img [src]="photo.src" [alt]="photo.alt" />
</button>
<input
type="checkbox"
kuiCheckbox
aria-label="Select photo"
[checked]="isSelected(photo.id ?? photo.src)"
(change)="toggle(photo.id ?? photo.src)"
/>
</div>
}
<span>{{ selected().size }} selected</span>
</div>Zoom and pan follow the pointer devices.
- Dragging a zoomed-in photo pans it. Panning stops when the pointer leaves the lightbox and always ends cleanly on pointer up or cancel, even outside the window (
setPointerCapture). - The photo is not natively draggable or selectable, so a pan never becomes the browser's drag-image or text-selection gesture.
- Mouse wheel and a trackpad pinch (a wheel event with
ctrlKey) zoom by onezoomStepper event; scrolling up zooms in. - A touchscreen two-finger pinch zooms continuously, scaling from where the pinch began.
Set any of these on the component or an ancestor to restyle one part. Each token is optional: when it is not set, the part uses the role or scale token in the default column.
| Name | Type | Default | Description |
|---|---|---|---|
| --kui-media-viewer-thumb-focus-ring-color | CSS custom property | --kui-color-focus | Thumb focus ring color |
| Name | Type | Default | Description |
|---|---|---|---|
| --kui-media-viewer-toolbar-padding | CSS custom property | --kui-space-4 | Toolbar padding |
| --kui-media-viewer-counter-font-size | CSS custom property | --kui-text-sm-size | Counter font size |
| --kui-media-viewer-toolbar-actions-gap | CSS custom property | --kui-space-2 | Toolbar actions gap |
| --kui-media-viewer-stage-padding-inline | CSS custom property | --kui-space-16 | Stage padding, inline |
| --kui-media-viewer-frame-radius | CSS custom property | --kui-radius-md | Frame corner radius |
| --kui-media-viewer-strip-gap | CSS custom property | --kui-space-2 | Strip gap |
| --kui-media-viewer-strip-padding-block | CSS custom property | --kui-space-3 | Strip padding, block |
| --kui-media-viewer-strip-padding-inline | CSS custom property | --kui-space-4 | Strip padding, inline |
| --kui-media-viewer-thumb-radius | CSS custom property | --kui-radius-sm | Thumb corner radius |
Every text the component owns (accessible names, visible words, announcements) is a typed message with an English default. Override it for the application with provideKikitaUi({ messages }), or for a subtree with provideKuiMessages.
| Name | Type | Default | Description |
|---|---|---|---|
| label | string | — | Name of the viewer when the caller gives none. Default Photo viewer. |
| zoomIn | string | — | Zoom in button. Default Zoom in. |
| zoomOut | string | — | Zoom out button. Default Zoom out. |
| close | string | — | Close button. Default Close photo viewer. |
| previous | string | — | Previous button. Default Previous photo. |
| next | string | — | Next button. Default Next photo. |
| goTo | KuiMessageFn<{ index: number; total: number; }> | — | Thumbnail button. Default Go to photo {index} of {total}. |
| position | KuiMessageFn<{ label: string; index: number; total: number; }> | — | Viewer name with the current position. Default {label}, photo {index} of {total}. |
| loadErrorTitle | string | — | Heading of the load error state. Default Could not load this photo. |
| loadErrorDescription | string | — | Description of the load error state. Default Check your connection and try again. |
Inputs verified against @kikita-labs/ui v2.0.0 public typings.
| Name | Type | Default | Description |
|---|---|---|---|
| kuiMediaViewer() | () => (data: KuiMediaViewerData) => Observable<void | undefined> | - | Returns the opener. Call it in an injection context (a constructor or field initializer), like kuiDialog(). The Observable completes when the lightbox closes (Close, Escape or backdrop) and carries no value. |
| items | readonly KuiMediaViewerItem[] | - | Required. Photos to browse, at least one. A single item hides the counter, Prev, Next and the thumbnail strip, leaving zoom and Close. |
| index | number | undefined | 0 | Index to open on, clamped to the valid range. |
| maxZoom | number | undefined | 3 | Upper zoom bound. |
| zoomStep | number | undefined | 0.5 | Zoom increment per Zoom in or Zoom out click and per wheel event. |
| ariaLabel | string | undefined | mediaViewer.label message | Base accessible name of the lightbox (Photo viewer). The position, photo N of total, is appended automatically. |
| messages | Partial<KuiMediaViewerMessages> | undefined | undefined | Text overrides for this viewer: name, zoom, close, previous, next, thumbnail, position and load error texts. They win over scoped and root messages. |
| onIndexChange | (index: number) => void | - | Called once on open with the clamped initial index and on every Prev, Next, thumbnail or keyboard navigation. |
| KuiMediaViewerItem.id | string | undefined | src | Stable identifier for tracking and thumbnail keys. Pass it only when two items can share a src. |
| KuiMediaViewerItem.src | string | - | Image URL. The viewer never fetches or transforms it. |
| KuiMediaViewerItem.alt | string | - | Alt text. Required; pass an empty string explicitly for a decorative photo. |
| KuiDialogSize "fullscreen" | dialog size | - | The panel is a KuiDialog with the fullscreen size, which any dialog content may also request. |
The panel is a modal dialog; focus handling comes from KuiDialog.
- The panel is
role="dialog"witharia-modal="true", labelled by a visually hidden heading such asPhoto viewer, photo 2 of 4, or just theariaLabelfor a single photo. - The focus trap, focus restore to the trigger on close and the page scroll lock come from
KuiDialog. - The counter is
aria-live="polite"and is not rendered for a single photo. - Close, Prev, Next, Zoom in and Zoom out are icon buttons that get the native
disabledat their bounds; Prev and Next are not rendered for a single photo. - Every thumbnail is a real button named
Go to photo N of totalwitharia-current="true"on the viewed one. - Escape closes; ArrowLeft and ArrowRight go to the previous and next photo without wrapping; Home and End jump to the first and last; Tab cycles inside the lightbox.
Deliberate omissions of the installed package.
- Video.
- A selectable prop: the grid and per-tile selection are your own composition.
- A pan clamp aware of the natural image size: panning uses a fixed offset budget per zoom step.
- Gallery grouping across several kuiMediaViewer() calls; each open is independent.