or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

core-positioning.mdfocus-management.mdindex.mdinteraction-hooks.mdlayout-components.mdlist-navigation.mdpositioning-middleware.mdtransitions.mdtree-context.md
tile.json

transitions.mddocs/

Transitions

CSS transition support hooks for smooth animations when floating elements enter and exit the DOM, with status tracking and style management.

Capabilities

useTransitionStatus Hook

Provides status string for CSS transitions to enable smooth animations during floating element lifecycle.

/**
 * Provides status string for CSS transitions
 * @param context - Floating UI context
 * @param props - Transition status configuration
 * @returns Transition status and mounting state
 */
function useTransitionStatus(
  context: FloatingContext,
  props?: UseTransitionStatusProps
): {
  isMounted: boolean;
  status: 'unmounted' | 'initial' | 'open' | 'close';
};

interface UseTransitionStatusProps {
  duration?: number | { open?: number; close?: number };
}

Usage Examples:

import { 
  useTransitionStatus, 
  useFloating, 
  useClick, 
  useInteractions 
} from '@floating-ui/react';
import { useState } from 'react';

// Basic transition status
function TransitionTooltip() {
  const [isOpen, setIsOpen] = useState(false);

  const { refs, floatingStyles, context } = useFloating({
    open: isOpen,
    onOpenChange: setIsOpen,
  });

  const click = useClick(context);
  const { getReferenceProps, getFloatingProps } = useInteractions([click]);

  const { isMounted, status } = useTransitionStatus(context, {
    duration: 200,
  });

  return (
    <>
      <button ref={refs.setReference} {...getReferenceProps()}>
        Toggle
      </button>
      {isMounted && (
        <div
          ref={refs.setFloating}
          style={{
            ...floatingStyles,
            background: 'black',
            color: 'white',
            padding: '8px',
            borderRadius: '4px',
            transition: 'opacity 200ms',
            opacity: status === 'open' ? 1 : 0,
          }}
          {...getFloatingProps()}
        >
          Transitioning tooltip
        </div>
      )}
    </>
  );
}

// Different durations for open/close
function AsymmetricTransition() {
  const [isOpen, setIsOpen] = useState(false);

  const { refs, floatingStyles, context } = useFloating({
    open: isOpen,
    onOpenChange: setIsOpen,
  });

  const hover = useHover(context);
  const { getReferenceProps, getFloatingProps } = useInteractions([hover]);

  const { isMounted, status } = useTransitionStatus(context, {
    duration: { open: 150, close: 300 },
  });

  return (
    <>
      <button ref={refs.setReference} {...getReferenceProps()}>
        Hover me
      </button>
      {isMounted && (
        <div
          ref={refs.setFloating}
          style={{
            ...floatingStyles,
            background: 'lightblue',
            padding: '12px',
            borderRadius: '6px',
            transition: `all ${status === 'close' ? '300ms' : '150ms'} ease-in-out`,
            transform: status === 'open' ? 'scale(1)' : 'scale(0.8)',
            opacity: status === 'open' ? 1 : 0,
          }}
          {...getFloatingProps()}
        >
          Asymmetric transition
        </div>
      )}
    </>
  );
}

useTransitionStyles Hook

Provides pre-configured styles for common CSS transitions with built-in timing and easing.

/**
 * Provides styles for CSS transitions with built-in configurations
 * @param context - Floating UI context
 * @param props - Transition styles configuration
 * @returns Transition styles and mounting state
 */
function useTransitionStyles(
  context: FloatingContext,
  props?: UseTransitionStylesProps
): {
  isMounted: boolean;
  styles: Record<string, React.CSSProperties>;
};

interface UseTransitionStylesProps {
  duration?: number | { open?: number; close?: number };
  initial?: React.CSSProperties | ((side: string) => React.CSSProperties);
  open?: React.CSSProperties | ((side: string) => React.CSSProperties);
  close?: React.CSSProperties | ((side: string) => React.CSSProperties);
  common?: React.CSSProperties | ((side: string) => React.CSSProperties);
  transform?: boolean;
}

Usage Examples:

import { useTransitionStyles } from '@floating-ui/react';

// Basic transition styles
function StyledTransition() {
  const [isOpen, setIsOpen] = useState(false);

  const { refs, floatingStyles, context } = useFloating({
    open: isOpen,
    onOpenChange: setIsOpen,
  });

  const click = useClick(context);
  const { getReferenceProps, getFloatingProps } = useInteractions([click]);

  const { isMounted, styles } = useTransitionStyles(context, {
    duration: 250,
    initial: {
      opacity: 0,
      transform: 'scale(0.8)',
    },
    open: {
      opacity: 1,
      transform: 'scale(1)',
    },
    close: {
      opacity: 0,
      transform: 'scale(0.8)',
    },
  });

  return (
    <>
      <button ref={refs.setReference} {...getReferenceProps()}>
        Styled Transition
      </button>
      {isMounted && (
        <div
          ref={refs.setFloating}
          style={{
            ...floatingStyles,
            ...styles,
            background: 'white',
            border: '1px solid gray',
            padding: '16px',
            borderRadius: '8px',
            boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
          }}
          {...getFloatingProps()}
        >
          Styled transition tooltip
        </div>
      )}
    </>
  );
}

// Placement-based transitions
function PlacementBasedTransition() {
  const [isOpen, setIsOpen] = useState(false);

  const { refs, floatingStyles, context } = useFloating({
    open: isOpen,
    onOpenChange: setIsOpen,
    placement: 'top',
  });

  const hover = useHover(context);
  const { getReferenceProps, getFloatingProps } = useInteractions([hover]);

  const { isMounted, styles } = useTransitionStyles(context, {
    duration: 200,
    // Function receives placement side for directional animations
    initial: (side) => ({
      opacity: 0,
      transform: `translateY(${side === 'top' ? '10px' : '-10px'})`,
    }),
    open: {
      opacity: 1,
      transform: 'translateY(0)',
    },
    close: (side) => ({
      opacity: 0,
      transform: `translateY(${side === 'top' ? '10px' : '-10px'})`,
    }),
    common: {
      transition: 'opacity 200ms ease, transform 200ms ease',
    },
  });

  return (
    <>
      <button ref={refs.setReference} {...getReferenceProps()}>
        Directional Animation
      </button>
      {isMounted && (
        <div
          ref={refs.setFloating}
          style={{
            ...floatingStyles,
            ...styles,
            background: 'purple',
            color: 'white',
            padding: '8px 12px',
            borderRadius: '4px',
          }}
          {...getFloatingProps()}
        >
          Slides in from placement direction
        </div>
      )}
    </>
  );
}

// Complex multi-stage transition
function MultiStageTransition() {
  const [isOpen, setIsOpen] = useState(false);

  const { refs, floatingStyles, context } = useFloating({
    open: isOpen,
    onOpenChange: setIsOpen,
  });

  const click = useClick(context);
  const { getReferenceProps, getFloatingProps } = useInteractions([click]);

  const { isMounted, styles } = useTransitionStyles(context, {
    duration: { open: 300, close: 200 },
    initial: {
      opacity: 0,
      transform: 'scale(0.5) rotate(-10deg)',
      filter: 'blur(4px)',
    },
    open: {
      opacity: 1,
      transform: 'scale(1) rotate(0deg)',
      filter: 'blur(0px)',
    },
    close: {
      opacity: 0,
      transform: 'scale(0.8) rotate(5deg)',
      filter: 'blur(2px)',
    },
    common: {
      transformOrigin: 'center',
    },
  });

  return (
    <>
      <button ref={refs.setReference} {...getReferenceProps()}>
        Complex Transition
      </button>
      {isMounted && (
        <div
          ref={refs.setFloating}
          style={{
            ...floatingStyles,
            ...styles,
            background: 'linear-gradient(45deg, #ff6b6b, #4ecdc4)',
            color: 'white',
            padding: '20px',
            borderRadius: '12px',
            fontWeight: 'bold',
          }}
          {...getFloatingProps()}
        >
          Complex multi-property transition
        </div>
      )}
    </>
  );
}

Transition Status Values

Status Lifecycle

The transition status follows this lifecycle:

  1. 'unmounted': Element is not in DOM
  2. 'initial': Element just mounted, initial styles applied
  3. 'open': Element is opening/open, open styles applied
  4. 'close': Element is closing, close styles applied
  5. 'unmounted': Element removed from DOM after close transition

Duration Timing

  • Number: Same duration for open and close
  • Object: Different durations for open and close phases
  • Default: 250ms if not specified

Animation Best Practices

Performance Optimization

// Use transform and opacity for best performance
const { styles } = useTransitionStyles(context, {
  initial: { opacity: 0, transform: 'scale(0.9)' },
  open: { opacity: 1, transform: 'scale(1)' },
  // Avoid animating properties that cause layout recalculation
});

Accessibility Considerations

// Respect user's motion preferences
const { styles } = useTransitionStyles(context, {
  duration: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : 200,
  // Shorter or no transitions for users who prefer reduced motion
});

Complex Sequences

// Use CSS keyframes for complex animations
const { styles } = useTransitionStyles(context, {
  common: {
    animation: 'complex-entrance 400ms ease-out',
  },
});

Integration with FloatingPortal

function PortalWithTransitions() {
  const { isMounted, styles } = useTransitionStyles(context);

  return (
    <>
      {isMounted && (
        <FloatingPortal>
          <div style={{ ...floatingStyles, ...styles }}>
            Portaled content with transitions
          </div>
        </FloatingPortal>
      )}
    </>
  );
}

Common Transition Patterns

Fade In/Out

{
  initial: { opacity: 0 },
  open: { opacity: 1 },
  close: { opacity: 0 },
}

Scale Animation

{
  initial: { opacity: 0, transform: 'scale(0.8)' },
  open: { opacity: 1, transform: 'scale(1)' },
  close: { opacity: 0, transform: 'scale(0.8)' },
}

Slide Animation

{
  initial: (side) => ({ 
    opacity: 0, 
    transform: `translateY(${side === 'top' ? '10px' : '-10px'})` 
  }),
  open: { opacity: 1, transform: 'translateY(0)' },
  close: (side) => ({ 
    opacity: 0, 
    transform: `translateY(${side === 'top' ? '10px' : '-10px'})` 
  }),
}