CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-react-aria--utils

Essential utility functions and React hooks for building accessible React Aria UI components

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

animation-and-transitions.mddocs/

Animation & Transitions

Enter/exit animation management with CSS integration and transition coordination for React components.

Capabilities

Enter Animations

Hook for managing enter animations when components mount or become visible.

/**
 * Manages enter animations for components
 * @param ref - RefObject to animated element
 * @param isReady - Whether animation should start (default: true)
 * @returns Boolean indicating if element is in entering state
 */
function useEnterAnimation(ref: RefObject<HTMLElement>, isReady?: boolean): boolean;

Usage Examples:

import { useEnterAnimation } from "@react-aria/utils";

function FadeInComponent({ children, show = true }) {
  const elementRef = useRef<HTMLDivElement>(null);
  const isEntering = useEnterAnimation(elementRef, show);
  
  return (
    <div 
      ref={elementRef}
      className={`fade-component ${isEntering ? 'entering' : 'entered'}`}
      style={{
        opacity: isEntering ? 0 : 1,
        transition: 'opacity 300ms ease-in-out'
      }}
    >
      {children}
    </div>
  );
}

// CSS-based keyframe animation
function SlideInComponent({ children, show = true }) {
  const elementRef = useRef<HTMLDivElement>(null);
  const isEntering = useEnterAnimation(elementRef, show);
  
  return (
    <div 
      ref={elementRef}
      className={`slide-component ${isEntering ? 'slide-enter' : 'slide-entered'}`}
    >
      {children}
    </div>
  );
}

// CSS for slideInComponent
/*
.slide-component {
  transform: translateX(-100%);
  transition: transform 400ms cubic-bezier(0.4, 0, 0.2, 1);
}

.slide-component.slide-entered {
  transform: translateX(0);
}

.slide-component.slide-enter {
  animation: slideIn 400ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
}

@keyframes slideIn {
  from { transform: translateX(-100%); }
  to { transform: translateX(0); }
}
*/

Exit Animations

Hook for managing exit animations before components unmount or become hidden.

/**
 * Manages exit animations before unmounting
 * @param ref - RefObject to animated element
 * @param isOpen - Whether component should be open/visible
 * @returns Boolean indicating if element is in exiting state
 */
function useExitAnimation(ref: RefObject<HTMLElement>, isOpen: boolean): boolean;

Usage Examples:

import { useExitAnimation } from "@react-aria/utils";

function Modal({ isOpen, onClose, children }) {
  const modalRef = useRef<HTMLDivElement>(null);
  const isExiting = useExitAnimation(modalRef, isOpen);
  
  // Don't render if closed and not exiting
  if (!isOpen && !isExiting) return null;
  
  return (
    <div 
      className={`modal-backdrop ${isExiting ? 'exiting' : ''}`}
      style={{
        opacity: isExiting ? 0 : 1,
        transition: 'opacity 250ms ease-out'
      }}
    >
      <div 
        ref={modalRef}
        className={`modal-content ${isExiting ? 'modal-exit' : 'modal-enter'}`}
        style={{
          transform: isExiting ? 'scale(0.95) translateY(-10px)' : 'scale(1) translateY(0)',
          transition: 'transform 250ms ease-out'
        }}
      >
        <button onClick={onClose}>Close</button>
        {children}
      </div>
    </div>
  );
}

// Notification with auto-dismiss animation
function Notification({ message, onDismiss, autoClose = 5000 }) {
  const [isOpen, setIsOpen] = useState(true);
  const notificationRef = useRef<HTMLDivElement>(null);
  const isExiting = useExitAnimation(notificationRef, isOpen);
  
  useEffect(() => {
    const timer = setTimeout(() => setIsOpen(false), autoClose);
    return () => clearTimeout(timer);
  }, [autoClose]);
  
  useEffect(() => {
    if (!isOpen && !isExiting) {
      onDismiss();
    }
  }, [isOpen, isExiting, onDismiss]);
  
  if (!isOpen && !isExiting) return null;
  
  return (
    <div 
      ref={notificationRef}
      className={`notification ${isExiting ? 'notification-exit' : 'notification-enter'}`}
      style={{
        transform: isExiting ? 'translateX(100%)' : 'translateX(0)',
        opacity: isExiting ? 0 : 1,
        transition: 'transform 300ms ease-in-out, opacity 300ms ease-in-out'
      }}
    >
      {message}
      <button onClick={() => setIsOpen(false)}>×</button>
    </div>
  );
}

Transition Coordination

Function for executing callbacks after all CSS transitions complete.

/**
 * Executes callback after all CSS transitions complete
 * @param fn - Function to execute after transitions
 */
function runAfterTransition(fn: () => void): void;

Usage Examples:

import { runAfterTransition } from "@react-aria/utils";

function AnimatedList({ items, onAnimationComplete }) {
  const [isAnimating, setIsAnimating] = useState(false);
  
  const animateList = () => {
    setIsAnimating(true);
    
    // Start animation by adding CSS class
    const listElement = document.querySelector('.animated-list');
    listElement?.classList.add('animate');
    
    // Wait for all transitions to complete
    runAfterTransition(() => {
      setIsAnimating(false);
      listElement?.classList.remove('animate');
      onAnimationComplete();
    });
  };
  
  return (
    <div>
      <button onClick={animateList} disabled={isAnimating}>
        {isAnimating ? 'Animating...' : 'Animate List'}
      </button>
      <ul className="animated-list">
        {items.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

// Complex animation sequence
function SequentialAnimation({ steps, onComplete }) {
  const [currentStep, setCurrentStep] = useState(0);
  const elementRefs = useRef<(HTMLDivElement | null)[]>([]);
  
  const animateStep = (stepIndex: number) => {
    const element = elementRefs.current[stepIndex];
    if (!element) return;
    
    // Add animation class
    element.classList.add('step-animate');
    
    // Wait for transition to complete
    runAfterTransition(() => {
      element.classList.remove('step-animate');
      
      if (stepIndex < steps.length - 1) {
        setCurrentStep(stepIndex + 1);
        // Animate next step
        setTimeout(() => animateStep(stepIndex + 1), 100);
      } else {
        onComplete();
      }
    });
  };
  
  useEffect(() => {
    if (currentStep < steps.length) {
      animateStep(currentStep);
    }
  }, [currentStep, steps.length]);
  
  return (
    <div>
      {steps.map((step, index) => (
        <div
          key={index}
          ref={el => elementRefs.current[index] = el}
          className={`step ${index <= currentStep ? 'active' : ''}`}
        >
          {step.content}
        </div>
      ))}
    </div>
  );
}

Advanced Animation Patterns

Complex animation scenarios combining enter/exit animations with coordination:

import { useEnterAnimation, useExitAnimation, runAfterTransition } from "@react-aria/utils";

function StaggeredList({ items, isVisible }) {
  const listRef = useRef<HTMLUListElement>(null);
  const isEntering = useEnterAnimation(listRef, isVisible);
  const isExiting = useExitAnimation(listRef, isVisible);
  
  useEffect(() => {
    if (!listRef.current) return;
    
    const listItems = Array.from(listRef.current.children) as HTMLElement[];
    
    if (isEntering) {
      // Stagger enter animations
      listItems.forEach((item, index) => {
        item.style.transitionDelay = `${index * 50}ms`;
        item.classList.add('item-enter');
      });
    } else if (isExiting) {
      // Reverse stagger for exit
      listItems.forEach((item, index) => {
        item.style.transitionDelay = `${(listItems.length - index - 1) * 30}ms`;
        item.classList.add('item-exit');
      });
    }
  }, [isEntering, isExiting]);
  
  if (!isVisible && !isExiting) return null;
  
  return (
    <ul ref={listRef} className="staggered-list">
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

// Shared element transition
function SharedElementTransition({ fromElement, toElement, onTransitionEnd }) {
  useEffect(() => {
    if (!fromElement || !toElement) return;
    
    // Get positions
    const fromRect = fromElement.getBoundingClientRect();
    const toRect = toElement.getBoundingClientRect();
    
    // Create shared element
    const sharedElement = fromElement.cloneNode(true) as HTMLElement;
    sharedElement.style.position = 'fixed';
    sharedElement.style.top = `${fromRect.top}px`;
    sharedElement.style.left = `${fromRect.left}px`;
    sharedElement.style.width = `${fromRect.width}px`;
    sharedElement.style.height = `${fromRect.height}px`;
    sharedElement.style.zIndex = '1000';
    sharedElement.style.pointerEvents = 'none';
    
    document.body.appendChild(sharedElement);
    
    // Hide original elements
    fromElement.style.opacity = '0';
    toElement.style.opacity = '0';
    
    // Animate to target position
    requestAnimationFrame(() => {
      sharedElement.style.transition = 'all 400ms cubic-bezier(0.4, 0, 0.2, 1)';
      sharedElement.style.top = `${toRect.top}px`;
      sharedElement.style.left = `${toRect.left}px`;
      sharedElement.style.width = `${toRect.width}px`;
      sharedElement.style.height = `${toRect.height}px`;
    });
    
    runAfterTransition(() => {
      // Clean up
      document.body.removeChild(sharedElement);
      fromElement.style.opacity = '';
      toElement.style.opacity = '';
      onTransitionEnd();
    });
  }, [fromElement, toElement, onTransitionEnd]);
  
  return null;
}

// Page transition component
function PageTransition({ currentPage, nextPage, direction = 'forward' }) {
  const containerRef = useRef<HTMLDivElement>(null);
  const [isTransitioning, setIsTransitioning] = useState(false);
  
  const transitionToPage = (newPage: React.ReactNode) => {
    if (!containerRef.current || isTransitioning) return;
    
    setIsTransitioning(true);
    const container = containerRef.current;
    
    // Add transition classes
    container.classList.add('page-transition');
    container.classList.add(direction === 'forward' ? 'forward' : 'backward');
    
    runAfterTransition(() => {
      // Update page content
      setCurrentPage(newPage);
      
      // Remove transition classes
      container.classList.remove('page-transition', 'forward', 'backward');
      setIsTransitioning(false);
    });
  };
  
  return (
    <div ref={containerRef} className="page-container">
      {currentPage}
    </div>
  );
}

CSS Integration Examples

CSS classes that work well with these animation hooks:

/* Enter animation classes */
.fade-component {
  transition: opacity 300ms ease-in-out;
}

.fade-component.entering {
  opacity: 0;
}

.fade-component.entered {
  opacity: 1;
}

/* Exit animation classes */
.modal-backdrop {
  transition: opacity 250ms ease-out;
}

.modal-backdrop.exiting {
  opacity: 0;
}

.modal-content {
  transition: transform 250ms ease-out;
}

.modal-content.modal-exit {
  transform: scale(0.95) translateY(-10px);
}

/* Staggered list animations */
.staggered-list li {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 300ms ease-out, transform 300ms ease-out;
}

.staggered-list li.item-enter {
  opacity: 1;
  transform: translateY(0);
}

.staggered-list li.item-exit {
  opacity: 0;
  transform: translateY(-10px);
}

/* Page transitions */
.page-container.page-transition.forward {
  transform: translateX(-100%);
}

.page-container.page-transition.backward {
  transform: translateX(100%);
}

Install with Tessl CLI

npx tessl i tessl/npm-react-aria--utils

docs

animation-and-transitions.md

event-management.md

focus-and-accessibility.md

id-and-refs.md

index.md

links-and-navigation.md

miscellaneous-utilities.md

platform-detection.md

props-and-events.md

scrolling-and-layout.md

shadow-dom-support.md

state-and-effects.md

virtual-events-and-input.md

tile.json