Nomadic cattle rustler and inventor of the electric lasso.
Company Website
Follow me on twitter
Contact me for frontend answers.

A journey to svg responsive enlightment, a.k.a. yet another post about the viewBox

March 09, 2019

The svg viewBox is a mysterious and confusing attribute that holds the key to responsive svg documents.

On a recent contract, I acquired a very difficult requirement were a quite large <svg /> element had to have a preview mode in a very small part of a view.

I’ve created a jsfiddle that roughly illustrates how it works in the real app.

In preview mode, the svg needs to scale appropriately as you can see in the bottom left corner:

pop out

If you press the Pop Out button, it transforms into full mode and takes up the whole browser viewport:

full

This led me to the mysterious world of the viewBox.

There are a whole host of popular posts on the svg viewBox but I found the examples I read too simple and most of these posts only use a very simple image for their code examples.

This post is probably the most popular by Sara Soueidan. I found this a very useful as a primer.

I read many, many, many similar blog posts about how best to approach this and it is important to get some important terminology out of the way before proceeding.

Browser Viewport

The browser’s viewport is the user’s visible area of a web page. This is often not the same size as the rendered page, in which case the browser provides scrollbars for the user to scroll around and access the content.

SVG Viewport

The svg viewport is analgous to the browser’s viewport only it is the visible area of an svg document. An svg document can logically be as wide and as high as you want but only part of the image can be visible at any one time.

You can think of the viewport as a window through which you can see a particular scene. The scene maybe entirely or partially visible through that window.

<svg width="500px" height="100px" style="border: 1px solid yellow">
    <circle r="150" cx="30" cy="30" fill="#f00" />
    <rect x="-75" y="-75" width="210" height="210" stroke="blue" stroke-width="8" style="fill: transparent"></rect>
  </svg>

The viewport above is the visible section although the real image spans a much greater area.

viewBox and New Coordinate systems

You can think of the viewBox of an svg element as a region of the svg that will be snapshotted and scaled to fit the svg viewport.

For example if we take the above example and add a viewBox attribute of viewBox="-500 -50 2000 200", then would you be surprised to see:

<svg width="500px" height="100px" viewBox="-500 -50 2000 200" style="border: 1px solid yellow">
  <circle r="150" cx="30" cy="30" fill="#f00" />
  <rect x="-75" y="-75" width="210" height="210" stroke="blue" stroke-width="8" style="fill: transparent"></rect>
</svg>

I will get into these values later but, the viewBox attribute allows us to specify a new user coordinate system.

Now the expression a new user coordinate system might make perfect sense to you but this baffled me at first. I had no idea what this meant. I know what the Euclidean Space or Cartesian space is. I understand that the svg coordinate space is different because the point (0,0)(0, 0) is at the top left hand corner with the xx and yy axis essentially reversed compared to normal graph coordinates with the values increasing as you go further right and down the respective axis.

But why on earth am I creating a new coordinate system and what is the old coordinate system for that matter.

As it turned out, understanding this would allow me to meet the requirements that troubled me so.

The only way to explain this is with an example.

Below is the example from earlier in the post of a meaningless red circle with an even more meaningless blue rectangle contained inside it:

<svg width="500px" height="100px" style="border: 1px solid yellow">
  <circle r="150" cx="30" cy="30" fill="#f00" />
  <rect x="-75" y="-75" width="210" height="210" stroke="blue" stroke-width="8" style="fill: transparent"></rect>
</svg>

The svg has a width of 500px and a height of 100px as specified in the svg element’s attributes.

The circle has a radius of 150 which as you can see will not have enough room on the svg viewport to fully display itself.

So what can we do? Can you guess?

The viewBox attribute

Now enters the hero of the piece. The mysterious viewBox attribute. As I stated earlier, the viewBox allows us to create a new user coordinate system. Let me explain what that means.

An svg starts out with an inital coordinate system that is 1:11:1 with the viewport coordinate system.

We control the viewbox through the viewBox attribute with the following parameters:

<svg viewBox="minX minY width height" />

minX and minY control moving the view of the image left and right or what is also known as panning. The width and height parameters control zooming in and out.

Zooming

The last 2 parameters, width and height control zooming.

If the last 2 parameters have the same dimensions or values as the width and height of the svg element then nothing changes.

The 2 coordinate systems map 1:11:1.

<svg width="500px" height="100px" style="border: 1px solid yellow">
  <circle r="150" cx="30" cy="30" fill="#f00" />
  <rect x="-75" y="-75" width="210" height="210" stroke="blue" stroke-width="8" style="fill: transparent"></rect>
</svg>

The above example has a viewBox of 0 0 500 100 which has the same dimensions as the svg viewPort. Nothing doing here then.

Here comes the magic…prepare yourself.

So what if we wanted the meaningless red circle and meaningless blue square to be zoomed out so it completely fits onto our svg viewport without altering the width and height attributes? We would of course add a viewBox attribute.

We could do something like this:

<svg width="500px" height="100px" viewBox="-200 -100 3000 400" style="border: 1px solid yellow">
  <circle r="150" cx="30" cy="30" fill="#f00" />
  <rect x="-75" y="-75" width="210" height="210" stroke="blue" stroke-width="8" style="fill: transparent"></rect>
</svg>

We have set the viewBox to these values -200 -100 3000 400.

What we have done is shift the new viewport 200 user values to the left and 100 user values down with the first 2 arguments.

With the next two values 3000 400 we are creating a new user coordinate system based on the existing svg coordinate system of 500x100.

What will happen now is that the user coordinate system is going to be scaled up to 3000x400. It will then be mapped to the viewport coordinate system so that every 1 unit in the new user coordinate system is equal to:

viewportWidth÷viewBoxWidthviewportWidth \div viewBoxWidth horizontally

and

viewportHeight÷viewBowHeightverticallyviewportHeight \div viewBowHeight vertically

In our case this equates to

500÷3000=16500 \div 3000 = \frac{1}{6} horizontally

100÷400=14100 \div 400 = \frac{1}{4} vertically.

This means that every 1 xx unit in the viewBox coordinate system is equal to 16\frac{1}{6} in the viewport coordinate system and every 1 yy unit in the viewBox coordinate system is equal to 14\frac{1}{4} in the viewport coordinate system.

Any child elements of the svg element will be positioned in the new user coordinate system specified in the viewbox.

Aspect Ratio

One thing that is not obvious in the above example is how the aspect ratio is being controlled.

Aspect ratio is the ratio of width to height of an image on screen.

Aspect ratios are written as mathmatical expressions using the format:

width:heightwidth:height

A square image has an aspect ratio of 1:11:1. An image that is twice as tall as it is wide has an aspect ratio of 1:21:2.

The viewBox can define an aspect ratio on the svg document. It defines how all the lengths and coordinates used inside the svg should be scaled to fit the available space.

The aspect ratio of an svg image is controlled through the preserverAspectRatio attribute of the svg document. The preserverAspectRatio attribute has no effect unless it is twinned with a viewBox attribute.

What is not obvious in our example is that if not preserverAspectRatio attribute is specified then a value of xMidYMid meet is intrinsically added.

We will get into what these values mean later.

You can also set preserveAspectRatio to none to show what happens when uniform scaling is not enforced.

<svg width="500px" height="100px" viewBox="-200 -100 3000 400" style="border: 1px solid yellow" preserveAspectRatio="none">
  <circle r="150" cx="30" cy="30" fill="#f00" />
  <rect x="-75" y="-75" width="210" height="210" stroke="blue" stroke-width="8" style="fill: transparent"></rect>
</svg>

The image is distorted because the aspect ratio of the image is 30:430:4.

If I remove the attribute altogether, then the default value of xMidYMid meet is added, which is the same as explicitly adding it:

<svg width="500px" height="100px" viewBox="-200 -100 3000 400" style="border: 1px solid yellow" preserveAspectRatio="xMidYMid meet">
  <circle r="150" cx="30" cy="30" fill="#f00" />
  <rect x="-75" y="-75" width="210" height="210" stroke="blue" stroke-width="8" style="fill: transparent"></rect>
</svg>

The parameters of the preserveAspectRatio attribute have the following syntax:

preserveAspectRatio="<align>[<meetOrSlice>]"preserveAspectRatio="<align> [<meetOrSlice>]"

The first parameter of preserveAspectRatio="xMidYMid meet" is xMidYmid which can be deconstructed to xMid and yMid instructions.

  • xMid align the midpoint xx value of the element’s viewBox with the midpoint xx value of the viewport

  • yMid align the midpoint yy value of the element’s viewBox with the midpoint yy value of the viewport.

The second parameter meet has the following characteristics:

  • aspect ratio is preserved
  • The entire viewBox is visible within the viewport
  • the viewBox is scaled up as much as possible while still meeting the other criteria.

preserveAspectRatio="xMidYMid meet" will align the element’s viewBox with the smallest x and the smallest Y of the viewPort.

A full list of the allowed values can be found here.

Responsive SVG

Now back to the matter in hand, I had researched a lot about the mysterious viewBox attribute and finally actually knew what it meant.

I had a look around and I did find this nice responsive react component in the excellent @vx npm package and I used this as a base as it did not quite meet my needs.

I made a few alterations and came up with this component:

import React from 'react'

export interface ResponsiveSVGProps {
  width: number;
  height: number;
  origin: { x: number, y: number };
  preserveAspectRatio?: string;
  innerRef?: React.RefObject<SVGSVGElement>;
  className?: string;
}

export const ResponsiveSVG: React.FC<ResponsiveSVGProps> = ({
  height,
  width,
  children,
  origin = { x: 0, y: 0 },
  preserveAspectRatio = 'xMinYMin meet',
  innerRef,
  className,
  hide,
}) => {
  const aspect = width / height

  return (
    <div
      style={{
        position: 'relative',
        overflow: 'visible',
        height: '0px',
      }}
    >
      <svg
        className={className}
        preserveAspectRatio={preserveAspectRatio}
        viewBox={`${origin.x} ${origin.y} ${width} ${Math.round(
          width / aspect
        )}`}
        ref={innerRef}
      >
        {children}
      </svg>
    </div>
  )
}

I think this is the uber responsive container and works for pretty much anything I’ve thrown at it.

I am allowing the user to pass in an origin prop that will simply allow the user to translate the svg document to the left or right of the natural position.

The real mnagic is in the last 2 arguments of the viewBox on lines 29 and 30.

line 22 works out the aspect ratio of the width and height props that are passed in to the component. I have this hook which will give me the width and height properties of a container div and respond to resize events which will cause a rerender and push new width and height props into the component. These values can be passed into the ResponsiveSVG component.

The contaier div in the component is given a height of 0 in order to collapse it so no additional height will throw off the aspect ratio we need.

The height of the image is calculated using this calculation:

height=width/aspectheight = width / aspect

And of course aspect is:

width/heightwidth / height

so given a height and width of 1200x8001200 x 800.

The new height is:

height=800/(1200/800)=533height = 800 / (1200 / 800) = 533

Writing this has been the final step in me understanding all these svg shenanigans.


Paul Cowan

Nomadic cattle rustler and inventor of the electric lasso.
Company Website
Follow me on twitter
Contact me for frontend answers.