Guide 04 · Developer docs

Input types, layouts, components and images

The platform's central abstraction, in one line: an input type defines what an editor can enter, a layout area binds one input type to a named slot, a layout groups areas and points at a template, a record or component holds the content for each area, and the template reads it back. Read this book straight after the template languages.

L1–L3
Unit 08 · Images end to end

Picture tags and responsive sources

Why the same template renders an img on one site and a picture tag with a WebP source on another, which sources the platform generates and how offeredSizes changes them, and when to write the picture element yourself.

Who this is for. Developers whose images come out as picture tags, or should. After this page you can answer "why did my image come out as a picture tag with a WebP source", judge whether the automatic sources suit a design, and build your own where they do not.

Why an image came out as a picture tag

The difference is a site setting, not the template. When Generate picture tag (key Files.images.generatePictureTag, off by default) is on, every image rendered through getHtml() on the site's pages, and every image embedded in a richtext area, becomes a <picture> element. The same call, with the setting off and then on:

HTML Generate picture tag: off
<img src="/files/images/pages/1/site-logo.png" alt="Site logo" class="img-fluid">
HTML Generate picture tag: on
<picture>
  <source srcset="/files/images/pages/1/site-logo.webp?f=png 1x" type="image/webp">
  <source srcset="/files/images/pages/1/site-logo.png 1x" type="image/png">
  <img src="/files/images/pages/1/site-logo.png" alt="Site logo" class="img-fluid">
</picture>

The markup is taken from a live site, with the file name generalised. The order is always the same: a WebP source (left out when the image is already WebP), a source in the original format, and an <img> fallback for browsers that ignore <picture>. The attributes you pass go on the <img>. An SVG, or an image whose dimensions cannot be determined, is always rendered as a plain <img>.

Because the template does not change, an image already converted in the template, such as toOptimizedFormat()->getHtml(), still works with the setting on; it has no separate WebP source to add.

High-density versions

Offer hq image (key Files.images.offerHqImage, on by default) adds a 2x version to each source for high-density screens, but only when three things are true together: picture tags are on, the image was resized in the template with getResized(), and the original is at least one and a half times the requested size in width or height. There is no point offering a sharper version the original cannot supply.

A 2x image has four times the pixels of the 1x one. The browser downloads only the one it needs, so the cost is extra files generated and stored, and heavier downloads for visitors with high-density screens. Turn it off only if you would rather serve every screen the smaller file.

How the sources are chosen: offeredSizes

Pass offeredSizes to getHtml() to say which versions the picture tag offers. Entries ending in x are pixel densities; entries ending in w are widths.

  • Densities only, such as ['1x', '2x'], give one source per format, each listing a 1x and a 2x file.
  • One width gives one source per format with no media query.
  • Several widths give one source per width, sorted from smallest to largest. Every width but the largest gets a max-width media query at that width; the largest gets a min-width query just above the next one down.
  • With picture tags off, a single <img> is rendered regardless of offeredSizes.

The media queries test the viewport's width. That is the question to ask of the automatic output: if the right image depends on the viewport alone, it is enough; if it depends on anything else, write your own (below).

Template code
{* densities *}
{$image->getResized(920, 480, 'fit')->getHtml(['offeredSizes' => ['1x', '2x'], 'alt' => $image->getName()|escape])}

{* or widths *}
{$image->getResized(920, 480, 'fit')->getHtml(['offeredSizes' => ['480w', '920w'], 'alt' => $image->getName()|escape])}
{# densities #}
{{ image.getResized(920, 480, 'fit').getHtml({'offeredSizes': ['1x', '2x'], 'alt': image.name|e}) }}

{# or widths #}
{{ image.getResized(920, 480, 'fit').getHtml({'offeredSizes': ['480w', '920w'], 'alt': image.name|e}) }}
Use one kind of entry per array

Do not mix densities and widths in one offeredSizes array, such as ['1x', '2x', '480w', '920w']. The platform then writes a width descriptor and a density descriptor for the same file into the same srcset, which is not valid srcset syntax, and browsers may ignore those sources. Pick densities or widths.

When the automatic output is not enough

The automatic sources choose by viewport width. Write your own <picture> when that is the wrong question:

  • Art direction: a different crop at each breakpoint, such as a wide banner on a desktop and a square on a phone.
  • Container-relative sizing: an image whose display size depends on the column it sits in rather than on the screen.

Build it from resized paths, resizing before converting, and take each source's type from the converted image, so it stays correct even when the original is already WebP or an SVG:

Template code
{$wide = $image->getResized(1600, 600, 'fit')}
{$square = $image->getResized(600, 600, 'fit')}
<picture>
  <source media="(max-width: 600px)" srcset="{$square->toOptimizedFormat()->getPath()}" type="{$square->toOptimizedFormat()->getMimeType()}">
  <source srcset="{$wide->toOptimizedFormat()->getPath()}" type="{$wide->toOptimizedFormat()->getMimeType()}">
  <img src="{$wide->getPath()}" alt="{$image->getName()|escape}" width="1600" height="600">
</picture>
{% set wide = image.getResized(1600, 600, 'fit') %}
{% set square = image.getResized(600, 600, 'fit') %}
<picture>
  <source media="(max-width: 600px)" srcset="{{ square.toOptimizedFormat().path }}" type="{{ square.toOptimizedFormat().mimeType }}">
  <source srcset="{{ wide.toOptimizedFormat().path }}" type="{{ wide.toOptimizedFormat().mimeType }}">
  <img src="{{ wide.path }}" alt="{{ image.name|e }}" width="1600" height="600">
</picture>

Use getPath() inside your own picture, not getHtml(): with picture tags on, getHtml() would put a picture inside your picture. The same approach gives a plain <img> with your own srcset and sizes, built from two or three resized paths, when the image needs width descriptors matched to a container. For how images fit into page speed as a whole, see Images and page speed.

Go deeper

Was this page helpful?

Your answer goes to the docs team.