Question:

Generating srcset in a similar way to WordPress

Elena: 5 days ago

I come from a WordPress background, so I'm attempting to create responsive images the way I'm accustomed to with WordPress. Perhaps that's the wrong approach, I'm not sure.

In WordPress I would define five images size: thumbnail, medium, medium_large, large, XL and I would specify a width for each. Typically I would define the widths as 414, 768, 1024, 1600 and 1920 pixels respectively. For the sizes attribute I would specify this manually on an image-by-image basis.

Outputting an image would look like this:

<img src="<?php echo wp_get_attachment_image_url( $attachment_id, 'large' ); ?>"
     srcset="<?php echo wp_get_attachment_image_srcset( $attachment_id ); ?>" />

The above would output all 5 image sizes to the srcset attribute.

So, back to Craft. I have setup 5 asset transforms under Settings using the same handles as above. I believe you can also set these up progmatically too.

I can then output each of my image sizes in the following way:

{% set image = entry.aboutImage.withTransforms(['thumbnail', 'medium', 'mediumLarge', 'large', 'xl']).first() %}

<img src="{{ image.getUrl('medium') }}" alt=""
     srcset="{{ image.getUrl('xl') }} {{ image.getWidth('xl') }}w,
             {{ image.getUrl('large') }} {{ image.getWidth('large') }}w,
             {{ image.getUrl('mediumLarge') }} {{ image.getWidth('mediumLarge') }}w,
             {{ image.getUrl('medium') }} {{ image.getWidth('medium') }}w,
             {{ image.getUrl('thumbnail') }} {{ image.getWidth('thumbnail') }}w"
     width="{{ image.getWidth() }}" height="{{ image.getHeight() }}" />

Whilst this works okay, it's kind of cumbersome to repeat this for every image. Also, if the user uploads a very small image then I'm guessing it won't be available in all sizes.

I noticed that there is also this method:

{{ asset.getImg({ width: 300, height: 300 }, ['1.5x', '2x', '3x']) }}

However I'd like to do the following, which doesn't work:

{{ asset.getImg({ width: 300, height: 300 }, ['thumbnail', 'medium', 'mediumLarge', 'large', 'xl']) }}

I've read about macros but so far haven't really been able to fathom out how I'd achieve the above using a macro, or it it's necessary.

Answer:
Easton: 5 days ago

You can write a function, a utility template or a macro to make repetitive tasks like generating responsive images easier. I usually prefer templates, because they're easy to extend and they can take a lot of optional arguments without the user needing to pass all of them.

Building your own utility templates is a good time investments, since you can create a set of parameters / options into the template that you can use wherever you want to display images in your templates.

For example, here's a simple responsive image template that will take an image asset and a list of transforms and output an <img> tag:

{# utils/responsive-image.twig #}
{%- set transforms = transforms|default([]) -%}
{%- set transformedImages = transforms|map(transform => image.copyWithTransform(transform)) -%}
{%- set srcset = transformedImages|map(image => "#{image.getUrl()} #{image.width}w") -%}
{%- set attributes = attributes ?? {} -%}

<img {{ attr(attributes|merge({ srcset: srcset|join(', '), })) }}>

Since this template accepts a list of transforms (https://docs.craftcms.com/api/v3/craft-elements-asset.html#public-methods), you can pass it any parameters that are accepted by Asset.getUrl() (https://docs.craftcms.com/api/v3/craft-elements-asset.html#public-methods). So both named transformes as well as hashes will work:

{% set image = entry.header_image.one() %}
{{ include('utils/responsive-image', {
    image: image,
    transforms: [{ width: 300, height: 300 }, { width: 600, height: 600 }],
}, with_context = false) }}
{% set image = entry.header_image.one() %}
{{ include('utils/responsive-image', {
    image: image,
    transforms: ['thumbnail', 'medium', 'mediumLarge'],
}, with_context = false) }}

Note the utility template also includes an optional parameter to specifiy additional attributes for the <img> tag. Here's a complete example:

{% set image = entry.header_image.one() %}
{{ include('utils/responsive-image', {
    image: image,
    transforms: [{ width: 300, height: 300 }, { width: 600, height: 600 }],
    attributes: {
        width: 300,
        height: 300,
        srcset: '(min-width: 300px) 300px, 100vw',
        class: 'my-responsive-image',
    },
}, with_context = false) }}

Once you have the basic setup in place, you can expand your utility template to handle more repetitive tasks for you. For example, my utility template for responsive images can:

  • Generate a <picture> with multiple sources.
  • Automatically generate alternative <source> tags with WebP images.
  • Apply a fixed aspect ratio to all transforms.
  • Handle SVGs gracefully.
  • Take the alt text from the asset's alt field, if it exists.
  • … and much more.