SMIL is dead! Long live SMIL! A Guide to Alternatives to SMIL Features

SMIL, SVG’s native animation specification, has been highly regarded because it offers so many bells and whistles for performant SVG animation rendering. Unfortunately, support for SMIL is waning in WebKit, and has never (nor will likely ever) exist for Microsoft’s IE or Edge browsers. Have no fear! We’ve got you covered. This article explores some of those SMIL-specific features and delves into the alternatives to achieve the same effects with a longer tail of support.

SMIL Feature: Motion Along a Path

One of the most compelling things that SMIL offers for realistic motion in SVG is motion along a path. Very few things in real life move along a straight line, so motion along a path allows us to mimic what we see in everyday life. Previously, you would pass an SVG path into animateMotion, with path and define the path data. You pick the element to animate by designating it with xlink:href="#thingtoanimate".

<animateMotion 
  xlink:href="#lil-guy" 
  dur="3s" 
  repeatCount="indefinite" 
  fill="freeze" 
  path="M 25,50 C 37.5,25 37.5,25 50,0 75,50 75,50 100,100 50,100 50,100 0,100 12.5,75 12.5,75 25,50 Z" />

See the Pen SMIL motion path by Sarah Drasner (@sdras) on CodePen.

An Alternative: CSS

Luckily for us, the motion along a path module is now moving into CSS. The support thus far is slim (Chrome, Opera, and Android only):, but Sara Soueidan has proposed adoption in Edge, and it has strong support thus far, with around 420+ votes at the time of the article’s publication. Please add your voice to ensure that this feature will be shipped. The voting for Firefox is here.
Voting for support in Safari, as far as I can gather, is a little more individual. I registered to fill out a bug report and requested motion path module in CSS as a feature.

In order to use motion along a path in CSS, you pass the path data into the offset-path property like so:

.move-me {
  offset-path: path('M3.9,74.8c0,0,0-106.4,75.5-42.6S271.8,184,252.9,106.9s-47.4-130.9-58.2-92s59.8,111.2-32.9,126.1 S5.9,138.6,3.9,74.8z');
}

See the Pen Playing with Motion Path Module in CSS by CSS-Tricks (@css-tricks) on CodePen.

I get path data from an SVG I make in Illustrator and then optimize in SVGOMG.

In this instance, I want to ensure that it follows the path from the starting point all the way to the end of the path, and you can see that it is a closed path by the small z at the end. This means this path is in a loop, so the weird little creature will end up at the same place he started from. I set these parameters in my keyframes values, only specifying the 100% value because the default is set at zero:

@keyframes motionpathguy {
  100% {
    motion-offset: 100%;
  }
}

and then call the animation on the element:

.move-me {
  animation: motionpathguy 10s linear infinite both;
}

An Alternative: GreenSock’s Motion Along a Path

If you want the widest current support and the most flexible implementation, you should be using GreenSock. GSAP’s Bezier-Plugin (packaged in by default with TweenMax) offers support back to IE7 for non-SVG elements, and back to IE9 for SVG (the broadest SVG animation support available). It works like gangbusters on mobile. I’ve written about this before on the David Walsh Blog, but here’s a small recap, plus a few extra new features that have come out since then:

Originally you would pass in an array of values:

bezier: {
  type: "soft",
  values:[{x:10, y:30}, {x:-30, y:20}, {x:-40, y:10}, {x:30, y:20}, {x:10, y:30}],
  autoRotate: true
}

But as you can see here, you can also have the option of autorotating (or not) just like SMIL’s rotate. If you’re missing SMIL’s ability to specify auto-reverse or auto:n parameter for initial position of rotation or degrees of rotation, GSAP allows you to use rotation:90 to change the number of degrees, or more finite control should you need it:

autorotate: [
  first position property, like "x",
  second position property, like "y",
  rotation property, typically "rotation" but can be “rotationY”,
  integer for radians/degrees the rotation starts from like 10,
  boolean for radians or degrees- radians is true
]

In SMIL, you can transform the path or group to change that orientation on the object while it’s moving. In GSAP, you can achieve this easily with autoRotate: false, and initializing the rotation with set. You can also transform the element on the SVG attribute itself like you would with SMIL, although that’s a little less elegant and harder to keep track of while you’re working.

TweenMax.set("#foo" {
  rotation: 90 // or whatever number
});

You can also set the type to thru, soft, quadratic, or cubic. More documentation on each of these are in the GreenSock API docs. A nice value of thru is being able to affect the amount of curviness on an element. If you think about the points as coordinates to bounce to and from, the curviness would control how direct a path to take between those points. 0 would be a direct path, 1 is a little looser, with 2 making a nice curve, and values of 3 and higher begin winding on itself.

See the Pen Demo for Curviness in GreenSock Bezier by Sarah Drasner (@sdras) on CodePen.

More recently, GreenSock has also exposed the ability to pass in path data like the CSS and SMIL modules, as you would with native SMIL. This comes as an extension to their MorphSVG plugin, so you would add the plugin, and use it like this:

TweenMax.to("#lil-guy", 3, {
  bezier: {
    MorphSVGPlugin.pathDataToBezier("#path", {align: "#lil-guy" }), 
    type: "cubic"
  },
  ease: Linear.easeNone,
  repeat: -1
});
<path id="path" d="M 25,50 C 37.5,25 37.5,25 50,0 75,50 75,50 100,100 50,100 50,100 0,100 12.5,75 12.5,75 25,50 Z" fill="none" />

See the Pen SMIL motion path by Sarah Drasner (@sdras) on CodePen.

The default would have set the upper left corner of the group I'm animating, in this case, #lil-guy, to the path trajectory. This would have made it visually look misaligned. So I set #lil-guy to use the centerpoint instead using TweenLite.set:

TweenLite.set("#lil-guy", {xPercent:-50, yPercent:-50}); 

You can also offset these paths by passing an object as the second parameter to that method and define an offsetX and offsetY within pathDataToBezier- beware that you might need to expand the viewBox so that the group or attribute you’re animating doesn’t get cropped out. Formatting nerds: I'm placing the objects on new lines for legibility reasons.

// offset the path coordinates by 125px on the x-axis, and 50px on the y-axis:
TweenMax.to("#lil-guy", 3, {
  bezier: {
    values: MorphSVGPlugin.pathDataToBezier("#path", {
      offsetX: 125, 
      offsetY: 50, 
      align: "#lil-guy"
    }),
    type: "cubic"
  },
  ease: Linear.easeNone,
  repeat: -1
});

See the Pen SMIL motion path by Sarah Drasner (@sdras) on CodePen.

You can even define a matrix coordinate for this positioning.

// scale the path coordinates up by 1.25 
// and shift it over 120px on the x-axis 
// and up 30px on the y-axis:
TweenMax.to("#lil-guy", 3, {
  bezier: {
    values: MorphSVGPlugin.pathDataToBezier("#path", {
      matrix:[1.5,0,0,1.5,120,-30], 
      align:"lil-guy"}),
    type: "cubic"
  },
  ease: Linear.easeNone,
  repeat: -1
});

See the Pen SMIL motion path by Sarah Drasner (@sdras) on CodePen.

Another option is to set the align member to "relative". This keeps it from jumping by keeping the position of each coordinate relative to x:0, y:0. In the previous pens, I used align to pair the movement with the #lil-guy group itself.

For more information on this new (as in new the day of this post!) feature in GreenSocks's Bezier Plugin API, check out their docs, and this great explanatory video.

SMIL Feature: Shape Morphing

Previously, you would be able to pass path data as values into animate attribute in order to morph a shape. Noah Blon has a great example:

See the Pen Sitepoint Challenge #1 in SVG and SMIL by Noah Blon (@noahblon) on CodePen.

An Alternative: Snap.svg or SVG Morpheus

Some libraries offer morphing path or shape values, such as Snap.svg and SVG Morpheus, but the caveat (even in SMIL) is that the shape has to have the same number of points or the morphing looks terrible or fails entirely. This is disappointing on the pre-processing end, because it means that you have to keep good track of what you’re making, or collaborate well with your designer to make sure you’re being fed this (sometimes arbitrary) midpoint data. The extra points also unnecessarily bloat your code.

An Alternative: GreenSock MorphSVG

I would highly recommend GSAP’s MorphSVG plugin, as it beautifully morphs shapes and paths with different amounts of points. See the toggle on the logo of this very website for a demo of the morph in action. Here’s another example:

See the Pen Interchangable Hipster by Sarah Drasner (@sdras) on CodePen.

Because MorphSVG plugin tweens path data, if you need to convert a shape, you would use their convertToPath option:

MorphSVGPlugin.convertToPath("ellipse"); 
// or circle, rect, etc

This allows us to do really complex shape tweening, and is a game-changer for all of motion on the web.

There are a few extra features that this plugin offers that really knocks it out of the park. The first is the utility plugin findShapeIndex. Let’s say you are unhappy with the way that the shape is morphing (though 9 times out 10, the auto preset will work just fine). You load up the plugin (don’t worry, you won’t add extra weight because it’s not needed in production), and you pass in two values: the ID of the first shape to tween and the ID of the second. A GUI will pop up where you can toggle between values, and it will also automatically use a repeat: -1, so that it will continually loop between the shapes.

findShapeIndex("#hex", "#star");
// you can comment out above line to automatically disable findShapeIndex() UI

See the Pen SMIL motion path by Sarah Drasner (@sdras) on CodePen.

Once you have that additional value, you would pass the shapeIndex within the morphSVG object:

TweenLite.to("#hex", 1, {morphSVG: { shape: "#star", shapeIndex: 1 }});

The second of these extra features is the plugin’s ability to parse cut-out-paths, which no other library offers. Finally, you can also reuse the first starting ID (rather than having to store that path data for reuse). It’s worth mentioning that when the plugin was first released, these features were not available, but GreenSock recognized the need for support, and so, included it.

Now that we aren’t tied down to a specified number of points, we’ve widened the possibility for different kinds of effects. Below, I made some smoke:

See the Pen Where There's Smoke by Sarah Drasner (@sdras) on CodePen.

SMIL Feature: DOM events

Things like hover, and click were nicely baked into SMIL. In order to kick things off, it was possible to specify begin="click" or begin="hover".

<animate 
    xlink:href="#rectblue"
    attributeName="x"
    from="0"
    to="300" 
    dur="1s"
    begin="click"
    values="20; 50"
    keyTimes="0; 1"
    fill="freeze" />

See the Pen SMIL motion path by Sarah Drasner (@sdras) on CodePen.

An Alternative: JavaScript

There are the native DOM events like onmouseenter and onmouseleave for hover and click for, ya know, clicks. You could use them to change to trigger your JavaScript-based animations.

An Alternative: JavaScript + CSS

You could use JavaScript to change a class name or directly change CSS styles. Here's on possibility: change the animation-play-state to start the animation from an event trigger.

.st0 {
  animation: moveAcross 1s linear both;
  animation-play-state: paused;
}
@keyframes moveAcross {
  to {
    transform: translateX(100px);
  }
}
document.getElementById("rectblue").addEventListener("click", function() {
  event.target.style.animationPlayState = "running";
});

or in jQuery:

$(".st0").on("click", function() {
  $(this).css("animation-play-state", "running");
});

See the Pen SMIL motion path by Sarah Drasner (@sdras) on CodePen.

This implementation would not immediately set this animation back to the beginning again as in the SMIL example. If you wanted to accomplish that, a previous article on CSS-Tricks details a few good ways of doing so.

An Alternative: Greensock

In GSAP, the restart is more simple. We could add the animations to a timeline, set it to paused, and then on click restart it. This implementation is a little closer to what you’d expect of SMIL, because we don’t have to do anything hacky like clone/reinsert a DOM node or change any properties set on the element.

// instantiate a TimelineLite    
var tl = new TimelineLite();

// add a tween to the timeline
tl.to(foo, 0.5, { left: 100 });

$(".st0").on("click", function() {
  tl.restart();
});

SMIL Feature: Run "X" after "Y" is completed

SMIL also allowed for more complicated timed events such as begin="circ-anim.begin + 1s". This is particularly useful when chaining animations.

An Alternative: CSS

In CSS, we could chain the animations by setting a delay on the second value:

.foo {
  animation: foo-move 2s ease both;
}
.bar {
  animation: bar-move 4s 2s ease both; 
  /* the 2 second value corresponds with the length of the iteration of the first. */
}

This way of working a bit of a bummer because you have to make sure you remember to change the first interval as well as the delay.

An Alternative: CSS Preprocessing

Maintaining and managing those intervals can get a little easier if we use a variable in (for example) Sass:

$secs: 2s;
.foo {
  animation: foo-move $secs ease both;
}
.bar {
  animation: bar-move 4s $secs ease both; 
}

Now we know that if we update the one value, they’ll stay in sync.

But if we want to always detect when the animation is complete, JavaScript offers some nice native functionality for this with animationEnd:

$("#rectblue").on("animationend", function() {   
  $(this).closest("svg").find("#rectblue2").css("animation-play-state", "running");     
});
#rectblue2 {
  animation: moveAcross 2s 1s ease both;
  animation-play-state: paused;
}

You may need to hit replay to see the animation below:

See the Pen SMIL motion path by Sarah Drasner (@sdras) on CodePen.

For the delay, we would bake it into the animation-delay property of CSS of the element itself, like above, or we could express this with a setTimeout:

setTimeout(function timeoutHandler() {
  // animation goes here, with any language
}, 1000); // wait for a second

An Alternative: Greensock

My favorite option is to add finite control of the animation timeline. We can use GreenSock’s TimelineLite for this, which can be expressed a few different ways:
Simple Timeline:

// instantiate a TimelineLite    
var tl = new TimelineLite();

// add a tween at the beginning of the timeline
tl.to(foo, 0.5, { left: 100 });

// use the position parameter "+=1" to schedule next tween 1 second after the previous tweens end
tl.to(foo, 0.5, { left: 200 }, "+=1");

Timeline with a relative label:

// add a label 0.5 seconds later to mark the placement of the next tween
tl.add("myRelativeLabel")
// use the label to specify an animation a second after the 
tl.to(foo, 0.5, { scale: 0 }, "myRelativeLabel+=1");

// or to use to this label for things like interaction 
tl.play("myRelativeLabel");

I prefer relative labels, because you can choose a point in time that a lot of things fire, or are delayed by, and even if that point in time adjusts, you don’t have to do any recalculation as you would have to in CSS.

The nice thing about the timeline is that you then have fine control over many different objects in just one place, and can offer things like repeatDelay (delays in between multiple repeats).

SMIL offers repeatDur, which lets you say how long the repeat iteration would be, if you don’t want it to be the default such as repeatDur="01:30". In GreenSock, you can speed up or slow down the timeline, thereby adjusting the repeat length with timeScale(n), or set a repeat: -1 for times that you would otherwise set repeatDur="indefinite".

Handy Dandy Replacement Reference Chart

Now that we’ve done a deep-dive into some of the most useful SMIL-specific features, it’s worth noting that here are many more replacements for SMIL functionality that you might already know about. We’ve made the small table below for quick reference of these simpler implementations.

SMIL Code Replacement Code Replacement Technology
keyTimes @keyframes CSS
keySplines cubic-bezier CSS
restart restart(); GSAP
calcMode="discrete" steps() CSS
remove kill();
clear();
clearProps: "all"
GSAP
freeze animation-play-state: paused CSS
freeze pause(); GSAP
fill animation-fill-mode CSS
repeatCount="indefinite" animation-iteration-count: infinite; CSS
repeatCount="indefinite" repeat: -1 GSAP
whenNotActive detect animation-play-state in JS CSS, vanilla JavaScript or jQuery
animateMotion path motion-path CSS
animateMotion path bezier GSAP
animate values (path morphing) MorphSVG GSAP
begin="hover" mouseover, mouseenter/
mouseout, mouseleave
jQuery, vanilla JS
begin="click" click jQuery, vanilla JS
begin="circ-anim.begin + 1s" animation-delay: $vars; SASS
begin="circ-anim.begin + 1s" timeline, position parameter ex "+=1" GSAP