Mastering the HTML Picture Tag: Responsive Images, WebP, and AVIF
Contents
I haven’t thought about HTML in a long time, but recently came across the usage of picture tag(s). I was so impressed that I wanted to blog about it. This is nothing new, and used a lot by the web already.
The picture tag gives us a lot of control over how images are loaded, solving two main problems: efficient formats and responsive sizing.
WebP Support
We all know WebP images are smaller and faster to load, but not every browser supported them initially (though support is great now). The picture tag allows us to serve WebP to browsers that support it, while falling back to PNG or JPEG for others.
|
|
The browser parses the <source> tags from top to bottom. The first one with a supported type is chosen. If none match, it falls back to the standard <img> tag.
Media Size (Responsive Images)
You don’t want to load a 4000px wide hero image on a mobile phone. It wastes bandwidth and slows down rendering. With the picture tag, we can switch images based on the viewport width using media queries.
|
|
In this case:
- Screens wider than 800px get
large.jpg. - Screens between 400px and 800px get
medium.jpg. - Anything smaller falls back to
small.jpg.
The Combination: Mobile vs. Desktop WebP
The real power comes when you combine both format switching and media queries. This allows for specific “Art Direction” where you might serve a cropped version of an image for mobile (to keep the subject visible) and a wide version for desktop, both in the most efficient format available.
|
|
This setup ensures:
- Desktop users get high-res, wide images in efficient WebP format.
- Mobile users get a smaller, potentially cropped image (saving massive bandwidth) in WebP.
- Legacy browsers (on any device) fall back to the JPEG version appropriate for their screen size (if you add media queries to the JPEGs too) or just the default
img.
Leveling Up: AVIF Support
If you want to go even further, AVIF (AV1 Image File Format) offers even better compression than WebP. The beauty of the picture tag is that you can just stack it on top. Browsers will grab the first format they understand.
|
|
You can combine this with media queries too, creating a robust “waterfall” of options:
- Desktop AVIF -> Desktop WebP -> Desktop JPEG
- Mobile AVIF -> Mobile WebP -> Mobile JPEG
Order Matters: First Match Wins
It is crucial to understand that the browser parses the <source> tags from top to bottom and stops at the first one that matches both the media query (if present) and the type support.
There is no complex scoring system. If you put the JPEG source at the top, the browser will see it, say “I support JPEG”, and download it immediately, ignoring your fancy AVIF and WebP files below.
Always order your sources from “Most Preferred” (newest/best format) to “Least Preferred” (fallback).
Future-Proofing
The picture tag is essentially future-proof. When a new, even better image format comes out in 5 years (let’s call it .superimg), you won’t need to rewrite your site’s logic or wait for all browsers to support it. You just add a new <source> line at the top:
|
|
Browsers that support it will take it; others will simply ignore it and move to the next line. This “progressive enhancement” built directly into HTML is why it’s often superior to complex JavaScript loading logic.
Doing it in Rails
In Rails, we can combine all of this—Art Direction, Format Switching (AVIF/WebP), and automatic variant generation—into a powerful, clean solution using ActiveStorage.
Let’s imagine we are building a product page. We want to show a high-quality, wide image for desktop users, and a smaller, square-cropped image for mobile users. And for each size, we want to prioritize AVIF, then WebP, then JPEG.
1. Define Variants in the Model
First, we tell ActiveStorage to pre-create these specific versions when a product image is uploaded. This ensures the images are ready to serve immediately, with no processing lag for the user.
|
|
2. The View Implementation
Now we can use the picture_tag helper to orchestrate all these sources. Rails will handle the URLs.
|
|
How this works in the browser:
-
Desktop User (Chrome/Firefox/Safari):
- Browser sees
media="(min-width: 1024px)". It matches. - It checks the first source:
type="image/avif". If supported, it downloads the Desktop AVIF. - If not, it checks
type="image/webp". If supported, it downloads the Desktop WebP. - If neither, it falls back to the Desktop JPEG.
- Browser sees
-
Mobile User:
- Browser ignores the
(min-width: 1024px)sources. - It sees the next source:
type="image/avif"(no media query, so it applies). It downloads the Mobile AVIF. - If AVIF isn’t supported, it tries the Mobile WebP.
- Finally, if all else fails (or on very old browsers), it loads the standard Mobile JPEG via the
<img>tag.
- Browser ignores the
This setup ensures every single user gets the absolute best image for their specific device and browser capabilities, all managed cleanly within Rails.
3. Cleaning Up with Helpers or Components
Writing that big block of HTML every time is tedious and error-prone. We can encapsulate this logic to keep our views clean.
Option A: A Simple Rails Helper
You can create a helper method that takes the attachment and handles the boilerplate.
|
|
Then in your view:
|
|
Option B: ViewComponent
If you prefer a more object-oriented approach (or if the logic gets more complex), a ViewComponent is perfect for this.
|
|
And in your view:
|
|
Both approaches drastically reduce noise in your templates and ensure consistent image handling across your entire application.
Why is this better than JavaScript?
You might argue that we could do all this with a bit of JavaScript. While true, the picture tag has significant advantages:
- Performance: The browser’s pre-parser can scan HTML and start downloading images before JavaScript and CSS are fully parsed and executed.
- No JS Dependency: It works even if JavaScript is disabled or fails to load.
- Semantics: It’s standard HTML, making it easier for search engines and accessibility tools to understand.
- Native Optimization: Browsers are highly optimized to handle these selections efficiently without the overhead of script execution.
It’s a robust, native solution that handles complexity declaratively.