Art direction with viewBox
An SVG image, whether included inline or loaded via an image tag, is just an XML file. Here, for example, is a typical SVG graphic with a few shapes:
<svg width=100 height=100>
<rect x=20 y=20 width=40 height=60 style="fill: blue"></rect>
<circle cx=60 cy=60 r=30 style="fill: purple"></circle>
</svg>
And here's what that looks like, rendered onto the page. I've outlined it in gray, so you can see the bounds of the image.
It's a simple domain-specific language, really. Within the svg element, you can think of its child elements as being absolutely-positioned. The circle may look a little different, being positioned using the center x, center y, and radius, but so far it looks and acts a lot like HTML. It's certainly more like HTML than a JPG image: if you change the height and width (or set it from CSS), the SVG doesn't scale up or down. Instead, you just get more or less space around your shapes.
But what if that weren't the case?
Adding a viewBox
Ideally, we write an SVG using three different "measurements:"
- Width and height attributes on the element itself, setting its "intrinsic" size and aspect ratio
- CSS to define the actual, on-page width and/or height
- a viewBox attribute to define the internal coordinate system
The first two are basically identical to creating a JPG image, which also has an intrinsic size and can be resized from CSS. The view box, however, lets us choose a coordinate system that doesn't match the rest of the page, or even the final dimensions of the image. Its format is a space-separated list of "x y w h", meaning the x and y coordinates of the top-left corner, followed by the width and height of the visible space.
This may be easier to understand visually. Here, for example, is the same graphic as before, but with a view box set to "0 0 50 50"—meaning that the SVG coordinate space starts at 0,0 and is 50x50 pixels.
We've essentially "zoomed in" on the upper left corner: the graphic still starts in the same place, but its "viewable area" is only half as big, so we cut off the bottom and right sides of the image. We can keep that same width and height, but instead move the origin with the first two parts of the attribute, setting a viewBox of "20 20 50 50". This puts our rectangle flush against the corner, since it was also defined to start at 20,20.
Note that in all of these examples, the attribute has the letter B capitalized: viewBox. Unlike HTML, SVG attributes are case-sensitive. If you don't capitalize it correctly, you won't see the desired behavior, and the default view box (starting at 0,0 and extending to the image's rendered width/height) will be used.
It may be helpful to think of the view box as a camera looking down on a sheet of graph paper. With the first two numbers, we tell the camera how to pan across the image. The second two numbers tell it how to "zoom," or more specifically, how many squares on the graph paper should be visible in either direction. Unlike a real camera, however, that zoom doesn't have to be uniform in both directions: you can set the width and height in different ways, and by default the SVG will try to fit on the largest axis. Here's a view box with a height of 200, but a width of 100:
Stretching out with preserveAspectRatio
How the SVG "camera" handles a mismatch between the size of the viewBox and the intrinsic height of the image (as defined by the width and height attributes) is set by the preserveAspectRatio attribute. This lets us either reposition the contents, stretch them, or cut them off as needed.
Try setting different viewBox and preserveAspectRatio values in this interactive playground.
Setting preserveAspectRatio to "none" causes the image to stretch and squish, much like if you set a width and height on a bitmap image that are different from its intrinsic width and height. This can certainly be useful at times, but it's usually not what you probably want. The other options are more interesting, and are set using a pair of keywords.
The first keyword dictates how the SVG will position content where there's a mismatch between its size and the view box. Essentially, you're telling it where to add the leftover space. For example, on the X axis, it can either push content against the left side ("xMin"), center it along the horizontal axis ("xMid"), or fit it against the right side ("xMax"). These are paired with equivalent settings for the vertical axis ("YMin," "YMid," and "YMax," respectively). In the interactive, these are set the same across both axes, but they don't have to be. "xMinYMax" is a valid option, for example, which would push the content to the left and toward the bottom (thus allocating leftover space to the top and right).
The second preserveAspectRatio keyword is either "meet" or "slice," and these decide how the graphic will decide to scale. If the "meet" keyword is used, the longest axis (either width or height) will be scaled to match the image's rendered size, leaving empty space on either side of the shorter axis. The "slice" keyword, on the other hand, tells the graphic to scale the shorter axis up, cutting off whatever falls outside the bounds of the graphic.
There are probably cases where "slice" makes sense, but for our purposes, "meet" is usually a better choice. By setting a view box around a given element, and using a "meet" value, you'll make sure that whole element is always in view. This is a great technique to use when creating SVG images that "re-frame" themselves on mobile, or in response to interaction. It's also especially handy when we take the next step: animating the view box to move around the image.
Ready for your closeup?
Unfortunately, although it is a regular request from people who use SVG regularly, it's not possible to define the view box from CSS, which means that we can't use CSS transitions to alter its value smoothly. Instead, we're going to have to use JavaScript to split the attribute into its four component values, then interpolate between them. We'll first declare a function that we can call, passing in the SVG and an object containing the desired view box values:
var pan = function(svg, final, duration = 1000) {
//Get the current viewBox value and convert to numbers
var [x, y, w, h] = svg.getAttribute("viewBox").split(" ").map(Number);
//current timestamp, same format as requestAnimationFrame
var start = performance.now();
}
Inside of that function, we'll define a function that can be called each frame, figure out where we are along the elapsed time (from 0 to 1) and then scale the values to match. The details of this kind of animation are covered in more detail in this chapter.
var frame = function(t) {
var elapsed = t - start;
var dt = elapsed / duration;
if (dt > 1) dt = 1;
var dx = x + (final.x - x) * dt;
var dy = y + (final.y - y) * dt;
var dw = w + (final.width - w) * dt;
var dh = h + (final.height - h) * dt;
//create the new viewBox
var box = [dx, dy, dw, dh].join(" ");
svg.setAttribute("viewBox", box);
if (dt < 1) requestAnimationFrame(frame);
};
Then, all that's left is to call the function in the next frame:
requestAnimationFrame(frame);
And our transition should work. Try it out by clicking the buttons below to toggle between a couple of camera positions:
To really get the most use out of this, we might need a way to get the coordinates (in view box space) of a given shape. Then we can tell our camera to zoom in on that particular item, whether a particular shape or (more likely) a layer group defined by the artist in Illustrator or their tool of choice. Luckily, SVG elements provide a helpful method for doing just that: calling any SVG element's getBBox() method will tell you how big it is, including all its child elements if it's a group element.
var circle = $.one(".camera-example");
var bounds = circle.getBBox();
//pan to fit the circle
pan(svg, bounds);
In 2016, Seattle voted on funding for Sound Transit 3, an enormous expansion of the existing bus and rail system for the city. In print, we spent a whole page on the additions, but online—and especially on a phone—that wasn't possible. However, because the page artist was able to break the Illustrator graphic into layers and then export those to SVG, it was easy to do a voting guide that zoomed and panned around the image as the user scrolled through the list of additions. It's a great example of how this technique can be let your newsroom re-use resources and still present a high-quality digital project.
If you'd like to do something similar, we've abstracted the view box functionality into a micro-library called Savage Camera. Feel free to look through the code and take pieces that are helpful, or use it yourself!