Image
A presentational framed image with aspect-ratio, rounding, a loading skeleton, and an error fallback.
- Status
- Since
0.1.0- Accessibility pattern
- img with required alt
Last updated
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Image before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify imageInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/imageRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
Usage
import { Image } from "@/components/ui/image";
<Image
src="https://cdn.example.com/cover.webp"
alt="Cover"
aspectRatio="video"
/>;Presentational only.
Imagetakes a fully-resolved, publicsrc. It does not resolve Cloudflare/R2 storage keys, build CDN URLs, pick optimized variants, or fetch data — that stays app-side. Resolve the key to a URL (and choose the optimized variant /srcSet) before passing it in. This mirrors the platformR2Image, which keeps key → URL resolution (getR2ImageUrl) in the app while this component owns only the presentation.
Examples
Aspect ratios
square (1:1) and video (16:9) reserve space so the layout never shifts as the image decodes;
auto lets the intrinsic size drive the box. The three square tiles below show the rendered
states side by side: a loaded image, the bare bg-muted placeholder (no src), and the
error fallback — when the src fails to load, the fallback node replaces the broken image.

Auto (default)
aspectRatio="auto" is the default — no ratio is enforced, so the image's own intrinsic
dimensions drive the box height. Constrain the width and the frame follows the source's natural
proportions (use square/video instead when you need a fixed, shift-free aspect).
Rounding
rounded controls the corner radius of the frame (and the image clipped inside it): none → sm
→ md (default) → lg → full. full clips the image to a circle for avatar-style media.
States
- Loading — a
bg-mutedskeleton pulses under the image until it decodes (prefers-reduced-motionsuppresses the pulse). An image in the server HTML is never hidden: the browser paints it as soon as it decodes, before hydration, and a cached one is there on first paint. Only an image mounted on the client that has not loaded yet fades in. - Loaded — the image is shown,
object-covercropped to the frame. - Error / empty — when the image fails (or no
srcis given), thefallbacknode renders; there is never a broken-image icon.
The loading skeleton is transient — it shows only between mount and decode, so it can't be captured in a static preview (the sample images decode immediately). In the docs you see only the loaded, placeholder, and error states above; the pulsing
bg-mutedskeleton appears in real use while a remote image is still in flight.
// square thumbnail with an initials fallback on error
<Image src={url} alt="Ada Lovelace" aspectRatio="square" fallback="AL" />Loading
Image defaults to loading="lazy" decoding="async", so an off-screen image costs nothing until
it scrolls near the viewport and its decode never blocks the frame it lands in. A long list of
thumbnails gets that for free.
For an above-the-fold image — a detail page's hero, the first row of a grid, anything visible
without scrolling — pass priority. It loads eager with fetchPriority="high", so the browser
fetches it from the HTML ahead of other images instead of deferring it and delaying LCP. Leave it
off below the fold, where lazy loading is the saving.
<Image src={hero} alt={product.name} aspectRatio="square" priority />An explicit loading or fetchPriority still wins over priority.
Placeholder
placeholder takes a tiny preview of the image as a data URL (a 16px blur, under 1 KB, stored
beside the file). It fills the frame as a blurred cover while the image loads — in place of the
pulsing skeleton — and fades out once the image has loaded. srcSet and sizes pass straight to the
<img>, so a stored image can offer its 480w and 1280w variants.
<Image
src={file.url480}
srcSet={file.srcset}
sizes="(min-width: 768px) 50vw, 100vw"
placeholder={file.blur}
alt=""
/>Playground
Combine an aspect ratio with a corner radius, then copy the generated JSX.
<Image src={url} alt="A scenic landscape" />API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
alt* | string | — | Accessible alt text describing the image. Required for meaningful images;
pass an empty string (alt="") for purely decorative images so screen
readers skip them. |
aspectRatio | "auto" | "square" | "video" | 'auto' | Aspect ratio of the framed box — reserves space so the layout doesn't shift
as the image decodes.
- square: 1:1.
- video: 16:9.
- auto: intrinsic size (no enforced ratio, default). |
decoding | "async" | "auto" | "sync" | 'async' | Native decoding hint. async keeps decode off the main thread so a large
image cannot block the frame it lands in. |
fallback | React.ReactNode | — | Content shown when the image fails to load (broken URL, network error) or
when no src is provided — e.g. an icon, initials, or a label. When omitted,
the bare bg-muted frame shows. |
loading | "eager" | "lazy" | 'lazy' | Native lazy-loading hint. Defaults to lazy so an off-screen image costs
nothing until it scrolls near the viewport (audit B4-08 — MarkdownView
already did this for its images). For an above-the-fold image, pass
priority instead, which also raises the fetch priority. |
placeholder | string | — | A tiny preview of the image as a data URL (a 16px blur, under 1 KB). It fills the frame as a
blurred cover until the image has loaded, then fades out under it — in place of the pulsing
skeleton. srcSet and sizes pass straight to the <img>. |
priority | boolean | false | An above-the-fold image — a detail page's hero, the first row of a grid, anything visible
without scrolling. Loads eager with fetchPriority="high", so the browser fetches it from
the HTML ahead of other images instead of deferring it and delaying LCP. Leave it off below
the fold. |
rounded | "full" | "lg" | "md" | "none" | "sm" | 'md' | Corner radius of the frame (and the image clipped inside it). |
src | string | — | Image source. Pass a fully-resolved, public URL — this component is purely
presentational and does NOT resolve storage keys. R2 (or any CDN) key → URL
resolution stays app-side; resolve before passing src. |
Data attributes and CSS variables on Image
| Attribute | Values |
|---|---|
data-aspect-ratio | mirrors a prop or state value |
data-slot | "image" | "image-fallback" | "image-img" | "image-placeholder" | "image-skeleton" |
data-state | mirrors a prop or state value |
Accessibility
- Requires an explicit
altprop. Pass descriptivealtfor meaningful images. - For purely decorative images or empty placeholder frames, pass
alt=""so screen readers skip the image intent deliberately. - The skeleton layer is
aria-hidden; the fallback carries the image's accessible name (role="img"plus thealt) unlessalt="", in which case it is decorative too — so a broken or missing image still announces what it was, and assistive tech never hears the same thing twice. - Must pass
axewith no violations.
| Contract | States tested |
|---|---|
| Behaviour | default, complete, empty, error, loading, lazy-loading |
| Accessibility | labeled |
| Visual | default, loading, error, empty |