Responsive Image Utility
The Responsive Image Utility snippet provides an easier way to add production-ready responsive images to your site without writing complex <picture> elements with multiple image sources.
Using a combination of modern image strategies, including WebinOne's Image Processor to automatically size and convert to WebP image format (with JPEG fallback), <picture> element with multiple srcset variations, and optional lazy-load support with blurred Low-Quality Image Placeholder (LQIP) proxies, and pulsing load effects. This utility takes the hard work out of modern responsive and performant images, helping your site load faster, provide users with a better perceived loading experience, avoid jarring layout shifts, and score better in performance and best-practice evaluations.
There are 3 components to get this setup:
-
Snippet
The Snippet contains most of the heavy-lifting and Liquid conditions, and you configure it by passing your image path and a few settings via parameters - which are all documented and examples provided in the Snippet code below.
Create a new Snippet in your site’s admin with the name “Responsive Image Utility” and adjust the alias to “image_utility”. Then, copy and paste the code below into the Snippet contents via the ‘Code’ view.
NOTE:
Set your fallback image if no image path is provided, by changing thisassignvalue in the code:{% assign _src = '/favicon.png' %}. The default/favicon.pngis used as this is the most likely image to exist on all sites, and falling back to the site's logo or icon generally makes sense.{% comment %}<!-- Treehouse CODE v1.0.0 -->{% endcomment %} {% comment %} image_utility — Responsive <picture> with WebP + JPEG srcset via WebinOne Image Processor. When loading='lazy' is set, a Low-Quality Image Placeholder (LQIP) is shown until the full image loads. The <img> in the LQIP path has no src attribute — this prevents Chromium from race-fetching the JPEG fallback. loading='lazy' latches onto the <source> selection instead, so the browser correctly picks the WebP srcset candidate at load time. Parameters: src Base image path. Default: /favicon.png alt Alt text. Default: 'Image for {page name}' class CSS classes. size Layout hint: 'full' (default) or 'half' full = spans the full container width (max 1320px) half = approx. two-column layout, e.g. col-lg-6 sizes Power-user override: explicit sizes string. Takes precedence over size. e.g. '(max-width: 1399px) calc(100vw - 48px), 1320px' ratio Aspect ratio preset — sets aspect-ratio CSS + <img> width/height for CLS prevention. 'landscape' = 3:2 (standard photography, product shots) 'wide' = 16:9 (blog headers, editorial widescreen) 'square' = 1:1 (social-style, product grid) 'portrait' = 2:3 (team members, person photos) width Exact intrinsic width — takes precedence over ratio. Power-user override. height Exact intrinsic height — takes precedence over ratio. Power-user override. loading 'lazy' activates the LQIP placeholder. Omit for above-fold / LCP images. fetchpriority 'high' for LCP / hero images. Omit otherwise. quality JPEG fallback quality 1-100 (default: 75) - WebP quality is automatic/fixed. NOTE: use params.* (not this.params.*) — this.params is cached at page scope across component calls; params is properly scoped per invocation. Styles for `.lazyload-blur` and `.lazyload-blur img` should be saved in your CSS file. Only dynamic values (`background-image` URL and `aspect-ratio/min-height`) are set inline. Fade-in is handled by JS function `initLazyBlur` which adds `.is-loaded` on load and should be saved in your JS file. Usage — above-fold / Largest Contentful Paint (LCP) without LQIP: {% component type: 'snippet', alias: 'image_utility', src: '/media/hero.jpg', alt: 'Your Image Description', class: 'img-fluid rounded', size: 'half', ratio: 'landscape', fetchpriority: 'high' %} Usage — below-fold with LQIP: {% component type: 'snippet', alias: 'image_utility', src: '/media/image.jpg', alt: 'Your Image Description', class: 'img-fluid rounded', size: 'full', ratio: 'wide', loading: 'lazy' %} Usage — exact dimensions override (for power-users): {% component type: 'snippet', alias: 'image_utility', src: '/media/image.jpg', alt: 'Your Image Description', width: '1200', height: '400', loading: 'lazy' %} {% endcomment %} {% assign _src = params.src | replace: ' ', '%20' %} {% assign _srcMode = '' %} {% if _src == blank %} {% assign _src = '/favicon.png' %} {% assign _srcMode = '&mode=boxpad' %} {% endif %}{% assign _alt = params.alt %} {% if _alt == blank %}{% assign _alt = 'Image for ' | append: this.name %}{% endif %} {% assign _class = params.class %} {% assign _size = params.size %} {% assign _sizes = params.sizes %} {% assign _quality = params.quality %} {% assign _ratio = params.ratio %} {% assign _width = params.width %} {% assign _height = params.height %} {% assign _loading = params.loading %} {% assign _fp = params.fetchpriority %} {% if _quality == blank %}{% assign _quality = '75' %}{% endif %} {% if _sizes == blank %} {% if _size == 'half' %} {% assign _sizes = '(min-width: 1400px) 636px, (min-width: 992px) 456px, (min-width: 768px) 696px, calc(100vw - 24px)' %} {% else %} {% assign _sizes = '(min-width: 1400px) 1296px, (min-width: 992px) 936px, (min-width: 768px) 696px, calc(100vw - 24px)' %} {% endif %} {% endif %} {% assign _img_w = '' %} {% assign _img_h = '' %} {% assign _css_ratio = '' %} {% assign _has_ratio = false %} {% if _width != blank %} {% if _height != blank %} {% assign _img_w = _width %} {% assign _img_h = _height %} {% assign _css_ratio = _width | append: '/' | append: _height %} {% assign _has_ratio = true %} {% endif %} {% endif %} {% unless _has_ratio %} {% if _ratio == 'wide' %} {% assign _img_w = '1200' %} {% assign _img_h = '675' %} {% assign _css_ratio = '16/9' %} {% assign _has_ratio = true %} {% elsif _ratio == 'square' %} {% assign _img_w = '800' %} {% assign _img_h = '800' %} {% assign _css_ratio = '1/1' %} {% assign _has_ratio = true %} {% elsif _ratio == 'portrait' %} {% assign _img_w = '534' %} {% assign _img_h = '800' %} {% assign _css_ratio = '2/3' %} {% assign _has_ratio = true %} {% elsif _ratio == 'landscape' %} {% assign _img_w = '1200' %} {% assign _img_h = '800' %} {% assign _css_ratio = '3/2' %} {% assign _has_ratio = true %} {% endif %} {% endunless %} {% if _loading == 'lazy' %} {% if _size == 'half' %} {% assign _lqip_url = _src | append: '?width=60&blur=50&format=webp' %} {% else %} {% assign _lqip_url = _src | append: '?width=160&blur=50&format=webp' %} {% endif %} <span class="lazyload-blur {{ _class }}" style="background-image:url('{{ _lqip_url }}'); {% if _has_ratio %}aspect-ratio:{{ _css_ratio }};{% else %}min-height:200px;{% endif %}"> <picture class="riu"> <source type="image/webp" srcset="{{ _src }}?format=webp&width=480{{ _srcMode }} 480w, {{ _src }}?format=webp&width=800{{ _srcMode }} 800w, {{ _src }}?format=webp&width=1200{{ _srcMode }} 1200w, {{ _src }}?format=webp&width=1500{{ _srcMode }} 1500w" sizes="{{ _sizes }}" > <source type="image/jpeg" srcset="{{ _src }}?width=480&quality={{ _quality }}{{ _srcMode }} 480w, {{ _src }}?width=800&quality={{ _quality }}{{ _srcMode }} 800w, {{ _src }}?width=1200&quality={{ _quality }}{{ _srcMode }} 1200w, {{ _src }}?width=1500&quality={{ _quality }}{{ _srcMode }} 1500w" sizes="{{ _sizes }}" > <img alt="{{ _alt }}" {% if _img_w != blank %}width="{{ _img_w }}"{% endif %} {% if _img_h != blank %}height="{{ _img_h }}"{% endif %} loading="lazy" decoding="async" > </picture> </span> {% else %} <picture class="riu"> <source type="image/webp" srcset="{{ _src }}?format=webp&width=480{{ _srcMode }} 480w, {{ _src }}?format=webp&width=800{{ _srcMode }} 800w, {{ _src }}?format=webp&width=1200{{ _srcMode }} 1200w, {{ _src }}?format=webp&width=1500{{ _srcMode }} 1500w" sizes="{{ _sizes }}" > <source type="image/jpeg" srcset="{{ _src }}?width=480&quality={{ _quality }}{{ _srcMode }} 480w, {{ _src }}?width=800&quality={{ _quality }}{{ _srcMode }} 800w, {{ _src }}?width=1200&quality={{ _quality }}{{ _srcMode }} 1200w, {{ _src }}?width=1500&quality={{ _quality }}{{ _srcMode }} 1500w" sizes="{{ _sizes }}" > <img src="{{ _src }}?width=800&quality={{ _quality }}{{ _srcMode }}" alt="{{ _alt }}" class="{{ _class }}" {% if _img_w != blank %}width="{{ _img_w }}"{% endif %} {% if _img_h != blank %}height="{{ _img_h }}"{% endif %} {% if _fp != blank %}fetchpriority="{{ _fp }}"{% endif %} decoding="async" > </picture> {% endif %} -
Javascript
The Javascript function checks when lazy-loaded images have loaded and removes the loading animation on the blurred proxy images.
Copy and paste the script into your existing global JS file.
//## Treehouse CODE v1.0.0 ## /* LQIP fade-in — adds .is-loaded to images inside .lazyload-blur once loaded. Opacity and transition are defined in evanlite.css; no inline styles used. */ (function () { function revealImg(img) { img.classList.add('is-loaded'); } function initLazyBlur() { document.querySelectorAll('.lazyload-blur img').forEach(function (img) { if (img.complete && img.naturalWidth > 0) { /* Already loaded (cached) — reveal immediately */ revealImg(img); } else { img.addEventListener('load', function () { revealImg(img); }); } }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initLazyBlur); } else { initLazyBlur(); } }()); -
CSS
The CSS sets up a few key styles for the lazy-loading effects..
Copy and paste the styles into your existing global CSS file.
/* Treehouse CODE v1.0.0 */ .lazyload-blur { position: relative; display:block; overflow:hidden; background-repeat: no-repeat; background-size: cover; } .lazyload-blur:has( .is-loaded) { background-image: none !important; aspect-ratio: auto !important; } .lazyload-blur img { display:block; width:100%; opacity:0; transition:opacity 0.4s ease; } .lazyload-blur img.is-loaded { opacity: 1; } .lazyload-blur::before { content: ''; position: absolute; inset: 0; background-color: rgb(255 255 255 / 30%); backdrop-filter: blur(10px); z-index: 0; animation: pulse 2.5s infinite; } .lazyload-blur picture { position: relative; z-index: 1; } picture.riu img { margin: 0; padding: 0; } @keyframes pulse { 50% { background-color: rgb(255 255 255 / 10%); } }
Usage
Wherever you have images on your page or in your layouts, you can replace the regular <img> element and instead, call in the Snippet and add parameters to it to set up your image preferences.
Below are some examples:
Usage — above-fold images / Largest Contentful Paint (LCP) without LQIP:
{% component type: 'snippet', alias: 'image_utility',
src: '/media/hero.jpg',
alt: 'Your Image Description',
class: 'img-fluid rounded',
size: 'half',
ratio: 'landscape',
fetchpriority: 'high' %}
Usage — below-fold images with LQIP:
{% component type: 'snippet', alias: 'image_utility',
src: '/media/image.jpg',
alt: 'Your Image Description',
class: 'img-fluid rounded',
size: 'full',
ratio: 'wide',
loading: 'lazy' %}
Usage — exact dimensions override (for power-users):
{% component type: 'snippet', alias: 'image_utility',
src: '/media/image.jpg',
alt: 'Your Image Description',
width: '1200',
height: '400',
loading: 'lazy' %}
Usage — within a module layout, wired up for Liquid data:
{% component type: 'snippet', alias: 'image_utility',
src: '{{this.MainImage}}',
alt: 'Image for {{this.Name}}',
class: 'img-fluid rounded',
size: 'full',
ratio: 'wide',
loading: 'lazy' %}
The Snippet will output markup similar to the following:
<span class="lazyload-blur img-fluid rounded" style="background-image:url('/images/image.jpg?width=160&blur=50&format=webp'); aspect-ratio:3/2;">
<picture class="riu">
<source type="image/webp" srcset="/images/image.jpg?format=webp&width=480 480w, /images/image.jpg?format=webp&width=800 800w, /images/image.jpg?format=webp&width=1200 1200w, /images/image.jpg?format=webp&width=1500 1500w" sizes="(min-width: 1400px) 1296px, (min-width: 992px) 936px, (min-width: 768px) 696px, calc(100vw - 24px)">
<source type="image/jpeg" srcset="/images/image.jpg?width=480&quality=75 480w, /images/image.jpg?width=800&quality=75 800w, /images/image.jpg?width=1200&quality=75 1200w, /images/image.jpg?width=1500&quality=75 1500w" sizes="(min-width: 1400px) 1296px, (min-width: 992px) 936px, (min-width: 768px) 696px, calc(100vw - 24px)">
<img alt="Your Image Description" width="1200" height="800" loading="lazy" decoding="async">
</picture>
</span>
How it Works
Format Selection
- WebP is served to browsers that support it (Chrome, Edge, Firefox, Safari 16+)
- JPEG fallback for older browsers (Safari 15 and below, IE11)
Responsive Sizing
- Browser downloads the smallest image that fits its viewport (high-density displays will use larger images)
- 480px, 800px, 1200px, and 1500px variants are generated on-demand by WebinOne's Image Processor
sizesattribute tells the browser which variant to load at each breakpoint
Lazy Loading & LQIP
- When
loading: 'lazy'is set, a very samll and blurred placeholder renders immediately - Full image loads when it scrolls into or near view (depending on the browser)
- Fade-in animation handled by JS function
CLS Prevention
- Aspect ratio presets or explicit
width/heightreserve space before the image loads, prevent layout shifts - Browser recalculates layout once — no jank or reflow
Performance Tips
- Use
fetchpriority: 'high' on hero / above-fold images only (1–2 per page max) - Use
loading: 'lazy' on below-fold images to defer load - Set an aspect
ratiopreset or use exactwidth/heightto prevent cumulative layout shift (CLS)
Comments or questions? Head over to the WebinOne forum to discuss with the community.