CSS @keyframes for SVG: a practical guide
SVG Weave team
Published
Which properties animate on SVG elements, how transform-box: fill-box fixes pivots, easing, staggering with animation-delay, reduced motion and debugging.
CSS @keyframes can animate most SVG presentation properties: transform, opacity, fill, stroke, stroke-dashoffset and filter. The two things that trip people up are pivot points (fix them with transform-box: fill-box) and attributes that are not CSS properties, such as points and viewBox, which CSS cannot animate. This guide covers both, plus easing, staggering, reduced motion and debugging.
If you would rather not write keyframes by hand, CSS @keyframes Generator generates them from a plain-English description.
Which properties animate on SVG elements
The rule: if it is a CSS property, you can put it in a keyframe. Many SVG attributes are also CSS properties, which is why fill="red" can be overridden or animated from a stylesheet.
| Property | Animates well? | Notes |
|---|---|---|
transform | Yes | Rotate, scale, translate, skew. Set the pivot first (below). |
opacity | Yes | Fades. Cheap and safe. |
fill, stroke | Yes | Color transitions. |
stroke-width | Yes | Good for pulses. |
stroke-dasharray, stroke-dashoffset | Yes | Line-draw effects. stroke-dashoffset is Baseline widely available. |
filter | Yes | Blur and glow. Can be costly on large areas. |
d (path data) | Partly | Chrome and Firefox only, not Safari. Both path() values need the same command structure. |
points, viewBox, x1/y1 on lines | No | Attributes, not CSS properties. Use SMIL or JavaScript. |
For the d property, MDN's page shows that interpolation works when both path() strings have the same sequence of commands. If you need morphing in every browser, use SMIL or a JavaScript library instead.
Anatomy of an SVG keyframe animation
Here is a complete file. The <style> goes inside the SVG so the animation travels with it.
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="240" height="240">
<style>
.badge {
transform-box: fill-box;
transform-origin: center;
animation: wobble 3s ease-in-out infinite;
}
@keyframes wobble {
0%, 100% { transform: rotate(-6deg); fill: #6366f1; }
50% { transform: rotate(6deg); fill: #ec4899; }
}
</style>
<rect class="badge" x="30" y="30" width="60" height="60" rx="12" fill="#6366f1" />
</svg>
Breakdown:
@keyframes wobblenames the animation and lists states by percentage.0%, 100%shares one state so the loop is seamless.animation: wobble 3s ease-in-out infiniteis shorthand for name, duration, timing function and iteration count.- Both
transformandfillanimate in the same keyframes. Browsers interpolate each property independently.
You can use from and to for two-state animations, and add animation-direction: alternate to ping-pong instead of repeating the whole cycle.
transform-box: fill-box and the pivot problem
Rotation and scaling happen around the transform origin. In SVG that origin behaves differently from HTML:
- The initial
transform-originis0 0for SVG elements other than the rootsvg. - The default
transform-boxisview-box, so that0 0is the top-left of the whole canvas, not of your shape.
So rotate(360deg) on a gear swings it around the canvas corner. transform-box (Baseline since January 2020) changes the reference box:
| Value | Reference box |
|---|---|
view-box | The nearest SVG viewport (default) |
fill-box | The element's own bounding box, excluding stroke |
stroke-box | The bounding box including stroke |
The standard fix:
#gear {
transform-box: fill-box;
transform-origin: center; /* or 50% 50% */
animation: spin 8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
For a pendulum or hinge, keep fill-box and move the origin: transform-origin: 50% 0% pivots on the top-center of the shape.
A caveat for groups: the bounding box of a <g> is the union of its children. If you animate a group and later add a shape to it, its center moves. When you want a fixed pivot regardless, set transform-box: view-box and give the origin in viewBox units, such as transform-origin: 60px 60px.
Timing and easing
Default easing is ease, which is fine for UI but looks mechanical for organic motion. Options, per MDN's reference (Baseline widely available):
linear: constant speed. Right for spinning gears and loaders, wrong for things that should feel physical.ease-in-out: slow start and end. Good default for looping float or pulse motion.cubic-bezier(x1, y1, x2, y2): custom curve.cubic-bezier(0.34, 1.56, 0.64, 1)overshoots slightly, a quick way to get a springy settle.steps(n): discrete jumps. Useful for flipbook-style sprites and blinking cursors.linear(...): piecewise-linear curves, which can approximate bounce and elastic easing.
The easing applies to each interval between keyframes, not to the animation as a whole. You can override it per keyframe:
@keyframes bounce {
0% { transform: translateY(0); animation-timing-function: ease-out; }
50% { transform: translateY(-30px); animation-timing-function: ease-in; }
100% { transform: translateY(0); }
}
This gives a proper bounce: decelerating on the way up, accelerating on the way down. With one easing for the whole cycle, both halves would look wrong.
Staggering with animation-delay
When several similar shapes animate together, identical timing looks robotic. A small offset per element fixes it. A custom property keeps the CSS short:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 40" width="360" height="120">
<style>
.bar {
transform-box: fill-box;
transform-origin: 50% 100%;
animation: grow 1.2s ease-in-out infinite;
animation-delay: calc(var(--i) * 0.15s);
}
@keyframes grow {
0%, 100% { transform: scaleY(0.3); }
50% { transform: scaleY(1); }
}
</style>
<rect class="bar" style="--i:0" x="10" y="5" width="14" height="30" fill="#0ea5e9"/>
<rect class="bar" style="--i:1" x="34" y="5" width="14" height="30" fill="#0ea5e9"/>
<rect class="bar" style="--i:2" x="58" y="5" width="14" height="30" fill="#0ea5e9"/>
<rect class="bar" style="--i:3" x="82" y="5" width="14" height="30" fill="#0ea5e9"/>
</svg>
Details worth knowing:
- Each bar scales from its own bottom edge:
fill-boxplustransform-origin: 50% 100%. - A positive delay holds the element at its un-animated state (full height here) until it starts, then it jumps to the
0%keyframe (scaleY(0.3)), so delayed bars pop. Fix withanimation-fill-mode: both(holds0%during the delay) or a negative delay (-0.3s), which starts the animation partway through its cycle so everything moves from the first frame. animation-fill-mode: backwardsalso avoids the pop;bothadditionally keeps the end state of finite animations.
Doing this by hand for 30 elements is where tools help. SVG Weave's animation generator writes staggered delays across similar elements as part of its output, and the CSS @keyframes Generator page shows what that looks like. For one-off icons, hand-written CSS is quicker.
Respecting prefers-reduced-motion
Looping decoration is exactly what prefers-reduced-motion is for. It is Baseline widely available, and it works inside an SVG's own <style> block:
.badge { animation: wobble 3s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.badge { animation: none; }
}
Two refinements:
- MDN's recommended pattern is to tone motion down instead of always removing it. Replace a scale or rotation with a gentle opacity fade, which is less likely to trigger discomfort.
- If the animation reveals content (a line draw), the reduced-motion state must show the finished result, not the hidden starting state. For stroke drawing, set
stroke-dashoffset: 0in the media query.
Debugging SVG animations
When nothing moves, or the wrong thing does, work through this list:
- Selector does not match. Open DevTools, find the element, and check the Styles pane shows your rule. A typo in the
idis the usual cause. - Animation name mismatch. The name in
animation:must equal the name after@keyframes, exactly. - Wrong pivot. If it moves but around the wrong point, you are missing
transform-box: fill-box. See above. - Loaded as
<img>. CSS inside the SVG can run, but external stylesheets and scripts cannot, per MDN. Keep all CSS in an inline<style>. - Standalone .svg is invalid XML. An unescaped
&or<in CSS breaks parsing of a standalone SVG file. Wrap the CSS in<![CDATA[ ... ]]>if you need them. Selectors with>are fine. - The property is an attribute, not CSS.
pointsorviewBoxcannot be keyframed. Animate a different property or use SMIL. - Existing
transformattribute. CSStransformoverrides atransform="..."presentation attribute on the same element rather than adding to it. Wrap the element in a<g>and animate the group.
Browser tools that help: the Chrome DevTools Animations panel and Firefox's Animations inspector let you pause, scrub and slow down running animations. For a quick check without them, add animation-play-state: paused to freeze a frame.
If you are weighing CSS against other approaches first, see how to animate an SVG: CSS, SMIL, JavaScript or Lottie.
FAQ
Can I animate any SVG attribute with CSS @keyframes?
No. CSS can animate SVG attributes only when they are also CSS properties, such as fill, stroke, opacity, transform and stroke-dashoffset. Attributes like points and viewBox are not, so use SMIL or JavaScript for those.
Why does my SVG rotate around the wrong point?
SVG elements default to transform-origin: 0 0 measured against the whole viewBox. Add transform-box: fill-box and transform-origin: center to the element so the pivot is the shape's own center.
How do I stagger animations across several SVG shapes?
Give each shape a different animation-delay. A custom property such as --i set per element with animation-delay: calc(var(--i) * 0.15s) keeps the stylesheet short. Negative delays start the loop partway through, so everything is already moving on the first frame.
Does CSS animation work on SVG used in an img tag?
Keyframes defined in an inline <style> block inside the SVG can run when loaded via <img>. Scripts and external resources cannot. Keep all styles inside the file and test your target browsers.
Should I animate the d attribute of a path with CSS?
Only if you do not need Safari. The CSS d property animates in Chrome and Firefox, not Safari, and both paths must have matching command structure. For cross-browser morphing, use SMIL or a JavaScript library.
Can an AI tool write the keyframes for me?
Yes. SVG Weave takes an SVG and a plain-English description of the motion and returns the SVG with CSS keyframes embedded. It is credit-based and paid, with packs from $5. Preview and edit the output, because generated timing and pivots are not always right on the first pass.