Fix: AutoAnimate Not Working — Transitions Not Playing, List Items Not Animating, or React State Changes Ignored
Part of: React & Frontend Errors
Quick Answer
How to fix @formkit/auto-animate issues — parent ref setup, React useAutoAnimate hook, Vue directive, animation customization, disabling for specific elements, and framework integration.
The Problem
Items are added to a list but no animation plays:
import { useAutoAnimate } from '@formkit/auto-animate/react';
function TodoList() {
const [parent] = useAutoAnimate();
const [items, setItems] = useState(['Task 1', 'Task 2']);
return (
<ul ref={parent}>
{items.map(item => <li key={item}>{item}</li>)}
</ul>
);
// Adding/removing items — no animation visible
}Or animations play for adds but not for reordering:
New items fade in, but drag-reordering shows no transitionOr the animation causes layout shifts:
Elements jump to new positions instead of smoothly transitioningWhy This Happens
AutoAnimate works by attaching a MutationObserver to a parent element, capturing the bounding rect of each direct child before and after a DOM mutation, then applying a Web Animations API keyframe sequence to animate the difference. That mechanism produces three categories of failure when something in the React-to-DOM pipeline doesn’t match what AutoAnimate expects to see.
The most common failure is putting the ref on the wrong element. useAutoAnimate returns a ref intended for the parent container — the <ul>, <div>, or <section> that wraps the animated children. If you put it on the children themselves, AutoAnimate observes the children’s children, which usually means nothing. The second is using array indices as React keys. React’s reconciliation uses key props to decide whether an element was moved, added, or removed; with index keys, React reuses the same DOM nodes and just mutates their content, so AutoAnimate sees zero adds, zero removes, and zero moves — only text content changing. Use stable IDs (item.id, crypto.randomUUID(), a hash) and reorder, add, and remove animations all start working.
The third category is CSS interference. AutoAnimate calculates positions using getBoundingClientRect, which assumes elements participate in normal layout flow. Elements with position: fixed, position: absolute, display: contents, or aggressive CSS transforms produce coordinate snapshots that don’t match the visible layout, so the calculated translation is wrong and animations either jump to the wrong place or don’t play. Margin collapse between siblings can also produce a height delta during mutation that confuses the position calculation — using gap on a flex/grid container is dramatically more reliable. Finally, AutoAnimate honors prefers-reduced-motion by default, which is why a user with accessibility settings enabled sees no animation at all in production while the developer with a default macOS profile sees them fine in dev.
- The ref goes on the parent container — not the items, not a grandparent.
- Children need stable, unique keys —
key={index}defeats reorder detection. - Only direct children animate — wrappers must hold the ref too if you nest.
- Layout-affecting CSS breaks position math —
display: contents,position: absolute, and margin collapse are the usual suspects.
In Production: Incident Lens
When AutoAnimate breaks in production, the blast radius isn’t a crash — it’s Core Web Vitals. The library’s whole job is to smooth out DOM mutations, and when it fails the mutations still happen, just without the easing. Items pop into place instead of sliding, accordions open instantly instead of expanding, dynamic form fields appear with a hard jump. To users it looks like the UI is “broken” or “janky.” To Google’s CrUX dataset it shows up as a CLS (Cumulative Layout Shift) regression that drags your Lighthouse score and can knock pages out of the “Good” bucket on Core Web Vitals — which is a ranking signal.
How it surfaces: after a Next.js production deploy, Search Console’s Core Web Vitals report flags CLS regressions on pages with dynamic lists. Internal monitoring (web-vitals library piping to your analytics) confirms p75 CLS jumped from 0.05 to 0.18 on the affected routes. Engineers reproduce locally and see animations work in npm run dev but not in npm run build && npm start. The usual cause is React StrictMode double-invocation in dev hiding a key-prop bug, or a production-only CSS purge that introduced display: contents on a wrapper.
Monitoring signal: ship the web-vitals package and emit CLS to your analytics platform per route. Alert when p75 CLS exceeds 0.1 on any high-traffic route. For AutoAnimate specifically, add a Playwright test that drives the animation (clicks a button that adds an item) and asserts a non-zero animation duration via performance.getEntriesByType('measure') or by snapshotting the DOM at intervals — it’s the only reliable way to catch “animation just didn’t play” regressions before deploy.
Recovery sequence: revert the offending UI commit. While investigating, the immediate mitigation is forcing a placeholder height on dynamic regions so the layout doesn’t shift — e.g., min-height: 80px on a slot that may grow. Reproduce the bug in a production build (next build && next start), not dev. Check the key props in the changed components; an index-based key is the single most common regression. The postmortem preventive is a lint rule that flags key={index} in .map() calls (eslint-plugin-react has no-array-index-key) plus a CI gate that runs Lighthouse and fails the build if CLS exceeds 0.1 on critical routes.
Fix 1: React Setup
npm install @formkit/auto-animate'use client';
import { useAutoAnimate } from '@formkit/auto-animate/react';
import { useState } from 'react';
function AnimatedList() {
const [parent] = useAutoAnimate(); // Returns [ref, enable/disable function]
const [items, setItems] = useState([
{ id: '1', text: 'Buy groceries' },
{ id: '2', text: 'Walk the dog' },
{ id: '3', text: 'Write code' },
]);
function addItem() {
setItems([
{ id: crypto.randomUUID(), text: `Task ${items.length + 1}` },
...items, // New item at the top — animates in
]);
}
function removeItem(id: string) {
setItems(items.filter(item => item.id !== id)); // Animates out
}
function shuffle() {
setItems([...items].sort(() => Math.random() - 0.5)); // Animates reorder
}
return (
<div>
<button onClick={addItem}>Add</button>
<button onClick={shuffle}>Shuffle</button>
{/* ref goes on the PARENT container */}
<ul ref={parent}>
{items.map(item => (
// MUST use stable unique key — NOT array index
<li key={item.id} className="flex justify-between p-2 border-b">
<span>{item.text}</span>
<button onClick={() => removeItem(item.id)}>x</button>
</li>
))}
</ul>
</div>
);
}Fix 2: Customize Animation
import { useAutoAnimate } from '@formkit/auto-animate/react';
// Custom duration and easing
const [parent] = useAutoAnimate({
duration: 300, // milliseconds (default: 250)
easing: 'ease-in-out', // CSS easing function
disrespectUserMotionPreference: false, // Respect prefers-reduced-motion
});
// Full custom animation function
import autoAnimate, { type AutoAnimateOptions } from '@formkit/auto-animate';
const customAnimation: AutoAnimateOptions = (el, action, oldCoords, newCoords) => {
let keyframes: Keyframe[] = [];
if (action === 'add') {
// Custom enter animation
keyframes = [
{ opacity: 0, transform: 'scale(0.8) translateY(-20px)' },
{ opacity: 1, transform: 'scale(1) translateY(0)' },
];
} else if (action === 'remove') {
// Custom exit animation
keyframes = [
{ opacity: 1, transform: 'scale(1)' },
{ opacity: 0, transform: 'scale(0.8) translateX(100px)' },
];
} else if (action === 'remain' && oldCoords && newCoords) {
// Move animation
const dx = oldCoords.left - newCoords.left;
const dy = oldCoords.top - newCoords.top;
keyframes = [
{ transform: `translate(${dx}px, ${dy}px)` },
{ transform: 'translate(0, 0)' },
];
}
return new KeyframeEffect(el, keyframes, {
duration: 400,
easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
});
};
// Usage
const [parent] = useAutoAnimate(customAnimation);Fix 3: Enable/Disable Animations
'use client';
import { useAutoAnimate } from '@formkit/auto-animate/react';
import { useState } from 'react';
function ToggleableAnimations() {
const [parent, enable] = useAutoAnimate();
const [animationsEnabled, setAnimationsEnabled] = useState(true);
function toggleAnimations() {
const next = !animationsEnabled;
setAnimationsEnabled(next);
enable(next); // Enable or disable animations
}
return (
<div>
<label>
<input
type="checkbox"
checked={animationsEnabled}
onChange={toggleAnimations}
/>
Enable animations
</label>
<ul ref={parent}>
{/* items */}
</ul>
</div>
);
}
// Skip animation for specific elements
// Add data-no-auto-animate attribute
<ul ref={parent}>
<li key="static" data-no-auto-animate>This item won't animate</li>
<li key="animated">This item will animate</li>
</ul>Fix 4: Common Patterns
// Accordion
function Accordion() {
const [parent] = useAutoAnimate();
const [openId, setOpenId] = useState<string | null>(null);
return (
<div ref={parent}>
{sections.map(section => (
<div key={section.id}>
<button onClick={() => setOpenId(openId === section.id ? null : section.id)}>
{section.title}
</button>
{openId === section.id && (
<div className="p-4">{section.content}</div>
)}
</div>
))}
</div>
);
}
// Tab content transition
function AnimatedTabs() {
const [parent] = useAutoAnimate();
const [activeTab, setActiveTab] = useState('tab1');
return (
<div>
<div className="flex gap-2">
<button onClick={() => setActiveTab('tab1')}>Tab 1</button>
<button onClick={() => setActiveTab('tab2')}>Tab 2</button>
<button onClick={() => setActiveTab('tab3')}>Tab 3</button>
</div>
<div ref={parent}>
{activeTab === 'tab1' && <div key="tab1">Content 1</div>}
{activeTab === 'tab2' && <div key="tab2">Content 2</div>}
{activeTab === 'tab3' && <div key="tab3">Content 3</div>}
</div>
</div>
);
}
// Form fields (show/hide based on selection)
function DynamicForm() {
const [parent] = useAutoAnimate();
const [showExtra, setShowExtra] = useState(false);
return (
<form ref={parent} className="flex flex-col gap-4">
<input placeholder="Name" />
<input placeholder="Email" />
<label>
<input type="checkbox" onChange={(e) => setShowExtra(e.target.checked)} />
Show additional fields
</label>
{showExtra && (
<>
<input key="phone" placeholder="Phone" />
<input key="address" placeholder="Address" />
<input key="company" placeholder="Company" />
</>
)}
<button type="submit">Submit</button>
</form>
);
}Fix 5: Vue and Vanilla JS
<!-- Vue — v-auto-animate directive -->
<script setup>
import { vAutoAnimate } from '@formkit/auto-animate/vue';
import { ref } from 'vue';
const items = ref(['Item 1', 'Item 2', 'Item 3']);
</script>
<template>
<ul v-auto-animate>
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
<!-- With options -->
<ul v-auto-animate="{ duration: 500, easing: 'ease-out' }">
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
</template>// Vanilla JavaScript
import autoAnimate from '@formkit/auto-animate';
const parentElement = document.getElementById('list');
autoAnimate(parentElement);
// With options
autoAnimate(parentElement, { duration: 300 });
// Disable later
const controller = autoAnimate(parentElement);
controller.disable(); // Stop animating
controller.enable(); // ResumeFix 6: Nested Animations
// AutoAnimate only tracks direct children
// For nested animations, apply to each level
function NestedList() {
const [outerRef] = useAutoAnimate();
const [innerRef1] = useAutoAnimate();
const [innerRef2] = useAutoAnimate();
return (
<div ref={outerRef}>
<section key="group-a">
<h2>Group A</h2>
<ul ref={innerRef1}>
{groupAItems.map(item => (
<li key={item.id}>{item.text}</li>
))}
</ul>
</section>
<section key="group-b">
<h2>Group B</h2>
<ul ref={innerRef2}>
{groupBItems.map(item => (
<li key={item.id}>{item.text}</li>
))}
</ul>
</section>
</div>
);
}Still Not Working?
No animation on add/remove — check that the ref is on the parent container, not on individual items. Also verify that children have stable key props. Using key={index} breaks AutoAnimate because React reuses elements instead of adding/removing them.
Animations work for add/remove but not reorder — reorder detection requires unique, stable keys. key={item.id} works; key={index} doesn’t. When items are reordered with index keys, React doesn’t detect a move — it sees the same positions with different content.
Layout shifts or jumpy animations — elements with display: contents, position: absolute, or CSS transforms may confuse AutoAnimate’s position calculations. Use standard block or flex layouts. Also avoid margin collapse — use gap or padding instead.
Animations disabled in production — AutoAnimate respects prefers-reduced-motion by default. Users with this system setting enabled see no animations. Set disrespectUserMotionPreference: true to override (not recommended for accessibility).
Animations work in dev but not in production build — React StrictMode in development double-invokes setState, which can mask a missing key-prop bug. Run next build && next start (or your framework’s prod build) locally to catch this before deploy. Also check that your CSS purge tool (Tailwind, PurgeCSS) isn’t stripping a class AutoAnimate depends on for positioning.
Animation plays but ends in the wrong position — the parent container’s layout context changed mid-animation. Ensure the container has a stable position (relative or static), avoids overflow: hidden cutting off the animated element, and isn’t itself being resized during the same render. If the parent grows from 0 to its natural height while children animate in, the position math is computed against the wrong baseline.
ResizeObserver loop warning in the console — AutoAnimate plus a ResizeObserver-driven layout (some virtualized lists, masonry libraries) can interact badly because each animation step triggers a resize that triggers another observation. Debounce the resize handler or move AutoAnimate’s ref to a wrapper that doesn’t trigger your observer.
For related animation issues, see Fix: Framer Motion Not Working, Fix: GSAP Not Working, Fix: Next.js Hydration Failed, and Fix: Tailwind Classes Not Applying.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Lottie Not Working — Animation Not Playing, File Not Loading, or React Component Blank
How to fix Lottie animation issues — lottie-react and lottie-web setup, JSON animation loading, playback control, interactivity, lazy loading, and performance optimization.
Fix: Blurhash Not Working — Placeholder Not Rendering, Encoding Failing, or Colors Wrong
How to fix Blurhash image placeholder issues — encoding with Sharp, decoding in React, canvas rendering, Next.js image placeholders, CSS blur fallback, and performance optimization.
Fix: Embla Carousel Not Working — Slides Not Scrolling, Autoplay Not Starting, or Thumbnails Not Syncing
How to fix Embla Carousel issues — React setup, slide sizing, autoplay and navigation plugins, loop mode, thumbnail carousels, responsive breakpoints, and vertical scrolling.
Fix: GSAP Not Working — Animations Not Playing, ScrollTrigger Not Firing, or React Cleanup Issues
How to fix GSAP animation issues — timeline and tween basics, ScrollTrigger setup, React useGSAP hook, cleanup and context, SplitText, stagger animations, and Next.js integration.