Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
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

Add Image from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.

pnpm dlx shadcn@latest add @vegastack/image

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): nonesmmd (default) → lgfull. 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 over the frame until the image decodes; the image fades in on load (prefers-reduced-motion suppresses the pulse).
  • 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.

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.

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

API Reference

PropTypeDefaultDescription
alt*stringAccessible 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.ReactNodeContent 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).
srcstringImage 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-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 and fallback layers are aria-hidden / decorative, so assistive tech only sees the image (or nothing for decorative ones) — no duplicate announcements.
  • 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