SVG Animation and CSS Transforms: A Complicated Love Story

Avatar of Jack Doyle
Jack Doyle on (Updated on )

📣 Freelancers, Developers, and Part-Time Agency Owners: Kickstart Your Own Digital Agency with UACADEMY Launch by UGURUS 📣

The following is a guest post by Jack Doyle, author of the GreenSock Animation Platform (GSAP). Jack has been deep in the woods of web animation for a long time, trying to make it easier and better. He’s written here before, talking about how JavaScript animation can be the most performant choice (Google even recommends it). This time, he focuses on SVG animation, some pretty scary issues you may come across while manipulating them with CSS, and how you can solve those issues.

SVG is all the rage these days, and browser support is generally excellent…with one glaring exception: CSS transforms. This is particularly painful when it comes to animation because scale, position, rotation, and skew are so fundamental.

Buckle up, because we’re in for a bumpy ride. But don’t worry; this complicated love story has a happy ending. In this article, I’ll walk you through some of the problems and then show you a technique that harmonizes behavior across browsers and is baked into the latest version of the GreenSock Animation Platform (GSAP).

Browser bugs & inconsistencies

First, check out these animated GIFs showing the exact same CSS animation of two <rect> elements in various browsers (at least as of November 2014):

See the Pen GIFS: SVG + CSS Transform Problems by GreenSock (@GreenSock) on CodePen.

  • IE and Opera don’t honor CSS transforms at all on SVG elements. Instead, you must assign the value to the transform attribute.
  • Firefox doesn’t honor %-based origins.
  • Zooming in Safari breaks the sync between %-based and px-based origins.
  • Firefox doesn’t recognize keyword-based origins like "right bottom", and Safari alters them when the zoom is anything but 100%.
  • In all browsers, px-based origins are measured differently for SVG elements than other DOM elements (see below).

Here’s a video demonstrating those bugs:

Think of the transform-origin as the point around which all rotating and scaling occurs, as if a pin was holding it in place there,

Where’s my transform-origin?

Normally, if you want to rotate or scale from the center of an element, you’d set transform-origin: 50% 50%. Percentages are relative to the element’s own native size, so 50% of its width and 50% of its height lands smack-dab in the middle. All major browsers support %‑based origins for regular DOM elements, but only a few support them for SVG elements.

How about px-based values? Support is excellent, but there’s another problem: as shown below, SVG measures px-based values relative to the parent SVG’s 0,0 coordinate whereas every other DOM element (like a <div>) measures it relative to its own top left corner. So if you want to center it, you have to do the math to plot the coordinates in SVG (you can use getBBox() JS for this). Consequently, you cannot apply the same CSS to a regular DOM element and an SVG element and expect them to behave alike.

See the Pen SVG transform-origin demo by GreenSock (@GreenSock) on CodePen.

A solution

It’d be nice if I had a pure CSS trick that’d work here, but since IE and Opera completely ignore CSS transforms on SVG elements, and there are bugs in most other browsers, we’ll rely on one of the incredible strengths of JavaScript: its flexibility. Even if every browser fixes their bugs next week, we still couldn’t get around the fact that the SVG spec handles px-based transform origins very differently than other DOM elements, so CSS transforms are bound to that spec. As far as I can tell, the only viable longer-term solution is a JS-based one. Plus JS gives us a lot of other benefits, but that’s a subject for another article.

The goal: be able to animate various transform properties (rotation, scale, position, and skew) of SVG elements in the same way as “normal” DOM elements while delivering identical behavior across all major browsers. We should also be able to set the transform-origin using standard CSS-like values including percentages, px, or keywords like "right bottom". Oh, and it must be FAST.

In case you’re not already familiar with GreenSock’s GSAP, it’s a high-performance, professional‑grade JavaScript animation library with unmatched sequencing tools, runtime controls, and flexibility (animate literally anything JavaScript can touch). Google recommends it for JS-based animations, and it’s up to 20x faster than jQuery.

Here’s an overview of the technique employed under the hood in GSAP (we won’t get into the matrix math because it’s beyond the scope of this article). The demo below illustrates what GSAP does when you attempt to spin an SVG <rect> around its center by specifying a transform-origin of "50% 50%".

See the Pen SVG transform-origin solution by GreenSock (@GreenSock) on CodePen.

The resulting matrix() string should be fed to either the element’s CSS style or the transform attribute, whichever is necessary for that particular browser.

If that made no sense to you, that’s okay – it all happens automatically for you under the hood in GSAP.

The result

Not only do we get harmonized behavior across browsers and a consistent animation API for SVG and non-SVG elements, but the animation code we have to write is tiny compared to the (broken) pure CSS animation.

TweenMax.to("#svg, #div", 2, {
  rotation:360, 
  transformOrigin:"50% 50%"
});

Here’s a demo that shows a sequence of several origins. Notice everything works the same for the SVG <rect> and the <div>:

See the Pen SVG CSS transforms timeline by GreenSock (@GreenSock) on CodePen.

Tips for animating SVG with GSAP

I’m not going to explain GSAP’s API here (see the getting started article for that), but I’ll offer a few SVG-related tips. Snag GSAP on github or download it from GreenSock.com.

  • Rather than combining all of the transform components into a single “transform” string like in CSS, GSAP lets you define each one independently, and you don’t need to worry about order-of-operation (it’s always consistent). Also keep in mind that GSAP uses x and y to refer to translateX() and translateY(), and rotation for rotate(). Everything else is the same (scaleX, scaleY, skewX, and skewY).
    #yourID {
        animation-name: myAnimation;
        animation-duration: 2s;
        animation-timing-function: ease-out;
        transform-origin: right bottom;
    }
    @keyframes myAnimation {
        from {transform: none;}
        to {transform: rotate(270deg) scaleX(0.5) translateX(100px);}
    }
    TweenMax.to("#yourID", 2, {
        rotation:270, 
        scaleX:0.5, 
        x:100, 
        transformOrigin:"right bottom"
    });
  • You can pass in the raw element as the target (first parameter), or selector text, or an array of elements.
  • To animate numeric attributes of SVGs (rather than CSS properties), use an attr:{} object like:
    TweenMax.to("#circle", 2, {attr:{cx:200, cy:300, r:20}, ease:Power2.easeInOut});
  • You can also use 3D properties like z, rotationX, rotationY, and perspective but some browsers like IE and Opera won’t recognize those for SVG elements.
  • It is best to set the transformOrigin directly via GSAP rather than in CSS because various browsers report them inconsistently via getComputedStyle().
  • You can animate just about any CSS value including color properties like fill and stroke.
  • If you want to do much sequencing, I’d highly recommend watching the videos here and here. Sequencing is one of the most significant strengths of GSAP.

More resources