Bonusway design system

Data display

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.

K

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.

Bonusway

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.

PropTypeDefaultDescription
filltrue | 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`.

widthnever | number—

The image's intrinsic width in px — the aspect ratio and the srcset come from it.

heightnever | number—

The image's intrinsic height in px.

srcrequiredfrom PictureBasePropsstring | 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 PictureBasePropsstring—

Empty string for decorative art, as the HTML spec asks.

classNamefrom PictureBasePropsstring——
stylefrom PictureBasePropsCSSProperties—

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 PictureBasePropsstring—

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 PictureBasePropsReactNodenull

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 PictureBasePropsboolean—

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 PictureBasePropsImageQuality——
fetchPriorityfrom PictureBaseProps'high' | 'low' | 'auto'—

everywhere else, since marking everything high marks nothing high.

reffrom PictureBasePropsRef<HTMLImageElement>—

For the error-fallback pattern (see Avatar), which needs the element itself.

onErrorfrom PictureBasePropsReactEventHandler<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.

Tämä verkkosivusto käyttää evästeitä

Käytämme evästeitä parantaaksemme käyttökokemustasi sivustollamme ja mukauttaaksemme sinulle tarjoamaamme sisältöä. Tärkeät evästeet ovat välttämättömiä jotta palvelumme toimii oikein. Jos suostut siihen, että keräämme tietoja sinusta käyttämällä välttämättömiä evästeitä ja markkinointievästeitä, valitse "Hyväksy kaikki". Voit aina muuttaa valintaasi evästeasetuksista.

Evästeasetukset