Responsive images with Statamic, Tailwind and Glide

In Statamic it's pretty easy to handle responsive images using Glide and Antlers. I initially wrote this post over two years ago for Statamic v2 and it's proven to be the most visited page of my website. For my new website I decided to give it an update. Here we go.

My Starter Kit Peak contains full blown responsive images support. It doesn't use any addons or custom code, it's just native Antlers and Statamic. This is how I prefer to develop my websites as it leaves me in control and on a per site basis I can tailor everything to different demands.

Making sure you deliver the right assets to the client helps performance and positioning within search engines. It's important for a good user experience and fast loading times. So how can we do this? Let's start by configuring Statamic.

Configuring Statamic

Let's configure Statamic so that it automatically generates asset presets when a user uploads an image to the control panel. We want Statamic to generate different sizes for each image so we can serve users the lowest image resolution possible depending on their device without sacrificing image quality. Statamic uses the Glide library for this. Luckily Glide can also convert images to WebP. So let’s throw that in their as well.

Note: I use Imagick as an image processor instead of GD. I've found it produces better results with smaller files.

For all this magic you need to to edit your config/statamic/assets.php file. This is my current setup.

1'presets' => [
2 'xs-webp' => ['w' => 320, 'h' => 10000, 'q' => 85, 'fit' => 'contain', 'fm' => 'webp'],
3 'sm-webp' => ['w' => 480, 'h' => 10000, 'q' => 85, 'fit' => 'contain', 'fm' => 'webp'],
4 'md-webp' => ['w' => 768, 'h' => 10000, 'q' => 85, 'fit' => 'contain', 'fm' => 'webp'],
5 'lg-webp' => ['w' => 1280, 'h' => 10000, 'q' => 85, 'fit' => 'contain', 'fm' => 'webp'],
6 'xl-webp' => ['w' => 1440, 'h' => 10000, 'q' => 95, 'fit' => 'contain', 'fm' => 'webp'],
7 '2xl-webp' => ['w' => 1680, 'h' => 10000, 'q' => 95, 'fit' => 'contain', 'fm' => 'webp'],
8 'xs' => ['w' => 320, 'h' => 10000, 'q' => 85, 'fit' => 'contain'],
9 'sm' => ['w' => 480, 'h' => 10000, 'q' => 85, 'fit' => 'contain'],
10 'md' => ['w' => 768, 'h' => 10000, 'q' => 85, 'fit' => 'contain'],
11 'lg' => ['w' => 1280, 'h' => 10000, 'q' => 85, 'fit' => 'contain'],
12 'xl' => ['w' => 1440, 'h' => 10000, 'q' => 95, 'fit' => 'contain'],
13 '2xl' => ['w' => 1680, 'h' => 10000, 'q' => 95, 'fit' => 'contain'],
14],
The Statamic asset configuration file. These are the presets that get generated for every uploaded asset.

Having this in place, make sure you add the following to your deploy script (check out an example deploy script) and run it if you already have some assets uploaded to your asset container.

php please assets:generate-presets

This will make Statamic generate all the different asset versions we want. Running this after deployment makes sure that the images are always there should the image cache be cleared. If you don’t pre generate all the asset variants, Statamic will do this when the first visitor drops by. This will result in a terrible page load for this one unlucky person. After that the presets will be cached.

Personally I use Redis for queue management so users won’t notice any slowness during the upload process. It basically moves tasks to a queue in the background. It might take longer to complete but your server will still respond to other requests.

The Antlers <picture> partial

Now we’ve got our assets prepared, it’s time to make the _picture.antlers.html partial.

1{{#
2 @name Picture
3 @desc The picture component. A responsive imageset in a picture element. See `resources/views/components/_figure.antlers.html` for an example on how to use this.
4#}}
5 
6<!-- /components/_picture.antlers.html -->
7{{ if image }}
8 <picture>
9 {{ asset :url="image" }}
10 {{ if extension == 'svg' || extension == 'gif' }}
11 <img class="{{ class }}" src="{{ url }}" alt="{{ alt }}" />
12 {{ else }}
13 <source
14 srcset="
15 {{ glide:image preset='xs-webp' }} 320w,
16 {{ glide:image preset='sm-webp' }} 480w,
17 {{ glide:image preset='md-webp' }} 768w,
18 {{ glide:image preset='lg-webp' }} 1280w,
19 {{ glide:image preset='xl-webp' }} 1440w,
20 {{ glide:image preset='2xl-webp' }} 1680w"
21 sizes="{{ sizes }}"
22 type="image/webp"
23 >
24 <source
25 srcset="
26 {{ glide:image preset='xs' }} 320w,
27 {{ glide:image preset='sm' }} 480w,
28 {{ glide:image preset='md' }} 768w,
29 {{ glide:image preset='lg' }} 1280w,
30 {{ glide:image preset='xl' }} 1440w,
31 {{ glide:image preset='2xl' }} 1680w"
32 sizes="{{ sizes }}"
33 type="{{ image.mime_type }}"
34 >
35 <img
36 {{ if cover }}
37 class="object-cover w-full h-full {{ class }}"
38 style="object-position: {{ focus | background_position }}"
39 {{ else }}
40 class="{{ class }}"
41 {{ /if }}
42 src="{{ glide:image preset='lg' }}"
43 alt="{{ alt ensure_right='.' }}"
44 width="{{ image.width }}"
45 height="{{ image.height }}"
46 {{ if lazy }}
47 loading="lazy"
48 {{ /if }}
49 >
50 {{ /if }}
51 {{ /asset }}
52 </picture>
53{{ /if }}
54<!-- End: /components/_picture.antlers.html -->
The Antlers picture partial as available in Peak.

So what’s exactly happening here?

  1. We’re using a <picture> element because it can hold multiple <sources> for our images. For example if your browser doesn’t support WebP, it will fall back to JPG.

  2. On line 11, 37 and 40 you’ll also notice we can define classes for our <picture> element. I’ll show an example of how to do this when we call in our partial later.

  3. On line 2 we’re using the {{ asset }} tag that uses an image as a variable. First on line 10 it checks to see if the asset is an .svg or .gif file. In that there's nothing to optimise. Just render the asset with an alt description and be done with it.

  4. On line 14 it gets interesting. We’re defining a srcset for our image source with all the WebP presets we previously generated using our php configuration file. We’re basically providing the browser a set of images and telling it at what width to use it.

  5. The value of the sizes attribute on line 21 and 32 is completely dependent on where and how you’re going to use this image. It expects something like this: (min-width: 768px) 40vw, 90vwWhat we’re telling the browser here is that the image is being rendered at 40vw on screens larger than 768px and 90vw on other (smaller) screens. Together with the srcset the browser can now figure out which of the 6 images it should download for the current screen our user is viewing the website on.

  6. At line 26 we’re repeating the same stuff for browsers that don’t support WebP and need JPG’s.

  7. On line 33 we define the correct image type.

  8. On line 36 some optional classes and a inline styles can be added when you’re using the object-fit css property. This utilizes Statamic’s awesome focal point feature. That way we can object-fit: cover our image in a container and maintain focus on a user defined focal point. Here I use Tailwind utility classes.

  9. The default image on browsers that don’t support srcsets is the lg preset on line 42.

  10. And as a bonus: if your browser supports lazy loading that’s defined on line 46.

Calling in the partial

When we’re calling in the partial, just provide it with the necessary arguments. Take this example where I also using aspect ratio utilities to control sizing:

1<figure class="aspect-w-4 aspect-h-3 border border-primary dark:border-secondary">
2 {{ partial:components/picture :image="image" cover="true" lazy="true" sizes="(min-width: 768px) 33vw, 90vw" }}
3</figure>
Calling in the picture partial. You can do this wherever you need responsive images.

And that’s it. We’re rendering a responsive image thats being held in our image variable. We’re passing the <picture> element some utility classes to define its appearance, set cover to true, and we’re letting the browser figure out which asset to download for 90vw on mobile and 40vw on larger screens. You could also wrap the partial tag in a <figure> element with a <figcaption>. It all depends on your use case.

I hope it’s helpful. Feel free to drop me a line on Twitter if you have any questions or enhancements.

A rendered code example with Statamic, Tailwind and Alpine

  • Statamic
  • Peak
  • Tailwind CSS
  • Alpine JS