Skip to content

Fix: CSS Animation Not Working (@keyframes Has No Effect)

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

How to fix CSS animations not working — @keyframes not applying, animation paused, transform conflicts, reduced motion settings, and common animation property mistakes.

The Error

You define a CSS animation with @keyframes but the element does not animate. It stays static, flickers once, or animates only in one direction without repeating. No error appears — the animation just has no visible effect.

Common symptoms:

  • animation property is set but the element does not move.
  • The animation plays once and stops when you expected infinite.
  • animation-duration is set but the animation is instantaneous.
  • The element jumps to the final state without transitioning.
  • Animation works in Chrome but not Safari, or vice versa.
  • Animation stops working after adding transform or position to the element.

Why This Happens

CSS animations require two pieces wired together: a @keyframes at-rule that defines the timeline, and an animation property on the element that points to that timeline by name and supplies a duration. If either piece is missing, malformed, or shadowed by a more specific rule, the browser silently computes the animation as none and the element renders in its base state. There is no console warning. The DevTools Animations panel is the only place the failure becomes obvious, because an unmatched name appears as “invalid” or the animation never enters the panel at all.

The second class of failures comes from layout and accessibility. Animations do not run on elements that are not in the render tree, so display: none ancestors block everything including opacity and transform transitions. The prefers-reduced-motion media query, which mirrors an OS-level accessibility setting, can disable animations entirely. A transform on an ancestor creates a containing block for fixed-position children and a new stacking context, which can make a translated child appear to “snap” rather than animate. And animations on properties the browser cannot interpolate (notably display, font-family, and some border shorthands) finish in zero frames regardless of duration.

A third, easy-to-miss cause: the rule is correctly written but the element never enters the state that triggers it. A class applied on mount in a single render frame may skip the transition because nothing changed between paint cycles. Forcing a reflow between the “before” and “after” states, or splitting the class application across two requestAnimationFrame calls, is the standard fix.

Animations fail when:

  • animation-duration is missing or 0s — the default duration is 0s, so the animation completes instantly.
  • @keyframes name does not match the animation-name value — a typo makes them invisible to each other.
  • animation-fill-mode is not set — after the animation ends, the element snaps back to its original state.
  • display: none or visibility: hidden — animations do not run on hidden elements.
  • prefers-reduced-motion media query is active — users with motion sensitivity may have system animations disabled.
  • will-change or transform on an ancestor creates a stacking context that interferes.
  • The animated property is not animatable — some CSS properties cannot be animated.

In Production: Incident Lens

A broken animation is rarely a “down” incident, but it can quietly degrade the experience on revenue-critical components. The blast radius depends entirely on where the animation lives. A failed micro-interaction on a profile avatar is invisible to most users. A failed expand/collapse on a pricing card, a stuck progress bar at checkout, or a frozen loading spinner on a payment screen costs conversions and produces a flood of support tickets that are hard to reproduce because the visual bug only appears on specific browsers, specific reduced-motion settings, or specific feature-flag rollouts.

How it surfaces. Bug reports cluster around words like “stuck,” “doesn’t open,” or “blank.” A spike in client-side errors is uncommon because nothing throws — instead the signal is behavioral: drop-off on a specific funnel step, increased rage clicks captured by session replay, or a sudden jump in support contacts mentioning the same component. Visual regression tests in CI catch this before deploy if they are set up; otherwise it ships.

Monitoring signals to wire up. Add visual regression snapshots (Chromatic, Percy, or Playwright toHaveScreenshot) on the components most exposed to motion. Send custom Sentry events from JavaScript that listens for animationcancel and animationend on key animations — receiving animationcancel without an animationend for the same instance points at a broken transition. Track real-user prefers-reduced-motion distribution so you know what fraction of your traffic will hit the reduced-motion branch.

Recovery sequence. The fastest mitigation is a CSS-only hotfix: revert the rule by class name or push a :where() override that restores the previous behavior. Because CSS ships through your normal deploy pipeline, recovery is bounded by build and CDN propagation time. While the deploy is in flight, you can use a feature flag to disable the affected component’s animation (set animation: none or transition: none via a body class) so the page still renders correctly without motion.

Postmortem preventives. Lock in a CSS reset baseline that explicitly sets animation-fill-mode and respects prefers-reduced-motion. Add visual regression tests for any component whose state change depends on a transition. In CI, run at least one suite with Emulate CSS media feature prefers-reduced-motion: reduce so the reduced-motion path is exercised. Finally, treat animation keyframes as part of the component’s API: when you rename a class, search for @keyframes name- references and rename them in the same change, or you will ship an animation that resolves to nothing.

Fix 1: Ensure animation-duration Is Set

The most common mistake: animation-duration defaults to 0s, so the animation completes in zero time — invisible to the eye.

Broken — no duration:

.box {
  animation-name: slide-in;
  /* Missing animation-duration — defaults to 0s */
}

@keyframes slide-in {
  from { transform: translateX(-100px); }
  to { transform: translateX(0); }
}

Fixed — always specify duration:

.box {
  animation: slide-in 0.5s ease-out;
  /* shorthand: name | duration | easing */
}

@keyframes slide-in {
  from { transform: translateX(-100px); }
  to { transform: translateX(0); }
}

Animation shorthand property order:

animation: name duration timing-function delay iteration-count direction fill-mode play-state;

/* Examples */
animation: fade-in 1s ease 0s 1 normal forwards running;
animation: spin 2s linear infinite;
animation: bounce 0.5s ease-in-out 3;

Pro Tip: Always use the animation shorthand instead of individual properties — it is harder to accidentally omit animation-duration when you must write it explicitly in the shorthand. The only exception is when you need to override a single animation property later.

Fix 2: Verify @keyframes Name Matches animation-name

The name in @keyframes must exactly match the animation-name value (case-sensitive):

Broken — name mismatch:

/* Defined as slide-in */
@keyframes slide-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* Referenced as slideIn — does not match */
.box {
  animation: slideIn 1s ease;
}

Fixed — exact match:

@keyframes slideIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

.box {
  animation: slideIn 1s ease;
}

Verify with DevTools: Open Chrome DevTools → Elements → select the animated element → Animations panel (in the three-dot menu → More tools → Animations). If the animation name shows as “invalid”, the keyframes rule is not found.

Fix 3: Fix animation-fill-mode for End State

By default, elements return to their original state after an animation ends. To keep the element at its final animated state, set animation-fill-mode: forwards:

Broken — element snaps back after animation:

.box {
  animation: fade-in 1s ease;
  /* After 1s, opacity returns to its original value */
}

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

Fixed — keep the final state:

.box {
  opacity: 0; /* Start hidden */
  animation: fade-in 1s ease forwards;
}

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

animation-fill-mode values:

  • none (default): element reverts to original state before and after animation.
  • forwards: element keeps the final keyframe state after animation ends.
  • backwards: element applies the first keyframe state during the delay period.
  • both: combines forwards and backwards.

Fix 4: Fix Infinite Animations That Stop

If animation-iteration-count: infinite is set but the animation stops after one cycle:

/* Broken — conflicting properties */
.spinner {
  animation: spin 1s linear infinite;
  animation-fill-mode: forwards; /* Conflicts with infinite — stops after first iteration */
}

animation-fill-mode: forwards combined with animation-iteration-count: infinite causes the animation to stop after the first iteration in some browsers. Remove fill-mode for infinite animations:

.spinner {
  animation: spin 1s linear infinite;
  /* No fill-mode needed for infinite animations */
}

@keyframes spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

Fix 5: Respect prefers-reduced-motion

Users can request reduced motion at the OS level (Settings → Accessibility → Reduce Motion). CSS respects this with the prefers-reduced-motion media query. If your animation does not run, check if the user has reduced motion enabled:

/* Default: animate */
.box {
  animation: slide-in 0.5s ease forwards;
}

/* Disable animation for users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
  .box {
    animation: none;
    /* Or provide a simplified alternative: */
    opacity: 1;
    transform: none;
  }
}

Check if this is the cause: Open Chrome DevTools → Rendering panel (three-dot → More tools → Rendering) → “Emulate CSS media feature prefers-reduced-motion” → Toggle to reduce. If the animation stops, this is the cause.

Best practice — design animations with reduced motion in mind:

@keyframes slide-in {
  from { transform: translateX(-100px); opacity: 0; }
  to { transform: translateX(0); opacity: 1; }
}

.box {
  animation: slide-in 0.5s ease forwards;
}

@media (prefers-reduced-motion: reduce) {
  .box {
    animation: fade-in 0.2s ease forwards; /* Simpler fade instead of slide */
  }

  @keyframes fade-in {
    from { opacity: 0; }
    to { opacity: 1; }
  }
}

Fix 6: Fix transform Conflicts and Stacking Context

When using transform in @keyframes on an element that also has a transform on its parent or itself, animations can behave unexpectedly:

Broken — transform on parent affects child animation:

.parent {
  transform: translateY(50px); /* Creates stacking context */
}

.child {
  animation: move-right 1s ease infinite;
}

@keyframes move-right {
  from { transform: translateX(0); }
  to { transform: translateX(100px); }
}
/* Works, but the child's transform is relative to its own coordinate space,
   which is already offset by the parent's transform */

For elements that need both a static transform and an animation:

/* Use a wrapper element for the static positioning */
.wrapper {
  transform: translateY(50px); /* Static position */
}

.animated {
  animation: move-right 1s ease infinite; /* Animation */
}

@keyframes move-right {
  from { transform: translateX(0); }
  to { transform: translateX(100px); }
}

Alternatively, combine transforms in keyframes:

@keyframes move-right-and-offset {
  from { transform: translateY(50px) translateX(0); }
  to { transform: translateY(50px) translateX(100px); }
}

Fix 7: Fix Safari-Specific Animation Issues

Safari sometimes requires -webkit- prefixes for older animation properties, and has quirks with certain keyframe configurations:

/* Add webkit prefix for maximum compatibility */
@-webkit-keyframes slide-in {
  from { -webkit-transform: translateX(-100px); transform: translateX(-100px); }
  to { -webkit-transform: translateX(0); transform: translateX(0); }
}

@keyframes slide-in {
  from { transform: translateX(-100px); }
  to { transform: translateX(0); }
}

.box {
  -webkit-animation: slide-in 0.5s ease forwards;
  animation: slide-in 0.5s ease forwards;
}

Modern Safari (15+) does not require -webkit- for most animation properties. Use Autoprefixer in your build pipeline to add prefixes automatically.

Safari animation with position: sticky or backface-visibility:

/* Safari sometimes needs this to enable hardware acceleration for animations */
.animated {
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  animation: slide-in 0.5s ease;
}

Debugging CSS Animations

Use Chrome DevTools Animations panel:

  1. Open DevTools → three-dot menu → More tools → Animations.
  2. Trigger the animation on the page.
  3. The panel shows a timeline of all active animations, their duration, keyframes, and playback state.
  4. Click on an animation to slow it down (0.1×, 0.25×) for inspection.

Pause and replay animations:

// In the browser console — pause all animations on an element
document.querySelector(".box").getAnimations().forEach(a => a.pause());

// Get animation details
document.querySelector(".box").getAnimations().forEach(a => {
  console.log(a.animationName, a.playState, a.currentTime);
});

Force an animation to replay by toggling the element:

const el = document.querySelector(".box");
el.classList.remove("animate");
void el.offsetWidth; // Trigger reflow — forces browser to re-evaluate
el.classList.add("animate");

Still Not Working?

Check animation-play-state. If animation-play-state: paused is set (by JavaScript or another CSS rule), the animation is frozen. Check computed styles in DevTools.

Check display: none ancestors. Animations on or inside display: none elements do not run. If you are animating an element into view, start from opacity: 0 or transform: scale(0) rather than display: none.

Check if the property is animatable. Not all CSS properties can be animated. display, font-family, and border-radius with border-* shorthand have quirks. Stick to transform, opacity, color, background-color, width, height, top, left for reliable animations.

Use opacity and transform for performance. Animating width, height, top, left, or margin triggers layout recalculation (reflow) on every frame — this is expensive. Animate transform and opacity instead — they run on the GPU and do not trigger reflow.

Check for a missing initial state when mounting. If you apply a class on the same paint frame the element appears, the browser may compute the “before” and “after” states identically and skip the transition. Render the element with the start class, force a reflow with void el.offsetWidth, then add the end class in a requestAnimationFrame callback. The double-requestAnimationFrame pattern is the most reliable way to guarantee the browser observes the start state before the end state.

Check Content Security Policy and inline style restrictions. Some CSP configurations strip inline style="animation:..." attributes added by JavaScript. The animation works locally but disappears in production where CSP is enforced. Move the animation into a stylesheet and toggle a class instead of setting inline styles, or extend your CSP style-src directive to allow what your code injects.

Check for content-visibility: auto skipping render work. Elements with content-visibility: auto that are off-screen do not run their animations until they enter the viewport. This is by design for performance, but it can look like a broken animation if you trigger it programmatically while the element is below the fold. Either scroll the element into view first or use IntersectionObserver to start the animation only when the element is visible.

For related CSS and animation issues, see Fix: CSS Flexbox not working, Fix: CSS position sticky not working, Fix: Framer Motion Not Working, and Fix: Playwright Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles