UPDATE: Turns out SVG has an attribute for exactly this use-case. It’s called preserveAspectRatio, and all I needed was <svg preserveAspectRatio="xMidYMid">. In fact, that’s the default if you omit the attribute. I feel very silly.

I have an <svg> element. I want it to do 3 things:

  1. Have a natural size it won’t scale beyond on large viewports.

  2. If the viewport is smaller than the natural size, it should become as tall as the viewport is, or as wide as the viewport is, whichever is smaller.

  3. Maintain its aspect ratio throughout.

This is proving to be a major buttpain.

The object-fit property

These constraints are basically identical to what background-fit: cover performs. This was so useful that the object-fit property was created to do the same thing with <img>:

See the Pen Scaling inline SVG with object-fit by Taylor Hunt (@Tigt) on CodePen.

While this is probably the cleanest and most future-proof solution, browser support is imperfect. Polyfill it?. I’m not keen; polyfilling has significant reliability and performance consequences in less-than-ideal network conditions (known as “real life”).

Relying on natural dimensions

A regular old <img> can have the following CSS:

  img {
  max-height: 100%;
  max-width: 100%;
}

And it will behave exactly like we want:

See the Pen Natural dimensions auto-sizing by Taylor Hunt (@Tigt) on CodePen.

A raster image has “natural dimensions,” which are based on the downloaded file’s actual pixel grid. The browser uses that information, so setting maximum dimensions on an image constrains it while respecting its inherent aspect ratio.

This is especially frustrating because <svg> with a viewBox and defined width/height attributes has inherent dimensions browsers should also be able to use. They do use it when the SVG is in an <img>, but only the latest Firefox and Chrome consider it with <svg>. However, I want the SVG to be interactive, which prevents using <img>.

We have a couple options here. We can use <object>, which might behave like we want. I'm not big on it for a few reasons, though:

  • I have to deal with full-blown SVG-in-XML. <script type="text/ecmascript"><![CDATA[, anyone? No?

  • Accessibility support is worse than inline SVG (which isn’t ideal either, but workable).

  • Worst of all, the content is blocked behind an HTTP request.

The other method is a bit of an ultragross hack, but… it works. We use an <img> with the correct aspect ratio, use that to influence the browser’s sizing of a container <div>, and then absolutely position the <svg> over the container. I’m calling this the “stretcher bar” approach, after the wooden frame used to hold up artists’ canvases.

It looks like Twitter uses <canvas width="X" height="Y"> for this purpose, which is a little easier to work with. However, beware that if JS is disabled, <canvas> disappears, like a reverse <noscript>.

Instead of generating tiny GIFs with proper dimensions, which sounds beyond ultragross, we could create a minimal SVG like this:

  <svg viewBox='0 0 {X} {Y}' xmlns='http://www.w3.org/2000/svg'/>

Where {X} and {Y} are replaced with width and height values that match the needed aspect ratio. A few notes about this snippet:

  • Since we don’t need anything inside the SVG, we can self-close the root <svg> tag.
  • We only need the one namespace, since we’re not bothering with XLink or whatever.
  • We’re using apostrophes (') instead of quotes (") because the former are legal characters in a URL and needn’t be encoded.
  • SVG files don’t need a DOCTYPE. In fact, it’s better to omit it.
  • We also don’t need the XML declaration (thingy that looks like <?xml version="1.0"?>) as long as the encoding is UTF-8.

We then URL encode it and stick it in an <img>'s data URI. For an aspect ratio of 4:3, that would go like this:

  <img src="data:image/svg+xml,%3Csvg viewBox='0 0 4 3' xmlns='http://www.w3.org/2000/svg'/%3E"
     alt="" class="stretcher-bar">

This is the smallest data URI I can get that works in all browsers. You might notice the only URL-encoding we’ve done is for < and >. It’s downright readable! Even spaces are allowed unencoded when the URI is wrapped in quotes.

(The alt="" lets assistive technologies know the image isn’t important content. Because it isn’t.)

We then use the following:

  <div class="svg-container">
    <img src="data:image/svg+xml,%3Csvg viewBox='0 0 4 3' xmlns='http://www.w3.org/2000/svg'/%3E" alt="" class="stretcher-bar">
    <svg viewBox="0 0 400 300" class="content-svg"> <!-- ... --> </svg>
</div>

  .svg-container {
  position: relative;
}

.stretcher-bar {
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
}

.content-svg {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  max-width: 100%;
  max-height: 100%;
}

And things go swimmingly:

See the Pen Scaling inline SVG with the stretcher-bar technique by Taylor Hunt (@Tigt) on CodePen.

This seems to work like a charm. However, I haven’t battle-tested it yet, and I’ll be the first to admit it’s a kludge. What else we got?

Viewport units

Since the vh and vw units key off height and width no matter where we use them, they provide another method of “crossing the streams” to interrelate an element’s height and width. If I had a container with dimensions equal to the viewport (and I do!), I could set the widthy properties to something in vh, and vice versa.

  svg {
  width: 100vw;
  height: (height/width * 100)vw;
  max-height: 100vh;
  max-width: (width/height * 100)vh;
}

This doesn’t make a whole lot of sense at first, so let’s walk through it. width: 100vw and max-height: 100vh are straightforward enough; make the element as wide as the screen, but no taller than the screen.

For height, 1vw = 1% of the viewport’s width. If our element’s aspect ratio is 16:9, that means the height becomes 9÷16 × 100 = 56.25vw, or 56.25% of the viewport width. Which makes sense; if the width of the element becomes 100vw, 9/16ths of that is 56.25vw.

For max-width, reverse the order; we’re setting it to 16/9 * 100 = 177.7778 vh. (It repeats "7" forever but browsers don’t care about being too accurate.) The result of the conflicting dimensional information is that each “half” only takes precedence when the other no longer matters: we don’t care about the image overflowing horizontally when it’s constrained by height, and vice-versa.

It’s not great having the dimension information in the CSS, but you can use a preprocessor to help out with the math. This might be a good place for inline styles, if you can’t guarantee shared aspect ratios.

Viewport unit support isn’t fantastic, but it’s easy to use CSS error-handling to set a usable, if ungainly fallback.

Unfortunately, there is one big problem: viewport units remain the same size even if the user zooms. Zooming with SVG is kind of a huge feature, and removing it is hostile to anyone with small screens or less-than-perfect eyesight.

I tried a wrapper <div> and setting overflow: auto on it, but viewport sizing appears to be zooming’s kiss of death no matter what. This solution is otherwise perfect, so if anyone out there has a way of reenabling zoom, please do let me know.

The padding-bottom hack

This old chestnut gets us halfway there. The reason it works is because the padding for the top and bottom of an element, if defined as a percentage, uses the width of the element instead of the height. But padding-right and padding-left also take from the width, so they don’t perform the same trick horizontally.

Unfortunately, using padding to define the height this way also prevents us from combining the padding-bottom hack with other techniques, since there's no such thing as max-padding. It might be possible with some wrapper <div>s, but we’ll still need a height-constraining method to pair it with.

Well, drat

Sadly, this continues the long tradition of concluding there is no ideal solution to a common layout problem. If you have any ideas or feedback, please do let me know.


47,853 4 25