Reference
TypeScript types
All public types are exported from the package root:
import type {
ImageStatus, // 'idle' | 'loading' | 'loaded' | 'error'
SrcSet, // { avif?: string; webp?: string; fallback: string }
ResponsiveSrc, // Record<string, string | SrcSet> — breakpoint-key → URL, or a format set for that breakpoint
BreakpointMap, // Record<string, string> — breakpoint-key → CSS media query
VImageKitOptions, // { breakpoints?: BreakpointMap }
LazyImgOptions, // { src, placeholder?, rootMargin?, threshold?, onLoad?, onError? }
ObjectFit, // 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'
FocalPoint, // { x: number; y: number } — fractions 0–1
Densities, // number[] | Record<number, string> — density descriptors
ImageMeta, // CLI manifest entry / `?vik` import shape, for the `image` prop
Layout, // 'fixed' | 'responsive' | 'fill' — the `layout` prop
} from 'vue-image-kit'ImageStatus
type ImageStatus = 'idle' | 'loading' | 'loaded' | 'error'The state machine transitions in order: idle → loading → loaded or idle → loading → error.
SrcSet
interface SrcSet {
avif?: string // Optional AVIF source URL
webp?: string // Optional WebP source URL
fallback: string // Required — used as the <img src> fallback
}LazyImgOptions
interface LazyImgOptions {
src: string
placeholder?: string
rootMargin?: string
threshold?: number
onLoad?: () => void
onError?: (e: Event) => void
}The v-lazy-img directive accepts either a plain string (the src) or a LazyImgOptions object.
Working with typed options in v-lazy-img
import type { LazyImgOptions } from 'vue-image-kit'
const bgOptions: LazyImgOptions = {
src: '/hero.jpg',
placeholder: 'data:image/jpeg;base64,...',
rootMargin: '100px',
onLoad: () => analytics.track('hero_loaded'),
}<div v-lazy-img="bgOptions" class="hero" />SSR compatibility
| Scenario | Behaviour |
|---|---|
Server render — <VImage> | Renders <img loading="lazy"> with src and alt; no IO, no canvas |
| Server render — aspect-ratio | A <div> with aspect-ratio: width/height is rendered when width and height are provided |
| Blurhash on server | Canvas code is inside onMounted — not executed; a blank container is rendered instead |
IntersectionObserver on server | Not used; the server renders a plain <img> |
| Hydration | After mount, onMounted sets up IO (if lazy: true) or immediately starts loading (if lazy: false) |
v-lazy-img on server | Directive hooks (mounted, unmounted) are not called during SSR — no IO is created |
useLazyLoad on server | Returns { isIntersecting: true } immediately — the caller proceeds as if in-viewport |
Nuxt usage:
No special configuration is required. The component renders correctly in both SSR and client modes. If you need to know whether the client has mounted, use Vue's onMounted:
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const mounted = ref(false)
onMounted(() => {
mounted.value = true
})
</script>
<template>
<VImage v-if="mounted" src="/photo.jpg" alt="Photo" blurhash="..." />
<div v-else style="aspect-ratio: 16/9; background: #e5e7eb;" />
</template>Architecture
VImage.vue
│ props: src, alt, width, height,
│ blurhash, thumbhash, placeholder,
│ widths, sizes, sources, breakpoints,
│ lazy, rootMargin, threshold, fit,
│ maxRetries, retryDelay,
│ fetchpriority, decoding
│
├──▶ useImage(options)
│ │
│ ├── useLazyLoad({ rootMargin, threshold })
│ │ IntersectionObserver (SSR-safe)
│ │ isIntersecting: Ref<boolean>
│ │ observe(elRef) → starts watching
│ │
│ ├── State machine
│ │ idle → loading → loaded
│ │ → error (retryCount >= maxRetries)
│ │ → idle → loading (retry, exponential backoff)
│ │ lazy=true → watch(isIntersecting) → loading
│ │ lazy=false → onMounted → loading
│ │
│ └── imgAttrs: ComputedRef
│ src = fallback URL
│ srcset = generateSrcset(src, widths)
│ sizes = generateSizes(sizes)
│ style = { objectFit: fit }
│
├──▶ useBlurhash({ blurhash, width, height })
│ onMounted → decodeBlurhash(hash, width, height)
│ → new ImageData(pixels, width, height)
│ → ctx.putImageData(imageData, 0, 0)
│ canvasRef: Ref<HTMLCanvasElement | null>
│ SSR: returns null ref (canvas code never runs)
│
├──▶ useBreakpoints(breakpoints?)
│ Merges local breakpoints prop with global plugin breakpoints
│ resolveMediaSources(sources) → sorted [{ media, src }]
│
├──▶ effectivePlaceholder: ComputedRef<string | undefined>
│ placeholder prop → used as-is (LQIP base64)
│ thumbhash prop → decodeThumbHash(hash) → PNG data URL
│ neither → undefined (no blur-up placeholder)
│
├──▶ Template structure (client)
│ <span wrapper :style="{ aspectRatio, position: relative }">
│ <canvas v-if="blurhash && width && height && !isError" />
│ ← BlurHash canvas placeholder
│ <img aria-hidden
│ v-if="effectivePlaceholder && !isError" />
│ ← LQIP / ThumbHash blur-up
│ <span v-if="isError"> ← error state
│ <slot name="error"><svg .../></slot>
│ </span>
│ <picture v-if="shouldRenderImg && !isError && needsPicture">
│ ← format/art-direction sources
│ <source v-for media/srcset /> ← responsive art direction
│ <source type="image/avif" />
│ <source type="image/webp" />
│ <img v-bind="imgAttrs" :decoding :fetchpriority @load @error />
│ </picture>
│ <img v-if="shouldRenderImg && !isError && !needsPicture"
│ v-bind="imgAttrs" :decoding :fetchpriority @load @error />
│ ← simple img (no picture)
│ <span v-if="isIdle && !blurhash && !effectivePlaceholder" />
│ ← grey background (no placeholder)
│ </span>
│
└──▶ Template structure (SSR)
<img :src :alt :width :height :decoding :fetchpriority
:loading="lazy ? 'lazy' : 'eager'" />
vLazyImg (Directive)
│ mounted(el, binding)
│ resolveOptions(binding) → { src, placeholder, rootMargin, ... }
│ createObserver(el, options)
│ IntersectionObserver → on intersect:
│ if placeholder: el.style.backgroundImage = url(placeholder)
│ new Image()
│ onload → el.style.backgroundImage = url(src); onLoad()
│ onerror → onError(e)
│ updated → disconnect old observer, create new one
│ unmounted → observer.disconnect()
Utils (pure functions, zero Vue deps)
│ blurhash-decode.ts
│ decodeBlurhash(hash, width, height) → Uint8ClampedArray ← RGBA pixels
│
│ thumbhash-decode.ts
│ decodeThumbHash(hash: string | Uint8Array) → string ← PNG data URL
│
└── srcset.ts
generateSrcset(src, widths) → string
generateSizes(sizes?) → string
buildSizes(map, breakpoints) → string
generatePreloadLink(href, options) → stringBundle size & peer dependencies
| Entry point | Raw | Gzip | Peer deps |
|---|---|---|---|
vue-image-kit ESM | 42.1 kB | 13.0 kB | vue ^3.0 |
vue-image-kit CJS | 31.9 kB | 11.3 kB | vue ^3.0 |
vue-image-kit/cdn ESM | 10.8 kB | 2.4 kB | — |
Measured from the actual build output (npm run build), not maintained by hand — CI fails if this drifts past the thresholds in .github/workflows/ci.yml.
Ships as tree-shakeable ESM (vue-image-kit.js) and CommonJS (vue-image-kit.cjs). "sideEffects": false in package.json — unused exports are eliminated by the bundler. If you only import vLazyImg or a single composable, the bundler will exclude everything else (VImage, blurhash decoder, etc.).
Tree-shaking example — use only the directive:
// Only vLazyImg and its IO logic is included in the bundle.
// VImage, useBlurhash, decodeBlurhash are not imported → not bundled.
import { vLazyImg } from 'vue-image-kit'
app.directive('lazy-img', vLazyImg)License
MIT