Fix: AutoAnimate Not Working — Transitions Not Playing, List Items Not Animating, or React State Changes Ignored
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 adds CSS transitions to direct children of a parent element when they’re added, removed, or moved:
- The ref must be on the parent, not the children —
useAutoAnimatereturns a ref that goes on the container element (e.g.,<ul>,<div>) that wraps the animated children. Putting it on the children themselves does nothing. - Children must have stable, unique keys — React’s reconciliation uses
keyprops to track which elements moved, were added, or removed. Using array indices as keys prevents AutoAnimate from detecting moves. Use stable IDs. - Only direct children are animated — AutoAnimate watches the container’s immediate children. Nested elements (grandchildren) aren’t tracked. If your items are wrapped in an extra div, the wrapper needs the ref.
- CSS can interfere — elements with
position: fixed,position: absolute, ordisplay: contentsbreak AutoAnimate’s position calculations. The library works by capturing element positions before and after a DOM mutation, then animating the difference.
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)}>×</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).
For related animation issues, see Fix: Framer Motion Not Working and Fix: GSAP Not Working.
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.