Picture
The one way this app renders an image. It wraps next/image so three decisions live in one place: whether the optimizer may fetch this URL at all (@/config/imageHosts answers, and the same list builds next.config.ts’s remotePatterns), how hard it may compress it (@/config/imageQuality, which builds images.qualities the same way), and what to draw when there is no src — a fallback, never a broken `<img>`.
import Picture from '@/components/ui/Picture';Source: src/components/ui/Picture.tsx
Examples
width + height, or fill
Give the intrinsic size, or say fill and let a positioned parent decide. Tailwind’s preflight puts height:auto on every image, so width/height are the aspect ratio and the srcset — the rendered box is whatever the CSS says. With fill, always pass sizes.


No src, no broken image
API image fields are routinely null, and `<img src="">` requests the current page as an image: a broken icon that still takes layout space. An empty (or blank) src renders the fallback instead — or nothing, when there is none.
What gets optimized
Left: a local asset, served through /_next/image as WebP at the size it is drawn. Right: an SVG, which is left alone — dangerouslyAllowSVG is off, and a vector has nothing to gain. The same as-is treatment covers unknown hosts, blob: previews and data: URIs, so a partner CDN’s image is never a 400 from the optimizer.

Three compression steps
quality picks how hard the optimizer compresses. 60 for logos, avatars, icons and thumbnails — flat marks where the extra bytes buy nothing visible. 75 (the default, so it can be left off) for product and hero photography. 85 for the carousel’s full-bleed banner art, where type is set into the image and artefacts show. Only the values in @/config/imageQuality are served: Next coerces anything else to the nearest one listed, silently, so a number outside that list is a different image rather than an error.


Props
<Picture> props
These props come in alternative sets
The type is a union, so some combinations below are not valid together — passing one prop can require or exclude another. The component file states which.
| Prop | Type | Default | Description |
|---|---|---|---|
fill | true | false | — | Stretch to a positioned parent (`relative`/`absolute`/`fixed`) and take no intrinsic size. Pair it with `object-cover`/`object-contain` in `className` and always pass `sizes`. |
width | never | number | — | The image's intrinsic width in px — the aspect ratio and the srcset come from it. |
height | never | number | — | The image's intrinsic height in px. |
srcrequiredfrom PictureBaseProps | string | null | undefined | — | The image URL. Nullable on purpose: almost every image in this app comes from an API field that may be absent, and the point of this component is that a missing one renders `fallback` rather than a broken `<img>`. |
altrequiredfrom PictureBaseProps | string | — | Empty string for decorative art, as the HTML spec asks. |
classNamefrom PictureBaseProps | string | — | — |
stylefrom PictureBaseProps | CSSProperties | — | Prefer `className`. This is for the rare box that has to beat Tailwind's preflight (`max-width: 100%`, `height: auto`) with a specificity a utility class cannot reach — the app-store badges, which a narrow menu panel silently squeezed before they were pinned this way. |
sizesfrom PictureBaseProps | string | — | Required whenever the image stretches — with `fill`, or with CSS that makes it responsive. Without it the browser assumes the image is as wide as the viewport and downloads the largest candidate in the srcset. |
fallbackfrom PictureBaseProps | ReactNode | null | Rendered in place of the image when `src` is empty. Nothing, by default. |
loadingfrom PictureBaseProps | 'lazy' | 'eager' | — | `lazy` by default (that is `next/image`'s own default, and a change from the raw `<img>` these replaced, which were all eager). Set `eager` for anything above the fold — a page's hero, the header logo — where lazy loading trades a little bandwidth for a visible pop-in and a worse LCP. |
unoptimizedfrom PictureBaseProps | boolean | — | Forces the image to be served as-is even when the host IS allowlisted. The one case that needs it: a layout whose box is the image's OWN intrinsic size (no width and no height in CSS, only `max-*` caps). Optimizing such an image attaches a density-descriptor srcset, and since the optimizer never enlarges a source, a 2× candidate that comes back at the source's own width renders at HALF the size on a retina screen. There is no `sizes` value that fixes it — the fix is to not optimize. |
qualityfrom PictureBaseProps | ImageQuality | — | — |
fetchPriorityfrom PictureBaseProps | 'high' | 'low' | 'auto' | — | everywhere else, since marking everything high marks nothing high. |
reffrom PictureBaseProps | Ref<HTMLImageElement> | — | For the error-fallback pattern (see Avatar), which needs the element itself. |
onErrorfrom PictureBaseProps | ReactEventHandler<HTMLImageElement> | — | Client components only — a server component cannot serialise a function. |
Accessibility
- alt is required, and an empty string is the right answer for decorative art — it is how a screen reader is told to skip it rather than read a filename.
- A fallback that carries meaning (a store’s initial) should be text, not an empty box: it is what replaces the image for everyone, not just for assistive tech.