How-to library · Developer docs

How-to library

Task-shaped pages: a title that starts with a verb, the situation in a few lines, the steps, the working code in both engines, and why this way rather than another. Found from a search, done, and left.

How-to Images

Reserve layout space with the aspect ratio to avoid layout shift

A resized image rendered with getHtml() gets width and height attributes automatically, which is all a browser needs. For unresized images and for wrappers, use getWidth(), getHeight() and an aspect-ratio style.

Context

Context. A page jumps when an image arrives if the browser did not know its size. It knows when the <img> carries width and height, or when the element or its wrapper has an aspect-ratio. getHtml() on a resized image adds width and height from the size you asked for; on an image that was not resized it adds neither, and a lazy-loaded slider or a CSS background gets no attributes at all.

Steps

  1. Resize every image you render, even to its own size class; the attributes come free.
  2. Where an image is not resized, pass width and height yourself from the object.
  3. Where the image is a background or loads later, put the ratio on the wrapper from the object's dimensions.
  4. Style images fluidly (max-width: 100%; height: auto) so the attributes set the ratio, not the rendered size.

Template code

Template code
{* resized: width and height are emitted for you *}
{$image->getResized(800, 450, 'fit')->getHtml(['alt' => $image->getName()|escape, 'loading' => 'lazy'])}

{* not resized: pass them *}
{$image->getHtml(['width' => $image->getWidth(), 'height' => $image->getHeight(), 'alt' => $image->getName()|escape])}

{* a wrapper that reserves the space before anything loads *}
<div class="media" style="aspect-ratio: {$image->getWidth()} / {$image->getHeight()}">
  {$image->getResized(1200, null)->getHtml(['alt' => $image->getName()|escape, 'loading' => 'lazy'])}
</div>
{{ image.getResized(800, 450, 'fit').getHtml({'alt': image.name|e, 'loading': 'lazy'}) }}

{{ image.getHtml({'width': image.width, 'height': image.height, 'alt': image.name|e}) }}

<div class="media" style="aspect-ratio: {{ image.width }} / {{ image.height }}">
  {{ image.getResized(1200, null).getHtml({'alt': image.name|e, 'loading': 'lazy'}) }}
</div>

Why this and not that

Not a fixed height in CSS: it crops or letterboxes whatever the author uploads. Not a pixel width in a style attribute passed to getHtml(): the platform takes pixel widths and heights out of the style, resizes to them and emits them as attributes, which is helpful and surprising in equal measure. Asking for only a width gives a width attribute plus an aspect-ratio style, which is enough.

Go deeper

Was this page helpful?

Your answer goes to the docs team.