# How to play animated SVG in Flutter (flutter_svg won't)

> flutter_svg shows animated SVGs as still pictures. svg_animate plays their SMIL and CSS @keyframes in Flutter, in pure Dart. How it works, and the alternatives.

- Author: [Ivan Chernoknizhnikov](https://treamz.me/), Flutter & iOS Engineer
- Published: 2026-08-15
- Updated: 2026-11-10
- URL: https://treamz.me/blog/flutter-svg-animation
- Tags: Flutter, Dart, SVG, Animation, Lottie, Performance

**Short answer:** flutter_svg doesn't play SVG animations: it skips SMIL
elements like `<animate>` and ignores `<style>`, so the file shows as a still
picture. To play the animation that's already in the SVG, add
[svg_animate](https://pub.dev/packages/svg_animate) and use
`AnimatedSvgPicture` where you'd use `SvgPicture`. It takes the same arguments:

```bash
flutter pub add svg_animate
```

```dart
AnimatedSvgPicture.asset('assets/spinner.svg', width: 48)
```

It plays SMIL, CSS `@keyframes` and CSS motion paths, in pure Dart. The rest of
this post is how it works, the three bugs real files exposed, and how it
compares with Lottie, Rive and the other SVG packages.

Our designer sent me a folder of animated SVGs. A spinner, a progress bar, a
few icons. Nice stuff, all of it animated right there in the file with plain
CSS and SMIL.

I dropped them into flutter_svg and got static pictures.

I spent an evening convinced I'd broken something. Wrong widget, wrong asset
path, something dumb. Turns out no: flutter_svg just skips `<animate>`. And it
ignores `<style>` too, which is worse, because that means a stylesheet-driven
SVG doesn't even render with the right colors, let alone move.

The usual answer is "convert it to Lottie". I didn't want to go back to the
designer and ask him to rebuild everything in After Effects when the animation
was already sitting in the file he'd sent me. So I wrote svg_animate.

## How svg_animate works: a flipbook of static SVGs

flutter_svg draws SVGs by compiling them into a binary format with
`vector_graphics_compiler`, then replaying that. That compiler is good. I had no
interest in writing a renderer.

So: don't. Take the animated document, work out what every animated attribute
is at time *t*, write those values back into the markup, and hand the result to
the existing compiler. Do that once per frame ahead of time, then flip through
the compiled frames like a flipbook.

A frame costs exactly what a static SVG costs to draw, because it *is* a static
SVG. You pay at load instead.

```dart
AnimatedSvgPicture.asset('assets/spinner.svg', width: 48)
```

Same arguments as `SvgPicture`. Reuses flutter_svg's `SvgTheme` and
`ColorMapper` so nothing in an existing app breaks. If the file has no animation
in it, it renders like a normal `SvgPicture` and doesn't even start a ticker.

Two evenings later the spinner was spinning. I felt great about myself for
roughly a week.

## Bug 1: an SVGator banner that rendered blank

A 450×450 promo banner exported from SVGator. Coins, sparkles, a train, a
"BUY BONUS" badge. 90 KB of markup with five WebP images base64'd into it.

It rendered as nothing at all. Not "wrong" — a blank box.

Here's where I want to be honest about how I debugged it, because I got it wrong
first. I built a minimal test case: a tiny SVG with one `<image>` in it,
referenced through `<use>`. Nothing drew. I tried the same file through plain
flutter_svg. Nothing drew there either. Great, I thought, images just don't work
on this pipeline, not my problem.

That conclusion was garbage. I'd hand-rolled the test PNG as a base64 string and
it was truncated — zlib couldn't even decompress it. I'd proven nothing except
that a broken PNG doesn't render.

Generated a real one. Ran it again. Images render fine. `<use>` pointing at an
image renders too, just at the wrong size.

So back to the real file, this time properly: feed the raw SVG straight into
`vector_graphics_compiler`, no code of mine involved.

```
Failed assertion: 'width > 0 && height > 0'  (writeDrawImage)
```

It dies in the compiler. Before any animation logic. `SvgPicture` would have
shown the same blank box.

Bisecting the file by deleting things: strip the 19 `<use href="#img">`
references and it compiles. The compiler loses an image's declared width and
height when the image is reached through a reference, gets zero, and then
refuses to draw something of zero size — killing the whole picture rather than
that one element.

Fix on my side: expand a `<use>` that points at an `<image>` into the image
itself before compiling. Only images — a `<use>` on a shape is fine, and
copying those would just bloat the document.

The banner rendered. Coins and all.

## Bug 2: slow frames and 27.5 MB of memory

Every compiled frame carries the images inside it, because each frame is a
self-contained picture. 82% of each 96 KB frame was image payload. At the
default 60 fps that's 300 frames and **27.5 MB**.

Worse than the memory: changing frame cost **6.8 ms**. The frame budget at 60fps
is 16.7 ms. That's 40% of it, just to decode, on an M-series Mac. On a mid-range
phone it would have blown straight through.

Two fixes, and I'm not proud of the first one.

**The image cache.** I went reading how vector_graphics decodes images and found
this:

```dart
final listener = FlutterVectorGraphicsListener(id: loader.hashCode, ...);
// ...
int _createImageKey(int imageId, int format) => Object.hash(_id, imageId, format);
```

It keys Flutter's global image cache by the hash code of the loader you hand it.
And mine was:

```dart
int get hashCode => Object.hash(identityHashCode(frames), frameIndex);
```

`frameIndex` in there. Every single frame opened its own namespace in the cache
and re-decoded the same five bitmaps. I had disabled a cache that was sitting
right there, with my own line of code, and then measured the result and blamed
the architecture.

Dropped `frameIndex`. Frames are still unequal to each other — that's what `==`
is for — and two unequal objects sharing a hash code is perfectly legal Dart.
Here it's the whole point. 6.8 ms → 1.4 ms.

**The duplicated bytes.** For memory I nearly went and wrote a parser for the
binary format to find and hoist the image section. Then I realised I didn't need
to understand the format at all: consecutive frames of one animation are the
same document with a few numbers changed, and the encoder puts everything the
picture embeds at the front. So just compare the frames byte by byte and find
the longest run they all start with.

On the banner that run is 78,772 bytes. The compiler reports 78,538 bytes of
images. It found them, without knowing what it was looking at.

Store that once, keep only the tails, glue a frame back together when it's
asked for. 27.5 MB → 5.1 MB, and reassembling costs about 0.1 ms.

I like this one because it doesn't depend on the encoder's layout. If the format
ever stops front-loading resources, the shared run just gets shorter and nothing
breaks.

## Bug 3: a CSS spinner with no 0% keyframe

Last one, and it's the most embarrassing, because it's the most common CSS
animation on earth:

```css
@keyframes spin { to { transform: rotate(360deg) } }
```

No `0%`. My code filled the missing end from the element's own `transform`
attribute, and when the element didn't have one, it fell back to... the other
keyframe. So both ends said 360 and the thing sat perfectly still while
everything around it animated.

The missing end has to come from the value the property has *outside* the
animation — no rotation for a transform, fully opaque for an opacity. With one
wrinkle: you can't substitute an empty transform, because two transform lists
only interpolate if they're built the same way. The stand-in has to keep the
shape: `translate(0 0)` against a translate, `scale(1)` against a scale.

## What svg_animate doesn't support

Filters. At all. `vector_graphics` drops `<filter>` entirely, so drop shadows
and glows just don't happen — the shapes draw, the effect doesn't. Same for
`mix-blend-mode: plus-lighter`, which is exactly what editors reach for to make
something glow.

Also no `d` morphing, no `<script>`, no `:hover`, no event-based `begin`. If you
need any of that, [full_svg_flutter](https://pub.dev/packages/full_svg_flutter)
covers a lot more of the format. It's a good package. It also bundles a
JavaScript runtime, which is a fair price for what it does and not one I wanted
to pay for a spinner.

Mine adds two pure-Dart packages, both already in flutter_svg's dependency tree.

## svg_animate vs Lottie, Rive and other Flutter SVG packages

"Animated SVG" means different things in different packages. Only some of them
play the animation that's already written into the file:

| Package | What it animates | Plays the animation already in the SVG | Notes |
| --- | --- | --- | --- |
| svg_animate | SMIL, CSS `@keyframes`, CSS motion paths | Yes | Pure Dart, on flutter_svg's own compiler; no filters |
| flutter_svg | Nothing: draws a still picture | No | Skips `<animate>`, ignores `<style>` |
| lottie | Lottie JSON files | No | The animation has to be rebuilt in After Effects and exported as Lottie |
| rive | `.riv` files from the Rive editor | No | The animation has to be rebuilt in Rive |
| anim_svg | SMIL and CSS `@keyframes`, converted to Lottie at runtime | Yes, except SVGator's script-driven exports | Renders with the native thorvg engine through FFI |
| full_svg_flutter | Much more of SVG | Yes | Bundles a JavaScript runtime |
| animated_svg | A transition between two SVG images, such as play to pause | No | For icon toggles, not animated files |
| drawing_animation | A line-drawing effect on `<path>` strokes | No | Draws paths progressively |

If your designer already animated the file, in SVGator, by hand, or anywhere
else that writes SMIL or CSS, use a package from the "Yes" rows. If you're
starting from scratch and want a timeline editor, Lottie or Rive is the better
tool.

## Install svg_animate from pub.dev

[pub.dev/packages/svg_animate](https://pub.dev/packages/svg_animate)

SMIL, CSS `@keyframes`, CSS motion paths (which is how SVGator writes every
movement, so its exports work), play/pause/seek through a controller. It's one
of the [projects I maintain](https://treamz.me/projects/#svg-animate).

If you've got an SVG that renders wrong, send it. Every single thing in this
post came from someone's file not working.

## FAQ

### Does flutter_svg support animated SVGs?

No. flutter_svg draws an SVG as a still picture: SMIL elements such as
`<animate>` are skipped, and `<style>` blocks are ignored entirely, so CSS
`@keyframes` don't play and colours set in a stylesheet don't apply either.

### How do I play an animated SVG in Flutter?

Add svg_animate with `flutter pub add svg_animate` and use
`AnimatedSvgPicture` where you would use `SvgPicture`. It takes the same
arguments:

```dart
AnimatedSvgPicture.asset('assets/spinner.svg', width: 48)
```

It plays the SMIL and CSS keyframe animations in the file, and an
`AnimatedSvgController` adds play, pause and seek. A file with no animation in
it renders like a normal `SvgPicture`.

### Do I need Lottie to animate an SVG in Flutter?

Not when the animation is already in the SVG. Files written with SMIL or CSS
`@keyframes`, including exports from SVGator, play as they are with
svg_animate, with no round trip through After Effects.

### What's the difference between svg_animate and animated_svg?

They solve different problems. svg_animate plays the animation inside an SVG
file: SMIL elements and CSS `@keyframes`. animated_svg doesn't read animation
from the file; it transitions between two separate SVG images, such as a play
icon turning into a pause icon.

### Does svg_animate work with SVGator exports?

Yes, with SVGator's CSS export: SVGator writes movement as CSS motion paths,
which svg_animate plays, and the image-heavy banner in this post is an SVGator
export. SVGator's JavaScript export depends on an embedded script, and
svg_animate doesn't run `<script>`.

### Which SVG animation features does svg_animate support?

SMIL (`<animate>`, `<animateTransform>`, `<animateMotion>` and `<set>`), CSS
`@keyframes` and CSS motion paths. It interpolates numbers, colours, lengths
and transform lists, and it loads SVGs from assets, the network, files, memory
or a string.

### What doesn't svg_animate support?

SVG filters, so no drop shadows or glows; `mix-blend-mode: plus-lighter`;
morphing a path's `d` attribute; `<script>`; `:hover`; and event-based `begin`.
For those, [full_svg_flutter](https://pub.dev/packages/full_svg_flutter) covers
more of the format, at the cost of bundling a JavaScript runtime.

### Is svg_animate fast enough for production?

Every frame is compiled ahead of time and costs the same to draw as a static
SVG. On the image-heavy banner in this post, switching frames went from 6.8 ms
to 1.4 ms once decoded images were cached, and memory went from 27.5 MB to
5.1 MB once the bytes every frame shares were stored only once.

### Which platforms does svg_animate run on?

Android, iOS, macOS, Windows and Linux.
