Responsive images in Contao from a single upload
Define Contao image sizes once and render <picture> markup with WebP fallbacks from one high-resolution upload.
19 Sept 2025, 11:32 UTC

One upload, many breakpoints
A 3000×2000 pixel hero image looks sharp on a desktop monitor, but Contao serves that same file to a phone on a metered connection unless you configure it otherwise. The result is wasted bandwidth and a slower Largest Contentful Paint, a web performance metric that measures when the main content appears. Contao can generate responsive <picture> and srcset markup from a single high-resolution upload, but only after you define image sizes and call the right Twig function in your template.
This post walks through defining image sizes, rendering responsive markup, handling art direction with focal points, and clearing the image cache when definitions change. It assumes Contao 4.4 or later; native loading='lazy' on images arrived in Contao 4.9.
1. Define image sizes
Image size definitions live in contao/config/imageSizes.php or in the back end under Themes → Image Sizes. Each definition specifies width, height, a resize mode such as proportional, box, or crop, and an optional CSS class. The resize mode tells Contao’s Imagine library—the image processing component—how to fit the source into the target dimensions.
return [
'teaser' => [
'width' => 400,
'height' => 300,
'resizeMode' => 'crop',
'class' => 'img-teaser',
],
'full' => [
'width' => 1200,
'height' => 800,
'resizeMode' => 'proportional',
],
];
After saving, assign sizes to content elements such as Image, Text with Image, or Gallery. Contao stores the chosen image by UUID—a unique identifier in the file manager—not by filesystem path.
2. Render markup in Twig
Use Contao’s picture() Twig function, passing the image UUID and the size key:
{{ picture(image.uuid, 'teaser') }}
The function emits the full responsive markup, including WebP sources when a WebP-capable Imagine driver and MIME configuration are present. It does not accept a raw path string. Passing a missing UUID throws a runtime error in production, so wrap the call in a null check when the image might be absent:
{% if image.uuid %}
{{ picture(image.uuid, 'teaser') }}
{% endif %}
3. Art direction, focal points, and WebP
Art direction means showing different crops of the same image at different breakpoints. Assign variants such as teaser, full, and mobile to the same UUID, then set a focal point in the file manager. Contao uses those coordinates when cropping so the subject stays in frame.
WebP output is not enabled by default. It requires the WebP Imagine extension—commonly installed as contao/imagine-webp—and explicit MIME registration in config/mime.php. A MIME type tells the browser how to interpret a file, such as image/jpeg versus image/webp. After enabling it, clear the image cache and reload the page to confirm a <source type='image/webp'> tag appears before the JPEG fallback.
4. Cache invalidation and memory limits
Contao caches derivatives under the configured assets path, often files/_shared_/ or var/cache/prod/images/. The cache key includes the image UUID, size key, and modification timestamp, so replacing a source image invalidates the derivative automatically. Changing an image size definition does not regenerate existing derivatives. Derivative filenames typically follow the pattern {uuid}_{sizeKey}_{hash}.{ext}, which helps you confirm the right variant was generated.
To clear only image derivatives, run the console command from the Contao project root:
# Run from the Contao project root
# Requires shell access and write permission to var/cache/
php vendor/bin/contao-console contao:cache:clear --image --env=prod
Expected check: reload a page that uses the cleared size and confirm new derivative files appear under the image cache directory. Risk: pages may show missing or broken images briefly until Contao regenerates the derivatives on the next request.
Large batches of high-resolution source images can exhaust memory_limit during Imagine processing. If you import 50+ images above 20 megapixels, raise PHP memory and execution limits for the console process, or import in smaller batches.
Worked example: verify the output
- Create a test page and add an Image content element.
- Upload a 3000×2000 pixel JPEG.
- Assign the
teaserandfullsizes defined above. - Open the published page and inspect the front-end source.
- Look for markup similar to:
<picture>
<source srcset='/files/_shared_/teaser/abc123_teaser_a1b2c3.webp 400w' type='image/webp'>
<source srcset='/files/_shared_/teaser/abc123_teaser_a1b2c3.jpg 400w'>
<img src='/files/_shared_/teaser/abc123_teaser_a1b2c3.jpg' alt='...'>
</picture>
The exact hash in the filename changes with the source modification time. If you do not see <picture> markup, confirm the content element has an image size assigned and that the template uses picture() rather than a plain <img> tag.
Limitation: larger HTML payload
Responsive markup adds multiple source entries to the HTML. On pages with many images, combine this with native lazy loading and HTTP/2 multiplexing. Monitor the transferred HTML size; if it grows too much, reduce the number of image sizes per page or serve fewer variants.
Closing checklist
- Define image sizes in
contao/config/imageSizes.phpor the back end. - Use
picture(uuid, 'sizeKey')in Twig, with a null check. - Set focal points in the file manager for crop variants.
- Enable WebP only after installing the Imagine extension and registering MIME types.
- Run
contao:cache:clear --imageafter changing size definitions.
One upload should not mean one file for every screen. With sizes defined and picture() in place, Contao handles the derivatives so your visitors download only what their viewport needs.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.