> From the [Strife developer docs](https://strife.app/docs/how-to-guides/responsive-images). Every page is listed in [llms.txt](https://strife.app/docs/llms.txt).

# Responsive images

Serve each visitor an image file that fits their screen: add widths to fields.image(), write srcset from the value and describe your layout with sizes. Patterns for heroes, content images, card grids and fixed-size images.

A responsive image gives the browser the same picture in several widths and lets it pick the one that fits the visitor's screen. A phone then downloads a phone-sized file, not the desktop one, and a high-density screen still gets a sharp image.

The work is split in two:

* **Strife writes `srcset`.** A field with `widths` gets a list of candidates — the same crop in several widths — whenever an editor saves the image.
* **Your site writes `sizes`.** Only your layout knows how wide the image is on the page. On lazy-loaded images you can let the browser measure it instead.

The browser does the rest: it weighs the rendered width, the screen's pixel density and things like a data-saver setting, and picks a candidate. `srcset` and `sizes` describe; they don't instruct.

## 1. Add widths to the field

```ts
fields.image({
  label: 'Image',
  width: 1344,
  height: 896,
  format: 'avif',
  widths: 'auto',
})
```

* **`width`** is the largest candidate. Set it to the largest width the image is shown at, in CSS pixels, times two — 1344 for an image that is at most 672px wide. The browser never gets a larger file than this.
* **`widths: 'auto'`** gives about 25 % steps from 320px up to `width`. Use an array (`widths: [400, 800, 1344]`) when you know your layout's widths.
* The field's crop is the same in every candidate; only the size changes.

From the next time an editor picks or crops an image in that field, in either of Strife Studio's interfaces, its value has a `source.srcset`:

```json
{
  "width": 1344,
  "height": 896,
  "alt": "A lake below wooded mountains",
  "source": {
    "url": "https://cdn.strife.app/…/w:1344/…",
    "srcset": "https://cdn.strife.app/…/w:320/… 320w, https://cdn.strife.app/…/w:400/… 400w, …, https://cdn.strife.app/…/w:1344/… 1344w",
    "placeholder": "#4d6a6f"
  }
}
```

> [!NOTE]
> `source.srcset` is absent on images saved before you added `widths`, on SVG and GIF images (vector graphics scale on their own, and an animated GIF would be rendered once per width), on images written through the API rather than in Strife Studio, and, in Studio's classic interface, on images smaller than the field's size. Your markup keeps working: the browser uses `src`.

## 2. Write the markup

Every pattern below uses the same three things: `srcset` from the value, a `sizes` that describes your layout, and `width`/`height` so the browser can reserve the space before the image arrives.

```css
img {
  max-inline-size: 100%;
  block-size: auto;
}
```

### Full-width hero

The page's largest image at the top, usually its Largest Contentful Paint (LCP) element. It spans the viewport, so `sizes="100vw"`.

```astro
<img
  src={hero.source.url}
  srcset={hero.source.srcset}
  sizes="100vw"
  width={hero.width}
  height={hero.height}
  alt={hero.alt}
  fetchpriority="high"
/>
```

* Never put `loading="lazy"` on the LCP image, and use `fetchpriority="high"` on one or two images at most.
* The image is in the HTML from the server, so the browser finds it right away; you don't need a `<link rel="preload">`.
* For a 3:2 hero shown up to about 1440 CSS pixels wide, `width: 2880, height: 1920`.

To keep a hero from growing too tall on wide screens, cap its height with CSS. The box is then wider than the image, so the image still scales with the width and `sizes` is still right; only a little height is cropped.

```css
.hero {
  inline-size: 100%;
  aspect-ratio: 3 / 2;
  max-block-size: min(100dvh - 4rem, 1000px);
  overflow: hidden;
}
.hero img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
}
```

> [!WARNING]
> Give the box a definite width (`inline-size: 100%`). With `width: auto`, `aspect-ratio` turns the height cap into a width cap and the box shrinks instead of cropping.

### Content image

An image in a text column, further down the page. It loads lazily, so `sizes` can start with `auto`: the browser measures the image's width when it is about to load. Browsers that don't support `auto` yet read the rest of the list, so describe the column after the comma.

```astro
<img
  src={image.source.url}
  srcset={image.source.srcset}
  sizes="auto, (min-width: 44.5rem) 42rem, calc(100vw - 2.5rem)"
  loading="lazy"
  width={image.width}
  height={image.height}
  alt={image.alt}
/>
```

The same value can appear in several places with a different `sizes` in each; the field's `width` only caps the largest candidate.

### Card grid

Many images in a grid that changes its number of columns. Describe one card's width per breakpoint. The cards below the fold load lazily with `auto`:

```astro
<img
  src={card.source.url}
  srcset={card.source.srcset}
  sizes="auto, (min-width: 64rem) 33vw, (min-width: 40rem) 50vw, 100vw"
  loading="lazy"
  width={card.width}
  height={card.height}
  alt={card.alt}
/>
```

The first row is probably visible when the page loads, and an image in the viewport should not be lazy-loaded. Leave out `loading="lazy"` there, and with it `auto`:

```astro
<img
  src={card.source.url}
  srcset={card.source.srcset}
  sizes="(min-width: 64rem) 33vw, (min-width: 40rem) 50vw, 100vw"
  width={card.width}
  height={card.height}
  alt={card.alt}
/>
```

> [!WARNING]
> Use `sizes="auto"` only together with `loading="lazy"`. On an image that isn't lazy-loaded, browsers treat it as a 300×150 box and ignore the rest of the list.

### Fixed size

Avatars, logos and thumbnails are always the same size in CSS; only the screen's pixel density changes. Two candidates — 1x and 2x — are enough, and `sizes` is just the image's width:

```ts
fields.image({ label: 'Avatar', width: 192, height: 192, format: 'avif', widths: [96, 192] })
```

```astro
<img
  src={avatar.source.url}
  srcset={avatar.source.srcset}
  sizes="96px"
  width={avatar.width}
  height={avatar.height}
  alt={avatar.alt}
/>
```

The same value works at another size with another `sizes`, for example `sizes="48px"` in a list.

## 3. A complete example

The patterns above differ only in `sizes` and in whether the image is lazy-loaded. A small component keeps the rest in one place: an image without a value renders nothing, an image without `source.srcset` falls back to `src`, `sizes="auto"` is used only on lazy images, and the placeholder colour fills the space while the file loads.

```astro title="src/components/StrifeImage.astro"
---
import type { Image } from '@strifeapp/types';

interface Props {
  image: Image | null | undefined;
  /** How wide the image is in your layout, e.g. "100vw" or "(min-width: 64rem) 33vw, 100vw". */
  sizes: string;
  /** The page's main image (its LCP element): loaded at once, at high priority. */
  priority?: boolean;
  class?: string;
}

const { image, sizes, priority = false, class: className } = Astro.props;
const source = image?.source;
const srcset = source?.srcset || undefined;
---

{image && source?.url && (
  <img
    class={className}
    src={source.url}
    srcset={srcset}
    sizes={srcset ? (priority ? sizes : `auto, ${sizes}`) : undefined}
    width={image.width}
    height={image.height}
    alt={image.alt ?? ''}
    loading={priority ? undefined : 'lazy'}
    decoding={priority ? undefined : 'async'}
    fetchpriority={priority ? 'high' : undefined}
    style={source.placeholder ? `background-color: ${source.placeholder}` : undefined}
  />
)}

<style>
  img {
    max-inline-size: 100%;
    block-size: auto;
  }
</style>
```

Use it with the field from step 1 and a `sizes` per place in your layout:

```ts title="schemas/home.ts"
import { defineType, fields } from '@strifeapp/schema';

export const home = defineType({
  name: 'home',
  title: 'Home',
  fields: {
    heading: fields.text({ label: 'Heading', localizable: true }),
    // A full-width hero, at most 1600 CSS pixels wide: 3200 for 2x screens.
    hero: fields.image({ label: 'Hero', width: 3200, height: 1350, format: 'avif', widths: 'auto' }),
  },
});
```

```astro title="src/pages/index.astro"
---
import StrifeImage from '../components/StrifeImage.astro';

const page = await getPage('/'); // however your site loads the document
---

<!-- The LCP image: at once, at high priority, across the whole viewport. -->
<StrifeImage image={page.hero} sizes="100vw" priority />

<!-- Further down: lazy, in a column at most 42rem wide. -->
<StrifeImage image={page.figure} sizes="(min-width: 44.5rem) 42rem, calc(100vw - 2.5rem)" />
```

The hero then reaches the browser like this (URLs shortened):

```html
<img
  src="https://cdn.strife.app/…/w:1672/….avif"
  srcset="https://cdn.strife.app/…/w:320/….avif 320w, …, https://cdn.strife.app/…/w:1672/….avif 1672w"
  sizes="100vw"
  width="3200"
  height="1350"
  alt="A skater mid-air above a concrete ramp"
  fetchpriority="high"
  style="background-color: #9aa4ac"
/>
```

`width` and `height` are the field's size: they only give the aspect ratio, so the browser reserves the right space before the file arrives and nothing jumps. In this example the original is 1672 pixels wide, so the largest candidate is 1672w, not 3200w: Strife never makes a candidate wider than the original.

### Check what the browser picked

Open the page with DevTools' device toolbar, select the image in the **Elements** panel and read `currentSrc` in the **Properties** pane (or filter the **Network** panel by **Img**). The `w:` in the URL is the candidate's width. For the hero above, a 390-pixel phone at 3x needs 1170 pixels and gets the 1216w candidate, and a 768-pixel tablet at 2x needs 1536 and gets 1672w, the largest there is. Reload after each change of screen size: a browser that already has a larger candidate in its cache may keep using it.

> [!NOTE]
> A change to `width`, `height` or `widths` in your schema reaches each image the next time an editor picks or crops it; until then the image keeps the candidates it was saved with. To get large candidates, upload originals at least as wide as the field's `width`.

### Live preview

While an editor zooms or pans an image in Strife Studio, the preview receives a temporary picture in `source.url` and no `source.srcset`; the candidates follow when the editor lets go. If your page updates images live (through `subscribe`, see [Custom Live Preview implementation](https://strife.app/docs/reference/live-preview/custom-live-preview-implementation.md)), remove `srcset` and `sizes` when the value has none. Otherwise the browser keeps showing the old candidates instead of the new picture:

```js
img.src = image.source.url;
if (image.source.srcset) {
  img.srcset = image.source.srcset;
  img.sizes = sizes;
} else {
  img.removeAttribute('srcset');
  img.removeAttribute('sizes');
}
```

## Placeholder

`source.placeholder` is the image's average color. Use it as a background so the reserved space isn't empty while the image loads:

```astro
<img … style={image.source.placeholder ? `background-color: ${image.source.placeholder}` : undefined} />
```

## When the shape has to change

Everything here is the same crop in different sizes. When a design needs a different crop per screen — a tall image on phones and a wide one on desktop — that is art direction, done with `<picture>` and a `<source media>` per shape. Cropping a wide image with CSS to fill a tall phone screen makes the phone download pixels it never shows.

## Further reading

* [Learn Images: Descriptive syntaxes](https://web.dev/learn/images/descriptive) — `srcset`, `sizes` and how the browser chooses
* [Learn Images: Prescriptive syntaxes](https://web.dev/learn/images/prescriptive) — `<picture>` and art direction
* [Optimize Largest Contentful Paint](https://web.dev/articles/optimize-lcp) — the LCP image
* [Browser-level image lazy loading](https://web.dev/articles/browser-level-image-lazy-loading)
* [MDN: Using responsive images in HTML](https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Responsive_images)
* [MDN: `sizes`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/sizes), including `auto`
