Image Variants and Responsive Images

Every eligible <img> in your rendered HTML gets a srcset and a sizes attribute added at build time. You do not write that markup, and you cannot write it better by hand — but you do need to know when the rewrite applies, what it produces, and how to stop it.

The variants

Six responsive widths exist, and all six can appear in a srcset.

VariantWidthHeightIn srcset?
small320240Yes
medium640480Yes
large1280720Yes
xl19201080Yes
xxl25601440Yes
original51202880Yes

The names are xl and xxl — never xlarge or xxlarge.

A variant URL is formed by string replacement: the segment original in the image-delivery path is swapped for the variant name. So …/imagedelivery/abc/def/original becomes …/imagedelivery/abc/def/medium. That is the whole mechanism, and it is why the rewrite only makes sense on a Leed image-delivery URL.

Four further named sizes exist and never appear in a srcset:

VariantWidthHeightWhere it is used
social800418The Open Graph and Twitter card image on every page
socialtiny12867Registered, but referenced by no shipped Leed template — available to your own
profile400400Author avatars in the blog layout and the JSON feed
profiletiny4848Inline avatar icons and bylines

Ask for one of those by name from a template helper. They are fixed sizes for a fixed job, not rungs on a ladder.

What the rewrite does

For each eligible image, in this order:

  1. If the image carries a width attribute above 5120, clamp it to 5120.
  2. Build srcset as a comma-separated list of <url> <width>w pairs, one per chosen variant.
  3. Add sizes="100vw" — only if the image has no sizes of its own.
  4. Stash the untouched URL on data-pristine.
  5. Drop data-responsiver if it was present, since it has now been consumed.

For a lazy-loading image — one with data-src rather than src — everything above is written to the data- attributes instead: data-src and data-srcset.

Given this input:

<img src="https://x.test/cdn-cgi/imagedelivery/abc/def/original" width="1792">

the build emits exactly this:

<img data-pristine="https://x.test/cdn-cgi/imagedelivery/abc/def/original"
     sizes="100vw"
     srcset="https://x.test/cdn-cgi/imagedelivery/abc/def/small 320w,
             https://x.test/cdn-cgi/imagedelivery/abc/def/medium 640w,
             https://x.test/cdn-cgi/imagedelivery/abc/def/large 1280w,
             https://x.test/cdn-cgi/imagedelivery/abc/def/original 1792w"
     src="https://x.test/cdn-cgi/imagedelivery/abc/def/original"
     width="1792">

(Line breaks added for reading; the real attribute is one line.)

How the width list is chosen

With no width attribute, you get all six variants. With width="N", you get the variants at or below N, plus one entry carrying the declared width itself whenever anything larger was dropped — which is what produces the original 1792w row above.

Two width-list quirks, documented as behavior rather than advice

Both are longstanding behavior inherited from the plugin this replaces, and published pages already carry them. Neither breaks anything visible; both will confuse you if you read the emitted srcset closely.

A declared width that exactly matches a variant repeats it. The declared width is a string and the variant widths are numbers, so the “did we already cover this” check never matches:

<!-- width="640" -->
srcset="…/small 320w, …/medium 640w, …/medium 640w"

A non-numeric width is carried through verbatim, as a single entry built from the raw text:

<!-- width="undefined" -->
srcset="…/original undefinedw"

The same applies to an empty width (original w), a zero (original 0w), a negative (original -5w) and a padded one, which keeps its surrounding whitespace. Exponential notation is compared numerically but emitted as written: width="1e3" yields small 320w, medium 640w, original 1e3w.

The ways an image is skipped

Six rejection conditions, applied in this order, plus two exclusions that happen before any of them are reached.

ConditionDetected byUse it when
Direct child of <picture>The selector, :not(picture) > imgYou have declared the sources yourself and want art direction
Inside a <template>The DOM query never descends into template contentNever deliberately — it is a consequence, not a control
data-responsiver="false"The attributeYou want one specific image left exactly as written
Neither src nor data-srcAttribute checkAn image whose source arrives from script
An existing srcsetAttribute checkYou wrote the srcset by hand
No src, but a data-srcsetAttribute checkA lazy loader that already has its own list
A src ending in .svgString check on the URLVectors do not need variants — this is automatic
A data: URI with no data-src or data-srcsetString check on the URLAn inline placeholder that is not a lazy-load stub

The <picture> exclusion is the one to reach for deliberately:

Here is the whole decision, in the order the code applies it:

flowchart TD
  S([Every img in the rendered page]) --> P{Direct child<br/>of picture?}
  P -->|yes| X[Left untouched]
  P -->|no| R{data-responsiver<br/>= false?}
  R -->|yes| X
  R -->|no| H{Has src or<br/>data-src?}
  H -->|no| X
  H -->|yes| E{Already has<br/>a srcset?}
  E -->|yes| X
  E -->|no| D{No src, but<br/>a data-srcset?}
  D -->|yes| X
  D -->|no| V{src ends<br/>in .svg?}
  V -->|yes| X
  V -->|no| U{Bare data: URI?}
  U -->|yes| X
  U -->|no| W[Clamp width, build srcset,<br/>add sizes, stash data-pristine]
  W --> C{Is it a Leed<br/>image-delivery URL?}
  C -->|yes| K[Kept]
  C -->|no| B[Undone — src restored,<br/>srcset and sizes removed]

Non-CDN images are put back

The last step is a reversal, and it is the reason the decision tree has a tail.

After the rewrite runs, the build checks the stashed data-pristine URL. If it is not a Leed image-delivery URL, everything just written is taken back off: the original src is restored, and srcset, sizes and data-pristine are removed. The log names the image:

Image url doesn't support srcset: https://example.com/photo.jpg...

So an image pointing at a third-party host is left exactly as you wrote it. That is correct — there is no variant ladder behind someone else’s URL.

The practical consequence is the reason to use the asset manager rather than committing image files. An image committed into src/static/images/ and referenced by path gets no responsive variants at all. Not because it is forbidden, but because a static file has no variant ladder behind it: there is no medium of it to fetch. Only an image uploaded as an asset and referenced by its delivery URL participates in any of this.

That is a delivery argument rather than a housekeeping one. A hero image committed to the repository ships its full-resolution self to a phone; the same image uploaded as an asset ships a 320-wide variant. This documentation set follows that rule for every one of its screenshots.

What this means for your templates

Four rules, all of them consequences of the above.

  • Always set a real width. It bounds the variant list and it stops layout shift.
  • Wrap in <picture> when you want art direction — a different crop at a different breakpoint — and Leed keeps its hands off entirely.
  • Write your own sizes when the image is not full-bleed. The default of 100vw is a promise that the image fills the viewport width; a 400px avatar that claims 100vw makes the browser fetch a variant far larger than it needs. Your value is kept as written.
  • Remember this runs on .html output only. The image-domain and script-include rewrites also apply to .htm; this one deliberately does not.

The rewrite is one of several DOM passes over the rendered page, and the order between them is load-bearing — image domains are made absolute before the variant URLs are built from them. The full order is on how templates work. The helpers that emit these <img> tags in the first place, including the ones that ask for social or profiletiny by name, are on image and media helpers.

This is not the CSS rewrite

A short disambiguation, because the two get conflated and they are neither the same stage nor the same mechanism.

The transform on this page runs over your rendered HTML, after templates have produced it, and it builds a srcset from a Leed image-delivery URL.

Separately, PostCSS rewrites url() paths inside your stylesheets during the Tailwind compile: any url() whose path contains images/ is rewritten to /static/images/…. That is a path fix, it produces no variants, and it is described on the Tailwind build.

What a visitor actually receives once these URLs reach the CDN — the resizing, the format negotiation, the adaptive video — is on asset delivery and protection. Authors set width, alt text and optional captions from the editor; the markdown form of all three is on images and figures.

ESC