Skip to content

Image

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/image.json
See it liveA ratio box with a picture inside, a broken source showing its fallback, and a source that never resolves showing the placeholder.→

A loading picture’s layout jump is not a styling defect - it is a missing number. The browser has nowhere to put the box until the file arrives and says how big it is, so everything below the picture slides down and back up. AspectRatio is that number, given up front by the caller instead of discovered by the browser: a plain box on CSS aspect-ratio that reserves its space the instant it is in the tree.

<AspectRatio ratio="video" className="rounded-lg">
<Image src={cover} alt={work.title} className="absolute inset-0" fallback={<ImageOffIcon />} />
</AspectRatio>

Image carries the same three states Avatar’s picture already does - loading, loaded, failed - because a face and a photograph fail to load for the same reasons. While loading it shows Skeleton’s own look (animate-pulse bg-soft) rather than a second grey rectangle invented for this one component, so the set has one loading language rather than two that drift apart. Once loaded it fades in over --duration-base; a sudden picture reads as a flash the eye has to catch, and the theme’s own reduced-motion media query already cuts every duration near zero for a reader who asked for that, so nothing extra had to be wired up here.

On error it shows fallback instead of the browser’s own broken-image icon. A blank product-branded slot reads as “the picture is missing”; the browser’s glyph reads as “the product is broken.” fallback is a prop because the right text or icon is the product’s word, never a default this component would have to pick in one language.

A cached picture is the state a naive version gets wrong. An <img> already in the browser’s cache is already complete the instant it mounts and fires no load event at all - there is nothing left to load. Without checking img.complete && naturalWidth in a layout effect on mount, that picture’s placeholder would sit forever, waiting for an event that already happened. useLayoutEffect rather than useEffect, so a cached picture never paints its placeholder for even one frame.

alt is required, with no default. A decorative picture says so explicitly with alt=""; there is no third option that means “nobody decided yet,” because that option is how a picture ships with no description at all.

Not Base UI. Base UI’s Avatar package is not installed in this workspace, and this line’s own Avatar already reimplements the same loaded/failed state by hand for the same reason: a picture is common enough, and the state small enough, that a dependency would cost more than the useState it replaces. Image follows that precedent instead of adding a second one.

Props

Read from the types in ui/image.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.

AspectRatio

A box that keeps its shape before, during and after whatever fills it loads. size-full on the child is the contract: this component reserves the rectangle, the child covers it.

PropTypeDefault
ratioAspectRatioVariantThe box's shape: square, video, or a raw number as width / height - 4 / 3, 21 / 9 - for anything the named steps do not cover.
childrenReactNode

And every prop of React's HTMLAttributes<HTMLDivElement> except children, passed on.

Image

A picture that reserves its box, shows Skeleton's placeholder while it loads, fades in once it has, and shows fallback instead of a broken-image glyph if it can't be loaded at all.

PropTypeDefault
fitcover | contain'cover'How the picture fills a box that is not its own shape. cover for a photo standing in for the whole box; contain for a logo or a diagram where cropping would cut off the point of it.
altrequiredstringRequired, with no default: a decorative picture says so explicitly with alt="" rather than by omission, which is indistinguishable from a picture nobody described yet.
fallbackReactNodeWhat stands in the box when the picture fails to load - the product's text or icon, not a default this component would have to write in some one language.

And every prop of React's ImgHTMLAttributes<HTMLImageElement> except onLoad, onError, placeholder, passed on.

Accessibility

The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 14 rules applied and passed, and none failed. The accessibility report has every item.

History

Each release that changed ui/image.tsx - the file a product copies - newest first.

  1. v0.37.0
    • fix give every loop a rest of its own under reduced motion 18511f0
  2. v0.31.0
    • feat a picture that holds its box before it arrives 3114a6e