Skip to content
omaris

Containment

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.

import { ImageViewer } from 'omaris'
Learn

Examples

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 />

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>

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>

When to use it

Use it for

  • A gallery where a thumbnail opens full-screen. Wrap the grid in ImageViewer and mark each picture with ImageViewerItem. It lifts off that spot and settles back on close.
  • One picture in a card, a chat bubble or a ticket. A single ImageViewerItem is enough.
  • Opening from something that is not a thumbnail: a "View all 12 photos" button, a keyboard shortcut, a route. Pass images and drive open and index yourself.
  • Actions on the picture (download, delete, set as cover) in the actions snippet, before the close button.

Not for

  • Flicking through pictures on the page → Carousel. Wrap the carousel's items in ImageViewerItem and you have both.
  • A wall of thumbnails → Masonry, with an ImageViewer around it.
  • Cropping or rotating for keeps → Image Cropper. R rotates the view here; nothing is saved.
  • Video → Video Player.
  • Any modal that is not a picture → Dialog.

Do

  • Give ImageViewerItem the full-size src and put the thumbnail <img> inside it. The page loads small; the viewer loads big.
  • Use the same alt on both. The viewer announces it as the picture opens.
  • Set loop for a small set you step through with the arrow keys or buttons. Swiping never wraps either way: it stops at the first and last picture with a rubber band, as a phone's gallery does. Leave loop off when the order means something, like before and after.
  • Give width and height when you know them. The picture then opens at its real shape before the full image has loaded.
  • Keep dismissible on. A pull down, a pinch in and a tap on the black are how a phone closes it.

Don't

  • Put a link or a button inside ImageViewerItem. It is already a button.
  • Hang anything of your own on a tap on the picture. On a phone a tap shows and hides the controls and a double-tap zooms; a mouse click zooms at the pointer.
  • Turn toolbar and arrows off together on a desktop. A mouse then has no way to move on or out.
  • Raise maxScale for small originals. Five times a 400px thumbnail is pixels.
  • Give a decorative thumbnail an ImageViewerItem. A full-screen 40px icon is a bad surprise.

API

ImageViewer

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.

Wrap the thumbnails and mark each one:

Or drive it yourself, with no thumbnails on the page:

Gestures: pinch and wheel to zoom, double-tap (or click) to toggle it, drag to pan, swipe sideways for the next one — a zoomed picture pans to its edge first — and pull down or pinch in to close. Tap the picture to hide the controls. Keys: arrows, Home/End, +/-/0 for zoom, R to rotate, Escape to close.

import { ImageViewer } from 'omaris'
<ImageViewer>  {#each photos as p}    <ImageViewerItem src={p.full} alt={p.alt}>      <img src={p.thumb} alt={p.alt} class="rounded-lg" />    </ImageViewerItem>  {/each}</ImageViewer>
<ImageViewer images={photos} bind:open bind:index />

Props

images
ImageViewerImage[]

Images to show, when not using ImageViewerItems.

open bindable

Defaults to false

boolean

Bindable.

index bindable

Defaults to 0

number

Which image is showing. Bindable.

loop

Defaults to false

boolean

The arrow keys and buttons wrap from the last picture to the first. Swiping never does — it stops at either end with a rubber band, the way a phone's gallery does.

maxScale

Defaults to 5

number

Furthest you can zoom in.

toolbar

Defaults to true

boolean

Show the top bar: counter, zoom, rotate, close.

arrows

Defaults to true

boolean

Show the prev/next arrows on wide screens.

dismissible

Defaults to true

boolean

Allow the pull-down and the scrim tap to close it.

actions
Snippet<[{ image: ImageViewerImage; index: number }]>

Extra toolbar actions, rendered before the close button.

onchange
(index: number) => void
onclose
() => void
children
Snippet

ImageViewerItem

Marks a thumbnail as openable in the surrounding ImageViewer.

Wraps whatever you put inside — an <img>, a card, an avatar — in a button, and remembers where it is so the viewer can open from, and close back to, that exact spot.

import { ImageViewerItem } from 'omaris'

Props

class
string
children
Snippet