Skip to content
SVG Weave

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.

PropertyAnimates well?Notes
transformYesRotate, scale, translate, skew. Set the pivot first (below).
opacityYesFades. Cheap and safe.
fill, strokeYesColor transitions.
stroke-widthYesGood for pulses.
stroke-dasharray, stroke-dashoffsetYesLine-draw effects. stroke-dashoffset is Baseline widely available.
filterYesBlur and glow. Can be costly on large areas.
d (path data)PartlyChrome and Firefox only, not Safari. Both path() values need the same command structure.
points, viewBox, x1/y1 on linesNoAttributes, 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 wobble names the animation and lists states by percentage. 0%, 100% shares one state so the loop is seamless.
  • animation: wobble 3s ease-in-out infinite is shorthand for name, duration, timing function and iteration count.
  • Both transform and fill animate 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:

So rotate(360deg) on a gear swings it around the canvas corner. transform-box (Baseline since January 2020) changes the reference box:

ValueReference box
view-boxThe nearest SVG viewport (default)
fill-boxThe element's own bounding box, excluding stroke
stroke-boxThe 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-box plus transform-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 with animation-fill-mode: both (holds 0% 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: backwards also avoids the pop; both additionally 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.

CSS keyframes embedded in an SVG with explicit transform origins. Keyframes in this demo were hand-written to illustrate the output format.

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: 0 in the media query.

Debugging SVG animations

When nothing moves, or the wrong thing does, work through this list:

  1. Selector does not match. Open DevTools, find the element, and check the Styles pane shows your rule. A typo in the id is the usual cause.
  2. Animation name mismatch. The name in animation: must equal the name after @keyframes, exactly.
  3. Wrong pivot. If it moves but around the wrong point, you are missing transform-box: fill-box. See above.
  4. Loaded as <img>. CSS inside the SVG can run, but external stylesheets and scripts cannot, per MDN. Keep all CSS in an inline <style>.
  5. 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.
  6. The property is an attribute, not CSS. points or viewBox cannot be keyframed. Animate a different property or use SMIL.
  7. Existing transform attribute. CSS transform overrides a transform="..." 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.

Try the CSS @keyframes Generator

Generate CSS @keyframes for SVG elements from a plain-English description: ready-to-paste keyframes with timing, easing and staggered delays, in the SVG.

Related tools

  • CSS @keyframes Generator

    Generate CSS @keyframes for SVG elements from a plain-English description: ready-to-paste keyframes with timing, easing and staggered delays, in the SVG.

  • SVG Animation Generator

    Describe the motion in plain English and get an animated SVG with embedded CSS @keyframes. Works on SVGs you upload, paste or generate. No JavaScript.