PXD

Image Preview

A thumbnail strip that opens a full-screen viewer with zoom, rotation, panning and gallery navigation.

Default

<script setup>
function createImage(label, color) {
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800"><rect width="1200" height="800" fill="${color}"/><text x="600" y="660" fill="#fff" font-family="sans-serif" font-size="160" font-weight="600" text-anchor="middle">${label}</text></svg>`

  return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
}

const images = [{ src: createImage('1', '#2563eb'), alt: 'Blue cover' }]
</script>

<template>
  <PImagePreview :data="images" />
</template>

With more than one image the viewer shows the current position and lets you switch with the arrows or the keyboard.

<script setup>
function createImage(label, color) {
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800"><rect width="1200" height="800" fill="${color}"/><text x="600" y="660" fill="#fff" font-family="sans-serif" font-size="160" font-weight="600" text-anchor="middle">${label}</text></svg>`

  return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
}

const images = [
  { src: createImage('1', '#2563eb'), alt: 'Blue cover' },
  { src: createImage('2', '#059669'), alt: 'Green cover' },
  { src: createImage('3', '#d97706'), alt: 'Amber cover' },
]
</script>

<template>
  <PImagePreview :data="images" />
</template>

Custom Thumbnail

<script setup>
function createImage(label, color) {
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800"><rect width="1200" height="800" fill="${color}"/><text x="600" y="660" fill="#fff" font-family="sans-serif" font-size="160" font-weight="600" text-anchor="middle">${label}</text></svg>`

  return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
}

const images = [
  { src: createImage('1', '#2563eb'), alt: 'Blue cover' },
  { src: createImage('2', '#059669'), alt: 'Green cover' },
  { src: createImage('3', '#d97706'), alt: 'Amber cover' },
]
</script>

<template>
  <PImagePreview :data="images">
    <template #item="{ index }">
      <span class="size-full flex items-center justify-center bg-gray-100 text-foreground-secondary">
        {{ index + 1 }}
      </span>
    </template>
  </PImagePreview>
</template>

Async Sources

An item’s src can be a function returning a promise of the URL. The component resolves it once per item, and the loading / error slots replace the viewer image while it is pending or after it rejects.

<script setup>
function createImage(label, color) {
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800"><rect width="1200" height="800" fill="${color}"/><text x="600" y="660" fill="#fff" font-family="sans-serif" font-size="160" font-weight="600" text-anchor="middle">${label}</text></svg>`

  return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
}

function loadImage(label, color) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(createImage(label, color)), 800)
  })
}

const images = [
  { src: () => loadImage('1', '#2563eb'), alt: 'Blue cover' },
  { src: () => loadImage('2', '#059669'), alt: 'Green cover' },
]
</script>

<template>
  <PImagePreview :data="images">
    <template #loading="{ index }">
      <span class="size-full flex items-center justify-center text-sm text-white/70">
        Loading image {{ index + 1 }}…
      </span>
    </template>
  </PImagePreview>
</template>

On-demand Requests

request fires when the viewer opens or switches to an image, so the host can fetch that item and write the resolved URL back into data.

Requested: none
<script setup>
function createImage(label, color) {
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800"><rect width="1200" height="800" fill="${color}"/><text x="600" y="660" fill="#fff" font-family="sans-serif" font-size="160" font-weight="600" text-anchor="middle">${label}</text></svg>`

  return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
}

const images = [
  { src: createImage('1', '#2563eb'), alt: 'Blue cover' },
  { src: createImage('2', '#059669'), alt: 'Green cover' },
  { src: createImage('3', '#d97706'), alt: 'Amber cover' },
]
const requested = ref([])

function onRequest(index) {
  requested.value = [...new Set([...requested.value, index])]
}
</script>

<template>
  <div class="gap-3 flex flex-col">
    <PImagePreview :data="images" @request="onRequest" />
    <span class="text-sm text-foreground-secondary">
      Requested: {{ requested.length ? requested.map((index) => index + 1).join(', ') : 'none' }}
    </span>
  </div>
</template>

Viewer Only

With variant="viewer" the component renders nothing but the dialog, so it can be opened from any trigger through the exposed open method.

<script setup>
function createImage(label, color) {
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800"><rect width="1200" height="800" fill="${color}"/><text x="600" y="660" fill="#fff" font-family="sans-serif" font-size="160" font-weight="600" text-anchor="middle">${label}</text></svg>`

  return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
}

const images = [
  { src: createImage('1', '#2563eb'), alt: 'Blue cover' },
  { src: createImage('2', '#059669'), alt: 'Green cover' },
  { src: createImage('3', '#d97706'), alt: 'Amber cover' },
]
const preview = ref()

function open(index) {
  preview.value.open(index)
}
</script>

<template>
  <div class="gap-2 flex flex-wrap">
    <PButton v-for="(image, index) in images" :key="index" @click="open(index)">
      Image {{ index + 1 }}
    </PButton>

    <PImagePreview ref="preview" variant="viewer" :data="images" />
  </div>
</template>

Props

NameTypeDefaultDescription
data(string | ImagePreviewItem)[]-Images rendered as thumbnails; an item’s src is a URL or a function returning a promise of one
variant'default' | 'viewer''default'Render the thumbnails together with the dialog, or the dialog alone
indexnumber0Image the preview opens at
z-indexnumber-Stacking order of the preview, applied through the --image-preview-index variable
close-on-press-escapebooleantrueClose the preview when Esc is pressed
close-on-click-overlaybooleantrueClose the preview when the area around the image is clicked

Events

NameTypeDescription
index-change(index: number) => voidEmitted when the preview switches to another image.
request(index: number, item: ImagePreviewItem) => voidEmitted when the viewer opens or switches to an image, so its source can be supplied on demand.

Slots

NameDescription
itemThumbnail content. Slot props: image, index, src, status.
loadingViewer placeholder while the current source is loading. Slot props: index, item.
errorViewer placeholder when the current source fails to load. Slot props: index, item.

Methods

NameTypeDescription
open(index?: number) => voidOpen the preview at an optional image index.
close() => voidClose the preview.
zoomIn() => voidIncrease the zoom by one step.
zoomOut() => voidDecrease the zoom by one step.
rotate() => voidRotate the image by 90 degrees clockwise.
reset() => voidReset zoom, rotation and position.
download() => Promise<void>Download the current image to the local machine.

Source

Source