SVG animation performance and accessibility checklist
SVG Weave team
Published
A practical checklist for fast, accessible SVG animation: transform and opacity, will-change, off-screen pausing, reduced motion, title and desc, aria-hidden, SVGO.
For smooth SVG animation, animate transform and opacity, keep the number of animated elements small, pause what is off-screen, and wrap all motion in prefers-reduced-motion. For accessibility, hide decorative SVGs with aria-hidden="true" and give meaningful ones a title and desc. Run SVGO last and check that it did not break your animation.
The rest of this post is the checklist, with the reasoning and runnable code for each item. Measure in DevTools before you optimize: the advice below is a set of defaults, not a substitute for profiling your own page.
Part 1: Performance
1. Animate transform and opacity, not geometry
Browsers handle animation in stages: recompute style, lay out, paint, then composite. The earlier a property forces the pipeline to start, the more work each frame costs. transform and opacity change how already-drawn content is placed or blended, so they sit at the cheap end. Changing geometry sits at the expensive end.
In SVG, this applies to attributes and properties that change shapes:
/* Costly: changes the shape's geometry on every frame */
@keyframes grow-bad {
from { r: 10px; }
to { r: 30px; }
}
/* Cheaper: same visual idea, drawn once and scaled */
@keyframes grow-good {
from { transform: scale(1); }
to { transform: scale(3); }
}
The same goes for x, y, cx, cy, width, height and path data (d). If you can express the motion as translate, rotate, scale, skew or fade, do. For the full list of what each property triggers, look it up in the MDN animation performance guide and the web.dev animations guide.
One honest caveat: "cheap" does not always mean "runs on the compositor thread" for the inside of an SVG. Browsers can offload transform and opacity animations on ordinary HTML boxes to the compositor, but support for animated elements inside an SVG has historically been weaker. A 2016 Chromium code review says SVG element transform animations were not run on the compositor because SVG was composited on the main thread. That may have changed since, so check the browsers you care about in the Performance panel instead of assuming. The safest case is animating the outer <svg> element, or an HTML wrapper around it, because that is a normal box.
2. Animate groups, not every path
One <g> with one animation moves all its children together. Fifty paths each with their own animation means fifty animations to run and fifty elements to repaint.
<svg viewBox="0 0 200 200" width="200" height="200">
<g class="wing-flap">
<path d="M100 100 L40 60 L60 110 Z" fill="#6366f1"/>
<path d="M100 100 L160 60 L140 110 Z" fill="#6366f1"/>
</g>
</svg>
.wing-flap {
transform-box: fill-box;
transform-origin: center;
}
@media (prefers-reduced-motion: no-preference) {
.wing-flap {
animation: flap 1.2s ease-in-out infinite alternate;
}
}
@keyframes flap {
to { transform: scale(1, 0.7); }
}
If you need a stagger, apply it to a handful of groups with animation-delay, not to every shape.
3. Set transform-origin on purpose
For SVG elements the default transform-box is view-box, so transform-origin: center means the center of the whole SVG viewport, not the shape. Rotations then swing around the wrong point. Use transform-box: fill-box; transform-origin: center; as in the example above. See transform-box on MDN. This is a correctness issue more than a speed one, but a wrong pivot is the most common reason people reach for extra wrapper elements and extra animations.
4. Be careful with will-change
will-change: transform tells the browser to prepare an element for animation, usually by promoting it to its own layer. It sounds like a free speed-up. It is not. MDN says plainly that will-change is a last resort for fixing existing performance problems, not something to sprinkle in anticipation, and that overuse can increase memory use and slow the page down.
Practical rules:
- Do not put it on every animated element in a stylesheet by default.
- If you have measured a stutter and it helps, apply it only to the element that needs it.
- Where possible add it just before an animation starts and remove it when the animation ends.
5. Count your animated nodes
Cost scales with how many elements animate at once and how much each repaint covers. There is no universal number, because it depends on the device, the browser and the complexity of the shapes. Treat these as guidelines to test against:
- Dozens of independently moving paths in one SVG is a smell. Consider grouping.
- Large blur or drop-shadow filters on animated elements are repainted as the element changes. If you need a glow, test it on a mid-range phone.
- Large SVGs with many nodes that stay static are cheaper than the same SVG with every node animating, so keep the animated part small.
- Repeating
stroke-dashoffsetanimations on many paths force repaints. One or two is fine.
For reference, SVG Weave's generator limits itself to about 15 independently animated elements per file, which is a reasonable starting budget, not a hard rule.
6. Do not animate with JavaScript per frame unless you must
A requestAnimationFrame loop that calls setAttribute on 200 elements every frame will usually lose to CSS animations. If you need script control (play, pause, seek), the Web Animations API gives you that on top of the same engine as CSS:
const el = document.querySelector('.wing-flap');
const anim = el.animate(
[{ transform: 'scale(1, 1)' }, { transform: 'scale(1, 0.7)' }],
{ duration: 1200, iterations: Infinity, direction: 'alternate', easing: 'ease-in-out' }
);
anim.pause(); // later: anim.play();
Avoid mixing reads and writes. Calling getBBox() or getBoundingClientRect() after changing styles, in a loop, forces the browser to recompute layout each time. Read everything first, then write.
7. Pause animations that are off-screen
An infinite loop keeps running when it is scrolled out of view or covered by another element, unless you stop it. For inline SVG you can pause it with animation-play-state and an IntersectionObserver:
.is-paused,
.is-paused * {
animation-play-state: paused !important;
}
const targets = document.querySelectorAll('[data-pause-offscreen]');
const io = new IntersectionObserver((entries) => {
for (const entry of entries) {
entry.target.classList.toggle('is-paused', !entry.isIntersecting);
}
});
targets.forEach((el) => io.observe(el));
<svg data-pause-offscreen viewBox="0 0 100 100" width="100" height="100">
<!-- animated content -->
</svg>
See IntersectionObserver on MDN and animation-play-state. Two limits: this works only for inline SVG, because the page cannot reach inside an <img> or CSS background to pause its animation; and loading="lazy" delays fetching an image but does not stop it animating once loaded.
Part 2: Accessibility
8. Respect prefers-reduced-motion
Some users get dizzy or distracted by motion and set their OS to reduce it. CSS exposes that choice through prefers-reduced-motion. There are two patterns. The safer one is to make the static drawing the default and add motion only for users who have not asked for less:
.logo-spin {
transform-box: fill-box;
transform-origin: center;
}
@media (prefers-reduced-motion: no-preference) {
.logo-spin {
animation: spin 8s linear infinite;
}
}
@keyframes spin {
to { transform: rotate(360deg); }
}
The opt-out version removes animation in a reduce block:
@media (prefers-reduced-motion: reduce) {
.logo-spin,
.logo-spin * {
animation: none !important;
}
}
Both only work well if the un-animated state is a sensible resting pose. If an element starts invisible (opacity: 0) and an animation fades it in, removing the animation leaves it invisible. Set the final, visible state in the base styles and let the animation start from the hidden state via from keyframes.
For content that moves for more than five seconds on its own, WCAG 2.2.2 requires a way to pause, stop or hide it unless the motion is essential. See Pause, Stop, Hide in WCAG 2.2 Understanding docs. A decorative background loop is a typical case that needs it.
9. Hide decorative SVGs
If the SVG adds nothing a screen-reader user needs, such as a background flourish or an icon next to a visible label, hide it:
<svg aria-hidden="true" focusable="false" viewBox="0 0 24 24" width="24" height="24">
<!-- decorative icon -->
</svg>
aria-hidden="true" removes it from the accessibility tree. focusable="false" stops legacy browsers from putting it in the tab order. For an SVG in an <img> tag, use an empty alt="".
10. Give meaningful SVGs a title and desc
If the graphic carries information (a diagram, an animated explainer, an icon button with no visible text), give it an accessible name and, when needed, a longer description:
<svg role="img" aria-labelledby="chart-title chart-desc"
viewBox="0 0 200 100" width="200" height="100">
<title id="chart-title">Signups per month</title>
<desc id="chart-desc">Bar chart showing signups rising from January to June.</desc>
<!-- bars -->
</svg>
The <title> element should be the first child of its parent. role="img" and aria-labelledby pointing at the ids make the name reliable across screen readers, since support for title alone has been inconsistent. Keep the name short and put the detail in desc.
Two more notes. Browsers may show title text as a tooltip on hover, which is fine for meaningful graphics but another reason not to add one to decorative ones. And if the animation conveys a state change (loading, success), do not rely on the motion alone: pair it with text or a live region, as in the loading spinners post.
Part 3: Ship it
11. Optimize with SVGO, then test the animation
SVGO strips editor metadata, comments and redundant attributes and shortens path data:
npx svgo animated.svg --output animated.min.svg
SVGO's default preset includes plugins that touch things animations depend on. Check each one:
mergePathsandconvertShapeToPathchange element structure. If CSS targets acircleor a specific path, it may no longer match.cleanupIdsremoves unused ids and can shorten the ones that are referenced. SVGO backs off when astyleelement is present, but test rather than trust.removeDescis in the default preset. According to its docs, it removes only an emptydescor one that is just editor attribution, so a real description survives. TheremoveTitleplugin is not in the default preset, so do not enable it for SVGs that need a name.
Disable what breaks your file:
// svgo.config.js
module.exports = {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
mergePaths: false,
convertShapeToPath: false,
cleanupIds: false,
},
},
},
],
};
Run it with npx svgo animated.svg --config svgo.config.js. Then open the output and watch the animation, with the reduced-motion setting both on and off.
Quick checklist
| Check | Why |
|---|---|
Only transform and opacity in keyframes | Avoids per-frame layout and geometry work |
| Groups animated, not every path | Fewer animations and repaints |
transform-box: fill-box with explicit origin | Correct pivot without extra wrappers |
No blanket will-change | Extra layers cost memory |
| Off-screen inline SVGs paused | No wasted frames |
Motion inside prefers-reduced-motion: no-preference | Respects user setting |
Decorative SVG has aria-hidden="true" | Less screen-reader noise |
Meaningful SVG has role="img", title, desc | Accessible name and description |
| SVGO run, animation retested | Smaller file, nothing broken |
Let a generator handle the boilerplate
If you would rather describe motion than write keyframes, SVG Weave's SVG Animation Generator turns a prompt into CSS @keyframes inside your SVG. By its own design it prefers transform and opacity, sets explicit transform origins and loops with plain CSS, so the output follows several items on this list. It is credit-based and paid, with packs from $5. Review the output against the checklist either way; you still decide about reduced motion, labels and where it is placed.
FAQ
Is SVG animation faster than GIF or video?
It depends on the content. A small, simple SVG is usually a fraction of the file size of a GIF and stays sharp at any size. A complex SVG with hundreds of animated nodes can cost more CPU than a short video. Measure both on your target devices.
Does CSS animation on SVG use the GPU?
Not reliably for the contents of the SVG. transform and opacity are the properties browsers are best able to offload, but support for elements inside an SVG has varied by browser and version. Animate the outer element when you can, and confirm in the Performance panel.
How do I test performance of an SVG animation?
Open DevTools, record in the Performance panel while the animation runs, and look for long frames and repeated layout or paint work. In Chrome, the Rendering tab's paint flashing shows what repaints each frame. Test on a mid-range phone, not just your laptop.
Do I need to support prefers-reduced-motion for tiny animations?
Yes for anything that moves continuously, scales a lot or travels across the screen. A short color fade or a small hover effect is usually fine. When unsure, wrap it. The cost is a few lines of CSS.
Should decorative animated SVGs have a title?
No. Use aria-hidden="true" so screen readers skip them. A title on a decorative graphic adds noise and may produce a tooltip.
Will SVGO break my animation?
It can. Plugins that merge paths, convert shapes, or rename ids can stop CSS selectors from matching. Run SVGO, watch the result, and turn off the plugin responsible. Keeping ids and class names stable makes this much easier.
Related
- How to convert a PNG or JPG to SVG if you are starting from a raster file.
- Animated SVG loading spinners with pure CSS for ready-made accessible examples.