Common pitfalls of SVG

If you need a vector-based image, there's really nothing else on the web platform that works as well as SVG. That doesn't mean it's a great experience—in fact, SVG is filled with traps just waiting to catch the unwary and waste hours of your time. To be forewarned of these is to be forearmed. Or, in other words: I have run with these scissors for you, now let me tell you what I've learned before I bleed out.

Attributes vs. properties

An attribute is the string-based markup on a given tag, while a property is the JavaScript mirror of that attribute that exists on the element after parsing. It's the difference between <element id="x"> (attribute) and element.id (property). This distinction is often blurry, because most DOM properties are "live" values linked to the matching HTML attribute. For example, if you update element.style in some way, you'll expect the style attribute on the element itself to be updated, and vice versa.

However, not all attributes are live, and in SVG they carry an additional caveat: if you try to access a property directly, instead of getting the value itself, you'll probably get an object of type "SVGAnimatedString" for your trouble. This object is a wrapper for "base" and "animated" values, and you can't set its value. Even better, in IE this is not true: since Microsoft never supported SVG declarative animations, its attributes are the same as any other HTML type.

The workaround for this is simple and effective: never set a property on an SVG element directly. Use the getAttribute() and setAttribute() methods instead. The code for doing so is much more verbose, particularly when working with the element's class, but it's safer and more reliable in a cross-browser environment.

// instead of these:
rect.className += " active";
var id = circle.id;
path.fill = "red";

// do this:
rect.setAttribute("class", rect.getAttribute("class") + " active");
var id = circle.getAttribute("id");
path.setAttribute("fill", "red");

More like text lay-about, amirite?

Using text in an SVG feels thrilling at first: you can place it anywhere you want! It can be scaled arbitrarily! You can place it on a path, like one of those horrible Word "TextArt" printouts that people love to use for leaving nasty notes on the communal refridgerator!

Unfortunately, once you try to place a significant amount of text into a graphic, it becomes clear that SVG just wasn't designed for doing real layout. You can't wrap text, it gets distorted by the underlying coordinate system, and scaling text up and down directly is rarely what you want for responsive graphics.

At this times, it may be helpful to remember that it is possible to visually mix SVG and HTML to get the best of both worlds. For example, in this quiz, we generated the draw-your-own-line graphs using SVG for the shapes, but all labels are placed over top of the image using absolute positioning and regular HTML. Doing so means that they're easier to scale up and down using media queries. This solution isn't workable for every graphic, but when possible it's hugely beneficial to let SVG handle only the vectors, and leave the rest in HTML.

Lost city of Z

It's weird, but SVG has no concept of z-index the way that the rest of your page does. On the one hand, this makes it much simpler than CSS, where arbitrary styles can create a "stacking context," around which z-index may have unpredictable effects. On the other hand, that conceptual simplicity is cold comfort when you realize that you can't create a pleasing CSS-only hover effect on your map because the thick outline you want to add to states under the mouse sometimes gets hidden under other shapes.

The only way to bring an SVG shape to the top (or send it to the back) is to actually alter its order within the DOM. That means writing JavaScript. Luckily, we can take advantage of a quirk in DOM methods to make it a bit less painful: adding an element as a child automatically moves it to the end of the list, even if it already belongs to the parent. So surfacing a given element can be as simple as this:

svgChild.parentElement.appendChild(svgChild);

Disabling mouse events

In HTML, we can create a visual element and then put its label inside, which provides a nice event cascade and a natural style hook for hover or focus. However, since you can't nest text inside of SVG shapes, you may end up placing labels side-by-side with the elements they're labeling. Depending on how you're generating the initial SVG, this may be profoundly unhelpful.

For example, at the Seattle Times we often created network graphics using Gephi, which can output to an SVG and mark the (unlabeled) nodes with IDs for databinding. However, if you then create labels on top of those nodes, you'll end up with an element that "blocks" mouse events from getting to the underlying shape, and you'll have to figure out how to handle interactions that might come from multiple places in the DOM.

There are two ways to get around this problem. The first is to make sure that all your interaction-related markup, like IDs or data-X attributes, are placed on groups containing both the label and the shape. If your shapes come from a graphical workflow, such as Illustrator, it may do this for you. You can also generate them at runtime, swapping the group for the shape and then re-attaching the latter inside the former:

// find all IDs in the image
var tagged = $("svg [id]");
tagged.forEach(function(element) {
  // create a container group
  var g = document.createElementNS(SVGNS, "g");
  // move the ID out to the group
  g.setAttribute("id", element.getAttribute("id"));
  element.removeAttribute("id");
  // put the group in the element's place
  element.parentNode.replaceChild(g, element);
  // now add the element back
  g.appendChild(element);
});

However, a simpler solution exists, which is to make it so that the text labels just don't actually trigger events at all. You can do this using the pointer-events style property, which is always supported on SVG (and increasingly—happily!—supported on HTML as well):

svg text.label {
  pointer-events: none;
}

With this style set, the element will be essentially invisible to the mouse, allowing any clicks or taps to slide right by and end up on the shape underneath, as you would hope.

Improving SVG performance

Over the past few years, spurred in many ways by the constant drumbeat for performance coming from Chrome, browsers have gotten faster and faster at rendering HTML and CSS. The same isn't true for SVG: opacity and transform aren't hardware-accelerated, and most other SVG properties trigger costly repaints on every frame. The result is that SVG is very smooth with a small number of elements, but increasingly choppy with more complex graphics (or on low-powered mobile platforms) compared to a CSS animation or canvas.

There's not much you can do about this if your graphic simply must be in a single SVG image. However, one solution that has worked well for me, depending on the graphic, is to break the SVG into multiple smaller images, each of which can then be animated from CSS using GPU-friendly transforms. For example, on this Boeing-centric timeline, each aircraft is loaded from a single SVG, then unpacked into its own SVG tag within the backdrop using a little bit of JavaScript. The resulting animations can take advantage of all the benefits of SVG, while still animating smoothly (since nothing within the plane image itself has to change).

That said, it is also worth spending some time making sure that you have abstractions available when working on dataviz using SVG, and that it's possible to switch rendering layers if necessary. This is also a good reason to learn a little canvas and WebGL—for many use cases, SVG simply can't be fast enough at this time in most browsers. With experience, you'll start to get a better sense for cases where the performance burden of SVG is too high before development.