Skip to content

Fix: AutoAnimate Not Working — Transitions Not Playing, List Items Not Animating, or React State Changes Ignored

FixDevs ·

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 transition

Or the animation causes layout shifts:

Elements jump to new positions instead of smoothly transitioning

Why 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 childrenuseAutoAnimate returns 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 key props 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, or display: contents break 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();   // Resume

Fix 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.

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