Skip to content

Loading & Error Handling

Error state & fallback slot

Default fallback — if no #error slot is provided, a grey rectangle with a broken-image SVG icon is shown:

vue
<VImage src="/missing.jpg" alt="Missing" :width="400" :height="300" />
<!-- Shows: grey rectangle + SVG icon -->

Custom fallback via slot:

vue
<VImage src="/missing.jpg" alt="Missing" :width="400" :height="300">
  <template #error>
    <div class="error-placeholder">
      <img src="/no-image.svg" alt="" />
      <p>Image is currently unavailable</p>
    </div>
  </template>
</VImage>

Handling errors in JavaScript:

vue
<script setup lang="ts">
function handleError(e: Event) {
  console.error('Image failed to load:', e)
  // Report to Sentry, switch to a fallback URL, etc.
}
</script>

<template>
  <VImage src="/photo.jpg" alt="Photo" @error="handleError" />
</template>

Lazy loading

<VImage> uses IntersectionObserver for lazy loading — not the native loading="lazy" attribute — for full control over when loading starts.

vue
<!-- Default: loads when the image is 200px from the viewport -->
<VImage src="/photo.jpg" alt="Photo" />

<!-- Custom rootMargin — start loading 500px before the viewport -->
<VImage src="/photo.jpg" alt="Photo" root-margin="500px" />

<!-- Load when 50% of the image is visible -->
<VImage src="/photo.jpg" alt="Photo" :threshold="0.5" />

<!-- Disable lazy loading — load immediately (above the fold) -->
<VImage src="/photo.jpg" alt="Photo" :lazy="false" />

How it works

  1. On mount — an IntersectionObserver is created and begins watching the wrapper element
  2. When the element enters the viewport (accounting for rootMargin) — the image src is set and loading begins (status: 'loading')
  3. When the image loads — status transitions to 'loaded'; the placeholder fades out
  4. The observer disconnects after the first intersection — no unnecessary callbacks

SSR behaviour

On the server, IntersectionObserver is unavailable. <VImage> renders a plain <img loading="lazy"> without any JavaScript-driven state. After hydration, onMounted sets up the IO as normal.

fetchpriority & decoding

Control browser prioritization and decoding strategy:

vue
<!-- Hero image: load first, decode async -->
<VImage src="/hero.jpg" alt="Hero" :lazy="false" fetchpriority="high" decoding="async" />

<!-- Below-the-fold: deprioritize -->
<VImage src="/footer-banner.jpg" alt="Banner" fetchpriority="low" />
PropTypeDefaultDescription
fetchpriority'high' | 'low' | 'auto'Browser fetch priority hint
decoding'async' | 'sync' | 'auto''async'Image decoding mode

Error retry

Automatically retry failed image loads with exponential backoff:

vue
<VImage src="/flaky-image.jpg" alt="Photo" :max-retries="3" :retry-delay="500" />
PropTypeDefaultDescription
maxRetriesnumber0Max retry attempts
retryDelaynumber1000Initial delay in ms (doubles each retry)

Network-aware loading

useNetworkAware() wraps the browser's Network Information API — saveData (the user opted into data savings) and effectiveType ('slow-2g' | '2g' | '3g' | '4g'). SSR-safe (saveData starts false on the server) and reactive to the connection's change event. Support is Chromium-only today (Firefox/Safari don't implement the API) — saveData just stays false there, so nothing breaks, it simply can't help.

ts
import { useNetworkAware } from 'vue-image-kit'

const { saveData, effectiveType } = useNetworkAware()

Two places already use it:

  • useImagePreloader() silently skips preload() calls while saveData is on — preloading trades bandwidth for a smoother later transition, the wrong trade once the user asked to save data.
  • VImage's respectSaveData prop (opt-in, default false): while saveData is on, it neutralizes priority (the image stays lazy instead of being forced eager/high-priority) and downgrades src to the smallest URL it can actually find one for — the lowest key in a densities map, or the smallest w candidate in image.srcset (a manifest/?vik value). Plain widths has no distinct URL to downgrade to (see generateSrcset — the browser negotiates via the w descriptor against one URL, not a URL per width) so it's a no-op there.
vue
<VImage
  src="/photo.jpg"
  alt="Photo"
  priority
  respect-save-data
  :densities="{ 1: '/photo.jpg', 2: '/photo@2x.jpg' }"
/>

For a direct check outside a component (e.g. before kicking off a batch preload yourself), isSaveDataEnabled() is the same check without the reactive wrapper:

ts
import { isSaveDataEnabled } from 'vue-image-kit'

if (!isSaveDataEnabled()) {
  await preload(nextSlideUrls)
}

useImagePreloader

Preload a batch of images before navigation — useful for galleries and carousels.

vue
<script setup lang="ts">
import { useImagePreloader } from 'vue-image-kit'

const { preload, progress, isComplete, errors } = useImagePreloader()

async function goToNextSlide() {
  await preload(['/slide-2.jpg', '/slide-3.jpg'])
  // All images are cached — transition is instant
  currentSlide.value++
}
</script>

<template>
  <div v-if="!isComplete">Loading {{ progress }}%…</div>
</template>