Paul Cowan Contract software developer

Make any SVG responsive with this React component

4 min read 1194

Make any SVG responsive with this React component

If you are putting out digital content into the world wide web to publicize your work, then there is a high chance that your audience will view it on a mobile device. For text and images, this is not a huge problem with modern CSS. But if the content is a data visualization using SVG elements, this might be a problem.

I initially ran into problems on mobile devices with some graphs I was working on creating. After a lot of fumbling, I made some reusable components that can shield me from this pain.

How much space does the SVG have to grow?

There are a few abstractions to consider when working out how much space is available to scale an SVG document.

the difference between browser viewport and SVG viewport

Browser viewport

The browser viewport is the visible area of a web page.

SVG viewport

The SVG viewport is analogous to the browser’s viewport only it is the visible area of an SVG document. An SVG document can be as high and wide as you want, but only part of the document can be visible at one time.

We want to avoid hardcoded width and height attributes like in the example below because they cannot adapt to the multitude of devices that might be viewing this document:

export const App: React.FC = () => (
  <svg width="1000" height="1000">
    <rect
      x="20%"
      y="20%"
      width="1000"
      height="1000"
      rx="20"
      style={{ fill: '#ff0000', stroke: '#000000', strokeWidth: '2px' }}
    />

    <rect
      x="30%"
      y="30%"
      width="1000"
      height="1000"
      rx="40"
      style={{ fill: '#0000ff', stroke: '#000000', strokeWidth: '2px', fillOpacity: 0.7 }}
    />
  </svg>
);

The above component renders this document:

svg document with hardcoded width and height attributes

The above SVG document of 2 <rect/> elements is larger than the SVG viewport, and because of this, only part of it is visible.

We made a custom demo for .
No really. Click here to check it out.

A magical attribute called the viewBox is the answer to a lot of our SVG responsive needs.

viewBox and coordinate systems

The definition from mdn for the viewBox attribute is:

The viewBox attribute defines the position and dimension, in user space, of an SVG viewport.

I don’t know about you, but I had more questions than answers when I first read that. What on earth is user space?

If you have sat maths to any level, then you will have come across euclidean space with the classic x and y axes:

euclidean 2d coordinate space

 

SVG coordinate space is different because the point (0, 0) is at the top left-hand corner and not the center.

If we add a viewBox attribute to the SVG that was created earlier, then we can regain control of the sizing of the SVG viewport:

export const App: React.FC = () => (
  <svg preserveAspectRatio="xMaxYMid meet" viewBox="0 0 529 470">
    <rect
      x="20%"
      y="20%"
      width="1000"
      height="1000"
      rx="20"
      style={{ fill: '#ff0000', stroke: '#000000', strokeWidth: '2px' }}
    />

    <rect
      x="30%"
      y="30%"
      width="1000"
      height="1000"
      rx="40"
      style={{ fill: '#0000ff', stroke: '#000000', strokeWidth: '2px', fillOpacity: 0.7 }}
    />
  </svg>
);

The two <rect/> elements are now smaller than the SVG viewport thanks to this magical attribute.

viewBox attribute added to svg

viewBox math

Below is an example of a viewBox attribute:

viewBox="-200 -100 3000 400"

The magical four numbers in the attribute can shrink or expand the elements and transform their position. But how?

The viewBox does a lot with very little, such as:

  • It defines the aspect ratio of the image
  • It defines how all the lengths and coordinates used inside the SVG should be scaled to fit the space available
  • It defines the origin of the new coordinate system

I like to remember the initial four values like this:

viewBox="minX minY width height"

A new set of coordinates are copied from the existing SVG viewport coordinates with the addition of the viewBox. This new set of coordinates is known as a user space. The mdn cryptic explanation mentioned this:

The viewBox attribute defines the position and dimension, in user space, of an SVG viewport.

The first two values of the viewBox are -200 and -100, and these values will shift the image right and down from the top-left origin:

<svg
  width="500px"
  height="100px"
  viewBox="-200 -100 3000 400"
>

The last two values, 3000 and 400, allow us to zoom into or away from our SVG image.

The SVG element has a width of 500px and a height of 100px. With the addition of the viewBox attribute, a new user coordinate system of 3000 units and 400 hundred units vertically is at our disposal.

The new user space maps to the viewport coordinate system, and 1 unit of the new space is equal to this calculation:

height = SVG viewport height ÷ viewBox height vertically
width = SVG viewport width ÷ viewBox width horizontally

newheightnewweight
The viewBox on its own will not cure all our responsive needs. We cannot use hardcoded values in the real world. We need to get new values when the component is mounted, or it resizes.

Responsive SVG

Below is my ResponsiveSVG component from my very own @cutting/svg package:

export function ResponsiveSVG<E extends HTMLElement = HTMLElement>({
  elementRef,
  children,
  preserveAspectRatio = 'xMinYMin slice',
  innerRef,
  className,
  options = {},
}: PropsWithChildren<ParentsizeSVGProps<E>>): JSX.Element | null {
  const { width, height } = useParentSize(elementRef, options);

  const aspect = width / height;

  const adjustedHeight = Math.ceil(width / aspect);

  return (
    <div
      data-selector="cutting-svg-container"
      style={{
        position: 'relative',
        overflow: 'visible',
        height: '1px',
      }}
    >
      <svg
        style={{ overflow: 'visible' }}
        className={className}
        preserveAspectRatio={preserveAspectRatio}
        viewBox={`0 0 ${width} ${adjustedHeight}`}
        ref={innerRef}
      >
        {children}
      </svg>
    </div>
  );
}

The above component makes use of my useParentSize hook that uses a ResizeObserver to watch for changes in a container <div/> element. Any time a change of dimensions happens in the target <div />, a resize event raises new width and height values and the component will re-render in terms of these new values.

To ensure our SVG element keeps its shape at different sizes, we need to calculate its height in terms of its width. This proportional relationship is known as the aspect ratio.

Aspect ratio

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

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

The calculation for the adjusted height is:

const aspect = width / height;
const adjustedHeight = Math.ceil(width / aspect);

These values help us complete the viewBox:

<svg
  style={{ overflow: 'visible' }}
  className={className}
  preserveAspectRatio={preserveAspectRatio}
  viewBox={`0 0 ${width} ${adjustedHeight}`}
  ref={innerRef}
>

The preserveAspectRatio attribute

The viewBox attribute has a sidekick, preserveAspectRatio. It does not have any effect unless the viewBox exists. The preserveAspectRatio describes how an SVG document should scale if the aspect ratio of the viewBox does not match the aspect ratio of the viewPort. Most of the time, the default behavior works, which is:

preserveAspectRatio="xMidYMid meet"

xMidYMid meet is a bit like the CSS rule background-size: contain;. A slice value will scale the image to fit the more generous dimensions and slice off the extra.

preserveAspectRatio="xMidYMid slice"

The slice value is analogous to the overflow: hidden CSS rule.

preserveAspectRatio

Epilogue

With my ResponsiveSVG component, I am armed and ready for any device userland can throw at me, and below is how it looks when I resize my browser to something minimal:

SVG document resizing as the dimensions change

 

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Paul Cowan Contract software developer

Leave a Reply