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:
<VImage src="/missing.jpg" alt="Missing" :width="400" :height="300" />
<!-- Shows: grey rectangle + SVG icon -->Custom fallback via slot:
<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:
<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.
<!-- 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
- On mount — an
IntersectionObserveris 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) - When the element enters the viewport (accounting for
rootMargin) — the imagesrcis set and loading begins (status: 'loading'), and the placeholder element is swapped for the real<img>/<picture> - When the image loads —
statustransitions to'loaded'; the placeholder background is covered by the now-painted photo, instantly by default (the opt-infadeInprop softens the box's appearance instead) - 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:
<!-- 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:
<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.
import { useNetworkAware } from '@macrulez/vue-image-kit'
const { saveData, effectiveType } = useNetworkAware()Two places already use it:
useImagePreloader()silently skipspreload()calls whilesaveDatais on — preloading trades bandwidth for a smoother later transition, the wrong trade once the user asked to save data.VImage'srespectSaveDataprop (opt-in, defaultfalse): whilesaveDatais on, it neutralizespriority(the image stays lazy instead of being forced eager/high-priority) and downgradessrcto the smallest URL it can actually find one for — the lowest key in adensitiesmap, or the smallestwcandidate inimage.srcset(a manifest/?vikvalue). Plainwidthshas no distinct URL to downgrade to (seegenerateSrcset— the browser negotiates via thewdescriptor against one URL, not a URL per width) so it's a no-op there.
<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:
import { isSaveDataEnabled } from '@macrulez/vue-image-kit'
if (!isSaveDataEnabled()) {
await preload(nextSlideUrls)
}