A place to cache linked articles (think custom and personal wayback machine)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

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.

Contents

  1. Some ground floor stuff
    1. Why bother?
    2. srcset is preferable
  2. Unintuitive behavior & gotchas
    1. Media conditions, NOT media queries
    2. Troublesome CSS units
    3. sizes will affect how your image displays
  3. Media-aware images
    1. Printer-friendly
    2. E-ink-friendly
    3. Night mode & bright sunlight
  4. The deepest backwards-compatibility possible
  5. Height and width-constrained srcset
    1. object-fit: cover
    2. object-fit: contain
  6. Responsive bitmaps inside inline SVG
    1. Fixing IE
    2. Building the sizes
    3. Making things worst with not-quite-entire-viewport scaling
      1. The browser viewport
      2. The <svg> element
      3. The viewBox
      4. The <image>
      5. Assembling
  7. That’s all, folks

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, and not. Do not use commas as a substitute for or.
  • If using the or or not 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.

Some valid media conditions:
  • (min-width: 500px)
  • (max-width: 300px) and (orientation: portrait)
  • ((min-height: calc(50vw - 100px)) or (light-level: dim))
  • (Yep, an empty M.C. is totes O.K.)
Some valid lengths:
  • 200px
  • 40em
  • calc(40rem - 10vw)
  • 78.95vmax
Some valid <source-size-value>s:
  • (min-width: 500px) 200px
  • (max-width: 300px) and (orientation: portrait) 40em
  • ((min-height: calc(50vw - 100px)) or (light-level: dim)) calc(40rem - 10vw)
  • 78.95vmax

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

So sayeth the spec:

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 ems 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 emin 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 ems for the perfect line length, and images inside it are max-width: 100%, they can end up sized in ems 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 srcsets 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:

  1. 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.
  2. 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:

  1. SVG doesn’t have anything like srcset. You get the one xlink:href, and you’ll like it.
  2. 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:

  1. 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.
  2. 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.
  3. 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 for sizes.
    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:

  1. 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.
  2. Otherwise, the chrome only takes up vertical space. It will be 200 pixels tall.

So now I’ve got four boxes to analyze:

  1. The browser viewport
  2. The available space within that viewport once the chrome is done, taken up by the <svg> element
  3. The SVG viewport (“viewBox”)
  4. 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

  1. 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.)

  2. 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:

  1. Is the browser viewport wide or tall?`
  2. 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.

  1. When (orientation: landscape), the available width = 100vw - 200px and the available height = 100vh - 100px
  2. Otherwise, the available width = 100vw and the available height = 100vh - 200px

So with those complicating factors, what aspect ratio do we really want?

  1. 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.)
  2. 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:

  1. "(orientation: landscape) and (min-width: calc((7/8 * (100vh - 100px)) + 200px))"
  2. "(orientation: landscape)"
  3. "(min-width: calc(7/8 * (100vh - 200px)))"
  4. ""

Alright, let’s take it from the top.

  1. 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.
  2. 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).

  3. 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.
  4. 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à:

  1. (orientation: landscape) and (min-width: calc((7/8 * (100vh - 100px)) + 200px)) calc((100vh - 100px) * 8/7 * 0.7142)
  2. (orientation: landscape) calc((100vw - 200px) * 0.7142)
  3. (min-width: calc(7/8 * (100vh - 200px))) calc((100vh - 200px) * 8/7 * 0.7142)
  4. 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.)