Appearance
@vibe-labs/design-vue-images
Vue 3 image components for the Vibe Design System. LQIP blur-up crossfade, lazy loading, global load queue with concurrency control, and responsive <picture> support.
Installation
ts
import { VibeImage, VibeBgImage, VibePicture } from "@vibe-labs/design-vue-images";Requires the types from @vibe-labs/design-components-images. No CSS import needed (styles are inline/scoped).
Components
VibeImage
Standard image with LQIP blur-up crossfade, lazy loading, and auto aspect-ratio.
Usage
vue
<!-- Basic -->
<VibeImage src="/photos/hero.jpg" alt="Hero shot" />
<!-- LQIP blur-up -->
<VibeImage src="/photos/hero-full.jpg" thumb-src="/photos/hero-thumb.jpg" alt="Hero shot" />
<!-- Eager loading (above the fold) -->
<VibeImage src="/hero.jpg" alt="Hero" loading="eager" fetchpriority="high" />
<!-- Fixed dimensions -->
<VibeImage src="/avatar.jpg" alt="User" :width="200" :height="200" />
<!-- Fit and position -->
<VibeImage src="/photo.jpg" alt="Photo" fit="contain" position="top-center" />
<!-- Border radius (token shorthand) -->
<VibeImage src="/photo.jpg" alt="Photo" radius="lg" />
<VibeImage src="/photo.jpg" alt="Photo" radius="full" />
<!-- No crossfade -->
<VibeImage src="/photo.jpg" alt="Photo" :crossfade="false" />
<!-- Custom duration -->
<VibeImage src="/photo.jpg" alt="Photo" :duration="500" />
<!-- Srcset for responsive -->
<VibeImage
src="/photo-800.jpg"
srcset="/photo-400.jpg 400w, /photo-800.jpg 800w, /photo-1200.jpg 1200w"
sizes="(max-width: 600px) 400px, 800px"
alt="Responsive"
/>
<!-- Polymorphic (render as different element) -->
<VibeImage :as="NuxtImg" src="/photo.jpg" alt="Via NuxtImg" />
<!-- Placeholder + error slots -->
<VibeImage src="/photo.jpg" alt="Photo">
<template #placeholder><MySkeleton /></template>
<template #error><MyErrorState /></template>
</VibeImage>Props
| Prop | Type | Default | Description |
|---|---|---|---|
src | string | — | Image URL |
thumbSrc | string | — | Low-quality placeholder URL (blurred while full loads) |
alt | string | "" | Alt text |
srcset | string | — | Responsive srcset |
sizes | string | — | Responsive sizes |
width | string | number | — | Width |
height | string | number | — | Height |
fit | ImageFit | "cover" | object-fit |
position | ImagePosition | — | object-position |
loading | ImageLoadingStrategy | "lazy" | lazy (IntersectionObserver) or eager |
fetchpriority | FetchPriority | — | Browser fetch priority |
decoding | ImageDecoding | "async" | Decoding hint |
crossfade | boolean | true | Enable crossfade transition |
duration | number | 300 | Crossfade duration (ms) |
radius | string | — | Border radius (token name or CSS value) |
aspect | string | auto | Aspect ratio (auto-computed from intrinsic dimensions) |
draggable | boolean | false | HTML draggable |
as | string | Component | "img" | Polymorphic render element |
Events
| Event | Payload | Description |
|---|---|---|
load | (src: string, dimensions: ImageDimensions) | Full image loaded |
error | (src: string | undefined) | Image failed |
statusChange | (status: ImageStatus, src: string | null) | Status transition |
Slots
| Slot | Description |
|---|---|
placeholder | Custom loading placeholder |
error | Custom error state |
How LQIP Works
thumbSrcloads immediately (tiny, fast)- Displays blurred (
blur(20px)+scale(1.1)to hide edges) srcloads in background via the image queue- Once loaded, crossfades from blurred thumb to sharp full image
- Auto aspect-ratio computed from intrinsic dimensions to prevent CLS
VibeBgImage
Background image container with LQIP, overlay support, and slot for content.
vue
<!-- Basic -->
<VibeBgImage src="/hero.jpg" aria-label="Hero background">
<h1>Welcome</h1>
</VibeBgImage>
<!-- With overlay -->
<VibeBgImage src="/hero.jpg" :overlay="true">
<h1 class="text-white">Darkened overlay</h1>
</VibeBgImage>
<!-- Custom overlay gradient -->
<VibeBgImage src="/hero.jpg" overlay="linear-gradient(to bottom, transparent, rgba(0,0,0,0.8))">
<h1>Gradient overlay</h1>
</VibeBgImage>
<!-- Fixed (parallax-style) -->
<VibeBgImage src="/hero.jpg" fixed aria-label="Parallax background" />
<!-- LQIP -->
<VibeBgImage src="/hero-full.jpg" thumb-src="/hero-thumb.jpg" aria-label="Hero" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
src | string | — | Image URL |
thumbSrc | string | — | LQIP placeholder URL |
fit | BgFit | "cover" | background-size |
position | BgPosition | "center" | background-position |
loading | ImageLoadingStrategy | "lazy" | Loading strategy |
fixed | boolean | false | background-attachment: fixed |
overlay | string | boolean | — | Overlay gradient (true = 40% black) |
width / height | string | number | — | Container dimensions |
radius | string | — | Border radius |
aspect | string | — | Aspect ratio |
ariaLabel | string | — | Accessible label (sets role="img") |
VibePicture
Responsive <picture> element with multiple sources, LQIP, and crossfade.
vue
<VibePicture
src="/photo.jpg"
thumb-src="/photo-thumb.jpg"
alt="Responsive photo"
:sources="[
{ media: '(min-width: 1200px)', srcset: '/photo-xl.webp', type: 'image/webp' },
{ media: '(min-width: 800px)', srcset: '/photo-lg.webp', type: 'image/webp' },
{ srcset: '/photo-sm.webp', type: 'image/webp' },
]"
/>Smart rendering: thumb renders as plain <img> (blurred) to avoid <source> elements triggering premature full-res loads. Full image renders as <picture> with all sources.
Props
Inherits all VibeImage props except as, srcset, sizes, plus:
| Prop | Type | Default | Description |
|---|---|---|---|
sources | PictureSource[] | [] | Responsive sources (media, srcset, type, sizes) |
Composables
useImageLoad(src)
Reactive image loader with generation tracking (prevents stale loads). Returns { status, currentSrc, dimensions }.
ts
import { useImageLoad } from "@vibe-labs/design-vue-images";
const src = ref("/photo.jpg");
const { status, currentSrc, dimensions } = useImageLoad(src);
// status: "idle" | "loading" | "loaded" | "error"useLazyLoad(target, options?)
IntersectionObserver-based visibility detection for lazy loading.
ts
import { useLazyLoad } from "@vibe-labs/design-vue-images";
const el = ref<HTMLElement | null>(null);
const { isVisible } = useLazyLoad(el, {
rootMargin: "200px", // start loading 200px before visible
threshold: 0,
enabled: () => props.loading === "lazy",
});useImageQueue() / imageQueue
Global singleton queue with configurable concurrency (default: 3 concurrent loads).
ts
import { useImageQueue } from "@vibe-labs/design-vue-images";
const { queue, clear, setConcurrency } = useImageQueue();
setConcurrency(5); // allow more parallel loads on fast connections
clear(); // cancel pending loads (e.g. on route change)useRadius(radius)
Maps radius token shorthand to CSS values.
ts
import { useRadius } from "@vibe-labs/design-vue-images";
const radius = useRadius(toRef(props, "radius"));
// "lg" → "var(--radius-lg)"
// "full" → "var(--radius-full)"
// "8px" → "8px"
// "var(--x)" → "var(--x)"Image Statuses
| Status | Description |
|---|---|
idle | No src set |
loading | Image loading (or thumb loaded, full pending) |
loaded | Full image loaded |
error | Load failed |
Dependencies
| Package | Purpose |
|---|---|
@vibe-labs/design-components-images | Type definitions and constants |
Build
bash
npm run buildBuilt with Vite + vite-plugin-dts. Outputs ES module with TypeScript declarations.