Every image in a Markdown page.

::callout{type="tip" title="See it rendered"}
Every example below is a code block. [The image fixtures](~/demo/images) are the
same markup actually drawn — a dark twin, an original narrower than the column,
a vector, an animation and an image that refuses to zoom.
::

## What it adds

**Two variants.** A screenshot of a dark editor on a white page — or the
reverse — is the most common ugly thing in documentation, and CSS cannot fix it
because the pixels are wrong, not the frame. A page may name a second file:

```md
![The sidebar](/shots/sidebar.png){dark="/shots/sidebar-dark.png"}
```

**Two requests, not one file.** The image in the page and the image in the zoom
dialog ask for different widths, so a phone drawing a screenshot into a 48rem
column no longer downloads the 2400px original to do it. Both are `srcset`s
generated by `@nuxt/image`; neither is a file anyone has to prepare.

**Click to zoom**, unless the image asks not to:

```md
![A diagram](/diagram.svg){zoom="false"}
```

A zoomable image carries a small zoom indicator in its corner, permanently
rather than on hover — a cursor is nothing on a touch device and invisible in a
screenshot. The image is one button: `Tab` reaches it with a visible ring,
`Enter` and `Space` open the dialog, `Escape` closes it and returns focus. With
`zoom="false"` there is no indicator, no ring and nothing to tab into.

**A background for a transparent image.** A diagram whose ink is drawn for a
light surface vanishes on a dark page. Name a colour per theme — any CSS colour,
`var(--card)` included:

```md
![A diagram](/diagram.svg){background="#fafafa" dark-background="#171717"}
```

Colours rather than classes, on purpose: the layer's stylesheet is built from its
own components, never from Markdown, so a `dark:bg-…` class written in a page
would have no CSS behind it.

The viewer takes the whole screen over a blurred page. Its controls — zoom out,
the zoom level (which resets it), zoom in, open the original in a new tab, close —
float in a row above the image and the alt text sits below it, so neither covers
the picture. The wheel zooms around the cursor and a pinch around the fingers; a
click on the image zooms to 200% at that point and a second click returns; `+`,
`-` and `0` do the same from the keyboard. A zoomed image is dragged to pan, and
loads the original file so it stays sharp. Clicking the space around it closes
the viewer.

## API

### Props

| Prop     | Type      | Default | Notes                                              |
| :------- | :-------- | :------ | :------------------------------------------------- |
| `src`    | `string`  | —       | As written                                         |
| `alt`    | `string`  | —       | As written                                         |
| `dark`   | `string`  | —       | A second file, served under the dark theme         |
| `zoom`   | `boolean` | `true`  | `false` leaves the image inert                     |
| `width`  | `number`  | —       | Optional; see **Dimensions** below                 |
| `height` | `number`  | —       | Optional; see **Dimensions** below                 |
| `sizes`  | `string`  | —       | Overrides the in-page policy; see **Sizes** below  |
| `background`      | `string` | — | A CSS colour behind a transparent image, in the page and the viewer |
| `dark-background` | `string` | — | The same under the dark theme; falls back to `background`          |

## Sizes

The theme knows one thing `@nuxt/image` cannot: how wide the box is that it
draws an image into. That is the whole of what it configures.

- **In the page** — `sm:95vw md:768px`. Below `md` the article is the viewport
  less its gutters; from `md` up it is capped at `max-w-3xl` whatever the
  screen, so the slot stops growing rather than climbing on a 4K display.
- **In the dialog** — `sm:96vw md:96vw lg:96vw xl:1264px`, tracking the
  viewport up to the dialog's own `min(96vw, 80rem)` cap.

Both name only the breakpoints `@nuxt/image` already ships, so a consumer that
redefines `image.screens` keeps the redefinition. The layer declares no
`screens` of its own.

An image that is **not** as wide as the prose column — a logo, a badge, a
diagram set in a margin — says so:

```md
![The badge](/badge.png){sizes="sm:50vw md:200px"}
```

`sizes` is `@nuxt/image`'s own syntax and reaches the in-page image only. The
dialog keeps its policy: one describing the prose column cannot describe a
dialog.

## Dimensions

`width` and `height` are optional and stay that way — most pages omit them, and
requiring them would break every page already written.

**Supply them anyway where you can.** They are what lets the browser reserve the
box before the file arrives; without them the image has no intrinsic ratio until
it loads, and the text below it moves when it does. The component preserves the
ratio of whatever pair it is given and invents nothing when given none: there is
no default aspect ratio and no crop, because a wrong ratio is worse than a
reflow.

An image smaller than the column is left at its own size rather than stretched.

## Formats

**SVG and animated GIF are passed through untouched**, in the page and in the
dialog, with no `srcset` at all. A vector has no pixels to resample — asking a
provider for a 608px wide one rasterises a file that was sharp at every size —
and a GIF run through a raster pipeline comes back as its first frame.

This is what the component does, not a claim about providers: some pass these
formats through, some do not, and a documentation layer cannot know which one a
consumer configured. Responsive sources are for the raster formats the zoom
exists for — PNG, JPEG, WebP, AVIF.

## Providers

**The layer sets no `image` key.** Which service transforms an image is the
consumer's, the way `sources` is: a provider the layer picked is a provider
every consumer has to undo, and it decides an account and a bill as much as a
URL. Configure [`@nuxt/image`](https://image.nuxt.com/get-started/configuration)
as you would in any Nuxt app; the component only hands it two `sizes` strings.

Left alone, `@nuxt/image` picks for itself, and the three cases a documentation
site meets are:

- **A Node server.** `ipx` transforms on demand at `/_ipx/…`. It needs `sharp`,
  a native binding, which the Node output already carries.
- **A generated site** (`nuxt generate`). `ipxStatic` writes every variant at
  build time. Nothing runs afterwards, and the output grows by the variants —
  which is the trade a fully static site is making anyway.
- **An edge runtime** — Cloudflare Workers, and anything else that is neither
  Node nor static. No transformer runs and images are served as committed.
  **A prerendered site on such a runtime must not be configured into `ipx`**: it
  is a native binding and cannot run there at all. Reach for a CDN provider if
  the transforms are wanted.

The dialog's variants are generated on a static build even though the dialog is
closed when the page is rendered — the component registers them with the
prerender crawler itself. Nothing is downloaded until the dialog opens.

An image with `zoom="false"` gets none of them. There is no dialog to open, so
every dialog-sized file written for it would be output nobody can ever request.

## Notes

The dark variant is named explicitly rather than guessed by convention. Deriving
`-dark` from a filename means a missing file becomes a broken image, and the
author never finds out.

An image from a source cloned out of another repository has no route of its own:
Content downloads the tree into `.data/content/`, and nothing copies its assets
into `public/`. Paths in these examples are absolute into the consuming site's
own `public/`, which is where a documentation image has to live today.