Image
Headless image with loading states, candidate fallback chains, and deferred loading.
| Package | @vizejs/ui/image |
| Maturity | stable |
| Own the source | vize lib pull image |
| Requires | context, measure |
| Aliases | image, img, picture, lazy image, image fallback |
| Covers | HTML img, Radix Avatar image loading status, Chakra UI Image, Next.js Image placeholder |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { ImageContent, ImageFallback, ImagePlaceholder, ImageRoot } from "@vizejs/ui/image";
Or copy the source into your project with vize lib pull image (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Image with a loading placeholder and a text fallback shown when the source fails. -->
<script setup lang="ts">
import { ImageContent, ImageFallback, ImagePlaceholder, ImageRoot } from "@vizejs/ui/image";
</script>
<template>
<ImageRoot src="/photos/harbor.jpg">
<ImageContent alt="Fishing boats moored in the harbor at dusk" :width="800" :height="600" />
<ImagePlaceholder>Loading photo…</ImagePlaceholder>
<ImageFallback>Harbor photo unavailable</ImageFallback>
</ImageRoot>
</template>
API
ImageContent
Source: src/families/media/image/image-content.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
alt (required) |
string |
required |
Native alternative text. Pass "" for decorative images. |
srcset |
string |
undefined |
Responsive candidates for the first source. Dropped once the chain falls back, so a failing srcset cannot shadow later candidates. |
sizes |
string |
undefined |
Native sizes paired with srcset. |
width |
number | string |
undefined |
Intrinsic width used by the browser to reserve layout space. |
height |
number | string |
undefined |
Intrinsic height used by the browser to reserve layout space. |
loading |
ImageLoading |
"lazy" |
Native loading policy. |
decoding |
ImageDecoding |
"async" |
Native decoding policy. |
fetchPriority |
ImageFetchPriority |
undefined |
Native fetch-priority hint. |
crossOrigin |
ImageCrossOrigin |
undefined |
Native CORS policy. |
referrerPolicy |
ImageReferrerPolicy |
undefined |
Native referrer policy. |
Exposed
| Member | Type | Description |
|---|---|---|
status |
ImageStatus |
Current loading lifecycle state. |
src |
string | undefined |
Source currently attached to the native image, or undefined while idle or failed. |
candidateIndex |
number |
Zero-based position of the attached candidate in the candidate chain, or -1. |
candidateCount |
number |
Number of safe candidates in the chain. |
element |
HTMLImageElement | null |
Rendered native image element, or null after every candidate failed. |
ImageFallback
Source: src/families/media/image/image-fallback.vue
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
ImagePartSlotState |
Fallback content rendered after every candidate failed or none was safe. |
Exposed
| Member | Type | Description |
|---|---|---|
element |
HTMLSpanElement | null |
Rendered fallback element while visible. |
visible |
boolean |
Whether the fallback is rendered. |
ImagePlaceholder
Source: src/families/media/image/image-placeholder.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
delay |
number |
0 |
Milliseconds the image must stay pending before the placeholder renders, so cached images do not flash a skeleton. Positive delays render nothing on the server. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
ImagePartSlotState |
Placeholder content such as a skeleton, blur-up preview, or spinner. |
Exposed
| Member | Type | Description |
|---|---|---|
element |
HTMLSpanElement | null |
Rendered placeholder element while visible. |
visible |
boolean |
Whether the placeholder is rendered. |
ImageRoot
Source: src/families/media/image/image-root.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
src |
ImageSource | null |
undefined |
Image source or ordered candidate chain. Each failed candidate advances to the next safe one; unsafe or malformed candidates are skipped and never rendered. |
defer |
boolean |
false |
Wait until the root intersects the viewport before attaching any source. Complements native loading="lazy" for background-critical or custom scroll roots. |
rootMargin |
string |
"200px" |
Intersection margin used while defer waits for visibility. |
allowInsecure |
boolean |
false |
Permit unencrypted http: candidates for local development. |
Events
| Event | Payload | Description |
|---|---|---|
load |
[nativeEvent: Event | null, src: string] |
Fired after the attached candidate loads. nativeEvent is null for pre-hydration loads. |
error |
[nativeEvent: Event | null, src: string] |
Fired after one candidate fails, before the next candidate is attached. |
statusChange |
[status: ImageStatus, previous: ImageStatus, reason: ImageStatusChangeReason] |
Fired after every distinct status transition with the cause of the transition. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
ImageSlotState |
Compound Image parts. Receives the current loading lifecycle state. |
Exposed
| Member | Type | Description |
|---|---|---|
status |
ImageStatus |
Current loading lifecycle state. |
src |
string | undefined |
Source currently attached to the native image, or undefined while idle or failed. |
candidateIndex |
number |
Zero-based position of the attached candidate in the candidate chain, or -1. |
candidateCount |
number |
Number of safe candidates in the chain. |
element |
Element | null |
Rendered root element. |
retry |
() => boolean |
Restart the candidate chain from the first safe source. Reports whether a retry started. |
Behavior
Normative state x input -> outcome table for image-root.vue, image-content.vue,
image-placeholder.vue, and image-fallback.vue (@vizejs/ui/image). Every row
is proven by the named test. A row without a passing test is a contract violation.
The lifecycle is idle -> loading -> loaded | error. ImageRoot owns the source
candidate chain and the lifecycle; ImageContent renders the native <img>,
ImagePlaceholder renders while pending, and ImageFallback renders after the
chain is exhausted. Every part mirrors the lifecycle through data-status.
| ID | State | Input | Outcome | Evidence |
|---|---|---|---|---|
| I1 | source present | render | root is loading; native image receives the first safe candidate, srcset/sizes, loading="lazy", decoding="async", and hints |
renders a loading native image with placeholder, native attributes, and no fallback |
| I2 | loading | native load |
lifecycle becomes loaded, placeholder unmounts, load emits (event, src), statusChange reports load |
load settles the lifecycle, hides the placeholder, and emits load |
| I3 | loading | native error |
the next safe candidate is attached without srcset; after the last failure the image unmounts and the fallback renders |
advances through the candidate chain and renders the fallback after the last failure |
| I4 | missing or unsafe | render | lifecycle is error; no <img> or unsafe URL is rendered; http: requires allowInsecure |
missing and unsafe sources render the fallback without forwarding a source |
| I5 | settled | src change / retry() |
a different chain restarts at the first candidate (source); an equal chain keeps state; retry() restarts or reports false |
source replacement and retry restart the chain |
| I6 | defer |
render / intersect | lifecycle stays idle without src until the root intersects (rootMargin), then loads and stops observing |
deferred images stay idle until visible and fall back to eager without observers |
| I7 | defer, no IO |
mount | without IntersectionObserver the source attaches on mount |
deferred images stay idle until visible and fall back to eager without observers |
| I8 | defer |
defer becomes false |
the source attaches immediately | turning defer off attaches the source immediately |
| I9 | placeholder delay | pending longer / shorter | positive delay renders the placeholder only after the delay and never after the image settles |
delayed placeholders wait before rendering and never render after settling |
| I10 | hydration race | image settled before hydration | a complete image with pixels is loaded; a complete image is decoded to settle loaded or error; events report null |
reads images that settled before hydration attached listeners |
| I11 | exposed instance | read | root exposes status, attached source, candidate index/count, element, and retry() |
exposes typed lifecycle state and live parts |
| I12 | missing provider | setup | compound parts fail closed with the shared context diagnostic | compound parts require a matching root provider |
| I13 | candidate policy | resolveImageCandidates() |
trims, filters by the shared media-source policy, de-duplicates in order, and freezes the chain | resolves safe, de-duplicated candidate chains |
| I14 | SSR | isolated requests | loading, error, idle, and delayed-placeholder markup is byte-identical and never contains unsafe candidates | renders byte-identical loading image markup across isolated SSR requests |
| I15 | SSR | render | fallback, idle, and delayed-placeholder states are deterministic on the server | renders fallback, idle, and delayed-placeholder states deterministically on the server |
| I16 | SSR / hydration | hydrate | server markup hydrates without warnings or node replacement | hydrates server image markup without warnings or node replacement |
| I17 | types | compile | lifecycle, reasons, candidate chains, slot state, and exposes are closed and read-only | image.types.test-d.ts |
Styling hooks: data-status on every part, data-deferred on the root, and
data-candidate on the image. No CSS ships with the primitive; hide the image
while data-status="loading" if a placeholder should occupy the same box.