Skip to content

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

FixDevs ·

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 both a @keyframes rule and an animation property on the element. They 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.

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.

For related CSS layout issues, see Fix: CSS Flexbox not working and Fix: CSS position sticky 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