or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

animation-controls.mdconfiguration.mddom-utilities.mdgestures.mdindex.mdlayout-presence.mdmotion-components.mdmotion-values.mdscroll-view.md
tile.json

layout-presence.mddocs/

Layout and Presence

Components and hooks for layout animations and enter/exit animations with shared layout transitions.

Capabilities

AnimatePresence Component

Enables exit animations for components leaving the DOM and manages component presence lifecycle.

/**
 * Wrapper component that enables exit animations for its children
 * @param props - AnimatePresence configuration
 * @returns JSX element managing presence animations
 */
function AnimatePresence(props: AnimatePresenceProps): JSX.Element;

interface AnimatePresenceProps {
  /**
   * Child components to manage presence for
   */
  children?: React.ReactNode;
  
  /**
   * Whether to animate on initial mount (default: true)
   */
  initial?: boolean;
  
  /**
   * Custom data passed to all child animations
   */
  custom?: any;
  
  /**
   * Called when all exit animations complete
   */
  onExitComplete?: () => void;
  
  /**
   * Animation mode for multiple children
   * - "sync": All children animate simultaneously (default)
   * - "wait": Wait for exit to complete before animating new children
   * - "popLayout": Exiting children are removed from layout flow immediately
   */
  mode?: "sync" | "wait" | "popLayout";
  
  /**
   * Root element for presence calculations (default: nearest positioned ancestor)
   */
  root?: React.RefObject<Element>;
  
  /**
   * Whether presence affects layout calculations (default: true)
   */
  presenceAffectsLayout?: boolean;
  
  /**
   * Propagate presence context to nested AnimatePresence components
   */
  propagate?: boolean;
  
  /**
   * Exit animation anchor point
   */
  anchorX?: number;
}

Usage Examples:

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

// Basic presence animation
function BasicPresenceExample() {
  const [isVisible, setIsVisible] = useState(true);
  
  return (
    <div>
      <button onClick={() => setIsVisible(!isVisible)}>
        Toggle
      </button>
      
      <AnimatePresence>
        {isVisible && (
          <motion.div
            initial={{ opacity: 0, scale: 0.8 }}
            animate={{ opacity: 1, scale: 1 }}
            exit={{ opacity: 0, scale: 0.8 }}
            transition={{ duration: 0.3 }}
            className="w-32 h-32 bg-blue-500 rounded mt-4"
          >
            Animated content
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
}

// Wait mode for sequential animations
function WaitModeExample() {
  const [currentItem, setCurrentItem] = useState(0);
  const items = ["First", "Second", "Third"];
  
  return (
    <div>
      <button 
        onClick={() => setCurrentItem((prev) => (prev + 1) % items.length)}
      >
        Next Item
      </button>
      
      <AnimatePresence mode="wait" onExitComplete={() => console.log("Exit complete")}>
        <motion.div
          key={currentItem}
          initial={{ x: 300, opacity: 0 }}
          animate={{ x: 0, opacity: 1 }}
          exit={{ x: -300, opacity: 0 }}
          transition={{ duration: 0.5 }}
          className="w-64 h-32 bg-green-500 rounded mt-4 flex items-center justify-center text-white"
        >
          {items[currentItem]}
        </motion.div>
      </AnimatePresence>
    </div>
  );
}

// List with exit animations
function ListPresenceExample() {
  const [items, setItems] = useState([1, 2, 3, 4, 5]);
  
  const removeItem = (id: number) => {
    setItems(items.filter(item => item !== id));
  };
  
  const addItem = () => {
    setItems([...items, Math.max(...items, 0) + 1]);
  };
  
  return (
    <div>
      <button onClick={addItem} className="mb-4 px-4 py-2 bg-blue-500 text-white rounded">
        Add Item
      </button>
      
      <AnimatePresence>
        {items.map(item => (
          <motion.div
            key={item}
            initial={{ opacity: 0, height: 0 }}
            animate={{ opacity: 1, height: "auto" }}
            exit={{ opacity: 0, height: 0 }}
            transition={{ duration: 0.3 }}
            className="bg-gray-200 p-4 mb-2 rounded overflow-hidden"
          >
            <div className="flex justify-between items-center">
              <span>Item {item}</span>
              <button 
                onClick={() => removeItem(item)}
                className="px-2 py-1 bg-red-500 text-white rounded text-sm"
              >
                Remove
              </button>
            </div>
          </motion.div>
        ))}
      </AnimatePresence>
    </div>
  );
}

LayoutGroup Component

Enables shared layout animations between components and coordinates layout transitions.

/**
 * Provides shared layout animation context for its children
 * @param props - LayoutGroup configuration
 * @returns JSX element providing layout group context
 */
function LayoutGroup(props: LayoutGroupProps): JSX.Element;

interface LayoutGroupProps {
  /**
   * Child components to share layout animations
   */
  children: React.ReactNode;
  
  /**
   * Unique identifier for this layout group
   */
  id?: string;
  
  /**
   * Inherit layout group from parent (default: true)
   */
  inherit?: boolean;
}

Usage Examples:

import { LayoutGroup, motion } from "framer-motion";
import { useState } from "react";

// Shared layout animations
function SharedLayoutExample() {
  const [selected, setSelected] = useState(0);
  const items = ["Tab 1", "Tab 2", "Tab 3"];
  
  return (
    <LayoutGroup>
      <div className="flex space-x-2 mb-4">
        {items.map((item, index) => (
          <motion.button
            key={index}
            onClick={() => setSelected(index)}
            className={`px-4 py-2 rounded relative ${
              selected === index ? "text-white" : "text-gray-600"
            }`}
            style={{
              WebkitTapHighlightColor: "transparent"
            }}
          >
            {selected === index && (
              <motion.div
                layoutId="activeTab"
                className="absolute inset-0 bg-blue-500 rounded"
                transition={{ type: "spring", bounce: 0.2, duration: 0.6 }}
              />
            )}
            <span className="relative z-10">{item}</span>
          </motion.button>
        ))}
      </div>
      
      <motion.div
        key={selected}
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        className="p-4 bg-gray-100 rounded"
      >
        Content for {items[selected]}
      </motion.div>
    </LayoutGroup>
  );
}

// Card expansion example
function CardExpansionExample() {
  const [selectedId, setSelectedId] = useState<string | null>(null);
  const items = [
    { id: "1", title: "Card 1", subtitle: "First card" },
    { id: "2", title: "Card 2", subtitle: "Second card" },
    { id: "3", title: "Card 3", subtitle: "Third card" }
  ];
  
  return (
    <LayoutGroup>
      <div className="grid grid-cols-3 gap-4">
        {items.map(item => (
          <motion.div
            key={item.id}
            layoutId={item.id}
            onClick={() => setSelectedId(item.id)}
            className="bg-white rounded-lg shadow-md p-4 cursor-pointer"
            whileHover={{ scale: 1.05 }}
          >
            <motion.h3 layoutId={`title-${item.id}`} className="font-bold">
              {item.title}
            </motion.h3>
            <motion.p layoutId={`subtitle-${item.id}`} className="text-gray-600">
              {item.subtitle}
            </motion.p>
          </motion.div>
        ))}
      </div>
      
      {selectedId && (
        <motion.div
          className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          onClick={() => setSelectedId(null)}
        >
          <motion.div
            layoutId={selectedId}
            className="bg-white rounded-lg shadow-lg p-8 max-w-md w-full mx-4"
            onClick={(e) => e.stopPropagation()}
          >
            {items.find(item => item.id === selectedId) && (
              <>
                <motion.h3 
                  layoutId={`title-${selectedId}`} 
                  className="font-bold text-2xl mb-4"
                >
                  {items.find(item => item.id === selectedId)?.title}
                </motion.h3>
                <motion.p 
                  layoutId={`subtitle-${selectedId}`} 
                  className="text-gray-600 mb-4"
                >
                  {items.find(item => item.id === selectedId)?.subtitle}
                </motion.p>
                <p>Expanded content goes here...</p>
                <button 
                  onClick={() => setSelectedId(null)}
                  className="mt-4 px-4 py-2 bg-blue-500 text-white rounded"
                >
                  Close
                </button>
              </>
            )}
          </motion.div>
        </motion.div>
      )}
    </LayoutGroup>
  );
}

Presence Hooks

Hooks for accessing presence state and managing component lifecycle within AnimatePresence.

/**
 * Access presence state within AnimatePresence
 * @returns Tuple of [isPresent, safeToRemove]
 */
function usePresence(): [boolean, () => void];

/**
 * Check if component is currently present
 * @returns Boolean indicating presence state
 */
function useIsPresent(): boolean;

/**
 * Access presence context data
 * @returns Presence context information
 */
function usePresenceData(): PresenceContextData | null;

interface PresenceContextData {
  /**
   * Whether component is present
   */
  isPresent: boolean;
  
  /**
   * Function to call when safe to remove component
   */
  onExitComplete?: () => void;
  
  /**
   * Custom data passed from AnimatePresence
   */
  custom?: any;
  
  /**
   * Whether initial animation should be performed
   */
  initial?: boolean;
}

Usage Examples:

import { usePresence, useIsPresent, motion } from "framer-motion";
import { useEffect } from "react";

// Custom exit animation component
function CustomExitComponent({ children }: { children: React.ReactNode }) {
  const [isPresent, safeToRemove] = usePresence();
  
  useEffect(() => {
    if (!isPresent) {
      // Perform custom exit logic
      console.log("Component is exiting");
      
      // Call safeToRemove when ready to be removed from DOM
      const timer = setTimeout(() => {
        safeToRemove();
      }, 1000);
      
      return () => clearTimeout(timer);
    }
  }, [isPresent, safeToRemove]);
  
  return (
    <motion.div
      animate={isPresent ? "enter" : "exit"}
      variants={{
        enter: { opacity: 1, scale: 1 },
        exit: { opacity: 0, scale: 0.8 }
      }}
      transition={{ duration: 0.5 }}
    >
      {children}
    </motion.div>
  );
}

// Conditional rendering based on presence
function ConditionalComponent() {
  const isPresent = useIsPresent();
  
  return (
    <motion.div
      animate={{ 
        backgroundColor: isPresent ? "#3B82F6" : "#EF4444",
        scale: isPresent ? 1 : 0.9
      }}
      className="p-4 rounded text-white"
    >
      {isPresent ? "I'm here!" : "I'm leaving..."}
    </motion.div>
  );
}

Layout Animation Props

Props for automatic layout animations on size and position changes.

interface LayoutAnimationProps {
  /**
   * Enable automatic layout animations
   * - true: Animate both position and size changes
   * - "position": Animate only position changes
   * - "size": Animate only size changes
   */
  layout?: boolean | "position" | "size";
  
  /**
   * Unique identifier for shared layout animations
   */
  layoutId?: string;
  
  /**
   * Mark element as layout root for optimization
   */
  layoutRoot?: boolean;
  
  /**
   * Enable layout animations during scroll
   */
  layoutScroll?: boolean;
  
  /**
   * Dependencies that trigger layout recalculation
   */
  layoutDependency?: any;
  
  /**
   * Called when layout animation starts
   */
  onLayoutAnimationStart?: () => void;
  
  /**
   * Called when layout animation completes
   */
  onLayoutAnimationComplete?: () => void;
}

Usage Examples:

import { motion } from "framer-motion";
import { useState } from "react";

// Automatic layout animations
function LayoutAnimationExample() {
  const [isExpanded, setIsExpanded] = useState(false);
  
  return (
    <motion.div
      layout
      onClick={() => setIsExpanded(!isExpanded)}
      className={`bg-blue-500 text-white p-4 rounded cursor-pointer ${
        isExpanded ? "w-96 h-48" : "w-32 h-16"
      }`}
      transition={{ type: "spring", damping: 20, stiffness: 300 }}
      onLayoutAnimationStart={() => console.log("Layout animation started")}
      onLayoutAnimationComplete={() => console.log("Layout animation completed")}
    >
      <h3>Click to expand</h3>
      {isExpanded && (
        <motion.p
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          className="mt-2"
        >
          This content appears when expanded!
        </motion.p>
      )}
    </motion.div>
  );
}

// Shared layout ID animations
function SharedLayoutIdExample() {
  const [selectedItem, setSelectedItem] = useState<string | null>(null);
  const items = ["item1", "item2", "item3"];
  
  return (
    <div>
      <div className="grid grid-cols-3 gap-4 mb-4">
        {items.map(item => (
          <motion.div
            key={item}
            layoutId={item}
            onClick={() => setSelectedItem(item)}
            className="bg-green-500 text-white p-4 rounded cursor-pointer"
            whileHover={{ scale: 1.05 }}
          >
            {item}
          </motion.div>
        ))}
      </div>
      
      {selectedItem && (
        <motion.div
          layoutId={selectedItem}
          className="bg-green-500 text-white p-8 rounded text-center"
          onClick={() => setSelectedItem(null)}
        >
          Selected: {selectedItem}
          <p className="mt-2 text-sm">Click to deselect</p>
        </motion.div>
      )}
    </div>
  );
}

Layout Utilities

Utility hooks and functions for advanced layout animation control.

/**
 * Perform instant layout transitions without animation
 * @param callback - Function to execute during instant transition
 */
function useInstantLayoutTransition(): (callback: () => void) => void;

/**
 * Reset projection calculations for layout animations
 */
function useResetProjection(): () => void;

/**
 * Enable instant transitions temporarily
 * @returns Function to trigger instant transition
 */
function useInstantTransition(): (callback: () => void) => void;

/**
 * Disable instant transitions for a callback
 * @param callback - Function to execute with instant transitions disabled
 */
function disableInstantTransitions(callback: () => void): void;

Usage Examples:

import { 
  motion, 
  useInstantLayoutTransition, 
  useResetProjection 
} from "framer-motion";
import { useState } from "react";

function LayoutUtilitiesExample() {
  const [items, setItems] = useState([1, 2, 3, 4, 5]);
  const instantTransition = useInstantLayoutTransition();
  const resetProjection = useResetProjection();
  
  const shuffleItems = () => {
    // Shuffle without animation
    instantTransition(() => {
      setItems([...items].sort(() => Math.random() - 0.5));
    });
  };
  
  const resetLayout = () => {
    // Reset layout calculations
    resetProjection();
  };
  
  return (
    <div>
      <div className="mb-4 space-x-2">
        <button 
          onClick={shuffleItems}
          className="px-4 py-2 bg-blue-500 text-white rounded"
        >
          Instant Shuffle
        </button>
        <button 
          onClick={resetLayout}
          className="px-4 py-2 bg-green-500 text-white rounded"
        >
          Reset Layout
        </button>
      </div>
      
      <div className="grid grid-cols-5 gap-2">
        {items.map(item => (
          <motion.div
            key={item}
            layout
            className="bg-purple-500 text-white p-4 rounded text-center"
            transition={{ type: "spring", damping: 25, stiffness: 300 }}
          >
            {item}
          </motion.div>
        ))}
      </div>
    </div>
  );
}

Advanced Presence Patterns

Staggered Exit Animations:

<AnimatePresence>
  {items.map((item, index) => (
    <motion.div
      key={item.id}
      exit={{ 
        opacity: 0, 
        x: -100,
        transition: { delay: index * 0.1 }
      }}
    >
      {item.content}
    </motion.div>
  ))}
</AnimatePresence>

Nested Presence:

<AnimatePresence>
  {isVisible && (
    <motion.div exit={{ opacity: 0 }}>
      <AnimatePresence mode="wait">
        <motion.div key={currentTab} exit={{ x: -100 }}>
          Tab content
        </motion.div>
      </AnimatePresence>
    </motion.div>
  )}
</AnimatePresence>

Custom Exit Timing:

function CustomExitTiming() {
  const [isPresent, safeToRemove] = usePresence();
  
  useEffect(() => {
    if (!isPresent) {
      // Custom async exit logic
      performAsyncCleanup().then(() => {
        safeToRemove();
      });
    }
  }, [isPresent, safeToRemove]);
  
  // Component implementation...
}