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. Internally driven by useLazyLoad(), exposed on its own for headless usage.

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 idle placeholder element (see Layout without a wrapper — there's no persistent wrapper, just this one element until loading starts)
  2. When the element enters the viewport (accounting for rootMargin) — the image src is set and loading begins (status: 'loading'), and the placeholder element is swapped for the real <img>/<picture>
  3. When the image loads — status transitions to 'loaded'; the placeholder background is covered by the now-painted photo, instantly by default (the opt-in fadeIn prop softens the box's appearance instead)
  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" />
  • fetchpriority — 'high' | 'low' | 'auto', optional. Browser fetch priority hint.
  • decoding — 'async' | 'sync' | 'auto' · default: '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" />
  • maxRetries — number · default: 0. Max retry attempts.
  • retryDelay — number · default: 1000. Initial 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 '@macrulez/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 '@macrulez/vue-image-kit'

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