Skip to main content

Reorderable Lists with Framer Motion

A reorderable list allows users to drag items to change their order. Without animation, the list jumps as items swap positions. With Framer Motion's Reorder component, items smoothly animate to their new positions while the user drags, creating a polished, responsive feel.

The Reorder component combines layout animation, drag handling, and state management into a declarative API. It detects when items reorder, measures position changes, and animates smoothly—all without explicit keyframe code.

How Reorderable Lists Work

When a user drags an item, Framer Motion tracks its position relative to other items. When the drag ends, it updates the React state to reflect the new order. Layout animation then smoothly animates all items from their old positions to new positions. This two-phase approach (manual drag + automatic layout animation) feels responsive and natural.

The key is that layout animation happens after the DOM reflows, so no jank occurs. Framer Motion batches measurements and uses GPU-accelerated transform to move elements without triggering reflow.

Basic Reorderable List

Here is a simple task list where you can drag items to reorder:

import { motion, Reorder } from 'framer-motion';
import { useState } from 'react';

export default function ReorderableTaskList() {
const [tasks, setTasks] = useState([
{ id: 1, title: 'Learn React', done: false },
{ id: 2, title: 'Build a project', done: false },
{ id: 3, title: 'Deploy to production', done: false },
]);

const addTask = (title) => {
setTasks([...tasks, { id: Date.now(), title, done: false }]);
};

const deleteTask = (id) => {
setTasks(tasks.filter(task => task.id !== id));
};

const toggleTask = (id) => {
setTasks(tasks.map(task =>
task.id === id ? { ...task, done: !task.done } : task
));
};

return (
<div style={{ maxWidth: '400px', margin: '0 auto', padding: '20px' }}>
<h1>Task List</h1>

<Reorder.Group
axis="y"
values={tasks}
onReorder={setTasks}
as="ul"
style={{ listStyle: 'none', padding: 0 }}
>
{tasks.map((task) => (
<Reorder.Item
key={task.id}
value={task}
as="li"
style={{
padding: '12px',
backgroundColor: task.done ? '#e8f5e9' : '#fff',
border: '1px solid #ddd',
borderRadius: '6px',
marginBottom: '10px',
cursor: 'grab',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', flex: 1 }}>
<input
type="checkbox"
checked={task.done}
onChange={() => toggleTask(task.id)}
/>
<span style={{
textDecoration: task.done ? 'line-through' : 'none',
color: task.done ? '#999' : 'inherit',
}}>
{task.title}
</span>
</div>
<button
onClick={() => deleteTask(task.id)}
style={{
backgroundColor: '#ff6b6b',
color: 'white',
border: 'none',
borderRadius: '4px',
padding: '4px 8px',
cursor: 'pointer',
}}
>
Delete
</button>
</Reorder.Item>
))}
</Reorder.Group>

<button
onClick={() => addTask('New task')}
style={{
marginTop: '10px',
padding: '10px 20px',
backgroundColor: '#0066cc',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
}}
>
Add Task
</button>
</div>
);
}

Drag any task to reorder. When you drop it, the list reorders and all items animate to their new positions smoothly. The Reorder.Group manages the drag state; Reorder.Item wraps each draggable item.

Multi-axis Reordering

For 2D grids, use axis="xy" to allow dragging in both directions:

<Reorder.Group
axis="xy"
values={items}
onReorder={setItems}
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '10px',
}}
>
{items.map((item) => (
<Reorder.Item
key={item.id}
value={item}
style={{
padding: '20px',
backgroundColor: '#ddd',
borderRadius: '8px',
cursor: 'grab',
}}
>
{item.name}
</Reorder.Item>
))}
</Reorder.Group>

This creates a Pinterest-style grid where you can drag tiles to rearrange.

Drag Visual Feedback

Provide visual feedback while dragging: lift the item, change opacity, or add a shadow:

import { motion, Reorder } from 'framer-motion';
import { useState } from 'react';

export default function CustomDragFeedback() {
const [items, setItems] = useState([
{ id: 1, label: 'Item A' },
{ id: 2, label: 'Item B' },
{ id: 3, label: 'Item C' },
]);

return (
<Reorder.Group
axis="y"
values={items}
onReorder={setItems}
style={{ padding: '20px' }}
>
{items.map((item) => (
<Reorder.Item
key={item.id}
value={item}
initial={{ opacity: 1 }}
whileHover={{ scale: 1.02 }}
whileDrag={{ scale: 1.05, opacity: 0.8, boxShadow: '0 10px 30px rgba(0,0,0,0.3)' }}
transition={{ type: 'spring', stiffness: 400, damping: 40 }}
style={{
padding: '15px',
backgroundColor: '#f0f0f0',
borderRadius: '8px',
marginBottom: '10px',
cursor: 'grab',
}}
>
<span>⋮⋮ </span>
{item.label}
</Reorder.Item>
))}
</Reorder.Group>
);
}

The whileDrag prop adds visual lift during drag. The grab cursor and drag handle icon (⋮⋮) signal draggability.

Animated List with Add/Remove

Combine Reorder with AnimatePresence to animate new items entering and old items leaving:

import { motion, Reorder, AnimatePresence } from 'framer-motion';
import { useState } from 'react';

export default function DynamicReorderableList() {
const [items, setItems] = useState([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
]);
const [nextId, setNextId] = useState(3);

const addItem = () => {
setItems([...items, { id: nextId, name: `Item ${nextId}` }]);
setNextId(nextId + 1);
};

const removeItem = (id) => {
setItems(items.filter(item => item.id !== id));
};

return (
<div style={{ maxWidth: '300px', margin: '0 auto', padding: '20px' }}>
<button onClick={addItem}>Add Item</button>

<Reorder.Group
axis="y"
values={items}
onReorder={setItems}
style={{ marginTop: '20px' }}
>
<AnimatePresence>
{items.map((item) => (
<Reorder.Item
key={item.id}
value={item}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.2 }}
style={{
padding: '10px',
backgroundColor: '#ddd',
marginBottom: '10px',
borderRadius: '4px',
display: 'flex',
justifyContent: 'space-between',
}}
>
<span>{item.name}</span>
<button onClick={() => removeItem(item.id)}>Remove</button>
</Reorder.Item>
))}
</AnimatePresence>
</Reorder.Group>
</div>
);
}

New items fade in from the left. Deleted items fade out to the right. The remaining items animate to fill the gap smoothly.

Nested Reorderable Lists

Create a nested hierarchy where parent and child lists are independently reorderable:

<Reorder.Group
axis="y"
values={projects}
onReorder={setProjects}
>
{projects.map((project) => (
<Reorder.Item key={project.id} value={project}>
<div style={{ padding: '10px', backgroundColor: '#f9f9f9', marginBottom: '10px' }}>
<h3>{project.name}</h3>

<Reorder.Group
axis="y"
values={project.tasks}
onReorder={(newTasks) => {
const updated = projects.map(p =>
p.id === project.id ? { ...p, tasks: newTasks } : p
);
setProjects(updated);
}}
style={{ paddingLeft: '20px' }}
>
{project.tasks.map((task) => (
<Reorder.Item key={task.id} value={task}>
<div style={{ padding: '8px', backgroundColor: '#e8e8e8', marginBottom: '8px' }}>
{task.title}
</div>
</Reorder.Item>
))}
</Reorder.Group>
</div>
</Reorder.Item>
))}
</Reorder.Group>

Each nest level has its own Reorder.Group with independent state management. Dragging within a level reorders that level only.

Performance: Virtual Lists

For large lists (100+ items), use virtualization to render only visible items. Combine with Reorder for memory efficiency:

import { FixedSizeList } from 'react-window';
import { Reorder } from 'framer-motion';

function VirtualReorderableList({ items, onReorder }) {
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{({ index, style }) => (
<div style={style}>
<Reorder.Item
value={items[index]}
onReorder={(newItems) => {
// Update with new order
onReorder(newItems);
}}
>
{items[index].name}
</Reorder.Item>
</div>
)}
</FixedSizeList>
);
}

Virtual lists render only 10–15 items at a time, keeping the DOM lean and animation smooth even with 1000+ items.

Common Patterns

PatternUse CaseExample
Single-axis reorderLists, queues, priorityTask lists, email inboxes
Multi-axis reorderGrids, dashboardsPinterest boards, kanban columns
Nested reorderHierarchies, foldersFile explorers, project structures
Nested + add/removeDynamic hierarchiesCMS content trees

Key Takeaways

  • Reorder component handles drag-and-drop reordering with automatic layout animation.
  • Use axis="y" for vertical lists, axis="xy" for 2D grids.
  • Combine with AnimatePresence to animate add/remove operations.
  • Provide visual feedback with whileHover and whileDrag props.
  • Use virtual lists (react-window) for 100+ item performance.

Frequently Asked Questions

Can I reorder items programmatically without dragging?

Yes. The onReorder callback accepts the new array directly. Call setItems([newOrder]) to reorder programmatically and trigger layout animation.

Does Reorder work with filtered or sorted lists?

Yes, but the reordering updates the underlying data. If you filter or sort after reordering, the visual order may differ from the data order. Consider maintaining a separate position/order field in your data.

Can I disable drag on certain items?

Not with the standard Reorder API, but you can wrap non-draggable items in a custom component that prevents onReorder updates. Or use a variant that disables the cursor.

What is the drag threshold (how far must I drag before reorder starts)?

Framer Motion uses a sensible default (5–10 pixels). You can't customize this, but it's intentionally conservative to avoid accidental reorders during clicks.

How do I persist the reordered list to a server?

Call an API in an effect: useEffect(() => { api.updateOrder(items); }, [items]). Debounce to avoid spamming requests.

Further Reading