Skip to content
duxt

ProseImg

An image: theme-aware, responsive, and zoomable.

Every image in a Markdown page.

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:

![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 srcsets generated by @nuxt/image; neither is a file anyone has to prepare.

Click to zoom, unless the image asks not to:

![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:

![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

PropTypeDefaultNotes
srcstring—As written
altstring—As written
darkstring—A second file, served under the dark theme
zoombooleantruefalse leaves the image inert
widthnumber—Optional; see Dimensions below
heightnumber—Optional; see Dimensions below
sizesstring—Overrides the in-page policy; see Sizes below
backgroundstring—A CSS colour behind a transparent image, in the page and the viewer
dark-backgroundstring—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:

![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 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.

Was this page helpful?