Skip to main content

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

LibraryBundle SizePerformanceEaseBest For
CSS Transitions0 KB60 fps (GPU)LowSimple transitions
Framer Motion40 KB60 fps (optimized)MediumComplex layouts, drag
React Spring10 KB60 fps (physics)MediumBouncy, natural feel
CSS Animations0 KB60 fps (GPU)LowInfinite, 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 transform and opacity; 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.

Further Reading