A lot of product pages have one small moving illustration near a feature: a queue draining, a cache filling, a chart ticking up. It's usually a GIF, a video or a Lottie file. For most of these, an SVG with a few lines of CSS animation inside it is lighter, sharper and easier to keep on brand.
Here's how I add them, the three ways to put one on a page, and the two things people skip: reduced motion and pausing offscreen.
What "animated SVG" means here
An SVG file is code. It can carry its own <style> block, and CSS animations inside that block play wherever the SVG renders. No player script, no video element. It stays sharp at any size because it's vector, and it can use your site's colors.
A minimal example: three bars that rise in turn.
<svg viewBox="0 0 120 48" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="t">
<title id="t">Three bars rising in turn</title>
<style>
.bar {
fill: currentColor;
transform-box: fill-box;
transform-origin: bottom;
animation: rise 1.4s ease-in-out infinite;
}
.b2 { animation-delay: 0.15s; }
.b3 { animation-delay: 0.3s; }
@keyframes rise {
0%, 100% { transform: scaleY(0.4); }
50% { transform: scaleY(1); }
}
@media (prefers-reduced-motion: reduce) {
.bar { animation: none; }
}
</style>
<rect class="bar b1" x="10" y="8" width="24" height="40" rx="3" />
<rect class="bar b2" x="48" y="8" width="24" height="40" rx="3" />
<rect class="bar b3" x="86" y="8" width="24" height="40" rx="3" />
</svg>
Two details worth copying:
-
transform-box: fill-boxplustransform-origin: bottommakes each bar scale from its own bottom edge, not from the corner of the SVG. - The reduced-motion rule lives inside the file, so it goes wherever the file goes.
Three ways to put it on a page
1. As an image
<img src="/illustrations/bars.svg" alt="Three bars rising in turn" width="240" height="96" />
This is the simplest. The CSS animation inside the file still plays. The trade-off: the page can't reach inside it. Your page CSS won't style it, currentColor won't pick up your text color, and you can't pause it from outside.
Use this when the illustration has fixed colors and you just need it to show up.
2. Inline in the HTML
Paste the <svg> markup straight into the page. Now it's part of the DOM:
-
currentColorfollows the color of the parent element, so it matches light and dark themes for free. - CSS variables from your page work inside it.
- Page scripts can pause it.
The cost is markup in your HTML, so keep the SVG clean (no editor metadata, no hidden layers).
3. As a React component
Same as inline, wrapped in a component:
export function Bars(props) {
return (
<svg viewBox="0 0 120 48" role="img" aria-label="Three bars rising in turn" {...props}>
<style>{`
.bars-bar {
fill: currentColor;
transform-box: fill-box;
transform-origin: bottom;
animation: bars-rise 1.4s ease-in-out infinite;
}
.bars-b2 { animation-delay: 0.15s; }
.bars-b3 { animation-delay: 0.3s; }
@keyframes bars-rise {
0%, 100% { transform: scaleY(0.4); }
50% { transform: scaleY(1); }
}
@media (prefers-reduced-motion: reduce) {
.bars-bar { animation: none; }
}
`}</style>
<rect className="bars-bar" x="10" y="8" width="24" height="40" rx="3" />
<rect className="bars-bar bars-b2" x="48" y="8" width="24" height="40" rx="3" />
<rect className="bars-bar bars-b3" x="86" y="8" width="24" height="40" rx="3" />
</svg>
);
}
One gotcha: a <style> inside an inline SVG is global to the page. If two illustrations both define .bar or @keyframes rise, they collide. Prefix class names and keyframe names per illustration, as above.
Keep it on brand with currentColor and variables
Inline SVG can read your design tokens:
.bars-bar { fill: var(--illo-ink, currentColor); }
.bars-b3 { fill: var(--illo-accent, #14b8a6); }
:root { --illo-ink: #0f172a; --illo-accent: #0d9488; }
[data-theme=dark] { --illo-ink: #e2e8f0; --illo-accent: #2dd4bf; }
Switch the theme and the illustration follows, with no second file for dark mode.
Respect reduced motion
The @media (prefers-reduced-motion: reduce) block in the examples turns the animation off for people who asked their OS for less motion. Make sure the still frame still makes sense: pick keyframes where the resting state (here, animation: none leaves the bars at full height) reads as a finished picture.
Pause it when it scrolls away
An illustration halfway down the page doesn't need to animate while nobody can see it. An IntersectionObserver can toggle a class:
.is-paused,
.is-paused * {
animation-play-state: paused;
}
import { useEffect, useRef } from "react";
export function usePauseOffscreen() {
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const io = new IntersectionObserver(([entry]) => {
el.classList.toggle("is-paused", !entry.isIntersecting);
});
io.observe(el);
return () => io.disconnect();
}, []);
return ref;
}
// usage
// const ref = usePauseOffscreen();
// <Bars ref={ref} /> (forward the ref, or wrap Bars in a <div ref={ref}>)
This only works for inline SVG. An <img> is a closed box to the page.
SVG or Lottie?
Lottie is good for long, character-style animation exported from After Effects. For a small explanatory loop next to a feature, CSS-animated SVG wins on the basics: no player script on the page, plain text you can diff in a pull request, and colors that follow your theme.
If you'd rather not build these by hand
Disclosure: I make Illos, a library of animated SVG illustrations for product sites. Each one works the way this post describes: CSS animation inside the file, React or plain SVG, light and dark, reduced motion handled. There's a free set you can use in commercial work with no account and no credit needed, and Pro adds skills so Claude Code, Cursor, Codex or any coding agent can place illustrations in your codebase in the same style.
- The longer guide: https://illos.dev/guides/animated-svg?ref=devto
- The free set: https://illos.dev/library/free?ref=devto
Questions about any of the code above are welcome in the comments.
Top comments (0)