Skip to content

Gallery

Gallery is a full-featured media component built on top of Carousel. You give it a list of items and pick a display mode — the same data renders as a packed masonry grid, a justified row layout, a uniform grid, a file-list, an inline slider, an autoplaying slideshow, or a headless lightbox that you drive from your own thumbnails.

Every mode (except lightbox, which has no thumbnails) opens a full-screen modal carousel on click, with pinch-to-zoom, swipe-to-dismiss, keyboard navigation, page navigation for PDFs, and built-in renderers for video, 360° panoramas, and iframe embeds.

import { Gallery, type GalleryItem, type GalleryDisplay } from '@delightstack/components';

The minimum is a list of items. Strings are treated as image URLs; objects accept the full GalleryItem shape. Include width and height whenever you can — they let masonry layouts reserve the correct space before each image loads.

View code
<script lang="ts">
import { Gallery, type GalleryItem } from '@delightstack/components';
const items: GalleryItem[] = [
{ src: '/photos/ridge.jpg', width: 1200, height: 800, name: 'Mountain ridge' },
{ src: '/photos/doorway.jpg', width: 800, height: 1200, name: 'Quiet doorway' },
{ src: '/photos/coast.jpg', width: 1600, height: 900, name: 'Coastline' },
// ...
];
</script>
<Gallery {items} />

The same items array can render seven different ways. Flip between them below to compare — note how the layout, aspect ratios, and clickable region differ.

Brick-style packed grid that respects each image’s aspect ratio. Best for varied photo sets.

View code
<script lang="ts">
import { Gallery, type GalleryDisplay } from '@delightstack/components';
let display = $state<GalleryDisplay>('masonry');
</script>
<Gallery {items} {display} aspect_ratio="16/9" />
ModeLayoutWhen to use it
masonry (default)Brick-packed grid that respects each image’s aspect ratio. Wider images span more columns.Photo sets where the original framing matters and the mix of orientations is part of the appeal.
masonry-rowJustified rows. Each row fills the full width edge-to-edge, varying the row height to preserve aspect ratios.Press galleries, photojournalism, or any “Flickr-style” gallery where horizontal alignment matters.
gridUniform square cells. Images crop to object-fit: cover to fit the cell.Product grids, contact sheets, or anywhere consistency beats faithfulness to the original ratio.
listVertical list with a small thumbnail + name. Compact, scannable.File managers, attachment lists, document libraries.
sliderInline carousel — one item at a time with paging controls below.Hero sections, product image switchers, embedded story viewers.
slideshowSame shell as slider but tuned for autoplay (subtle zoom + crossfade).Background slideshows, kiosks, “now playing” displays.
lightboxRenders nothing. You provide your own thumbnails; the carousel opens when you set slide or call open().When you need full control of the thumbnail layout — mixed with text, in cards, scattered across the page, etc. See Lightbox below.

size adjusts how dense the thumbnails are on the delightstack numeric scale ('00''3', where '1' is the standard). It changes column counts for grid/masonry, row heights for masonry-row, and row height for list.

View code
<Gallery {items} display="masonry" size="2" />

spacing controls the gap between items, and radius controls the corner radius. Both use the delightstack numeric scale ('0''3', where '1' is the standard); radius scales with the gallery gap so the two stay visually balanced.

spacing
radius
View code
<Gallery {items} display="grid" spacing="2" radius="2" />

List mode is the odd one out: a small thumbnail plus a name in a single row. Use it for any “library of files” feel.

Size
Radius
View code
<Gallery {items} display="list" />

display="slider" embeds a single-image carousel in the page flow with arrows, page counter, fullscreen toggle, and an optional play button. The aspect_ratio prop locks the frame to a fixed shape so the layout doesn’t reflow as images load.

View code
<Gallery {items} display="slider" aspect_ratio="16/9" />

display="slideshow" is the same shell as slider but tuned for unattended playback: while autoplaying it applies a slow Ken-Burns-style zoom and crossfades between slides. Set duration (ms) to control the per-slide dwell time. Autoplay automatically pauses when the slideshow scrolls out of view.

View code
<Gallery
{items}
display="slideshow"
aspect_ratio="16/9"
autoplay
duration={4000}
meta_display_fullscreen="always" />

The actions prop is a 2D array — one row per item, one entry per button. Each button can have an icon, href, target, click handler, and a nested actions array for a dropdown menu. Hover any thumbnail in the demo below to reveal its download button.

View code
<script lang="ts">
import {
Gallery,
pickLargestSrc,
type GalleryItemAction,
} from '@delightstack/components';
const actions: GalleryItemAction[][] = items.map((item) => [
{
name: 'Download',
tooltip: 'Download original',
href: pickLargestSrc(item.src),
target: '_blank',
},
]);
</script>
<Gallery {items} display="masonry" {actions} action_display="hover" />

display="lightbox" is the “bring your own thumbnails” mode. Gallery renders nothing of its own — instead, you render the trigger elements (buttons, images, cards, anywhere on the page) and open the carousel by either:

  • Setting slide to the item index you want to open. slide = -1 closes the modal.
  • Calling gallery.open(index, fromElement?) via bind:this. The optional fromElement anchors the open animation to your trigger so the modal appears to zoom out of it.

Use this when the built-in grid modes don’t fit — e.g. you want thumbnails interleaved with prose, displayed as cards with custom metadata, or scattered across several sections of the page.

The Gallery renders nothing of its own here — the thumbnail layout below is plain HTML on this page. Clicking a tile calls gallery.open(index, event.currentTarget) which opens the carousel anchored to that tile.

View code
<script lang="ts">
import { Gallery, pickLargestSrc } from '@delightstack/components';
let gallery = $state<ReturnType<typeof Gallery>>();
</script>
<div class="thumbs">
{#each items as item, i}
<button onclick={(e) => gallery?.open(i, e.currentTarget)}>
<img src={pickLargestSrc(item.src)} alt={item.alt} />
</button>
{/each}
</div>
<Gallery bind:this={gallery} {items} display="lightbox" />

Gallery accepts the same item types as Carousel: image, video, pdf, embed, and custom. Set type (and panorama: true for 360° images) and Gallery handles the rest — the rich renderers load on demand the first time you open the lightbox on one.

PDFs are easy to spot in the thumbnail grid (the lightbox shows them with a built-in viewer and vertical page-flipping); video items show a play icon overlay; embeds show an embed icon; panoramas show a 360° icon.

View code
<script lang="ts">
import { Gallery, type GalleryItem } from '@delightstack/components';
// Tip: interleave images between rich items so the lightbox carousel never
// has to keep two heavy renderers (pdfjs, three.js, video) mounted at once.
const items: GalleryItem[] = [
{ src: '/photos/valley.jpg', width: 1600, height: 1000 },
{ src: '/photos/river.jpg', width: 1600, height: 1000 },
{ type: 'pdf', src: '/documents/paper.pdf', width: 850, height: 1100 },
{ src: '/photos/forest.jpg', width: 1600, height: 1000 },
{ type: 'image', panorama: true, src: '/panoramas/valley.jpg' },
{ src: '/photos/coast.jpg', width: 1600, height: 1000 },
{ type: 'video', src: '/videos/intro.mp4' },
{ src: '/photos/library.jpg', width: 1600, height: 1000 },
];
</script>
<Gallery {items} display="masonry-row" />

Set autoplay_video to start a video the moment the lightbox is launched onto it. Because opening is driven by a user gesture — a thumbnail click or the open() method — the browser allows playback with sound. Only the slide the lightbox opens to auto-plays, and only when it’s a video; other slide types are left alone, and navigating/swiping between slides does not auto-play.

Click any thumbnail below — the modal opens and that video starts playing.

View code
<script lang="ts">
import { Gallery, type GalleryItem } from '@delightstack/components';
const items: GalleryItem[] = [
{ type: 'video', src: '/videos/clip-1.mp4', poster: '/videos/clip-1.jpg' },
{ type: 'video', src: '/videos/clip-2.mp4', poster: '/videos/clip-2.jpg' },
{ type: 'video', src: '/videos/clip-3.mp4', poster: '/videos/clip-3.jpg' },
// ...
];
</script>
<Gallery {items} display="grid" autoplay_video />

For media types the built-ins don’t cover — a Matterport tour, a custom 3D viewer, a chart, an interactive form — set type: 'custom' on the item and provide the custom snippet. The snippet receives { item, active, gesture_disabled, onload, onerror }. See Carousel’s custom type docs for the full snippet signature.

Pair with disable_swipe: true when your widget needs horizontal pointer input.

The Gallery is in display="lightbox" mode (no thumbnails) since type: 'custom' items don't have a thumbnail representation. Open the modal using the buttons above. On the custom slide, drag the slider horizontally — the carousel won't try to swipe to the next slide.

View code
<script lang="ts">
import { Gallery, type GalleryItem } from '@delightstack/components';
const items: GalleryItem[] = [
{
type: 'custom',
src: '',
name: 'Interactive counter',
disable_swipe: true,
},
{ type: 'image', src: '/photos/sunset.jpg' },
];
let count = $state(0);
</script>
<Gallery {items} display="grid">
{#snippet custom({ item, active })}
<div class="my-slide" class:active>
<button onclick={() => count++}>{count}</button>
</div>
{/snippet}
</Gallery>
PropTypeDefaultNotes
itemsGalleryItem[][]Strings are treated as image URLs. See GalleryItem.
displayGalleryDisplay'masonry'See Display modes.
size'00' | '0' | '1' | '2' | '3''1'Thumbnail density (numeric scale; '1' standard, lower = smaller, higher = larger).
spacing'0' | '1' | '2' | '3''1'Gap between thumbnails (numeric scale; '0' removes the gap).
radius'0' | '1' | '2' | '3''1'Corner radius of thumbnails (numeric scale; '0' is square).
slidenumber (bindable)-1 (or 0 for slider)The currently displayed item index. -1 keeps the modal closed.
fit'cover' | 'contain''contain'object-fit for items inside the carousel.
aspect_ratiostringundefinedCSS aspect ratio for the slider/slideshow frame (e.g. "16/9"). Ignored when the modal is open.
autoplaybooleanfalseAuto-advance slides. Paused while scrolled off-screen.
autoplay_videobooleanfalseAuto-play a video when the lightbox is launched onto it (not when changing slides). Only the slide opened to, and only when it’s a video. The opening gesture lets the browser play with sound.
durationnumber8000Autoplay dwell time per slide (ms).
inlinebooleanautoWhen true, disables vertical/dismiss gestures. Defaults to true for slider when not fullscreen.
disable_fullscreenbooleanfalseHide the fullscreen toggle.
controls'default' | 'inline' | 'overlay' | 'disable''default'Where slider controls appear. default = inline when embedded, overlay when modal.
meta_display'none' | 'always' | 'hover''hover'When to show the name overlay on thumbnails.
meta_display_fullscreen'none' | 'always''none'When to show the caption in the fullscreen carousel.
action_display'none' | 'always' | 'hover''hover'When to show the action buttons on thumbnails.
actionsGalleryItemAction[][][]Per-item action buttons. See GalleryItemAction.
pagenumber (bindable)0Current page index inside a multi-page item (e.g. a PDF).
num_pagesnumber (bindable)1Number of pages in the current item. Set automatically for PDFs.
customSnippetundefinedRenderer for items with type: 'custom'. Forwarded to Carousel.
onclick(event, index) => void | falseundefinedReturn false from the handler to prevent the modal from opening.
stylestring''Inline style passed to the gallery root.

Bind the Gallery with bind:this={gallery} and call these directly:

MethodDescription
open(index, from?)Open the modal at index. Pass an HTMLElement as from to anchor the open animation to that element (typical for display="lightbox").
close()Close the modal. Exits fullscreen first if active.
goto(i)Jump to slide i. No-op when the modal isn’t open.
next(amount=1)Advance by amount slides (wraps at the end).
prev(amount=1)Go back by amount slides (wraps at the start).
play()Start the autoplay timer.
pause()Pause the autoplay timer.
toggleFullscreen()Enter or exit the browser’s fullscreen mode.
type GalleryItem = string | (Partial<CarouselItem> & { favorite?: boolean });

Items can be a plain image URL string, or an object:

FieldTypeNotes
idstringStable key — defaults to src.
type'image' | 'video' | 'pdf' | 'embed' | 'custom'Defaults to 'image'. See Carousel types for the full per-type behaviour.
srcstringURL, or a srcset string with width descriptors ("sm.jpg 400w, lg.jpg 1600w"). May be empty for custom.
width / heightnumberIntrinsic pixel dimensions. Required for masonry layouts so each tile can reserve space before the image loads.
namestringShort label, shown on the thumbnail and used as fallback alt text.
captionstringLonger caption, shown in the fullscreen overlay when meta_display_fullscreen="always".
altstringExplicit alt text override.
thumbhashstringBase64 ThumbHash — decoded internally into a tiny blurred placeholder while the full image loads.
prioritybooleanMark above-the-fold items so they get loading="eager" + fetchpriority="high".
panoramabooleanWhen type: 'image', render the image as a 360° panorama (uses Panorama, loaded lazily).
disable_swipebooleanSuppress the carousel’s horizontal swipe-to-change-slide gesture for this item. Useful for custom items with their own horizontal input.
disable_zoombooleanSuppress the carousel’s pinch-zoom for this item.
favoritebooleanIn masonry layouts, favorite items span twice the columns/rows.
type GalleryDisplay =
| 'masonry'
| 'masonry-row'
| 'grid'
| 'list'
| 'slider'
| 'slideshow'
| 'lightbox';
interface GalleryItemAction {
icon?: Component;
name?: string; // "Download", "Share", etc.
tooltip?: string; // optional secondary label (filename, size, etc.)
href?: string;
target?: '_blank' | '_self';
click?: (event: Event) => unknown;
actions?: GalleryItemAction[]; // nested → renders as a dropdown menu
}
  • The modal carousel uses focus-trapTab cycles within the modal, Escape closes it, and focus returns to the thumbnail that opened it (or, in display="lightbox", to whatever element triggered open()).
  • Each thumbnail is keyboard-activatable (Enter opens the modal) and has a focus ring.
  • Slide changes are announced via a polite live region (Media Item X of Y).
  • Arrow buttons and the close button have aria-labels and visually-hidden text.
  • The right-click context menu mirrors the action buttons so keyboard / touch users get parity.
  • Autoplay pauses automatically when the gallery is offscreen or after any user interaction.