Image Gallery

Free

Grid, masonry and strip layouts with a lightbox viewer, keyboard navigation and selectable mode.

</>sv-image-gallery galleryimageslightbox

Complexity tier: Interactive The primitive bar, plus declared async states, a keyboard model, focus restoration and undo for destructive actions.

import { SvImageGalleryComponent } from '@styloviz/display';

Demo & usage

Basic usage

Usage example — open the gallery for the full interactive demo.

An interactive demo for this component lives in the gallery.

Open live demo

More examples

Selectable grid with captions

example.htmlAngular
<sv-image-gallery
  [images]="images"
  layout="grid"
  [columns]="4"
  [captions]="true"
  [selectable]="true"
  [(selected)]="selectedImageIds" />

A selection that survives pagination

`selected` is a `model`, so the HOST owns the set — which is what makes it persist across a page turn or a route change, and is the only place it can persist. The component deliberately never prunes ids for images it cannot see: a gallery that "tidied up" the selection to match the images currently bound would silently delete the user's choices on every page turn, which is the persistence itself, destroyed by the tidying. What comes with keeping them is the duty to show them — select twelve on page one, turn to page two, and none of them is ticked while "Delete selected" still acts on all twelve. The strip says so, and "Clear selection" clears the hidden part too.

example.htmlAngular
<sv-image-gallery
  [images]="page()"
  [selectable]="true"
  [(selected)]="chosen" />

// chosen lives in the HOST, so it survives the page change:
readonly chosen = signal<Set<string>>(new Set());

When to use

  • Displaying a collection of images users browse and open full-screen.
  • You want grid/masonry/strip layouts plus a keyboard-friendly lightbox.
  • For a single rotating banner, use the Carousel.

Async states

  • loading
  • empty
  • error
  • retry

Item identity & owned state

Restorable state
  • selected

Bind these to put a saved view back — the open sections, the hidden columns, the selected rows — and read their Change events to store what the user does.

Inputs

NameTypeDefaultDescription
loadingbooleanfalseTrue while the images are being fetched.
errorSvDataError | string | nullnullLoad failure. Use `toSvDataError()` from `@styloviz/core` to normalize an `HttpErrorResponse`.
emptyMessagestring'No images yet'Copy for the no-data state.
filteredbooleanfalseTrue when the emptiness is caused by a filter rather than an empty set.
imagesreadonly GalleryImage[][]Images to display (src, alt, optional caption).
layoutGalleryLayout'grid'Layout: grid, masonry or strip.
ariaLabelstring''What this gallery is, e.g. `'Product photos'` — names the list of images. Two galleries on one product page (the shots and the customer uploads) both announce as an unnamed list of N items without this. Left unset, no name is invented: `<ul role="list">` already gives a listener the count and the boundaries, which is the part the arrangement used to carry alone.
columnsGalleryColumns3Number of columns (2–5).
lightboxbooleantrueOpen images in a full-screen lightbox on click.
captionsbooleantrueShow captions under images.
selectablebooleanfalseEnable multi-select mode.
zoombooleantrueProduct-viewer: enable hover-zoom on the main image.
zoomLevelnumber2Product-viewer: magnification factor for the hover-zoom.
viewerSizeGalleryViewerSize'compact'Width of the `viewer` layout. `compact` caps it at 28rem.
viewerAspectGalleryViewerAspect'square'Frame of the `viewer` layout's main image.
selectedSet<string> (two-way)new Set()Which images are selected (two-way). A `model`, so a host can preselect a saved set or clear the selection after acting on it — as a plain signal it could only be read from inside the template, which made "selectable" a mode the host could watch but not drive. This replaces a `selectionChange = output<string[]>()`: a `model` emits `selectedChange` on every write already, and shipping both would leave two differently-shaped ways to observe one fact. That is the mistake the search inputs made with `queryChange`, noted in `list-view.component.ts`.
activeIndexnumber0Optional externally-controlled active image (viewer layout). Clamps to range; resets on image change.

Outputs

EventPayload typeDescription
retryvoidThe host should re-run the failed load.
imageClickGalleryImageEmitted with the clicked image.
selectedChangeSet<string>Emits the new `selected` whenever it changes — the output half of `[(selected)]`.

Wire it to your API

This component is headless about transport: it renders what you give it and reports what the user asks for. Nothing below requires @styloviz/data — drive it from whatever you already use.

Template

Bound to the names this component actually exposes.

usage.htmlAngular
<sv-image-gallery
  [images]="data.items()"
  [loading]="data.loading()"
  [error]="data.error()"
  [filtered]="data.filtered()"
  (retry)="data.reload()"
/>

Host class

Drive the three async inputs from your own fetch.

host.component.tsTypeScript
// This component reports its state but does not emit a query, so drive
// it from your own fetch and bind the three async inputs.
readonly items = signal<Row[]>([]);
readonly loading = signal(false);
readonly error = signal<SvDataError | null>(null);

async load(): Promise<void> {
  this.loading.set(true);
  this.error.set(null);
  try {
    this.items.set(await this.api.fetchRows());
  } catch (e) {
    this.error.set(toSvDataError(e));
  } finally {
    this.loading.set(false);
  }
}
  • This component does not emit a query, so it renders what you give it. Fetch on your own trigger and bind `images`, `loading` and `error`.
  • Set `filtered` when a search or filter is active. It is the difference between "you have no orders" and "no orders match this filter" — telling a customer with 4,000 orders the first one reads as a bug.
  • Normalize failures with `toSvDataError()` from `@styloviz/core`. It derives `retryable` from the status, so a 404 does not get a "Try again" button that can only fail again.

Full walkthrough: Connecting to a backend.

Customization & theming

  • Grid, masonry or strip layouts (2–5 columns).
  • Lightbox with keyboard navigation.
  • Optional selectable mode.
  • Product-viewer layout: a large main image with hover-zoom (zoomLevel) and a thumbnail strip to switch images.

Best practices

  • Do: Provide descriptive `alt` text on every image for accessibility and SEO.
  • Do: Serve appropriately sized thumbnails; load full-res only in the lightbox.
  • Tip: Enable `selectable` for bulk operations like delete or add-to-album.
  • Do: Hold `selected` in the host — a signal, a store, a route param. It is the only place a selection can survive a page turn, and the component will not prune it behind your back.
  • Don't: Don't clear the selection yourself on every `images` change. That is the tidying the component avoids on purpose, and it deletes exactly the choices the user made on the previous page.

Accessibility

  • Focus after a mutation (X-12): closing the lightbox returns focus to the control that opened it — or, if the reader paged to another image, to THAT image's thumbnail (the viewer layout also makes it the active image). If neither survives, focus goes into the gallery, never `<body>`.
  • Every layout is a `<ul role="list">`, so a listener is told the images are ONE SET OF A KNOWN SIZE — "list, 12 items" — which was the only fact the grid conveyed visually and nothing carried. A table of alt texts would be the alt texts again in a worse container, so there is no companion summary here.
  • The `role="list"` is not redundant: Tailwind's preflight strips `list-style`, and that alone is enough for Safari/VoiceOver to stop treating the `<ul>` as a list.
  • Name the gallery with `ariaLabel` when a page has more than one.
  • The lightbox is a modal `dialog` with a focus trap; focus returns to the originating thumbnail on close, and ←/→/Esc navigate.
  • Built with safe DOM APIs (no HTML injection), so image-supplied alt/title/author cannot inject markup.
  • View and select controls are labelled buttons reachable by keyboard in every layout; images require descriptive alt text.
  • A selection that outlives the page it was made on is disclosed in a `role="status"` strip — "12 selected — 9 not on this page" — because the component deliberately never prunes ids for images it cannot see, and a reader who cannot tell would believe the selection is only what is ticked in front of them.

Feedback on Image Gallery

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.