React Kanban Animations: Smooth Transitions & FX
Animations in a kanban board improve perceived performance and UX. Watching a card smoothly slide to a new column feels snappier than instant teleportation; loading spinners and progress indicators communicate app state. Production design systems (Material Design 2026 guidelines) recommend 150-300 ms transitions for interactive elements, balancing responsiveness and visual polish.
CSS Transitions for Simple Animations
Start with CSS transitions for basic movements:
function Card({ task, isDragging }) {
return (
<div
className={`card ${isDragging ? 'dragging' : ''}`}
style={{
transition: isDragging ? 'none' : 'all 0.3s ease-out',
opacity: isDragging ? 0.5 : 1,
transform: isDragging ? 'scale(1.05)' : 'scale(1)',
}}
>
<h3>{task.title}</h3>
<p>{task.description}</p>
</div>
);
}
// CSS
const cardStyles = `
.card {
transition: all 0.3s ease-out;
background: white;
border-radius: 8px;
padding: 12px;
margin-bottom: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.card.dragging {
opacity: 0.5;
transform: scale(1.05);
}
`;
CSS transitions animate property changes smoothly. When a card moves from isDragging: false to true, the opacity and transform animate over 300 ms. The transition: none during drag prevents animation lag.
Using Framer Motion for Complex Animations
Framer Motion simplifies keyframe animations and layout transitions:
npm install framer-motion
import { motion } from 'framer-motion';
function Card({ task, columnId, onMoveTask }) {
return (
<motion.div
layout
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
className="card"
whileHover={{ y: -4, boxShadow: '0 8px 16px rgba(0,0,0,0.15)' }}
whileDrag={{ opacity: 0.5, scale: 1.05 }}
>
<h3>{task.title}</h3>
<p>{task.description}</p>
</motion.div>
);
}
function Column({ columnId, tasks, onTaskDropped }) {
return (
<motion.div className="column" layout>
<h2>{columnId}</h2>
<motion.ul layout>
{tasks.map((task) => (
<motion.li key={task.id} layout>
<Card task={task} columnId={columnId} onMoveTask={onTaskDropped} />
</motion.li>
))}
</motion.ul>
</motion.div>
);
}
Framer Motion properties:
layout: Animates when the DOM layout changes (e.g., when items are reordered).initial,animate,exit: Define entry, active, and exit animations.whileHover,whileDrag: Apply animations on interactions.transition: Controls duration, easing, stagger (for sequencing children).
The layout prop on the column and list ensures that when a card moves to another column, the layout shift animates smoothly rather than jumping.
Staggered Animations for Multiple Items
Animate a list of items sequentially:
import { motion } from 'framer-motion';
function Column({ columnId, tasks, onTaskDropped }) {
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1, // Delay each child by 100ms
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.3 },
},
};
return (
<motion.div
className="column"
variants={containerVariants}
initial="hidden"
animate="visible"
>
<h2>{columnId}</h2>
<motion.ul layout>
{tasks.map((task) => (
<motion.li key={task.id} variants={itemVariants}>
<Card task={task} columnId={columnId} />
</motion.li>
))}
</motion.ul>
</motion.div>
);
}
The staggerChildren property delays each child's animation, creating a cascade effect. This makes a list feel less jarring and more intentional.
Loading and Transition States
Animate loading states and transitions between columns:
function Card({ task, isLoading, loadingProgress }) {
return (
<motion.div className="card">
{isLoading && (
<motion.div
className="loading-overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<motion.div
className="progress-bar"
initial={{ width: '0%' }}
animate={{ width: `${loadingProgress}%` }}
transition={{ duration: 0.3 }}
/>
</motion.div>
)}
<h3>{task.title}</h3>
<p>{task.description}</p>
</motion.div>
);
}
The progress bar animates as loadingProgress updates, giving users feedback on operation status.
Spring Animations for Bouncy Effects
React Spring provides physics-based animations for a more natural feel:
npm install react-spring
import { useSpring, animated } from 'react-spring';
function Card({ task, isDragging }) {
const springProps = useSpring({
opacity: isDragging ? 0.5 : 1,
scale: isDragging ? 1.05 : 1,
config: { tension: 300, friction: 30 }, // Bouncier
});
return (
<animated.div
className="card"
style={{
opacity: springProps.opacity,
transform: springProps.scale.to((s) => `scale(${s})`),
}}
>
<h3>{task.title}</h3>
<p>{task.description}</p>
</animated.div>
);
}
React Spring's physics-based animation creates a "bouncy" feel as values settle into their final state. Adjust tension and friction to control bounciness.
Preventing Animation Jank
Ensure animations maintain 60 fps:
function Card({ task, isDragging }) {
return (
<motion.div
className="card"
style={{
// Use transform and opacity; these are GPU-accelerated
opacity: isDragging ? 0.5 : 1,
// Avoid animating these (forces re-layout):
// width, height, left, top, padding, margin
}}
whileHover={{ y: -4 }} // Use transform, not top/margin
transition={{ duration: 0.3, type: 'spring' }}
>
{task.title}
</motion.div>
);
}
GPU-accelerated properties (transform, opacity) animate at 60 fps. Layout properties (width, height, left, top) trigger expensive re-layouts and drop frames. Measure with the Performance tab in DevTools; aim for frame times under 16.6 ms.
Coordinating Animations with Drag
Synchronize drag animations with other UI feedback:
function Board() {
const [draggedTask, setDraggedTask] = React.useState(null);
const [tasks, setTasks] = React.useState(initialTasks);
return (
<motion.div className="board">
{draggedTask && (
<motion.div
className="drag-indicator"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
>
Moving: {draggedTask.title}
</motion.div>
)}
{Object.entries(tasks).map(([columnId, columnTasks]) => (
<Column
key={columnId}
columnId={columnId}
tasks={columnTasks}
draggedTaskId={draggedTask?.id}
/>
))}
</motion.div>
);
}
function Column({ columnId, tasks, draggedTaskId }) {
return (
<motion.div
className={`column ${draggedTaskId ? 'highlight' : ''}`}
animate={{
backgroundColor: draggedTaskId ? '#f5f5f5' : '#fff',
}}
transition={{ duration: 0.2 }}
>
{/* ... */}
</motion.div>
);
}
The column's background animates as the user drags. The drag indicator slides in from above. These coordinated animations create a cohesive, polished UX.
Performance Comparison: Animation Libraries
| Library | Bundle Size | Performance | Ease | Best For |
|---|---|---|---|---|
| CSS Transitions | 0 KB | 60 fps (GPU) | Low | Simple transitions |
| Framer Motion | 40 KB | 60 fps (optimized) | Medium | Complex layouts, drag |
| React Spring | 10 KB | 60 fps (physics) | Medium | Bouncy, natural feel |
| CSS Animations | 0 KB | 60 fps (GPU) | Low | Infinite, keyframe animations |
Key Takeaways
- CSS transitions: Use for simple property changes (opacity, transform, color).
- Framer Motion: Use for layout shifts, drag interactions, and complex sequences.
- React Spring: Use for physics-based, natural-feeling animations.
- GPU acceleration: Animate
transformandopacity; avoid layout properties. - Stagger children: Animate lists sequentially for visual polish.
Frequently Asked Questions
Does animation slow down my app?
No, if you animate GPU-accelerated properties (transform, opacity). CSS transitions and Framer Motion handle this automatically. Profile in DevTools; target frame times under 16.6 ms.
Should I animate every interaction?
No. Animations should feel natural and purposeful. Animate state transitions (loading, drag, reorder) but not every small change. Too much animation can feel cluttered.
How do I animate a card disappearing from the DOM?
Use exit animations: <motion.div exit={{ opacity: 0 }}> ... </motion.div>. Framer Motion keeps the DOM node briefly to animate the exit, then removes it.
Can I use Framer Motion and React Spring together?
Yes, but it's redundant. Pick one. Framer Motion is better for drag-and-drop; React Spring is better for physics-based effects. Use CSS transitions for simple cases.
How do I test animations in unit tests?
Use jest.useFakeTimers() to control animation timing. Or skip animation testing and rely on visual regression testing (Chromatic, Percy) to catch regressions.