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' Examples
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> 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
ImageViewerand mark each picture withImageViewerItem. It lifts off that spot and settles back on close. - One picture in a card, a chat bubble or a ticket. A single
ImageViewerItemis enough. - Opening from something that is not a thumbnail: a "View all 12 photos" button, a keyboard shortcut, a route. Pass
imagesand driveopenandindexyourself. - Actions on the picture (download, delete, set as cover) in the
actionssnippet, before the close button.
Not for
- Flicking through pictures on the page → Carousel. Wrap the carousel's items in
ImageViewerItemand you have both. - A wall of thumbnails → Masonry, with an
ImageVieweraround it. - Cropping or rotating for keeps → Image Cropper.
Rrotates the view here; nothing is saved. - Video → Video Player.
- Any modal that is not a picture → Dialog.
Do
- Give
ImageViewerItemthe full-sizesrcand put the thumbnail<img>inside it. The page loads small; the viewer loads big. - Use the same
alton both. The viewer announces it as the picture opens. - Set
loopfor 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. Leaveloopoff when the order means something, like before and after. - Give
widthandheightwhen you know them. The picture then opens at its real shape before the full image has loaded. - Keep
dismissibleon. 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
toolbarandarrowsoff together on a desktop. A mouse then has no way to move on or out. - Raise
maxScalefor 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 bindableDefaults to false
boolean Bindable.
index bindableDefaults 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