Components
Image
An image that never shows an empty box or pops in. The frame keeps its size from ratio, a small blurred version of the picture shows at once, and the full image fades in over it once it has loaded.
Preview
import { Image } from "@/components/ui/image"
const photo = {
src: "/preview/image.webp",
placeholder: "data:image/webp;base64,UklGRkIAAABXRUJQVlA4IDYAAADQAQCdASoQAAkAA4BaJQBOgCIfphDw8AD+vHG0aMGwi8/yX2fQS0b/tpRtPIXVO/hCeZK3qAA=",
alt: "Hills at dusk under a pink sky"
}
<Image src={photo.src} placeholderSrc={photo.placeholder} alt={photo.alt} ratio="16 / 9" className="w-80" />Usage
When to use
- Show a photo, cover or illustration whose space must be kept while it loads.
- Show a blurred version of the picture first and fade to the full image once it loads.
When not to use
- A person, team or workspace picture. Use Avatar.
- Content whose layout is known but whose data is still loading. Use Skeleton.
Examples
A cover image
The blurred copy shows at once, and the full image fades in over it.
import { Image } from "@/components/ui/image"
const photo = {
src: "/preview/image.webp",
placeholder: "data:image/webp;base64,UklGRkIAAABXRUJQVlA4IDYAAADQAQCdASoQAAkAA4BaJQBOgCIfphDw8AD+vHG0aMGwi8/yX2fQS0b/tpRtPIXVO/hCeZK3qAA=",
alt: "Hills at dusk under a pink sky"
}
<Image src={photo.src} placeholderSrc={photo.placeholder} alt={photo.alt} ratio="16 / 9" className="w-80" />Still loading
Without src yet, the blurred copy holds the frame.
import { Image } from "@/components/ui/image"
const photo = {
src: "/preview/image.webp",
placeholder: "data:image/webp;base64,UklGRkIAAABXRUJQVlA4IDYAAADQAQCdASoQAAkAA4BaJQBOgCIfphDw8AD+vHG0aMGwi8/yX2fQS0b/tpRtPIXVO/hCeZK3qAA=",
alt: "Hills at dusk under a pink sky"
}
<Image placeholderSrc={photo.placeholder} alt={photo.alt} ratio="16 / 9" className="w-80" />Do and don't
Do
- Always reserve the space: ratio, an aspect class or a fixed height.
- Make placeholderSrc when the image is added, at build time or on upload, and inline it as a data URL.
- Serve the full image at the size it is shown (srcSet and sizes pass through to the image).
Don't
- Don't load placeholderSrc from a URL of its own: it would need a placeholder too.
- Don't use it for an avatar. Use Avatar, which shows initials until the photo loads.
- Don't put text over the image; its contrast is not checked.
Props
| Name | Type | Values | Default | Description |
|---|---|---|---|---|
| src | string | - | - | The full image. Until it is set and has loaded, the blurred copy (or the gray frame) shows. A new src shows the blurred copy again until the new image loads. |
| alt required | string | - | - | What the image shows, for screen readers. "" for a decorative image. |
| placeholderSrc | string | - | - | A small blurred version of the same image, best an inline data URL of a few hundred bytes (16 by 9 pixels is enough), made when the image is added (at build time or on upload), so it shows with the page and needs no request. Without it the frame shows the soft gray fill. |
| ratio | string | - | - | Width to height of the frame, such as 16 / 9 or "4 / 3". Reserves the space before anything loads, so nothing moves. Or set an aspect class (aspect-video) or a height with className. |
Anatomy
| Part | Description |
|---|---|
| root | The frame: element corners, the soft gray fill, clips what is inside. ratio sets its aspect ratio, so the space is kept before anything loads. |
| blurred | placeholderSrc: a few pixels of the same picture, scaled to the frame and blurred, shown at once. |
| image | The full image on top. Transparent until it has loaded, then it fades in with duration-medium. |
Accessibility
Role: img
- alt is required by the type. Describe what the image shows; pass alt="" only for a decorative image.
- The blurred copy is hidden from screen readers (alt="" and aria-hidden), so the image is read once.
- No fade under prefers-reduced-motion: the full image appears in one step.
- A failed image shows the browser's broken-image box with the alt text, not an endless placeholder.
Install
npx shadcn add https://werewolf-ui.vercel.app/r/image.jsonThis also adds styles/werewolf.css, the tokens every component uses, and any other component it needs. Import that file once in your root CSS, after @import "tailwindcss", with a path relative to that CSS file (from app/globals.css it is @import "../styles/werewolf.css").
