<script setup lang="ts">
import { ref } from 'vue'
import { Button, Lightbox, type LightboxItem } from '@hina-ui/vue'
const open = ref(false)
const items: LightboxItem[] = [
{ id: 'image-1', src: '/sample.webp', alt: 'A hillside on a summer afternoon' },
]
</script>
<template>
<Button variant="outline" tone="neutral" @click="open = true">Open preview</Button>
<Lightbox v-model:open="open" :items="items" />
</template>
Usage
import { Lightbox, type LightboxItem } from '@hina-ui/vue'
Provide images through items, control visibility with v-model:open, and select the current image with v-model:index. Mount the component in the Vue component tree; event handlers can update these values.
Lightbox works independently of Image. The Button in the example only opens the preview.
Source transition
Return the source <img> element from the item’s source callback. The preview expands from that image and returns to its current position when closed. Set fit to match the source image’s cropping; rounded clipping on an outer container also participates in the transition.
The example wraps Image in a Button and reads the source image from the click event. The callback can also return an existing native <img> on the page.
<script setup lang="ts">
import { ref, shallowRef } from 'vue'
import { Button, Image, Lightbox, type LightboxItem } from '@hina-ui/vue'
const open = ref(false)
const source = shallowRef<HTMLImageElement | null>(null)
const items: LightboxItem[] = [
{
id: 'image-1',
src: '/sample.webp',
alt: 'A hillside on a summer afternoon',
fit: 'cover',
source: () => source.value,
},
]
function show(event: MouseEvent) {
source.value = (event.currentTarget as HTMLElement).querySelector('img')
open.value = true
}
</script>
<template>
<Button
variant="ghost"
tone="neutral"
:ripple="false"
aria-label="Open image preview"
class="h-auto w-64 cursor-zoom-in overflow-hidden rounded-xl border-0 p-0"
@click="show"
>
<Image
src="/sample.webp"
alt="A hillside on a summer afternoon"
:ratio="16 / 9"
:lazy="false"
class="w-64"
/>
</Button>
<Lightbox v-model:open="open" :items="items" />
</template>
Rectangle source
source can also return { x, y, width, height } in viewport coordinates, measured in CSS pixels. No <img> element is required. Pass existing bounds directly with source: () => bounds.
A valid previewSize lets the preview expand from the rectangle immediately. Otherwise, the first opening decodes src to obtain its intrinsic dimensions. Closing calls source again to read the latest bounds. Return null or undefined when the source is no longer visible to fade out instead. Rectangles with zero size, non-finite values or no intersection with the viewport are ignored.
The example uses a Button background image and returns the button’s DOMRect.
<script setup lang="ts">
import { ref, shallowRef } from 'vue'
import { Button, Lightbox, type LightboxItem } from '@hina-ui/vue'
const open = ref(false)
const target = shallowRef<HTMLElement>()
const items: LightboxItem[] = [
{
id: 'image-1',
src: '/sample.webp',
alt: 'A hillside on a summer afternoon',
source: () => (target.value?.isConnected ? target.value.getBoundingClientRect() : undefined),
},
]
function show(event: MouseEvent) {
target.value = event.currentTarget as HTMLElement
open.value = true
}
</script>
<template>
<Button
variant="ghost"
tone="neutral"
:ripple="false"
aria-label="Preview background image"
class="h-36 w-64 cursor-zoom-in rounded-none border-0 bg-cover bg-center bg-no-repeat p-0"
:style="{ backgroundImage: 'url(/sample.webp)' }"
@click="show"
/>
<Lightbox v-model:open="open" :items="items" />
</template>
Sizing and zoom
Images initially fit within the available area without exceeding their intrinsic size. The available area reserves space for the close button, toolbar and thumbnails, and is recalculated when the window changes size.
Double tapping switches between the initial and intrinsic sizes. Panoramas wider than 2:1 first fit the available height; tall images taller than 2:1 first fit the available width. Both are capped at intrinsic size. Use “Actual size” in the toolbar to go directly to 100%, or “Fit to window” to restore the initial fit.
Wheel, pinch and toolbar zoom can reach twice the intrinsic image size. The corresponding toolbar buttons are disabled at each limit. When a small image already appears at intrinsic size, double tapping does not enlarge it further; toolbar and gesture zoom remain available.
This example uses Image and ImageGroup to display images with different intrinsic dimensions.
128 × 128
256 × 256
1200 × 675
<script setup lang="ts">
import { Image, ImageGroup, Inline, Stack, Text } from '@hina-ui/vue'
const pictures = [
{ src: '/favicon.png', alt: 'Hina UI', size: '128 × 128' },
{ src: '/avatars/huh.webp', alt: 'Character portrait', size: '256 × 256' },
{ src: '/sample.webp', alt: 'A hillside on a summer afternoon', size: '1200 × 675' },
]
</script>
<template>
<ImageGroup>
<Inline align="start" class="gap-6">
<Stack v-for="picture in pictures" :key="picture.src" gap="xs" class="w-32">
<Image
:src="picture.src"
:alt="picture.alt"
preview
fit="contain"
class="h-24 rounded-md"
/>
<Text tone="muted" size="sm">{{ picture.size }}</Text>
</Stack>
</Inline>
</ImageGroup>
</template>
Preview dimensions
Set previewSize: { width, height } on an item to supply the final preview image’s intrinsic pixel dimensions. They describe preview when a separate larger image is provided, or src otherwise. Both dimensions must be finite and positive; omitted or invalid values retain automatic sizing.
Valid explicit dimensions determine the initial fit and zoom limit and are not replaced by decoded dimensions. Loading the larger image only replaces its pixels. Rectangle-source transitions can also start without waiting for decoding. These dimensions do not change the position or size returned by source.
The example wraps Image in a Button for its rectangle source and supplies the preview dimensions on the item. Image also accepts the same prop.
<script setup lang="ts">
import { ref, shallowRef } from 'vue'
import { Button, Image, Lightbox, type LightboxItem } from '@hina-ui/vue'
const open = ref(false)
const target = shallowRef<HTMLElement>()
const image = {
thumbnail: '/sample-thumbnail.webp',
original: '/sample.webp',
width: 1200,
height: 675,
}
const items: LightboxItem[] = [
{
id: 'image-1',
src: image.thumbnail,
preview: image.original,
previewSize: { width: image.width, height: image.height },
alt: 'A hillside on a summer afternoon',
source: () => (target.value?.isConnected ? target.value.getBoundingClientRect() : undefined),
},
]
function show(event: MouseEvent) {
target.value = event.currentTarget as HTMLElement
open.value = true
}
</script>
<template>
<Button
variant="ghost"
tone="neutral"
:ripple="false"
aria-label="Preview image"
class="h-auto w-64 cursor-zoom-in rounded-none border-0 p-0"
@click="show"
>
<Image
:src="image.thumbnail"
alt="A hillside on a summer afternoon"
:ratio="16 / 9"
:lazy="false"
class="w-64"
/>
</Button>
<Lightbox v-model:open="open" :items="items" />
</template>
Behaviour
srcandprevieware used as provided, including regular image addresses and valid blob URLs.sourceis optional. A visible source image supplies the position and clipping shape for opening and closing. Without a source, the preview opens with a fade and a small scale transition.previewsupplies an optional larger image. Without a validpreviewSize, its decoded dimensions determine the initial fit and zoom limit. Size changes animate after any opening transition finishes. If loading fails,srcremains in use.- Zooming, panning, rotation, downloading and paging behave like Image previews.
- The caller owns blob URLs and should keep them valid while the preview uses them.
- The component exposes controlled state. An application can connect imperative calls through shared state and a mounted preview component.
Accessibility
- The current item’s
altlabels its image and the preview dialog. - Focus remains inside the preview while it is open, and page scrolling is locked.
Escapecloses the preview;←and→move between images.
API
| Prop | Type | Default | Description |
|---|---|---|---|
items | LightboxItem[] | Required | Image sequence |
v-model:open | boolean | false | Whether the preview is open |
v-model:index | number | 0 | Current image index, starting at zero |
loop | boolean | false | Whether paging wraps at either end |
class | string | — | Additional classes for the preview layer |
LightboxItem
| Field | Type | Description |
|---|---|---|
id | string | Stable, unique identifier; use distinct identifiers for different images |
src | string | Image address |
alt | string | Image description and dialog name |
preview | string | Optional larger image address |
previewSize | { width: number; height: number } | Intrinsic dimensions of the final preview image; both must be finite and positive |
fit | ImageVariants['fit'] | Source image fit, used to calculate the opening transition |
source | () => LightboxSource | null | undefined | Optional function returning a source image or rectangle |
LightboxSource
Exported from the package root, with two supported source forms.
type LightboxSource = HTMLImageElement | { x: number; y: number; width: number; height: number }