Source originale du contenu
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.
Some ground floor stuff
I’m assuming you already have a basic grasp of responsive images. If not, I recommend Cloud Four’s introduction series.
Why bother?
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:
- Download size
- Doubling an image’s pixel density quadruples its area and can sextuple its filesize! Have a data plan? Not anymore you don’t.
- CPU load & battery drain
- Images also need to be decoded. Shrinking ‘em with CSS? Now the browser needs to scale and resample, too. The difficulty of all three grows exponentially with image size.
- Memory usage
- Big photographs can cause juddering in a mobile browser all by themselves. Resizing with CSS doesn’t help; the browser keeps the full thing around for zooming.
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.
Unintuitive behavior & gotchas
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:
- You cannot use media types (
screen
,print
,tv
, etc.) - Use any
(feature: value)
pairs you like. - You can chain them with the keywords
and
,or
, andnot
. Do not use commas as a substitute foror
. - If using the
or
ornot
keywords, you must wrap the entire media condition in parentheses. - If empty, it always evaluates to true.
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.
Troublesome CSS units
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…
- If your breakpoints are using
em
, you'll reflect them in your media conditions. - If you’re sizing your text container with
em
s for the perfect line length, and images inside it aremax-width: 100%
, they can end up sized inem
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 displays
sizes
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.
Printer-friendly
<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.
E-ink-friendly
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>
Night mode & bright sunlight
<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
.
The deepest backwards-compatibility possible
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:
- Width-based
srcset="giraffe@1x.jpg 300w, giraffe@1.5x.jpg 450w, giraffe@2x.jpg 600w, giraffe@3x.jpg 900w"
- Pixel density-based
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!
Height and width-constrained 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:
- If the screen’s aspect ratio is wider (greater) than the image’s aspect ratio, the image will be the full height of the screen.
- Otherwise, the image will be the full width of the screen.
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.
- Ratio = width ÷ height
- An image’s aspect ratio is its width divided by its height
- ⅘ = width ÷ 100vh
- Fill in our known values
- width = ⅘ × 100vh
- Isolate the width variable
- width = 80vh
- Simplify.
(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.
Responsive bitmaps inside inline SVG
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:
- SVG doesn’t have anything like
srcset
. You get the onexlink:href
, and you’ll like it. - Browser preloaders don’t fetch it like they do with your usual
<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>
.)
Fixing IE
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!
Building the 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:
- If the screen’s aspect ratio is wider (greater) than the
<svg>
’s aspect ratio (7:8), the<svg>
is height-constrained. Otherwise, it’s width-constrained. When the
<svg>
is width-constrained:- Image’s display width = 100vw × 71.43%
- Image’s display width = 71.43vw
- The
<svg>
will be 100vw wide, and the image will be 71.43% the width of that.
When the
<svg>
is height-constrained:- Image’s display height = 100vh × 75%
- Image’s display height = 75vh
- The
<svg>
will be 100vh tall, and the image will be 75% the height of that. However, we need its width forsizes
. - Ratio = width ÷ height
- We’ll use the definition of aspect ratio again…
- ⅚ = width ÷ 75vh
- Substitute the values we know…
- 62.5vh = width
- And solve.
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.
Making things worst with not-quite-entire-viewport scaling
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:
- If the screen is wider than it is tall (
orientation: landscape
), there’s chrome on all four sides. The horizontal chrome takes up 200 pixels, and the vertical chrome takes up 100 pixels. - Otherwise, the chrome only takes up vertical space. It will be 200 pixels tall.
So now I’ve got four boxes to analyze:
- The browser viewport
- The available space within that viewport once the chrome is done, taken up by the
<svg>
element - The SVG viewport (“viewBox”)
- The
<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.)
The browser viewport
Simple enough:
- width = 100vw
- height = 100vh
- aspect-ratio = 100vw/100vh
The <svg>
element
-
If the viewport is wider than it is tall (
orientation: landscape
):- width =
calc(100vw - 200px)
- The chrome will take up 200px of width, so the width available is the full width (100vw) minus that (200px).
- height =
calc(100vh - 100px)
- The chrome will take up 100px of height, and we calculate what’s left like we did with the width.
- aspect ratio =
calc((100vw - 200px) / (100vh - 100px))
- Substitute the values into the equation for calculating aspect ratios.
(I’m using
calc()
because the units don’t match, and only the browser will know how to calculate them together.) - width =
-
Otherwise:
- width = 100vw
- There’s no horizontal chrome at all when the browser is
orientation: portrait
, so it’s just 100vw. - height =
calc(100vh - 200px)
- There is 200px of vertical chrome, though.
- aspect ratio =
calc(100vw / (100vh - 200px))
- And substitute again.
The viewBox
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:
- Is the browser viewport wide or tall?`
- Does the available space have a wider or narrower aspect ratio than the viewBox?
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:
- aspect-ratio ≥ 7/8
- We want to know if the viewport’s aspect ratio is greater than or equal to 7/8 (the viewBox’s aspect ratio).
- 100vw ÷ 100vh ≥ 7/8
- Since the viewport’s aspect ratio equals 100vw/100vh, we substitute that.
- 100vw ≥ 7/8 × 100vh
- Multiply both sides by 100vh.
- width ≥ 7/8 × 100vh
- Since 100vw is the width of the viewport, substitute that back in.
min-width: calc(7/8 * 100vh)
- Convert into CSS syntax.
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.
- When
(orientation: landscape)
, the available width = 100vw - 200px and the available height = 100vh - 100px - Otherwise, the available width = 100vw and the available height = 100vh - 200px
So with those complicating factors, what aspect ratio do we really want?
-
If
(orientation: landscape)
:- aspect-ratio of available space = (100vw - 200px) ÷ (100vh - 100px)
- We want to find out if this is larger than 7/8.
- (100vw - 200px) ÷ (100vh - 100px) ≥ 7/8
- Like this. We need to isolate either the 100vw or the 100vh so we can turn it into the width or height viewport dimension.
- 100vw - 200px ≥ 7/8 × (100vh - 100px)
- Multiply both sides by (100vh - 100px).
- 100vw ≥ (7/8 × (100vh - 100px)) + 200px
- Add 200px to both sides.
- width ≥ (7/8 × (100vh - 100px)) + 200px
- Subsitute width for 100vw.
min-width: calc((7/8 * (100vh - 100px)) + 200px)
- Convert to CSS syntax. (Now that is an ugly media condition.)
-
Otherwise:
- aspect-ratio of available space = 100vw/(100vh - 200px)
- Second verse, same as the first. This time the chrome isn’t quite as complicated.
- 100vw ≥ 7/8 × (100vh - 200px)
- Multiply both sides by (100vh - 200px).
- width ≥ 7/8 × (100vh - 200px)
- Substitute width for 100vw.
min-width: calc(7/8 * (100vh - 200px))
- Convert to CSS syntax.
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.- viewBox’s height = available height =
calc(100vh - 100px)
- The viewBox will take up the entire height the
<svg>
element does. - aspect-ratio = height ÷ width
- 7/8 =
calc(100vh - 100px)
÷ width - Since we set the aspect ratio of the viewBox to be 7/8, we can substitute that and the width value we just figured into the aspect ratio equation.
- 7/8 × width =
calc(100vh - 100px)
- Multiply both sides by width.
- width =
calc(100vh - 100px)
× 8/7 - Divide both sides by 7/8.
- width =
calc((100vh - 100px) * 8/7)
- Convert to CSS syntax.
- viewBox’s height = available height =
-
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 alsocalc(100vw - 200px)
. -
If not, and if
"(min-width: calc(7/8 * (100vh - 200px)))"
, the viewBox is height-constrained.- height =
calc(100vh - 200px)
- The height that the
<svg>
element is taking up. - aspect-ratio = height ÷ width
- 7/8 =
calc(100vh - 200px)
÷ width - Substitute like before.
- width =
calc(100vh - 200px)
× 8/7 - I isolated the width variable in one step this time. if you forget how to do it, look back at Step 1.
- width =
calc((100vh - 200px) * 8/7)
- Convert to CSS syntax.
- height =
-
Otherwise, it’s width-constrained. This time, there’s no horizontal chrome at all, so the width is just 100vw.
We’re getting there!
The image
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.
Assembling
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.
That’s all, folks
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.)