title: When Responsive Images Get Ugly
url: http://codepen.io/Tigt/post/when-responsive-images-get-ugly
hash_url: 16d800a1fa
I don’t know about you, but I’m over the current crop of “How to use our new friends <picture>
and srcset
” tutorials. They show you some Baby’s First Markup which is nice for teaching purposes, but doesn’t prepare you for the ugliness that more… unusual applications require.
I’ve encountered a few corner-cases and quirks to beware of and found a few of said unusual applications. Maybe they can help you.
I’m assuming you already have a basic grasp of responsive images. If not, I recommend Cloud Four’s introduction series.
If the ensuing code has you despairing that responsive images are more trouble than they’re worth, refer to this section to remember that overlarge images are painful, in a variety of ways:
An oversized image wastes time, bandwidth, data, battery, and system resources. Even if you don’t use a polyfill, you should use responsive images for the 56% (and growing!) of browsers that support them; anything else would be irresponsible.
srcset
is preferable<picture>
is darling, but a plain <img srcset>
is more flexible: bandwidth-saving modes, browser heuristics, and user choices. It’s also more future-friendly; <picture>
is only as good as your media queries. srcset
is the smart choice.
Like everything else on the front-end, responsive images are fraught with edge cases and unforeseen applications. I’ve collected a couple things that have tripped me up, in the hopes that they won’t do the same to you.
The most important and trickiest part of responsive images is the sizes
attribute. If it doesn’t tell the browser exactly how big the image is going to be, you risk either a scaled-up mess or a scaled-down bloat. So naturally the sizes
syntax is error-prone.
In Standardese, the sizes
attribute takes a comma-separated list of <source-size-value>
s, which break down into a media condition (not a media query!) and a length, like this:
sizes="(media: condition) length,
(media: condition) length,
(media: condition) length"
The length is the width of the image if the media condition is true. It accepts a value, like 700px
, or a calc()
expression.
Media conditions are almost media queries. The spec codifies them in a formal CSS grammar, but for us mere mortals the rules are:
screen
, print
, tv
, etc.)(feature: value)
pairs you like.and
, or
, and not
. Do not use commas as a substitute for or
.or
or not
keywords, you must wrap the entire media condition in parentheses.But I've always preferred examples. Much simpler to get the shape of things from them than trying to string together a buncha rules.
Ugly? Sure. Powerful? You bet.
Now, because this is web development, and nothing can ever be easy, browsers aren’t required to have fully implemented media condition grammar before they implement responsive images. You may need backup conditions to work around that:
"((min-height: calc(50vw - 100px)) or (light-level: dim)) 500px,
(min-height: calc(50vw - 100px) 500px,
(light-level: dim) 500px"
As always, test, test, test. I recommend placehold.it for images that explicitly indicate their widths; lessens ambiguity and saves you from having to check the currentSrc
every time.
Note: Percentages are not allowed in a
<source-size-value>
, to avoid confusion about what it would be relative to. The vw unit can be used for sizes relative to the viewport width.
The browser image preloader wants the proper URL to request ASAP. It’s not waiting around for your CSS to download and parse, it needs units it can use now. And since the %
unit only works if you know how big an element’s parents are, the CSS is required to calculate it.
rem
and em
are supported, but be careful. They’re resolved like they are inside media queries: equal to the user’s default font-size
. (Which is often 16px, but not always.) If you’re respecting that default, you should be good to go.
But if you’re doing something like html {font-size: 10px}
to make the math easier, well, don’t. That doesn’t respect the user, and it prevents you from using the power of em
s in media queries.
You also can’t do html {font-size: 62.5%}
if you want to use em
in sizes
. What with preprocessors nowadays you don’t need to, and embracing the default user font-size
is a good practice. Prevents a lot of inheritance woes.
As for why you’d size an image with em
in the first place…
em
, you'll reflect them in your media conditions.em
s for the perfect line length, and images inside it are max-width: 100%
, they can end up sized in em
s too.As for those weirdo units, like ch
or ex
or pc
… If you use them, Tim Berners-Lee calls Håkon W. Lie, who calls Bert Bos, and he calls the authorities.
sizes
will affect how your image displayssizes
appears to be just hints about how your CSS scales the image, but it actually changes how the image will be displayed all on its own. Observe:
If your screen is big, you can see the last image is way enormous. It doesn't have a sizes
attribute, and browsers assume sizes="100vw"
when that happens. Widespread misuse of this default is why it’s no longer valid to omit sizes
on a responsive image.
If you aren’t using CSS to explicitly size the image at all times, like if you’ve got img { max-width: 100% }
and not all your images will be as big as their containers, be careful and test to make sure your sizes
attribute isn’t running amok. Browsers treat the length of the chosen <source-size-value>
like <img width>
: if you don’t override it with CSS, that’s how wide it will be.
Using <picture>
and the full power of the media
attribute, you can make especially important images (logos, diagrams, etc.) adapt best to device capabilities and circumstances.
<picture>
<source srcset="logo-printer-friendly.svg" type="image/svg+xml" media="print, (monochrome: 1)">
<img src="logo.png" alt="Butt-Touchers Incorporated">
</picture>
You’ve probably seen print stylesheet tutorials that display: none
images and substitute printer-friendly versions as background images. Since not all browsers print with background images & colors on (at least, not by default), this version is much more robust. For important content images, it’s way better.
I also specify the image for devices that can’t display colors (monochrome: 1
), because hey, we already made a B&W version for saving ink.
A device that qualifies for update-frequency: slow
(mostly e-Ink devices) probably wouldn’t play GIFs, but if it did, it would look very muddy and blurry as it would struggle to keep up.
In either case, it would be much nicer to display a still frame that’s representative of the GIF instead. (As opposed to showing the first frame, which is an often unsuitable default.) So that’s what we do here, and hey, print doesn't have an update frequency, right?:
<picture>
<source srcset="reaction-frame.png" media="print, (update-frequency: slow)">
<img src="reaction.gif" alt="Say whaaaaat?">
</picture>
<picture>
<source srcset="floorplan-dark.svg" media="(light-level: dim)">
<source srcset="floorplan-hicontrast.svg" media="(light-level: washed)">
<img src="floorplan.png" alt="The plans for how we'll renovate the apartment." longdesc="#our-plans" aria-describedby="our-plans">
</picture>
Using light level media queries, we can adapt images to be higher contrast and visible in strong sunlight conditions (washed
), or invert the colors to be less distracting in the dark (dim
).
These aren’t features you should scramble to implement, but keep them in your back pocket. The print-friendly one is useful today, light-level
is gaining implementation (Firefox already supports it), and cheapo e-Ink displays have a wealth of possibilities for hardware innovation we haven’t seen yet.
And you never know; you might someday find an obscure media query is perfect for your project, like color-index
or scan
.
Start with a boring ol’ HTML 2.0 <img>
:
<img src="giraffe.jpg" alt="A giraffe."
height="400" width="300">
The key realization is that once we add sizes
to this <img>
, browsers that understand <picture>
and width-based srcset
will ignore the src
, height
, and width
attributes.
“Width-based srcset
? What?” Ah, my fine hypothetical friend, there are two kinds of srcset
lists:
srcset="giraffe@1x.jpg 300w, giraffe@1.5x.jpg 450w, giraffe@2x.jpg 600w, giraffe@3x.jpg 900w"
srcset="giraffe@1.5x.jpg 1.5x, giraffe@2x.jpg 2x, giraffe@3x.jpg 3x"
The pixel density-based srcset
(with the x
descriptor) is the only kind that works in Safari, some earlier versions of Chrome & Opera, and Microsoft Edge right now. The newer width-based version (with the w
descriptor) only works in the newest versions of Firefox, Opera, and Chrome.
We want to make our images as responsive as can be, so we should use both kinds of srcset
, if possible. And we can! Just not quite how you would think. If we were to use a srcset
with both x
and w
descriptors in it, like this:
srcset="giraffe.jpg 100w, giraffe@2x.jpg 2x"
Well, browsers will do something, but I guarantee it won’t be what you want. We’ll have to get multiple srcset
s involved, using <picture>
and <source>
.
First, for the almost-modern browsers, I'll bolt on the x
descriptor srcset
:
<img src="giraffe.jpg" alt="A giraffe."
srcset="giraffe@1.5x.jpg 1.5x,
giraffe@2x.jpg 2x,
giraffe@3x.jpg 3x"
height="400" width="300">
Unlike the w
descriptor srcset
, the x
kind does take the src
attribute into account. It assumes it’s 1x
, so you don’t have to specify it again.
For x
srcset
, the width
and height
attributes just keep on doin’ what they’re doin’; all it does is swap out the image’s source if it’s on a higher-density screen. The new high-resolution source will take up the same amount of “CSS pixels” as before.
Next, we’ll turbocharge the markup for browsers that understand w
descriptor srcset
:
<picture>
<source srcset="giraffe@1x.jpg 300w,
giraffe@1.5x.jpg 450w,
giraffe@2x.jpg 600w,
giraffe@3x.jpg 900w"
sizes="300px" type="image/jpeg">
<img src="giraffe.jpg" alt="A giraffe."
srcset="giraffe@1.5x.jpg 1.5x,
giraffe@2x.jpg 2x,
giraffe@3x.jpg 3x"
height="400" width="300">
</picture>
“Hey, I thought you said that we should prefer plain srcset
!” Indeed, I did. If you look at the lone <source>
element, we don’t have a media
attribute, so browsers will use its srcset
just like if it were on the <img>
element instead. Same effect! (Note: the type="image/jpeg"
isn't necessary, but the HTML won't validate without it.)
This setup is as good as it gets, unless you want a responsive image polyfill for browsers that don’t understand any form of srcset
. Me, I’m fine serving a sensible fallback src
for my sites. Just don’t make it enormous!
srcset
Say you’ve got a nice big, fullscreen image. It’s a content image, not style, so you’re using an actual <img>
instead of a background-image
. You want to responsify it, because fullscreen on a phone is orders of magnitude different from fullscreen on desktop.
The first question is, is your image acting like contain
or cover
?
If you know how CSS’s background-size
or object-fit
properties work, that’s what I’m asking. Is your image going to get as big as it can without cropping itself, or is it covering up an element entirely and not leave any empty space? If you’re confused, try playing with David Walsh’s demo.
object-fit: cover
This one is easy: just set sizes
to 100vw
.
<img srcset="..." sizes="100vw" alt="A giraffe.">
object-fit: contain
This one is hard.
The problem is, srcset
currently only does widths. There is an h
descriptor coming, but it doesn’t do us any good now. Are we doomed to make our images too tall, or to use <picture>
and micromanage the browser logic?
I shouted my despair into the ether, that is, asked StackOverflow for ideas. The inestimable Alexander Farkas, maintainer of the HTML5shiv since 2011, and the author of/contributor to loads of awesome responsive image solutions, swooped in and nailed it in one:
I think I got it (1/2) is equal to (width/height):
<img srcset="http://lorempixel.com/960/1920/sports/10/w1920/ 960w" sizes="(min-aspect-ratio: 1/2) calc(100vh * (1 / 2))" />
Pretty brilliant. I’ll explain this terse code example, because I sure as heck had to go over it several times myself.
The lynchpin of the entire operation is the image’s aspect ratio. Without that, we can’t do a blamed thing.
Thankfully, it’s straightforward: divide the image’s width by its height. This gets you the “aspect multiplier” (probably not a real term). The actual aspect ratio is that multiplier written as a simplified fraction. It’s commonly separated like 16:9, but in CSS it’s 16/9
.
So if you had an image 300 pixels by 500 pixels, and another 600 pixels by 1000 pixels, they’d both have an aspect ratio of 3:5, or 3/5
in CSS.
Let's pretend we have an image with aspect ratio 4:5. In English, our logic is:
Using our 4/5
aspect ratio image from earlier, that translates to:
<img sizes="(min-aspect-ratio: 4/5) 80vh, 100vw">
Whoah! Where did that 80vh
come from?
This is the part I had trouble with. The first thing to understand is that we’re still telling the browser how wide the image is going to be. We know it’s going to be 100vh
tall, but we need to inform the browser what width that entails. Since we have the aspect ratio, and we have the height, we just need to solve for the width.
(I wish more than just Firefox & Safari implemented MathML.)
The second sizes
argument, the lone 100vw
length, is for when the viewport is tall enough that the image is width-constrained, which means it will take up the entire screen width. It’s actually optional, since browsers will assume 100vw
by default if no media conditions are true. So we can slim down to sizes="(min-aspect-ratio: 4/5) 80vh"
.
That’s the hard part over with. srcset
is just a list of images and widths, after all. Since this image should fill the screen of everyone, no matter the size, we’ll run the gamut between itty-bitty 300 pixels and big honking 4000 pixels. (Though what with smartwatches and those new 5k screens, that may not be enough…)
<img sizes="(min-aspect-ratio: 4/5) 80vh"
src="giraffe.png"
srcset="giraffe@0.75x.jpg 300w,
giraffe.png 400w,
giraffe@1.5x.jpg 600w,
giraffe@2x.jpg 800w,
giraffe@2.5x.jpg 1000w,
giraffe@3x.jpg 1200w,
giraffe@3.5x.jpg 1400w,
giraffe@4x.jpg 1600w,
[...pretend I wrote out everything in between]
giraffe@9.5x.jpg 3800w,
giraffe@10x.jpg 4000w"
alt="A giraffe.">
Is that an excessive amount of srcset
choices? Yeah, probably. There’s no silver bullet to figure out what widths to include, so you’ll have to experiment with what works for your site. It is a lot of markup, but the HTML weight gain is more than paid back with properly-sized images.
My personal use-case is shoving raster images inside <svg>
. You are probably not doing that, but the remainder of this blog post is as good an example as it gets for “how ugly can responsive images really be?”
Answer: THIS UGLY. Children and the squeamish are allowed to cover their faces during the upsetting parts.
Anyway, say I embed the raster image like this:
<image xlink:href="giraffe.png" x="5" y="13" width="200" height="400">
<title>A giraffe.</title>
</image>
This has two problems:
srcset
. You get the one xlink:href
, and you’ll like it.<img src>
. This is not a trivial slowdown; other assets (scripts, background images, etc.) can get scheduled before it, blocking the image.We can solve both issues in one fell swoop with <foreignObject>
:
<foreignObject x="5" y="13" width="200" height="400">
<img src="giraffe.png" srcset="image@2x.png 2x, etc..." alt="A giraffe.">
</foreignObject>
It can get as fancy as we want in there; <foreignObject>
accepts any HTML that can go inside <body>
. Except in Internet Explorer. (Edge, thankfully, does support <foreignObject>
.)
IE9, 10, & 11 all don’t support <foreignObject>
, but hope is not lost: just use <switch>
to give them <image
instead:
<switch>
<foreignObject x="5" y="13" width="200" height="400"
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<img src="giraffe.png" srcset="image@2x.png 400w, etc..." alt="a giraffe">
</foreignObject>
<image xlink:href="giraffe.png" x="5" y="13" width="200" height="400">
<title>a giraffe</title>
</image>
</switch>
That requiredFeatures
attribute lets IE know that the <foreignObject>
element requires the features described at that URL. IE doesn’t like, look it up or anything, the URLs of what a browser supports are hardcoded in.
A neat side-effect is that IE’s preloader will grab what’s inside the src
it won’t use, but it’s the same URL as the xlink:href
it will use. Bonus!
sizes
I’ve got a remaining wrinkle to deal with. The images don’t take up all of their <svg>
parent’s space, and they each have their own aspect ratios!
The first step to making sense of the mess is finding out what percentage of the <svg>
’s dimensions each of the images’ dimensions take up. For a setup like this:
<svg viewBox="0 0 1400 1600" preserveAspectRatio="xMidYMid">
<image x="200" y="200" width="1000" height="1200"/>
</svg>
(On the <svg>
element, I’ve placed preserveAspectRatio="xMidYMid"
, which has the viewBox act like object-fit: contain
and center itself. )
The image is roughly 71.43% the width of the <svg>
(1000 ÷ 1400), and 75% the height (1200 ÷ 1600). We could write a basic program to do that math for us. But assembling that information into a usable sizes
eluded me. Here were my conditions:
<svg>
’s aspect ratio (7:8), the <svg>
is height-constrained. Otherwise, it’s width-constrained.When the <svg>
is width-constrained:
<svg>
will be 100vw wide, and the image will be 71.43% the width of that.When the <svg>
is height-constrained:
<svg>
will be 100vh tall, and the image will be 75% the height of that. However, we need its width for sizes
.Phew. After crunching the numbers, our sizes
will look like this:
sizes="(min-aspect-ratio: 7/8) 71.43vw, 62.5vh"
Which, despite the contortions I took to make it, isn’t too scary-looking. Putting everything together, we end up with:
<svg viewBox="0 0 1400 1600" preserveAspectRatio="xMidYMid">
<switch>
<foreignObject x="200" y="200" width="1000" height="1000" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<img src="giraffe.png" srcset="image@2x.png 400w, etc..." alt="A giraffe." sizes="(min-aspect-ratio: 7/8) 71.43vw, 62.5vh">
</foreignObject>
<image xlink:href="giraffe.png" x="200" y="200" width="1000" height="1200">
<title>A giraffe.</title>
</image>
</switch>
</svg>
Complicated, yes. But so is the Web.
As a final problem, I’m going to have interface chrome for navigating these SVGs, so I won’t have the full viewport available. I’ll need a fancier sizes
.
First, I need to know how much space the chrome will use. It’s set up to behave like this:
orientation: landscape
), there’s chrome on all four sides. The horizontal chrome takes up 200 pixels, and the vertical chrome takes up 100 pixels.So now I’ve got four boxes to analyze:
<svg>
element<image>
as constrained by the viewBox.(If any parts of this process have horrible browser bugs, I’m going to be the first to find out.)
Simple enough:
<svg>
elementIf the viewport is wider than it is tall (orientation: landscape
):
calc(100vw - 200px)
calc(100vh - 100px)
calc((100vw - 200px) / (100vh - 100px))
(I’m using calc()
because the units don’t match, and only the browser will know how to calculate them together.)
Otherwise:
orientation: portrait
, so it’s just 100vw.calc(100vh - 200px)
calc(100vw / (100vh - 200px))
We’ll use the <svg>
from last time:
<svg viewBox="0 0 1400 1600" preserveAspectRatio="xMidYMid">
...
</svg>
Unlike the browser viewport and the <svg>
element, the aspect ratio of the viewBox doesn’t change when the orientation does. It’s also defined right inside the viewBox
attribute. We could technically use 1400/1600
, but I’ll simplify it to 7/8
.
As for the height and width, we have two media conditions that we need to check:
The first one is still (orientation: landscape)
, but the chrome has complicated the second. Ideally, I could do this:
(min-aspect-ratio: calc((100vw - 200px) / (100vh - 100px)))
…but CSS requires X/Y
string values for its aspect-ratio
queries. Using calc()
is right out.
Instead, we have to reverse-engineer the media query. Querying the aspect ratio, after all, is just syntactic sugar for crunching the viewport width and height. Like so:
viewport’s aspect-ratio = viewport width ÷ viewport height = 100vw ÷ 100vh
Let’s think about what we want using actual math symbols, instead of CSS’s hecked-up min-
/max-
system:
min-width: calc(7/8 * 100vh)
Now, if that’s all we wanted, I could precalculate the work for everyone’s browsers and use (min-width: 87.5vh)
. But I haven’t introduced the chrome’s space requirements yet.
(orientation: landscape)
, the available width = 100vw - 200px and the available height = 100vh - 100pxSo with those complicating factors, what aspect ratio do we really want?
If (orientation: landscape)
:
min-width: calc((7/8 * (100vh - 100px)) + 200px)
Otherwise:
min-width: calc(7/8 * (100vh - 200px))
Okay, cool. Now we have the second half of our media conditions. So we need the dimensions of the viewBox for each of these:
"(orientation: landscape) and (min-width: calc((7/8 * (100vh - 100px)) + 200px))"
"(orientation: landscape)"
"(min-width: calc(7/8 * (100vh - 200px)))"
""
Alright, let’s take it from the top.
If "(orientation: landscape) and (min-width: calc((7/8 * (100vh - 100px)) + 200px))"
, the viewBox is height-constrained. That means we’ll need the height to figure out the width.
calc(100vh - 100px)
<svg>
element does.calc(100vh - 100px)
÷ widthcalc(100vh - 100px)
calc(100vh - 100px)
× 8/7calc((100vh - 100px) * 8/7)
If not, and if "(orientation: landscape)"
, the viewBox is width-constrained.
That means the the viewBox will take up all of the width of the <svg>
element. Therefore, the width of the viewBox is also calc(100vw - 200px)
.
If not, and if "(min-width: calc(7/8 * (100vh - 200px)))"
, the viewBox is height-constrained.
calc(100vh - 200px)
<svg>
element is taking up.calc(100vh - 200px)
÷ widthcalc(100vh - 200px)
× 8/7calc((100vh - 200px) * 8/7)
Otherwise, it’s width-constrained. This time, there’s no horizontal chrome at all, so the width is just 100vw.
We’re getting there!
Like the viewBox, we’ll use the <image>
from last time:
<svg viewBox="0 0 1400 1600" preserveAspectRatio="xMidYMid">
<image x="200" y="200" width="1000" height="1200"/>
</svg>
We’re in the home stretch. Once we get the width values for this final box, we can assemble the sizes
.
Unlike before, I’m not going to need the step-by-step equation solving. (If you were feeling patronized, don’t be… those were for me to understand what the hell I was doing.) We can take the width values of the viewBox and then multiply those by the percentage of the viewBox the image takes up. Which is…
image-width ÷ viewBox-width = 1000/1400 = 71.42%
Of course, if you remember from way back at the top of this post, we can’t use percentages, so we’ll go with 0.7142 instead. Staple those widths onto the back of the media conditions and voilà:
(orientation: landscape) and (min-width: calc((7/8 * (100vh - 100px)) + 200px)) calc((100vh - 100px) * 8/7 * 0.7142)
(orientation: landscape) calc((100vw - 200px) * 0.7142)
(min-width: calc(7/8 * (100vh - 200px))) calc((100vh - 200px) * 8/7 * 0.7142)
71.42vw
What have I done.
Putting it all together for the final markup:
<svg viewBox="0 0 1400 1600" preserveAspectRatio="xMidYMid">
<switch>
<foreignObject x="200" y="200" width="1000" height="1200" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<picture>
<source srcset="giraffe@0.75x.jpg 300w,
giraffe.png 400w,
giraffe@1.5x.jpg 600w,
giraffe@2x.jpg 800w,
giraffe@2.5x.jpg 1000w,
giraffe@3x.jpg 1200w,
giraffe@3.5x.jpg 1400w,
giraffe@4x.jpg 1600w,
giraffe@4.5x.jpg 1800w,
giraffe@5x.jpg 2000w,
giraffe@5.5x.jpg 2200w,
giraffe@6x.jpg 2400w,
giraffe@6.5x.jpg 2600w,
giraffe@7x.jpg 2800w,
giraffe@7.5x.jpg 3000w,
giraffe@8x.jpg 3200w,
giraffe@8.5x.jpg 3400w,
giraffe@9x.jpg 3600w,
giraffe@9.5x.jpg 3800w,
giraffe@10x.jpg 4000w"
type="image/jpeg"
sizes="(orientation: landscape) and
(min-width: calc((7/8 * (100vh - 100px)) + 200px))
calc((100vh - 100px) * 8/7 * 0.7142),
(orientation: landscape)
calc((100vw - 200px) * 0.7142),
(min-width: calc(7/8 * (100vh - 200px)))
calc((100vh - 200px) * 8/7 * 0.7142),
71.42vw">
<img src="giraffe.jpg" alt="A giraffe."
srcset="giraffe@1.5x.jpg 1.5x,
giraffe@2x.jpg 2x,
giraffe@2.5x.jpg 2.5x,
giraffe@3x.jpg 3x,
giraffe@3.5x.jpg 3.5x,
giraffe@4x.jpg 4x,
giraffe@4.5x.jpg 4.5x,
giraffe@5x.jpg 5x"
height="400" width="300">
</picture>
</foreignObject>
<image xlink:href="giraffe.png" x="200" y="200" width="1000" height="1200">
<title>A giraffe.</title>
</image>
</switch>
</svg>
If it weren’t in the requirements to support photographic images, I could replace all of that with a single <image xlink:href="giraffe.svg">
. But I guess I built character this way.
I hope that you don’t have to do what I’m doing, partially because of everything you just saw, but mostly because it won’t make you any money. (Trust me.). But if you run into tricky responsive image situations, hopefully something from this post will guide you a little.
Thanks for sticking with me!
(Code for the Web, they said. It’s fun, they said.)