Skip to content

@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

PropTypeDefaultDescription
srcstringImage URL
thumbSrcstringLow-quality placeholder URL (blurred while full loads)
altstring""Alt text
srcsetstringResponsive srcset
sizesstringResponsive sizes
widthstring | numberWidth
heightstring | numberHeight
fitImageFit"cover"object-fit
positionImagePositionobject-position
loadingImageLoadingStrategy"lazy"lazy (IntersectionObserver) or eager
fetchpriorityFetchPriorityBrowser fetch priority
decodingImageDecoding"async"Decoding hint
crossfadebooleantrueEnable crossfade transition
durationnumber300Crossfade duration (ms)
radiusstringBorder radius (token name or CSS value)
aspectstringautoAspect ratio (auto-computed from intrinsic dimensions)
draggablebooleanfalseHTML draggable
asstring | Component"img"Polymorphic render element

Events

EventPayloadDescription
load(src: string, dimensions: ImageDimensions)Full image loaded
error(src: string | undefined)Image failed
statusChange(status: ImageStatus, src: string | null)Status transition

Slots

SlotDescription
placeholderCustom loading placeholder
errorCustom error state

How LQIP Works

  1. thumbSrc loads immediately (tiny, fast)
  2. Displays blurred (blur(20px) + scale(1.1) to hide edges)
  3. src loads in background via the image queue
  4. Once loaded, crossfades from blurred thumb to sharp full image
  5. 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

PropTypeDefaultDescription
srcstringImage URL
thumbSrcstringLQIP placeholder URL
fitBgFit"cover"background-size
positionBgPosition"center"background-position
loadingImageLoadingStrategy"lazy"Loading strategy
fixedbooleanfalsebackground-attachment: fixed
overlaystring | booleanOverlay gradient (true = 40% black)
width / heightstring | numberContainer dimensions
radiusstringBorder radius
aspectstringAspect ratio
ariaLabelstringAccessible 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:

PropTypeDefaultDescription
sourcesPictureSource[][]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

StatusDescription
idleNo src set
loadingImage loading (or thumb loaded, full pending)
loadedFull image loaded
errorLoad failed

Dependencies

PackagePurpose
@vibe-labs/design-components-imagesType definitions and constants

Build

bash
npm run build

Built with Vite + vite-plugin-dts. Outputs ES module with TypeScript declarations.