Skip to main content
Vuetify0 is now in alpha!
Vuetify0 Logo
Theme
Mode
Palettes
Accessibility
Vuetify One
Sign in to Vuetify One

Access premium tools across the Vuetify ecosystem — Bin, Play, Studio, and more.

Not a subscriber? See what's included

Image

Headless image component with state-driven placeholder and error fallback. Tracks the loading lifecycle and exposes retry(), with optional IntersectionObserver-driven lazy loading.

Usage

Image.Root owns the loading state machine via useImage. Image.Img renders the image element and reports load and error events to the context. Image.Placeholder is shown while idle or loading; Image.Fallback is shown on error.

Random photo
<script setup lang="ts">
  import { Image } from '@vuetify/v0'
</script>

<template>
  <div class="flex items-center justify-center">
    <Image.Root
      class="w-100 h-75 bg-surface-tint rounded overflow-hidden relative"
      src="https://picsum.photos/seed/v0-image/400/300"
    >
      <Image.Img
        alt="Random photo"
        class="w-full h-full object-cover"
        height="300"
        loading="lazy"
        width="400"
      />

      <Image.Placeholder class="absolute inset-0 flex items-center justify-center">
        <div class="w-full h-full bg-surface-tint animate-pulse" />
      </Image.Placeholder>

      <Image.Fallback class="absolute inset-0 flex items-center justify-center bg-error-container text-on-error-container text-sm">
        Image failed to load
      </Image.Fallback>
    </Image.Root>
  </div>
</template>

Anatomy

Anatomy
<script setup lang="ts">
  import { Image } from '@vuetify/v0'
</script>

<template>
  <Image.Root>
    <Image.Img />

    <Image.Placeholder />

    <Image.Fallback />
  </Image.Root>
</template>
Info

src is the only native image attribute that doesn’t live on Image.Img because it drives the load state machine — the idle → loading → loaded | error lifecycle can’t function without knowing the URL to track, so Image.Root owns it. Every other image attribute (alt, width, height, srcset, sizes, crossorigin, referrerpolicy, decoding, loading, fetchpriority) is a pure rendering concern and lives on Image.Img, where it describes the element that actually renders it.

Architecture

Image.Root calls useImage to manage status, and optionally wires up useIntersectionObserver when the lazy prop is set. Children consume the root context via useImageRoot.

Image lifecycle

Use controls to zoom and pan. Click outside or press Escape to close.

Image lifecycle

Lazy loading strategies

The component supports two distinct lazy loading mechanisms — pick the one that matches your requirements.

StrategyHowWhen to use
Nativeloading="lazy" on Image.ImgMost below-the-fold images. Zero JS, browser-managed.
Observerlazy prop on Image.RootWhen you need precise control over when the source is set — e.g. blur-up transitions, prefetch, or custom intersection thresholds.

You can combine both: use the observer for state control and loading="lazy" as a fallback for browsers without IntersectionObserver support.

Examples

Blur-up LQIP with observer loading

Wrap Image.Root in a reusable component that drives a fade-in transition from a low-quality placeholder (LQIP) to the full-resolution image. A tiny blurred thumbnail ships in the initial HTML (or as a small base64 string), visible immediately; the full image is withheld by the lazy prop until Image.Root intersects the viewport, at which point it loads and fades in over the blur.

Blur-up transition

Use controls to zoom and pan. Click outside or press Escape to close.

Blur-up transition

Reach for this pattern when you have many below-the-fold images and want both perceived performance (something visible instantly) and actual bandwidth savings (only load what’s seen). It shines on photo galleries, article covers, and image-heavy marketing pages where the blur-up effect is part of the aesthetic, not just a loading trick.

Three pieces make it work:

  • lazy prop on Image.Root — defers setting the real src until the container scrolls into view via useIntersectionObserver. Pair with root-margin (e.g. "200px") to start loading a bit before the container actually intersects.

  • LQIP inside Image.Placeholder — the blurred preview sits in the reserved frame (typically absolute inset-0). It’s decorative, so Image.Placeholder auto-sets aria-hidden="true".

  • data-state-driven opacity — the real Image.Img uses opacity-0 transition-opacity data-[state=loaded]:opacity-100, so the fade-in is CSS-only and doesn’t require threading slot props through the template.

Gotcha: the lazy prop needs a wrapper element to observe — it warns and skips observation when combined with renderless. If you need a lazy <picture>, observe a parent yourself with useIntersectionObserver and gate the source manually via useImage’s eager option.

FileRole
BlurUpImage.vueReusable component with LQIP placeholder, lazy intersection loading, and CSS-driven fade-in
observer.vueEntry point rendering a scrollable list of blur-up images to demonstrate the viewport trigger

A low-quality placeholder shows first; the full image fades in once it loads. Scroll to trigger the next image.

Photo 1
Photo 2
Photo 3

Picture element with format negotiation

Use renderless mode on both Image.Root and Image.Img to drop their wrapper elements and compose them directly inside a native <picture> element. The browser walks the <source> list and picks the first format it supports (WebP, AVIF, JPEG XL, etc.); Image.Img renders the fallback <img> that older browsers use. All loading state is still tracked by the surrounding Image.Root, so Image.Placeholder and Image.Fallback behave identically to the compound form — no extra wiring needed.

Reach for this pattern when you’re serving modern image formats for bandwidth or quality gains while keeping a universal fallback, or when you need full control over DOM structure (e.g., embedding inside a <figure> with a <figcaption>). It’s the only way to use Image.Root’s state machine with a native <picture> without losing either the format negotiation or the state-driven placeholder/fallback pattern.

Two details worth knowing:

  • renderless on bothImage.Root must go renderless to avoid wrapping <picture> in an extra <div>, and Image.Img must go renderless so you can place the inner <img> as <picture>'s last child (where the browser expects it).

  • lazy doesn’t work here — observer-driven lazy loading relies on a wrapper element to measure against. Combine native loading="lazy" on the inner <img> with fetchpriority hints for equivalent deferred loading, or wrap the whole composition in an outer element you observe manually.

The <source> children are fully browser-driven — Image.Img never sees their URLs. It just reports the final <img> element’s load/error events back to the context.

FileRole
PictureImage.vueReusable wrapper that emits a <picture> with typed <source> children; both Image.Root and Image.Img go renderless to drop wrapper elements
picture.vueEntry point passing WebP and fallback sources to demonstrate format negotiation
Format-negotiated photo

Recipes

Hero image with high priority

Set loading="eager" and fetchpriority="high" on hero images to optimize LCP. Always include width and height to prevent layout shift.

vue
<template>
  <Image.Root src="/hero.jpg">
    <Image.Img
      alt="Hero"
      width="1600"
      height="900"
      loading="eager"
      fetchpriority="high"
    />
    <Image.Placeholder>
      <img src="/hero-tiny.jpg" class="w-full blur-sm" />
    </Image.Placeholder>
  </Image.Root>
</template>

Retry on error

The retry() function is available from both Image.Root and Image.Fallback slot props.

vue
<template>
  <Image.Root src="/photo.jpg">
    <Image.Img alt="Photo" />
    <Image.Placeholder>Loading...</Image.Placeholder>
    <Image.Fallback v-slot="{ retry }">
      <button @click="retry">Retry</button>
    </Image.Fallback>
  </Image.Root>
</template>

Styling

Every Image sub-component exposes data-state reflecting the current status (idle, loading, loaded, or error). Prefer styling against these data attributes with CSS over threading slot props — the transitions stay CSS-only and the template stays declarative.

vue
<template>
  <Image.Img
    alt="Photo"
    class="opacity-0 transition-opacity data-[state=loaded]:opacity-100"
  />
</template>
css
/* Or with plain CSS */
[data-state='loaded'] { opacity: 1; }
[data-state='loading'] { animation: pulse 1s infinite; }
[data-state='error'] { border-color: red; }

Slot props (status, isLoaded, etc.) remain available for the rare cases where logic has to branch in the template, but reach for CSS + data attributes first.

Accessibility

ElementARIA / behavior
Image.Imgrole="img", accepts alt for accessible name
Image.Placeholderaria-hidden="true" — placeholder is decorative
Image.Fallbackrole="img" — provide alternate text inside the slot

Always pass width and height props on Image.Img to reserve layout space and prevent Cumulative Layout Shift while the image loads.

FAQ

API Reference

The following API details are for all variations of the Image component.

Image.Root

Props

src

string | undefined

Image source URL. Forwarded to Image.Img via context.

lazy

boolean | undefined

Defer loading until the root element intersects the viewport.

Default: false

rootMargin

string | undefined

IntersectionObserver root margin. Only used when lazy is true.

Default: "0px"

threshold

number | number[] | undefined

IntersectionObserver threshold(s). A number or array of numbers between 0.0 and 1.0 indicating the percentage of the target's visibility required to trigger the observer. Only used when lazy is true.

Default: 0

root

Element | null | undefined

IntersectionObserver root element. If omitted, the viewport is used. Pass a scroll container to observe intersections relative to it instead. Only used when lazy is true.

Default: null

namespace

string | undefined

Namespace for dependency injection.

Default: "v0:image"

Slots

default

ImageRootSlotProps

Image.Fallback

Props

namespace

string | undefined

Namespace for retrieving the Image context.

Default: "v0:image"

Slots

default

ImageFallbackSlotProps

Image.Img

Props

alt

string | undefined

Accessible alt text.

srcset

string | undefined

Responsive image candidates.

sizes

string | undefined

Image sizes hint for responsive selection.

width

string | number | undefined

Intrinsic image width — set to prevent layout shift.

height

string | number | undefined

Intrinsic image height — set to prevent layout shift.

crossorigin

"" | "anonymous" | "use-credentials" | undefined

CORS request mode.

referrerpolicy

string | undefined

Referrer policy for the request.

decoding

"sync" | "async" | "auto" | undefined

How the browser should decode the image.

Default: "async"

loading

"eager" | "lazy" | undefined

Native browser-managed lazy loading hint.

fetchpriority

"auto" | "high" | "low" | undefined

Resource priority hint.

namespace

string | undefined

Namespace for retrieving the Image context.

Default: "v0:image"

Events

load

[e: Event]

error

[e: Event]

loadstart

[src: string]

Slots

default

ImageImgSlotProps

Image.Placeholder

Props

namespace

string | undefined

Namespace for retrieving the Image context.

Default: "v0:image"

Slots

default

ImagePlaceholderSlotProps
Was this page helpful?

© 2016-1970 Vuetify, LLC
Ctrl+/