Skip to content
Component installs need the registry setup
VegaStack Design

Image

A presentational framed image with aspect-ratio, rounding, a loading skeleton, and an error fallback.

Status
stable
Since
0.1.0
Accessibility pattern
img with required alt

Last updated

A scenic landscape

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 image

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/image

Run 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. Image takes a fully-resolved, public src. 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 platform R2Image, 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.

Loaded image
Failed to load

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).

A landscape at its intrinsic ratio

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.

Rounded none
none
Rounded sm
sm
Rounded md
md
Rounded lg
lg
Rounded full
full

States

  • Loading — a bg-muted skeleton pulses under the image until it decodes (prefers-reduced-motion suppresses 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-cover cropped to the frame.
  • Error / empty — when the image fails (or no src is given), the fallback node 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-muted skeleton 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=""
/>
A landscape with a blur preview

Playground

Combine an aspect ratio with a corner radius, then copy the generated JSX.

A scenic landscape
<Image src={url} alt="A scenic landscape" />

API Reference

PropTypeDefaultDescription
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.
fallbackReact.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.
placeholderstring—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>.
prioritybooleanfalseAn 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).
srcstring—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

AttributeValues
data-aspect-ratiomirrors a prop or state value
data-slot"image" | "image-fallback" | "image-img" | "image-placeholder" | "image-skeleton"
data-statemirrors a prop or state value

Accessibility

  • Requires an explicit alt prop. Pass descriptive alt for 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 the alt) unless alt="", 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 axe with no violations.
ContractStates tested
Behaviourdefault, complete, empty, error, loading, lazy-loading
Accessibilitylabeled
Visualdefault, loading, error, empty

Do / Don't

Do
Pass a resolved, public URL and a descriptive alt; pick the aspectRatio that matches your layout to avoid shift.
Don't
Pass an R2/storage key or expect this component to fetch or resolve URLs — resolve them app-side first.

On this page