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
Add Image from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/imageUsage
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 over the frame until the image decodes; the image fades in on load (prefers-reduced-motionsuppresses the pulse). - 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.
Opt out for an above-the-fold hero, where deferring the fetch delays LCP instead of saving it:
<Image src={hero} alt="" loading="eager" aspectRatio="video" />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). Pass eager for an above-the-fold hero,
where deferring the fetch delays LCP instead of saving it. |
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-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 and fallback layers are
aria-hidden/ decorative, so assistive tech only sees the image (or nothing for decorative ones) — no duplicate announcements. - Must pass
axewith no violations.
| Contract | States tested |
|---|---|
| Behaviour | default, complete, empty, error, loading, lazy-loading |
| Accessibility | labeled |
| Visual | default, loading, error, empty |