CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-react-motion

A spring that solves your animation problems.

Pending
Overview
Eval results
Files

transition-motion.mddocs/

TransitionMotion Component

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.

Capabilities

TransitionMotion Component

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>
  );
}

defaultStyles Property

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>;

styles Property

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>;

children Property

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;

willEnter Property

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;

willLeave Property

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;

didLeave Property

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;

Core Types

TransitionStyle

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;
}

TransitionPlainStyle

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;
}

Lifecycle Function Types

/** 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;

Animation Lifecycle

Element Mounting (willEnter)

  1. New element appears in styles array
  2. willEnter called with element's TransitionStyle
  3. Returns initial PlainStyle values
  4. Element animates from initial to target values

Element Unmounting (willLeave)

  1. Element removed from styles array
  2. willLeave called with element's TransitionStyle
  3. If returns null: element removed immediately
  4. If returns Style: element animates to those values
  5. When animation completes, didLeave called
  6. Element finally removed from DOM

Default Behaviors

// 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: () => {}

Common Patterns

Slide In/Out List

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

Scale and Fade

willEnter={() => ({scale: 0, opacity: 0})}
willLeave={() => ({
  scale: spring(0, {stiffness: 300}),
  opacity: spring(0)
})}

Directional Exit Based on Data

willLeave={(styleThatLeft) => ({
  x: spring(styleThatLeft.data.direction === 'left' ? -200 : 200),
  opacity: spring(0)
})}

Conditional Enter/Leave Styles

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)
})}

Performance Considerations

  • Each element maintains separate animation state
  • Key stability is crucial for proper tracking
  • Large lists may benefit from virtualization
  • Complex willEnter/willLeave functions run frequently during transitions
  • Consider using React.memo for children components when data doesn't change frequently

Install with Tessl CLI

npx tessl i tessl/npm-react-motion

docs

index.md

motion.md

spring-system.md

staggered-motion.md

transition-motion.md

tile.json