SVG Animation Not Working? A Production Debugging Playbook
Debug SVG animation in layers: confirm the SVG is in the DOM you expect, inspect the animation object and computed styles, verify geometry and references, then profile the rendered result. This playbook turns “nothing moves” into a reproducible diagnosis.
Short answer: start by proving that an animation exists. In DevTools, run document.getAnimations(), inspect the SVG element's computed styles, and confirm the element lives in the same document your code queries. Then check timing, geometry, references, and motion preferences in that order.
Random edits make animation bugs expensive because several failures look identical on screen. A zero duration, a wrong selector, a broken mask reference, and an SVG loaded through <img> can all produce “nothing happens.” The fastest route is a layered diagnosis with one observable fact at each step.
Match the symptom to the first check
Nothing moves
- First check: document.getAnimations()
- Likely causes: No matching selector, zero duration, animation never created, reduced-motion override
Animation runs but element is invisible
- First check: Computed opacity, display, clipping, and paint
- Likely causes: Final state applied early, broken mask/clip path, missing fill or stroke
Wrong pivot or jump
- First check: transform-origin and transform-box
- Likely causes: Transform calculated against a different box than expected
Line drawing starts or ends badly
- First check: getTotalLength()
- Likely causes: Hard-coded dash length, path changed after design export
Only one of several instances works
- First check: IDs and URL references
- Likely causes: Duplicate gradient, mask, clip, or filter IDs
Works inline but not in img
- First check: Embedding context
- Likely causes: Page CSS/JS cannot reach the external image DOM
Works locally but not in production
- First check: Built SVG and response headers
- Likely causes: Optimizer changed IDs, sanitizer removed attributes, CSP or path issue
1. Confirm the element and its document
Select the exact element in the Elements panel and run $0 in the Console. If the artwork is loaded through <img src="animation.svg">, its internal paths are not children of the page DOM. A selector such as document.querySelector('.arm') cannot find them. Use inline SVG for page-controlled motion or keep the animation self-contained in the file.
Next, verify that your selector returns the intended element and only the intended element:
const target = document.querySelector('[data-animate="check"]');
console.assert(target, 'Animation target is missing');
console.log(target?.ownerDocument === document);Framework rendering can replace nodes after an animation is created. If the selected element exists but its animation disappears after a state update, inspect component lifecycle and keys before changing timing values.
2. Prove that the browser created an animation
Document.getAnimations() returns animations running on the document, including CSS Animations, CSS Transitions, and animations created with the Web Animations API. It is a compact health check:
const animations = document.getAnimations();
console.table(animations.map(animation => ({
state: animation.playState,
currentTime: animation.currentTime,
rate: animation.playbackRate,
target: animation.effect?.target?.className?.baseVal
})));An empty array means the browser did not create an animation in this document. Recheck selectors, class application, stylesheet loading, feature support, and lifecycle. If an animation exists, its playState narrows the problem: paused, finished, or idle each suggests a different next step.
Chrome's Animations panel can replay, slow, scrub, and edit the timing of supported CSS, Web Animations API, and View Transition animations. Use it to separate an authoring problem from a sequence that simply completed before you saw it.
3. Inspect computed timing, not only authored CSS
const style = getComputedStyle(target);
console.table({
name: style.animationName,
duration: style.animationDuration,
delay: style.animationDelay,
playState: style.animationPlayState,
opacity: style.opacity,
display: style.display
});Common failures include a duration resolving to 0s, a shorthand later resetting animation-fill-mode, a more specific rule setting animation: none, or an element remaining in an invisible initial state. Inspect the Computed panel and expand the property to find the winning declaration.
Negative delays, iteration counts, and fill modes can make a correct keyframe appear incorrect. Temporarily simplify to a one-second, one-iteration animation with no delay. Restore complexity after the element visibly moves.
4. Fix transform coordinate assumptions
SVG transforms often pivot around an unexpected point because the reference box differs from ordinary HTML. Make the intended behavior explicit:
.needle {
transform-box: fill-box;
transform-origin: center;
animation: turn 900ms ease-in-out both;
}If the motion still jumps, inspect the element's bounding box and parent transforms. Nested groups, exported transforms, and a changed viewBox can compound. Apply a temporary outline or contrasting fill to the target group so its real bounds are obvious.
5. Measure paths instead of guessing
For line-drawing effects, the dash length must match the rendered geometry. SVGGeometryElement.getTotalLength() returns the user agent's calculated path length:
const path = document.querySelector('.signature-path');
const length = path.getTotalLength();
path.style.strokeDasharray = length;
path.style.strokeDashoffset = length;
path.animate(
[{ strokeDashoffset: length }, { strokeDashoffset: 0 }],
{ duration: 1200, fill: 'forwards' }
);If a designer edits the path, this measurement stays correct while a copied magic number becomes stale. Also confirm the path has a visible stroke, an appropriate line cap, and no mask hiding the section you expect to reveal.
6. Audit IDs and references
Gradients, masks, clip paths, filters, markers, and ARIA labels often depend on url(#id) or an ID reference. Search the delivered DOM for duplicates and broken targets:
const ids = [...document.querySelectorAll('svg [id]')].map(node => node.id);
const duplicates = ids.filter((id, index) => ids.indexOf(id) !== index);
console.log([...new Set(duplicates)]);If the source works and the optimized build fails, diff the SVGs. An optimizer may shorten or remove IDs that JavaScript, CSS, or another file treats as public. Keep a contract list for those identifiers and configure optimization explicitly rather than depending on defaults.
7. Check reduced-motion and visibility logic
A well-built animation may correctly stop when the operating system requests reduced motion. In DevTools, emulate both preference states and inspect which media rule wins. Also review code that starts motion after an Intersection Observer callback, tab visibility change, route transition, or user interaction. Log the trigger before blaming the animation.
console.log(
matchMedia('(prefers-reduced-motion: reduce)').matches,
document.visibilityState
);The reduced-motion version must still communicate the final state. A checkmark can appear immediately; a progress indicator needs a non-motion status; a menu icon still needs a clear expanded state.
8. Profile motion that works but feels broken
Jank is a rendering problem, not a timing-design problem. Record the interaction in Chrome's Performance panel and look for long tasks, repeated layout, large paint areas, and expensive filters. SVG blurs, shadows, masks, and filters can dominate paint even when the file is small.
- Prefer transforms and opacity for frequent motion where the visual design allows.
- Reduce the filtered area and filter complexity.
- Avoid reading geometry and writing styles repeatedly in the same frame.
- Test on a representative mobile device and under realistic page load.
Build a minimal reproduction
Copy one SVG target, one style block, and the smallest script into a blank page. Remove the framework, optimizer, and animation library. If it fails there, the bug is in the asset or animation. If it works, add the production layers back one at a time: embedding, global CSS, build output, lifecycle, then runtime data.
The five-minute debugging sequence
- Select the intended SVG element and verify its document.
- Run document.getAnimations() and inspect playState.
- Check computed animation name, duration, play state, opacity, and display.
- Make transform box and origin explicit.
- Measure path geometry and inspect paint.
- Search for duplicate and missing IDs.
- Emulate reduced motion and log lifecycle triggers.
- Record a performance trace if motion runs but stutters.
Prevent repeats by adding these checks to the SVG animation production workflow and keeping the unoptimized source asset alongside its delivery artifact.
Frequently asked questions
Why does my CSS animation show animation-name but still not move?
Check the computed duration, whether the start and end values differ, whether the element is paused or finished, and whether another transform on a parent changes the visible result.
Why does the animation work once and fail after navigation?
The component may reuse a finished animation or replace its target node. Recreate or reset the animation at the appropriate lifecycle point and confirm the new node is targeted.
Why did SVGO break a working animation?
The animation may depend on IDs, classes, group structure, or precision that the optimized output changed. Treat those as public contracts, disable the relevant transform, and compare the delivered SVG with the source.
Sources and further reading
Related articles
SVG Animation Testing: Visual, Accessibility, and Performance CI
Test SVG animation with Playwright screenshots, reduced-motion checks, axe, ARIA snapshots, runtime assertions, browser coverage, and performance review.