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.
There are a few abstractions to consider when working out how much space is available to scale an SVG document.
The browser viewport is the visible area of a web page.
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:
The above SVG document of 2 <rect/> elements is larger than the SVG viewport, and because of this, only part of it is visible.
A magical attribute called the viewBox is the answer to a lot of our SVG responsive needs.
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:
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.
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:
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
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.
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 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} >
preserveAspectRatio
attributeThe 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.
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:
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowCreate a multi-lingual web application using Nuxt 3 and the Nuxt i18n and Nuxt i18n Micro modules.
Use CSS to style and manage disclosure widgets, which are the HTML `details` and `summary` elements.
React Native’s New Architecture offers significant performance advantages. In this article, you’ll explore synchronous and asynchronous rendering in React Native through practical use cases.
Build scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.