A spring that solves your animation problems.
—
The TransitionMotion component handles animations for mounting and unmounting elements with full lifecycle control. It's the most advanced component in React Motion, perfect for dynamic lists, modal animations, and any scenario where elements appear and disappear with custom transition effects.
Manages animations for dynamically mounting and unmounting elements with customizable enter/leave transitions.
/**
* Advanced component for animating mounting and unmounting elements
* Provides full lifecycle control with willEnter, willLeave, and didLeave hooks
*/
class TransitionMotion extends React.Component {
static propTypes: {
/** Initial styles for default elements (optional) */
defaultStyles?: Array<TransitionPlainStyle>,
/** Target styles or function returning styles based on previous state (required) */
styles: Array<TransitionStyle> | (previousInterpolatedStyles: ?Array<TransitionPlainStyle>) => Array<TransitionStyle>,
/** Render function receiving array of interpolated styles with keys (required) */
children: (interpolatedStyles: Array<TransitionPlainStyle>) => ReactElement,
/** Function defining how elements enter (optional, defaults to stripStyle) */
willEnter?: WillEnter,
/** Function defining how elements leave (optional, defaults to immediate removal) */
willLeave?: WillLeave,
/** Callback when element has finished leaving (optional) */
didLeave?: DidLeave
}
}Usage Examples:
import React, { useState } from 'react';
import { TransitionMotion, spring } from 'react-motion';
// Basic list transitions
function AnimatedList() {
const [items, setItems] = useState([
{key: 'a', data: 'Item A'},
{key: 'b', data: 'Item B'},
{key: 'c', data: 'Item C'}
]);
const addItem = () => {
const newKey = Date.now().toString();
setItems([...items, {key: newKey, data: `Item ${newKey}`}]);
};
const removeItem = (key) => {
setItems(items.filter(item => item.key !== key));
};
return (
<div>
<button onClick={addItem}>Add Item</button>
<TransitionMotion
styles={items.map(item => ({
key: item.key,
data: item.data,
style: {
opacity: spring(1),
scale: spring(1),
height: spring(60)
}
}))}
willEnter={() => ({
opacity: 0,
scale: 0.5,
height: 0
})}
willLeave={() => ({
opacity: spring(0),
scale: spring(0.5),
height: spring(0)
})}
>
{interpolatedStyles => (
<div>
{interpolatedStyles.map(({key, data, style}) => (
<div
key={key}
style={{
opacity: style.opacity,
transform: `scale(${style.scale})`,
height: `${style.height}px`,
background: '#f8f9fa',
border: '1px solid #dee2e6',
margin: '4px 0',
padding: '10px',
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}
>
<span>{data}</span>
<button onClick={() => removeItem(key)}>
Remove
</button>
</div>
))}
</div>
)}
</TransitionMotion>
</div>
);
}
// Modal with enter/exit animations
function AnimatedModal() {
const [showModal, setShowModal] = useState(false);
return (
<div>
<button onClick={() => setShowModal(true)}>
Show Modal
</button>
<TransitionMotion
styles={showModal ? [{
key: 'modal',
style: {
opacity: spring(1),
scale: spring(1),
backdropOpacity: spring(0.5)
}
}] : []}
willEnter={() => ({
opacity: 0,
scale: 0.8,
backdropOpacity: 0
})}
willLeave={() => ({
opacity: spring(0),
scale: spring(0.8),
backdropOpacity: spring(0)
})}
didLeave={() => console.log('Modal fully closed')}
>
{interpolatedStyles => (
<div>
{interpolatedStyles.map(({key, style}) => (
<div key={key}>
{/* Backdrop */}
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: `rgba(0, 0, 0, ${style.backdropOpacity})`,
zIndex: 1000
}}
onClick={() => setShowModal(false)}
/>
{/* Modal */}
<div
style={{
position: 'fixed',
top: '50%',
left: '50%',
transform: `translate(-50%, -50%) scale(${style.scale})`,
opacity: style.opacity,
background: 'white',
padding: '20px',
borderRadius: '8px',
boxShadow: '0 4px 20px rgba(0,0,0,0.3)',
zIndex: 1001,
maxWidth: '400px',
width: '90%'
}}
>
<h3>Modal Title</h3>
<p>Modal content goes here...</p>
<button onClick={() => setShowModal(false)}>
Close
</button>
</div>
</div>
))}
</div>
)}
</TransitionMotion>
</div>
);
}
// Todo list with complex transitions
function TodoList() {
const [todos, setTodos] = useState([
{id: 1, text: 'Learn React Motion', completed: false},
{id: 2, text: 'Build awesome animations', completed: false}
]);
const [newTodo, setNewTodo] = useState('');
const addTodo = () => {
if (newTodo.trim()) {
setTodos([...todos, {
id: Date.now(),
text: newTodo,
completed: false
}]);
setNewTodo('');
}
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? {...todo, completed: !todo.completed} : todo
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div style={{maxWidth: '400px', margin: '0 auto', padding: '20px'}}>
<div style={{marginBottom: '20px'}}>
<input
value={newTodo}
onChange={e => setNewTodo(e.target.value)}
onKeyPress={e => e.key === 'Enter' && addTodo()}
placeholder="Add new todo..."
style={{marginRight: '10px', padding: '8px'}}
/>
<button onClick={addTodo}>Add</button>
</div>
<TransitionMotion
styles={todos.map(todo => ({
key: todo.id.toString(),
data: todo,
style: {
opacity: spring(1),
height: spring(50),
x: spring(0)
}
}))}
willEnter={() => ({
opacity: 0,
height: 0,
x: -100
})}
willLeave={(styleThatLeft) => ({
opacity: spring(0),
height: spring(0),
x: spring(styleThatLeft.data.completed ? 100 : -100)
})}
>
{interpolatedStyles => (
<div>
{interpolatedStyles.map(({key, data, style}) => (
<div
key={key}
style={{
opacity: style.opacity,
height: `${style.height}px`,
transform: `translateX(${style.x}px)`,
background: data.completed ? '#d4edda' : '#f8f9fa',
border: '1px solid #dee2e6',
borderRadius: '4px',
margin: '4px 0',
padding: '10px',
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}
>
<div style={{display: 'flex', alignItems: 'center'}}>
<input
type="checkbox"
checked={data.completed}
onChange={() => toggleTodo(data.id)}
style={{marginRight: '10px'}}
/>
<span style={{
textDecoration: data.completed ? 'line-through' : 'none'
}}>
{data.text}
</span>
</div>
<button onClick={() => deleteTodo(data.id)}>
Delete
</button>
</div>
))}
</div>
)}
</TransitionMotion>
</div>
);
}Optional array of initial styles for elements that should be present by default.
/**
* Initial styles for default elements
* Each item must have key, data (optional), and style properties
*/
defaultStyles?: Array<TransitionPlainStyle>;Target styles array or function returning styles. Can be static array or dynamic function based on previous state.
/**
* Target styles or function returning styles based on previous state
* Static: Array of TransitionStyle objects
* Dynamic: Function receiving previous interpolated styles
*/
styles: Array<TransitionStyle> | (previousInterpolatedStyles: ?Array<TransitionPlainStyle>) => Array<TransitionStyle>;Render function that receives interpolated styles with keys and data for all currently active elements.
/**
* Render function receiving array of interpolated styles with keys
* Called every frame with current values for all active elements
*/
children: (interpolatedStyles: Array<TransitionPlainStyle>) => ReactElement;Function defining how new elements should enter. Returns initial style values for mounting elements.
/**
* Function defining how elements enter
* @param styleThatEntered - The TransitionStyle for the entering element
* @returns PlainStyle object with initial values for animation
*/
willEnter?: (styleThatEntered: TransitionStyle) => PlainStyle;Function defining how elements should leave. Returns target style values for unmounting elements, or null for immediate removal.
/**
* Function defining how elements leave
* @param styleThatLeft - The TransitionStyle for the leaving element
* @returns Style object for exit animation, or null for immediate removal
*/
willLeave?: (styleThatLeft: TransitionStyle) => ?Style;Optional callback fired when an element has completely finished its leave animation and been removed.
/**
* Callback fired when element has finished leaving
* @param styleThatLeft - Object with key and data of the left element
*/
didLeave?: (styleThatLeft: { key: string, data?: any }) => void;Object describing a transitioning element with unique key, optional data, and target style.
interface TransitionStyle {
/** Unique identifier for tracking element across renders */
key: string;
/** Optional data to carry along with the element */
data?: any;
/** Target style object for this element */
style: Style;
}Object with interpolated values passed to render function, containing current animation state.
interface TransitionPlainStyle {
/** Unique identifier for the element */
key: string;
/** Optional data associated with element */
data?: any;
/** Current interpolated style values */
style: PlainStyle;
}/** Function type for willEnter prop */
type WillEnter = (styleThatEntered: TransitionStyle) => PlainStyle;
/** Function type for willLeave prop */
type WillLeave = (styleThatLeft: TransitionStyle) => ?Style;
/** Function type for didLeave prop */
type DidLeave = (styleThatLeft: { key: string, data?: any }) => void;willEnter called with element's TransitionStylewillLeave called with element's TransitionStyledidLeave called// Default willEnter: strips spring configs to get plain values
willEnter: styleThatEntered => stripStyle(styleThatEntered.style)
// Default willLeave: immediate removal
willLeave: () => null
// Default didLeave: no-op
didLeave: () => {}<TransitionMotion
styles={items.map(item => ({
key: item.id,
data: item,
style: {x: spring(0), opacity: spring(1)}
}))}
willEnter={() => ({x: -100, opacity: 0})}
willLeave={() => ({x: spring(100), opacity: spring(0)})}
>
{styles => (
<div>
{styles.map(({key, data, style}) => (
<div
key={key}
style={{
transform: `translateX(${style.x}px)`,
opacity: style.opacity
}}
>
{data.text}
</div>
))}
</div>
)}
</TransitionMotion>willEnter={() => ({scale: 0, opacity: 0})}
willLeave={() => ({
scale: spring(0, {stiffness: 300}),
opacity: spring(0)
})}willLeave={(styleThatLeft) => ({
x: spring(styleThatLeft.data.direction === 'left' ? -200 : 200),
opacity: spring(0)
})}willEnter={(entering) => ({
opacity: 0,
scale: entering.data.type === 'important' ? 1.2 : 0.8
})}
willLeave={(leaving) => ({
opacity: spring(0),
y: spring(leaving.data.deleted ? 50 : -50)
})}Install with Tessl CLI
npx tessl i tessl/npm-react-motion