Image Viewer
Image viewer — tap a picture and it lifts off the page into a full-screen, zoomable, swipeable lightbox, then settles back where it came from. The feel of react-photo-view, for Svelte.
Walkthrough
1. Gallery
Tap a thumbnail and it lifts off the page into the lightbox, then settles back where it came from. Swipe between images — the strip stops at either end — pinch or double-tap to zoom, and pull down to close.
<script lang="ts"> import { ImageViewer, ImageViewerItem } from 'omaris'; const PHOTOS = [ { name: 'erbil', alt: 'Erbil citadel' }, { name: 'baghdad', alt: 'Baghdad riverside' }, { name: 'basra', alt: 'Basra canals' }, { name: 'duhok', alt: 'Duhok hills' } ];</script><ImageViewer> <div class="grid w-full grid-cols-2 gap-3 sm:grid-cols-4"> {#each PHOTOS as photo (photo.name)} <ImageViewerItem src="/docs/photos/{photo.name}.svg" alt={photo.alt} class="overflow-hidden rounded-shape-md" > <img src="/docs/photos/{photo.name}.svg" alt={photo.alt} class="aspect-[4/3] w-full object-cover" /> </ImageViewerItem> {/each} </div></ImageViewer> 2. Controlled
Hand it images and drive open and index yourself when the trigger is not a thumbnail — a "view all" button, a keyboard shortcut, a route.
Showing #1
<script lang="ts"> import { Button, ImageViewer } from 'omaris'; let open = $state(false); let index = $state(0); const IMAGES = [ { src: '/docs/photos/najaf.svg', alt: 'Najaf' }, { src: '/docs/photos/kirkuk.svg', alt: 'Kirkuk' }, { src: '/docs/photos/erbil.svg', alt: 'Erbil' } ];</script><div class="flex flex-col items-center gap-3"> <Button variant="tonal" onclick={() => { index = 0; open = true; }} > View all 3 photos </Button> <p class="text-body-sm text-muted-foreground">Showing #{index + 1}</p></div><ImageViewer images={IMAGES} bind:open bind:index /> 3. Shapes
Every picture is fitted to the screen on its own terms: a panorama fills the width, a portrait the height, and one smaller than the screen stays its own size. Zoomed in, a picture pans to its edge before the next one slides in.
<script lang="ts"> import { ImageViewer, ImageViewerItem } from 'omaris'; /** A gradient landscape of any size, so the shapes are real. */ function picture(width: number, height: number, hue: number) { const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='${width}' height='${height}' viewBox='0 0 ${width} ${height}'> <defs><linearGradient id='g' x1='0' y1='0' x2='1' y2='1'> <stop offset='0' stop-color='hsl(${hue} 70% 58%)'/> <stop offset='1' stop-color='hsl(${hue + 50} 65% 42%)'/> </linearGradient></defs> <rect width='100%' height='100%' fill='url(#g)'/> <circle cx='${width * 0.25}' cy='${height * 0.3}' r='${Math.min(width, height) * 0.12}' fill='white' fill-opacity='0.35'/> <path d='M0 ${height * 0.75} L${width * 0.3} ${height * 0.5} L${width * 0.55} ${height * 0.7} L${width * 0.8} ${height * 0.45} L${width} ${height * 0.65} V${height} H0Z' fill='black' fill-opacity='0.18'/> </svg>`; return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`; } const SHAPES = [ { alt: 'Panorama', src: picture(3000, 900, 200) }, { alt: 'Portrait', src: picture(900, 1600, 330) }, { alt: 'Square', src: picture(1400, 1400, 30) }, { alt: 'Small original', src: picture(320, 240, 140) } ];</script><ImageViewer> <div class="grid w-full grid-cols-2 gap-3 sm:grid-cols-4"> {#each SHAPES as shape (shape.alt)} <ImageViewerItem src={shape.src} alt={shape.alt} class="overflow-hidden rounded-shape-md"> <img src={shape.src} alt={shape.alt} class="aspect-square w-full object-cover" /> </ImageViewerItem> {/each} </div></ImageViewer> 4. Overridden
Every default is a prop. loop lets the arrow keys and buttons wrap from the last picture to the first (a swipe still stops at the end), maxScale caps the zoom, dismissible={false} keeps a pull or a tap on the black from closing it, and actions adds to the toolbar.
<script lang="ts"> import { ImageViewer, ImageViewerItem } from 'omaris'; const PHOTOS = [ { name: 'najaf', alt: 'Najaf' }, { name: 'kirkuk', alt: 'Kirkuk' }, { name: 'erbil', alt: 'Erbil citadel' } ];</script><ImageViewer loop maxScale={2} dismissible={false}> {#snippet actions({ image })} <a href={image.src} target="_blank" rel="noreferrer" class="grid size-10 place-items-center rounded-full transition-colors duration-200 ease-standard hover:bg-white/15 focus-visible:ring-2 focus-visible:ring-white/70 focus-visible:outline-none motion-reduce:transition-none" aria-label="Open the original" > <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="size-5"> <path d="M14 4h6v6M20 4l-9 9M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5" stroke-linecap="round" stroke-linejoin="round" /> </svg> </a> {/snippet} <div class="grid w-full grid-cols-3 gap-3"> {#each PHOTOS as photo (photo.name)} <ImageViewerItem src="/docs/photos/{photo.name}.svg" alt={photo.alt} caption={photo.alt} class="overflow-hidden rounded-shape-lg" > <img src="/docs/photos/{photo.name}.svg" alt={photo.alt} class="aspect-square w-full object-cover" /> </ImageViewerItem> {/each} </div></ImageViewer>