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.

media-viewer.ts
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.

Last viewed index: 0
<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.

0 selected
<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 one zoomStep per 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.

NameTypeDefaultDescription
--kui-media-viewer-thumb-focus-ring-colorCSS custom property--kui-color-focusThumb focus ring color
NameTypeDefaultDescription
--kui-media-viewer-toolbar-paddingCSS custom property--kui-space-4Toolbar padding
--kui-media-viewer-counter-font-sizeCSS custom property--kui-text-sm-sizeCounter font size
--kui-media-viewer-toolbar-actions-gapCSS custom property--kui-space-2Toolbar actions gap
--kui-media-viewer-stage-padding-inlineCSS custom property--kui-space-16Stage padding, inline
--kui-media-viewer-frame-radiusCSS custom property--kui-radius-mdFrame corner radius
--kui-media-viewer-strip-gapCSS custom property--kui-space-2Strip gap
--kui-media-viewer-strip-padding-blockCSS custom property--kui-space-3Strip padding, block
--kui-media-viewer-strip-padding-inlineCSS custom property--kui-space-4Strip padding, inline
--kui-media-viewer-thumb-radiusCSS custom property--kui-radius-smThumb 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.

NameTypeDefaultDescription
labelstring — Name of the viewer when the caller gives none. Default Photo viewer.
zoomInstring — Zoom in button. Default Zoom in.
zoomOutstring — Zoom out button. Default Zoom out.
closestring — Close button. Default Close photo viewer.
previousstring — Previous button. Default Previous photo.
nextstring — Next button. Default Next photo.
goToKuiMessageFn<{ index: number; total: number; }> — Thumbnail button. Default Go to photo {index} of {total}.
positionKuiMessageFn<{ label: string; index: number; total: number; }> — Viewer name with the current position. Default {label}, photo {index} of {total}.
loadErrorTitlestring — Heading of the load error state. Default Could not load this photo.
loadErrorDescriptionstring — Description of the load error state. Default Check your connection and try again.

Inputs verified against @kikita-labs/ui v2.0.0 public typings.

NameTypeDefaultDescription
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.
itemsreadonly KuiMediaViewerItem[]-Required. Photos to browse, at least one. A single item hides the counter, Prev, Next and the thumbnail strip, leaving zoom and Close.
indexnumber | undefined0Index to open on, clamped to the valid range.
maxZoomnumber | undefined3Upper zoom bound.
zoomStepnumber | undefined0.5Zoom increment per Zoom in or Zoom out click and per wheel event.
ariaLabelstring | undefinedmediaViewer.label messageBase accessible name of the lightbox (Photo viewer). The position, photo N of total, is appended automatically.
messagesPartial<KuiMediaViewerMessages> | undefinedundefinedText 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.idstring | undefinedsrcStable identifier for tracking and thumbnail keys. Pass it only when two items can share a src.
KuiMediaViewerItem.srcstring-Image URL. The viewer never fetches or transforms it.
KuiMediaViewerItem.altstring-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" with aria-modal="true", labelled by a visually hidden heading such as Photo viewer, photo 2 of 4, or just the ariaLabel for 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 disabled at their bounds; Prev and Next are not rendered for a single photo.
  • Every thumbnail is a real button named Go to photo N of total with aria-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.