Responsive Images
srcset + sizes
Pass widths to auto-generate the srcset attribute:
<VImage
src="/photo.jpg"
alt="Photo"
:widths="[400, 800, 1200]"
sizes="(max-width: 768px) 100vw, 50vw"
/>Renders:
<img
src="/photo.jpg"
srcset="/photo.jpg 400w, /photo.jpg 800w, /photo.jpg 1200w"
sizes="(max-width: 768px) 100vw, 50vw"
alt="Photo"
/>When widths is not provided, srcset is not added — the plain src is used. When widths is provided but sizes is not, sizes defaults to "100vw".
Density descriptors (1x / 2x / 3x)
For fixed-size images — icons, avatars, logos — use densities instead of widths. The browser picks the candidate matching the device pixel ratio; no sizes is needed. densities takes precedence over widths (the two descriptor types can't be mixed in one srcset).
:densities accepts two forms:
<!-- 1. Per-density URL map — distinct files (recommended for static assets). -->
<VImage
src="/avatar.png"
alt="Avatar"
:width="48"
:height="48"
:densities="{ 1: '/avatar.png', 2: '/avatar@2x.png', 3: '/avatar@3x.png' }"
/>
<!-- → srcset="/avatar.png 1x, /avatar@2x.png 2x, /avatar@3x.png 3x" -->
<!-- 2. Density list — reuses the single `src` for every density. Only useful
when the URL itself is resolution-aware (a CDN/DPR endpoint). -->
<VImage src="https://cdn.example.com/avatar?dpr=auto" alt="Avatar" :densities="[1, 2, 3]" />
<!-- → srcset="…?dpr=auto 1x, …?dpr=auto 2x, …?dpr=auto 3x" -->Using the utilities directly:
import { generateSrcset, generateSizes, generateDensitySrcset } from '@macrulez/vue-image-kit'
generateSrcset('/photo.jpg', [400, 800, 1200])
// → '/photo.jpg 400w, /photo.jpg 800w, /photo.jpg 1200w'
generateSizes('(max-width: 768px) 100vw, 50vw')
// → '(max-width: 768px) 100vw, 50vw'
generateSizes()
// → '100vw'
generateDensitySrcset('/logo.png', [1, 2, 3])
// → '/logo.png 1x, /logo.png 2x, /logo.png 3x'
// Distinct files per density via a URL map:
generateDensitySrcset({ 1: '/a.png', 2: '/a@2x.png' }, [1, 2])
// → '/a.png 1x, /a@2x.png 2x'WebP / AVIF source switching
When src is an object instead of a string, <VImage> renders a <picture> element with the appropriate <source> elements:
<VImage
:src="{
avif: '/photo.avif',
webp: '/photo.webp',
fallback: '/photo.jpg',
}"
alt="Photo"
:width="1200"
:height="800"
/>Renders:
<picture>
<source srcset="/photo.avif" type="image/avif" />
<source srcset="/photo.webp" type="image/webp" />
<img src="/photo.jpg" alt="Photo" width="1200" height="800" />
</picture>The browser picks the first format it supports. If only webp is provided, only one <source> is added. fallback is always required.
SrcSet object
interface SrcSet {
avif?: string // URL of the AVIF version
webp?: string // URL of the WebP version
fallback: string // Required — the original format (JPEG/PNG)
}Responsive sources — art direction
Use this when you need to serve a fundamentally different image (different crop, different composition) based on screen size. Implemented via named breakpoints — the browser picks the first matching <source media="...">.
Global breakpoints (set once when installing the plugin)
// main.ts
app.use(VImageKitPlugin, {
breakpoints: {
sm: '(max-width: 640px)',
md: '(max-width: 1024px)',
lg: '(min-width: 1025px)',
},
})Using in components — keys only
<VImage
src="/hero-desktop.jpg"
alt="Hero"
:sources="{
sm: '/hero-mobile.jpg',
md: '/hero-tablet.jpg',
}"
/>Generates:
<picture>
<source media="(max-width: 640px)" srcset="/hero-mobile.jpg" />
<source media="(max-width: 1024px)" srcset="/hero-tablet.jpg" />
<img src="/hero-desktop.jpg" alt="Hero" />
</picture><source> order is set automatically in ascending max-width order — required by <picture>, which picks the first matching source.
Per-component breakpoints
Merged with global breakpoints. Local keys take priority on conflict:
<VImage
src="/product-desktop.jpg"
alt="Product"
:breakpoints="{
xs: '(max-width: 375px)',
wide: '(min-width: 1600px)',
}"
:sources="{
xs: '/product-xs.jpg',
sm: '/product-mobile.jpg',
md: '/product-tablet.jpg',
wide: '/product-wide.jpg',
}"
/>The resulting <picture> contains <source> elements for xs, sm, md (from merged breakpoints), and wide — sorted automatically.
Combining with AVIF/WebP
Responsive sources (sources) and format sources (src as object) are independent and rendered together:
<VImage
:src="{ avif: '/hero.avif', webp: '/hero.webp', fallback: '/hero.jpg' }"
:sources="{ sm: '/hero-mobile.jpg' }"
alt="Hero"
/><picture>
<source media="(max-width: 640px)" srcset="/hero-mobile.jpg" />
<source srcset="/hero.avif" type="image/avif" />
<source srcset="/hero.webp" type="image/webp" />
<img src="/hero.jpg" alt="Hero" />
</picture>That covers "one crop set + one format set, independent of each other." For a different crop and different formats per breakpoint — e.g. a portrait AVIF/WebP crop on mobile, a landscape AVIF/WebP crop on desktop — a breakpoint's value in sources can itself be a { avif?, webp?, fallback } object instead of a plain URL:
<VImage
alt="Hero"
:sources="{
sm: { avif: '/hero-mobile.avif', webp: '/hero-mobile.webp', fallback: '/hero-mobile.jpg' },
md: { webp: '/hero-tablet.webp', fallback: '/hero-tablet.jpg' },
}"
src="/hero-desktop.jpg"
/><picture>
<source media="(max-width: 640px)" srcset="/hero-mobile.avif" type="image/avif" />
<source media="(max-width: 640px)" srcset="/hero-mobile.webp" type="image/webp" />
<source media="(max-width: 640px)" srcset="/hero-mobile.jpg" />
<source media="(max-width: 1024px)" srcset="/hero-tablet.webp" type="image/webp" />
<source media="(max-width: 1024px)" srcset="/hero-tablet.jpg" />
<img src="/hero-desktop.jpg" alt="Hero" />
</picture>Plain-URL and format-object breakpoints can be mixed freely in the same sources object. avif/webp are both optional per breakpoint — only the formats you actually have are emitted.
Per-breakpoint sizes — no layout shift on switch
A different crop for a different breakpoint usually also means a different aspect ratio — a tall portrait crop on desktop, a wide landscape crop on tablet. Without telling <VImage> about that, it can only reserve space using the root width/height, so the placeholder (and the blurhash preview, if you're using one) show the wrong proportions on any breakpoint whose crop doesn't match the root image — and the box visibly jumps once the real photo loads.
Give a sources entry its own width/height to fix that:
<VImage
src="/desktop-portrait.jpg"
:width="720"
:height="1237"
:sources="{
tablet: { src: '/tablet-landscape.jpg', width: 1400, height: 700 },
}"
:breakpoints="{ tablet: '(max-width: 1024px)' }"
alt="Hero"
/>With width/height on the tablet entry, <VImage> tracks which breakpoint's media query is currently active (reactively — it updates if you resize past the breakpoint) and reserves the placeholder box, and the blurhash decode, at that breakpoint's own aspect ratio instead of always falling back to the root image's. The matching <source> also renders with real width/height attributes, so once the browser picks it, the loaded image keeps that same reserved space — no jump between the placeholder and the final photo, on any breakpoint.
width and height are both-or-nothing per entry: give only one and it's dropped (with a console warning in development), rather than risk a distorted box.
A sources entry also accepts an ImageMeta object directly — the shape produced by the CLI/Vite-plugin manifest or a ?vik build-time import — so per-breakpoint metadata generated at build time can be passed straight through without picking it apart first:
<script setup>
import tabletMeta from '/tablet-landscape.jpg?vik'
</script>
<template>
<VImage
src="/desktop-portrait.jpg"
:width="720"
:height="1237"
:sources="{ tablet: tabletMeta }"
:breakpoints="{ tablet: '(max-width: 1024px)' }"
alt="Hero"
/>
</template>Entries without width/height keep working exactly as before — this is purely additive.
Per-breakpoint placeholders
A placeholder has the same problem as the box. A blurhash of the tall root photo, stretched into a wide tablet box, puts the colors in the wrong places. A sources entry can carry its own placeholder next to src — hazehash, blurhash, thumbhash, placeholder (an LQIP data URL) or placeholderColor:
<VImage
src="/desktop-portrait.jpg"
:width="720"
:height="1237"
blurhash="LEHV6nWB2yk8pyo0adR*.7kCMdnj"
:sources="{
tablet: {
src: '/tablet-landscape.jpg',
width: 1400,
height: 700,
blurhash: 'LKO2?U%2Tw=w]~RBVZRi};RPxuwH',
},
}"
:breakpoints="{ tablet: '(max-width: 1024px)' }"
alt="Hero"
/>While an entry's media query matches, <VImage> shows that entry's placeholder, at that entry's proportions, instead of the root image's.
- The matching entry replaces the root's whole placeholder set — it isn't merged field by field. A root
placeholderColordoesn't win over a source'sblurhash, and a rootblurhashdoesn't show behind a source that only has aplaceholderColor. - An entry without a placeholder of its own, and a breakpoint where no entry matches, use the root's.
placeholderModeapplies to every breakpoint. - No media query is evaluated on the server, so the server-rendered markup is always the root image's.
- An
ImageMetaentry (a?vikimport, as in the example above) brings its own placeholder along. - An entry that is a plain
{ avif, webp, fallback }object can't carry a placeholder; wrap it as{ src: { avif, webp, fallback }, blurhash }.
With a placeholders manifest registered, the entry for the source's src fills in the same fields — hazehash, blurhash, thumbhash, color, width and height — wherever the source doesn't set them itself. npx vue-image-kit placeholders can generate both forms: see Art-direction sources.
BreakpointMap
type BreakpointMap = Record<string, string>
// key — arbitrary name, value — CSS media queryBreakpoint priority
- Local
breakpointsprop on the component — high priority, overrides global keys on conflict. - Global
breakpointsfromVImageKitPlugin— base, available in all components.
This merge, along with sorting sources into ordered <source> entries, is done by useBreakpoints() — exposed on its own for headless art-direction setups.
Sizes attribute helper
buildSizes() builds a sizes attribute string from a breakpoint-keyed object — works with the plugin's named breakpoints.
import { buildSizes } from '@macrulez/vue-image-kit'
const breakpoints = { sm: '(max-width: 640px)', md: '(max-width: 1024px)' }
buildSizes({ sm: '100vw', md: '50vw', default: '33vw' }, breakpoints)
// → '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw'Preload links
generatePreloadLink() generates a <link rel="preload"> HTML string for critical above-the-fold images. Use in Nuxt's useHead or inject into SSR <head> to improve LCP.
import { generatePreloadLink, generateSrcset } from '@macrulez/vue-image-kit'
const srcset = generateSrcset('/hero.jpg', [400, 800, 1200])
const link = generatePreloadLink('/hero.jpg', {
srcset,
sizes: '100vw',
})
// → '<link rel="preload" as="image" href="/hero.jpg" imagesrcset="..." imagesizes="100vw">'In Nuxt:
<script setup lang="ts">
import { generatePreloadLink } from '@macrulez/vue-image-kit'
useHead({
link: [{ innerHTML: generatePreloadLink('/hero.jpg', { sizes: '100vw' }) }],
})
</script>Layout without a wrapper
<VImage> doesn't render inside a wrapper element. At any given moment it's exactly one box: a placeholder before loading starts, the real <img> once it does — the second replaces the first outright, it doesn't wrap it. When the image needs a <picture> (a src object, sources), the <picture> is only the container of the <source> elements: it is rendered with display: contents, so it makes no box of its own and the <img> inside it lays out as if it stood alone. Every layout style (below) applies directly to that one box.
The practical upshot: class, style, data-*, event listeners and any other attribute passed to <VImage> always land on the element that shows the image — the placeholder, the error box or the <img> (also inside a <picture>) — never on an intermediate wrapper. A rule such as .card-image { width: 100%; height: auto; border-radius: 10px } styles the picture itself, with or without sources, and needs no selector reaching into the component. The same holds for the server-rendered <img>, and the class is copied onto the image inside the <noscript> fallback of an ssrPlaceholder image.
Layout presets
The layout prop switches how that one rendered element is sized. Leaving it unset behaves identically to 'responsive' below in every respect, including its auto-generated sizes — there's no need to set layout="responsive" explicitly just to get that heuristic, leaving layout unset already gets it.
fixed — an exact width×height box, no responsive scaling (like a plain <img width height>):
<VImage src="/icon.jpg" alt="Icon" :width="64" :height="64" layout="fixed" />responsive — fills the container width, aspect-ratio preserved from width/height, and auto-generates a sizes value from width when sizes isn't given explicitly ((min-width: {width}px) {width}px, 100vw — "as wide as its intrinsic size, otherwise the full viewport width"). This is also exactly what you get by leaving layout unset:
<VImage
src="/photo.jpg"
alt="Photo"
:width="800"
:height="600"
:widths="[400, 800, 1200]"
layout="responsive"
/>Without width/height, there's no intrinsic ratio to preserve, so <VImage> doesn't force any sizing at all — the element renders at its own natural size, fully governed by whatever CSS you give it (e.g. a max-height on your own class).
fill — absolutely fills a positioned parent (position: absolute; inset: 0, applied directly to the element itself); the parent needs position: relative (or similar) itself. width/height become optional — common for hero banners or cards where the container defines the box:
<div style="position: relative; aspect-ratio: 16 / 9;">
<VImage src="/hero.jpg" alt="Hero" layout="fill" fit="cover" priority />
</div>