CSS keyframes vs SMIL: two ways to animate an SVG

See both kinds running

An SVG can animate itself two different ways, with no JavaScript in either. You can style it with CSS and drive it with @keyframes, or you can put SMIL elements — <animate>, <animateTransform> — inside the markup. Both work in every current browser. They are genuinely different systems, though, and the place that difference shows up is not where most comparisons look.

Two separate animation timelines side by side, each with its own keyframe markers feeding a rounded box
Two independent clocks in one document. Nothing synchronises them for you.

What CSS keyframes are good at

CSS wins on everything ergonomic. The animation lives in a stylesheet, so it can be shared across elements, overridden by a media query, paused with prefers-reduced-motion, and inspected in devtools like any other CSS. You get the whole cascade. If your animation is transforms, opacity and colour — which most are — CSS is the obvious choice.

CSS CSS
A CSS-keyframe animation, running live in this page.

What SMIL still does better

SMIL can animate things CSS cannot touch. Path data is the big one: <animate attributeName="d"> morphs one shape into another, and there is no CSS equivalent that works everywhere. It can also animate along a path with <animateMotion>, and it can chain animations declaratively — begin one when another ends, without a line of script.

You will read that SMIL is deprecated. The picture is murkier than that: a deprecation was announced years ago, then walked back, and the elements keep working. It is fair to say CSS is the safer default and SMIL is the specialist tool. It is not fair to say SMIL is dead.

They are written in different places

A CSS animation lives outside the shapes: you give an element a class, declare @keyframes in a <style> block or an external sheet, and the two are joined by a selector. One keyframe set can drive fifty elements, and you can restate it entirely inside a media query.

SMIL lives inside the shapes: an <animate> element is a child of the thing it animates, naming the attribute it changes and the values to move between. That makes a SMIL animation self-describing and impossible to lose track of, and also impossible to share — animating fifty elements means fifty child elements. It is the difference between a stylesheet and an instruction written on the object itself.

The part nobody mentions: freezing a frame

Here is the difference that actually costs time, and we found it the expensive way. If you want a still image of an animated SVG — a thumbnail, a preview, a poster frame — you have to stop the animation somewhere. The common trick for CSS is one line: set a hugely negative animation-delay, and every animation jumps to its end state.

That trick does nothing at all to SMIL. It rewrites animation-delay, which SMIL does not use — SMIL has its own begin attribute and its own clock. So the animation stays at time zero, and you screenshot the first frame: an empty canvas, or a letterform that has not started drawing, presented as the finished effect. Nothing errors. The image is simply wrong.

Of the 78 animation styles on this site, 16 are SMIL — including several of the most popular ones. So the still-capture code has to branch: CSS animations are driven through getAnimations(), and SMIL through the SVG element's own setCurrentTime(). Two systems, two clocks, two ways to ask them to hold still. Every image in the style gallery goes through that branch.

The word SMIL rendered in liquid metal, seeked to a moment mid-animation rather than its first frame
A SMIL animation held at 2.6 seconds. Freeze it the CSS way and you get a blank frame instead.

Which to reach for

Whichever you pick, the animation travels inside the file. That is what makes an SVG worth exporting at all, and it is how a self-contained animated SVG works — no runtime library, nothing to load. You can see either kind running in the generator.

More from the blog