To animate SVG with CSS, paste the SVG inline in the page, write a @keyframes rule, and hook it to a shape with animation-name and animation-duration. Hover transitions run on those same properties. They cover fill, stroke, opacity, transform and stroke-dashoffset, which is plenty for icons and loaders, and for most logo work too.

Whether any of it runs depends on how the SVG gets into the page. MDN documents that an SVG loaded through img or CSS background-image is treated as an image. Scripts are disabled, external resources such as stylesheets are blocked, and the page CSS never reaches the shapes inside.

The SVG 2 specification goes a step further. User input events have no effect in that context, so a :hover rule written inside the file never fires.

How to animate an SVG with CSS

Keyframes are the general tool. For a plain two-state change like a hover, a transition needs less code and gets the same result.

Both run on the same properties and the same timing functions. What differs is what starts them.

  1. Inline the SVG in the HTML and give the shape a class, so the page stylesheet can target it.
  2. Write a @keyframes rule with a name and at least a from and a to state.
  3. Attach the rule to the class with animation-name and animation-duration.
  4. Add timing: animation-timing-function, animation-iteration-count and animation-fill-mode.
  5. Slow the playback in the Chrome DevTools Animations panel and check each state.

Step 1 is where most failures start, and the embedding table further down shows why.

Keyframes for looping and multi-step motion

Here is what each animation property does.

  • animation-name connects the element to its @keyframes rule.
  • animation-duration is the length of one cycle, in seconds or milliseconds.
  • animation-timing-function sets the easing curve, ease-in-out or cubic-bezier() for example.
  • animation-delay is the wait before the first cycle starts.
  • animation-iteration-count takes a number of cycles, or infinite for a loop that never stops.
  • animation-direction set to alternate plays every second cycle backwards.
  • animation-fill-mode decides what happens once the run ends. With forwards the last keyframe stays put, while the default snaps the shape back to its starting style (per MDN).

Keyframes are not limited to from and to. Percentage stops in a CSS keyframes rule (0%, 40%, 100%) let one shape pulse, hold and settle inside a single loop.

The shorthand packs it all into one line: animation: pulse 1.2s ease-in-out infinite alternate.

Transitions for hover and class toggles

A transition only animates when a value changes, so nothing plays on page load.

That makes it a good fit for CSS hover effects on icons and buttons. Declare the properties once on the base state, for example transition: fill 0.3s ease, transform 0.3s ease, then change the values in the hover rule.

Transitions fire on :hover and :active, and on :focus-visible so keyboard users get the same feedback. A class toggled with classList.toggle works too, as does any other state change a script applies.

They also reverse on their own when the state ends. Keyframes don’t.

Choosing between keyframes and transitions

One start state and one end state means a transition. A loop, three or more states, or a start with no trigger means keyframes.

Transitions suit hover and focus feedback, open and closed toggles, and one-shot color or size changes. Keyframes suit spinners and loaders, pulses and attention loops, sequences with staggered delays, and anything that has to start on load.

I see keyframes used for hover states a lot, and it’s rarely worth it. You write more code, and the shape snaps back instead of easing when the pointer leaves.

Which SVG embedding methods let CSS animate the shapes?

Only inline SVG gives the page stylesheet access to the shapes. An SVG loaded through img or CSS background-image is treated as a separate image, so the only styles that run are the ones written inside the file.

MethodPage CSS reaches shapesStyles inside the fileTypical use
Inline SVGYesNot neededIcons, UI, per-shape control
<img> elementNoInternal style block runs, external files blockedSelf-running loops
CSS background-imageNoSame as imgDecorative loops
<object> or <embed>NoInternal and linked styles runFiles kept separate from the page

MDN’s page on SVG as an image spells out the restrictions in image contexts. JavaScript is disabled, and external resources such as stylesheets can’t load unless they are inlined through data: URLs. The same page says those limits don’t apply to iframe, object or embed.

One more restriction catches people out. The SVG 2 specification says interactivity is disabled in SVG-as-image, so user input events have no effect and a :hover rule written inside an img file never fires.

Looping keyframes inside an img file work fine. Hover and click effects on inner shapes don’t, though the img element itself can still be rotated, scaled or faded from the page CSS.

Inline wins for anything interactive. Pasting the svg element straight into the markup (the guide to SVG in HTML covers the details) costs a little page weight and gives up separate file caching.

For a loader that just spins, an img with an internal style block is fine, and the browser caches the file.

Which SVG properties can CSS animate?

Paint properties, transforms and a set of geometry properties are all fair game, along with path data in browsers that support the d property. Attributes with no CSS counterpart, such as viewBox, stay out of reach.

On the paint side that means fill, stroke, fill-opacity, stroke-opacity, stroke-width and opacity. The line-drawing pair, stroke-dasharray and stroke-dashoffset, gets its own section below. Movement runs through transform and the individual translate, rotate and scale properties. Geometry covers cx, cy, r, width and height on the shapes they apply to, and path data goes through the d property with a path() value.

The W3C SVG 2 specification marks cx, cy and r as animatable and ties each to specific elements: cx and cy apply to circle and ellipse, and r to circle. Its sizing section covers width and height on rect, svg, image and foreignObject. Support in browsers varies by property, so test before shipping.

Presentation attributes lose fights, which helps here. MDN says they carry a specificity of 0, so styles from a stylesheet or a style attribute override them. A hard-coded fill=”red” in the markup does not block a fill animation.

The d property needs care. MDN says a transition on d works only when both path() values have the same number of data points, and SVG 2 defines matching paths as having the same number and types of commands in the same order. Otherwise the value switches without interpolating.

Mozilla’s Bugzilla entry shows Firefox shipping the property in early 2022 (Firefox 97). MDN’s compatibility data still lists Safari as not supporting it, where the property parses and has no effect.

Path morphing that has to work in every browser is a job for a different tool. The final section covers the options.

How to rotate and scale an SVG shape around the right origin

SVG shapes pivot around the top-left corner of the viewBox unless told otherwise. Set transform-box: fill-box and transform-origin: center on the shape, and it rotates or scales around its own center.

MDN documents the default. transform-origin is 0 0 on every SVG element except the root svg element and an svg element directly inside foreignObject, which use 50% 50%. Because the default transform-box is view-box, those coordinates are measured in the viewBox.

The reference box is the part to pick carefully. view-box measures the origin from the viewBox, which suits a clock hand pivoting on a fixed point. fill-box measures against the shape’s own bounding box, so use it for spinners, gears and scale-from-center effects. stroke-box does the same with the stroke width counted in.

A loading spinner shows the pattern. Give the arc a class, set transform-box: fill-box and transform-origin: 50% 50%, then run a keyframes rule that ends at rotate(360deg) with animation: spin 1s linear infinite.

MDN’s transform-box page uses the same setup to spin a rectangle in place. Without fill-box, it describes the origin landing on the canvas center instead.

Exports from design tools often position shapes with a transform attribute. A CSS transform takes priority over it, so the shape jumps. Wrapping the shape in a g element and animating the group fixes it.

The origin is measured per element, so the class has to sit on the element that should turn. Put it on the group when the whole illustration rotates as one piece.

Percentages trip people up as well. With fill-box, 50% means half of the shape’s box, not half of the viewBox.

How to draw an SVG line with stroke-dasharray and stroke-dashoffset

Set stroke-dasharray to the path’s length, set stroke-dashoffset to the same value so the stroke is hidden, then animate the offset to 0. The line appears to draw itself from start to end.

  1. Give the path a stroke and fill: none, then add pathLength=”1″ (or 100) to the path element.
  2. Set stroke-dasharray to the same number as pathLength.
  3. Set stroke-dashoffset to that number too, which hides the whole line.
  4. Write a @keyframes rule that ends at stroke-dashoffset: 0.
  5. Attach it with a shorthand such as animation: draw 2s ease forwards.

MDN explains why step 1 works. pathLength rescales every distance computation on the path, and stroke-dasharray treats the end of the path as the pathLength value, so a dasharray of 90 on a path with pathLength 100 covers 90% of the stroke at any rendered size.

Without pathLength, read the real length with getTotalLength() in JavaScript and paste that number into both properties. Chris Coyier’s walkthrough on CSS-Tricks shows getTotalLength() as the way to get the number.

For hover-triggered drawing, swap the keyframes for a transition on stroke-dashoffset (Stefan Judis uses 0.75s ease-in-out) and set the offset to 0 in the hover rule.

Direction depends on the sign. A positive starting offset reveals the line from the beginning of the path, and a negative one reveals it from the end.

A few things break this effect.

  • Closed shapes start and stop at the path’s first point, so a circle or triangle always begins in one spot.
  • Round or square line caps can leave a dot in the hidden state. Use butt caps, or push the offset slightly past the length.
  • pathLength needs testing in current Safari, Firefox and Chrome, with the getTotalLength() number kept as a fallback.
  • MDN documents a path-length CSS property that overrides the attribute, but flags it as experimental.

Signature and handwriting effects need outlines. Live SVG text does not accept pathLength (MDN lists circle, ellipse, line, path, polygon, polyline and rect), so letters have to be converted to paths first.

How to keep SVG animation fast and accessible

Animate transform and opacity where the design allows, profile everything else in the Chrome DevTools Performance panel, and wrap decorative motion in a prefers-reduced-motion check.

Which properties stay cheap

web.dev’s animation guide says to restrict animations to opacity and transform where possible, because those stay on the compositing stage of the rendering path. Any other property needs checking against the rendering pipeline first.

transform and opacity are the compositor-friendly pair. Fill, stroke, stroke-width, stroke-dashoffset, cx, cy, r, width, height and d all deserve a check before shipping.

That puts line drawing outside the cheap pair, because it animates stroke-dashoffset. The web.dev guidance is written for HTML elements, so profile the SVG shapes instead of assuming the same behavior.

will-change: transform tells the browser what is about to animate. MDN calls it a last resort for existing performance problems, warns against applying it to many elements, and advises switching it on before the change and off afterward with script.

Respecting reduced motion

prefers-reduced-motion has two values, no-preference and reduce, and MDN lists it as Baseline Widely available since January 2020.

Put the reduce block after the animation rules with the same specificity, and the later rule wins. MDN’s guide to media queries for accessibility sets animation: none inside @media (prefers-reduced-motion: reduce).

Line drawing has a trap here. Switching the animation off leaves the stroke in its hidden base state, so set stroke-dashoffset: 0 in the same media block to keep the finished line visible.

Spinners and loops should stop and show a static state. Entrance movement can swap to a short fade. Motion that carries meaning, such as a progress indicator, is worth keeping, just slower and smaller.

Motion is only half of the accessibility picture for an animated graphic. Titles, descriptions and hiding decorative shapes from screen readers are covered in the guide to accessible SVG files.

Add the reduced-motion test to your web accessibility checklist so it gets checked on every release. Browser support for each property is worth a look in MDN’s compatibility tables or Can I use before launch.

Why is an SVG animation not working?

Most broken SVG animations come down to a handful of causes.

  • Nothing moves at all when the SVG loads through img or background-image, since page CSS never reaches the shapes. Recheck the embedding table above.
  • A shape that swings off position has its origin at the viewBox corner. Add transform-box: fill-box.
  • If the selector matches nothing after export, an optimizer or export setting probably renamed or removed the ids and classes.
  • A line that draws and then vanishes has fallen back to its base style once the animation ended, and that base style is the hidden stroke. Add animation-fill-mode: forwards, or make the drawn state the base style.
  • An inline style attribute beats a class selector. Illustrator’s SVG export can write CSS properties as presentation attributes instead, which WP Engine’s tutorial notes are easier to override.
  • And if the animation ran before anyone scrolled to it, that’s because a CSS animation starts when its style applies, not when the shape enters the viewport. Add the animation class with an IntersectionObserver, or use scroll-driven animations where supported.

SVGO deserves its own warning. Its default preset (the list on svgo.dev) includes Cleanup IDs, which removes unused IDs and minifies the ones in use, plus Remove Unknowns and Defaults, Collapse Groups and Convert Shape to Path.

The stylesheet lives outside the file, so SVGO sees an id the CSS depends on as unused. Check what SVG optimization tools strip before they touch a graphic you plan to animate.

Collapse Groups can also delete the wrapper g added for the transform-origin fix. SVGO’s docs say each plugin in preset-default can be disabled through its overrides setting.

If the file is already damaged, restore ids, add classes and re-wrap groups by editing SVG files in Inkscape or a code editor.

When is CSS not enough for SVG animation?

CSS runs out at path morphing between mismatched shapes, scrubbed or sequenced timelines, and anything that needs script-level control. SMIL, the Web Animations API or a library such as GSAP covers those cases.

MethodNeeds JavaScriptRuns in an img-loaded SVGTypical use
CSSNoYesLoaders, icons, hover and focus states
SMILNoYesSelf-contained files, animateMotion
Web Animations APIYesNoPause, seek and playback control
GSAPYesNoTimelines, sequencing, MorphSVG

CSS adds no scripts and no extra weight. The catch is that it can’t scrub a timeline, and path morphing only works between paths with matching structure.

SMIL is declarative, and animateMotion moves a shape along a path. Chromium announced an intent to deprecate it in 2015 and never followed through with a removal, and MDN lists animateMotion as Baseline Widely available since January 2020.

Element.animate() from the Web Animations API takes keyframes the way CSS does and returns an Animation object with playback controls. MDN lists it as Baseline Widely available since March 2020, with some parts varying by browser.

For real choreography, GSAP is the strongest option. Webflow made it free on April 30, 2025, including formerly paid plugins such as MorphSVG.

Motion along a path has a CSS route too. MDN marks the offset shorthand Baseline Widely available since September 2022, though its example moves an HTML element, so test it on SVG shapes before relying on it.

Anything that depends on JavaScript stops working when the SVG loads as an image, because MDN says scripts are disabled in that context. That leaves CSS or SMIL as the only choice for a self-running file in an img tag.

Skip the library when a transition or a keyframes rule does the job. A hover color change needs no script.

FAQ About SVG Animation with CSS

Does CSS animation on SVG work in every browser?

Yes for the core features. Keyframes, transitions, transform and opacity run on SVG elements in all current browsers.

Newer pieces vary by engine, including the d property and the path-length property, so check MDN’s compatibility tables or Can I use before relying on them.

How do I pause and resume an SVG animation?

Set animation-play-state to paused, then back to running. The animation freezes on its current frame instead of restarting.

Toggle the value with a :hover rule or a class. A transition can’t pause, so use keyframes when you need this control.

How can I stagger an animation across several SVG paths?

Give each path its own animation-delay. The shortest route is a custom property per element, such as –i set to 0, 1 and 2, then animation-delay: calc(var(–i) * 0.1s) in the shared rule.

nth-child selectors work too, but the rule list grows with every path.

Can I animate an SVG exported from Figma, Illustrator or Inkscape?

Yes. Export the file, paste it inline, and add a class to each shape you want to move.

Layer names usually carry over as ids, so naming layers before export saves cleanup afterward. Run the optimizer last, and keep Cleanup IDs from renaming the hooks your CSS uses.

What to Test Before Shipping SVG Animation with CSS

SVG animation with CSS ships safely when the static end state looks correct with no motion at all. Each effect then gets checked in the embedding method, browser and motion setting it will meet.

The order matters, because each step catches failures the next one hides.

  • Static end state with animation off
  • Embedding method and reduced-motion setting, then a pass in current Safari, Firefox and Chrome

A static fallback costs the sequence, since users who see it miss the order in which elements appear. MDN’s guide to media queries for accessibility notes that reducing animation also helps users with low battery or low-end devices, so the same fallback doubles as the cheap path for stroke and filter effects that web.dev places outside the compositor-friendly pair.

Scroll-triggered playback is the natural next step, and the guide to CSS animations on scroll covers it.

Bogdan Sandu
Latest posts by Bogdan Sandu (see all)