Skip to content

useTransition

Provides the ability to apply CSS transitions to a floating element, including correct handling of “placement-aware” transitions.

There are two different hooks you can use:

  • useTransitionStyles — a high level wrapper around useTransitionStatus that returns computed styles for you. This is simpler and can handle the majority of use cases.
  • useTransitionStatus — a low level hook that returns a status string to compute the styles yourself.

useTransitionStyles

This hook provides computed inline styles that you can spread into the stylestyle prop for a floating element.

This hook is a standalone hook that accepts the contextcontext object returned from useFloating()useFloating():

function App() {
  const {context} = useFloating();
  const {isMounted, styles} = useTransitionStyles(context);
 
  return (
    isMounted && (
      <div
        style={{
          // Transition styles
          ...styles,
        }}
      >
        Tooltip
      </div>
    )
  );
}
function App() {
  const {context} = useFloating();
  const {isMounted, styles} = useTransitionStyles(context);
 
  return (
    isMounted && (
      <div
        style={{
          // Transition styles
          ...styles,
        }}
      >
        Tooltip
      </div>
    )
  );
}
  • isMountedisMounted is a boolean that determines whether or not the floating element is mounted on the screen, which allows for unmounting animations to play. This replaces the openopen state variable.
  • stylesstyles is an object of inline transition styles (React.CSSPropertiesReact.CSSProperties).

The hook defaults to a basic opacity fade transition with a duration of 250ms.

useTransitionStyles Props

interface UseTransitionStylesProps {
  duration?: number | Partial<{open: number; close: number}>;
  initial?: CSSStylesProperty;
  open?: CSSStylesProperty;
  close?: CSSStylesProperty;
  common?: CSSStylesProperty;
}
interface UseTransitionStylesProps {
  duration?: number | Partial<{open: number; close: number}>;
  initial?: CSSStylesProperty;
  open?: CSSStylesProperty;
  close?: CSSStylesProperty;
  common?: CSSStylesProperty;
}

duration

default: 250250

Specifies the length of the transition in ms.

const {isMounted, styles} = useTransitionStyles(context, {
  // Configure both open and close durations:
  duration: 200,
 
  // Or, configure open and close durations separately:
  duration: {
    open: 200,
    close: 100,
  },
});
const {isMounted, styles} = useTransitionStyles(context, {
  // Configure both open and close durations:
  duration: 200,
 
  // Or, configure open and close durations separately:
  duration: {
    open: 200,
    close: 100,
  },
});

initial

default: {opacity: 0}

Specifies the initial styles of the floating element:

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

This will implicitly transition to empty strings for each value (their defaults of opacity: 1opacity: 1 and transform: scale(1)transform: scale(1)).

For placement-aware styles, you can define a function:

const {isMounted, styles} = useTransitionStyles(context, {
  initial: ({side}) => ({
    transform:
      side === 'top' || side === 'bottom'
        ? 'scaleY(0.5)'
        : 'scaleX(0.5)',
  }),
});
const {isMounted, styles} = useTransitionStyles(context, {
  initial: ({side}) => ({
    transform:
      side === 'top' || side === 'bottom'
        ? 'scaleY(0.5)'
        : 'scaleX(0.5)',
  }),
});

The function takes the following parameters:

interface Params {
  side: Side;
  placement: Placement;
}
interface Params {
  side: Side;
  placement: Placement;
}
  • sideside represents a physical side — with the vast majority of transitions, you’ll likely only need to be concerned about the side.
  • placementplacement represents the whole placement string in cases where you want to also change the transition based on the alignment.

close

default: undefinedundefined

By default, transitions are symmetric, but if you want an asymmetric transition, then you can specify close styles:

const {isMounted, styles} = useTransitionStyles(context, {
  close: {
    opacity: 0,
    transform: 'scale(2)',
  },
  // or, for side-aware styles:
  close: ({side}) => ({
    opacity: 0,
    transform:
      side === 'top' || side === 'bottom'
        ? 'scaleY(2)'
        : 'scaleX(2)',
  }),
});
const {isMounted, styles} = useTransitionStyles(context, {
  close: {
    opacity: 0,
    transform: 'scale(2)',
  },
  // or, for side-aware styles:
  close: ({side}) => ({
    opacity: 0,
    transform:
      side === 'top' || side === 'bottom'
        ? 'scaleY(2)'
        : 'scaleX(2)',
  }),
});

open

default: undefinedundefined

If you want the open state to transition to a non-default style, open styles can be specified:

const {isMounted, styles} = useTransitionStyles(context, {
  open: {
    transform: 'scale(1.1)',
  },
  // or, for side-aware styles:
  open: ({side}) => ({
    transform:
      side === 'top' || side === 'bottom'
        ? 'scaleY(1.1)'
        : 'scaleX(1.1)',
  }),
});
const {isMounted, styles} = useTransitionStyles(context, {
  open: {
    transform: 'scale(1.1)',
  },
  // or, for side-aware styles:
  open: ({side}) => ({
    transform:
      side === 'top' || side === 'bottom'
        ? 'scaleY(1.1)'
        : 'scaleX(1.1)',
  }),
});

common

default: undefinedundefined

If a style is common across all states, then this option can be specified. For instance, a transform origin should be shared:

const {isMounted, styles} = useTransitionStyles(context, {
  common: {
    transformOrigin: 'bottom',
  },
  // or, for side-aware styles:
  common: ({side}) => ({
    transformOrigin: {
      top: 'bottom',
      bottom: 'top',
      left: 'right',
      right: 'left',
    }[side],
  }),
});
const {isMounted, styles} = useTransitionStyles(context, {
  common: {
    transformOrigin: 'bottom',
  },
  // or, for side-aware styles:
  common: ({side}) => ({
    transformOrigin: {
      top: 'bottom',
      bottom: 'top',
      left: 'right',
      right: 'left',
    }[side],
  }),
});

Scale transforms

When animating the floating element’s scale, it looks best if the floating element’s transformOrigintransformOrigin is at the tip of the arrow. The arrow middleware provides data to achieve this.

View on CodeSandbox

useTransitionStatus

This hook provides a statusstatus string that determines if the floating element is in one of four states:

type Status = 'unmounted' | 'initial' | 'open' | 'close';
type Status = 'unmounted' | 'initial' | 'open' | 'close';

The floating element cycles through the order as seen below:

unmounted -> initial -> open -> close -> unmounted
unmounted -> initial -> open -> close -> unmounted

This hook is a standalone hook that accepts the contextcontext object returned from useFloating()useFloating():

function App() {
  const {context, placement} = useFloating();
  const {isMounted, status} = useTransitionStatus(context);
 
  return (
    isMounted && (
      <div id="floating" data-status={status}>
        Tooltip
      </div>
    )
  );
}
function App() {
  const {context, placement} = useFloating();
  const {isMounted, status} = useTransitionStatus(context);
 
  return (
    isMounted && (
      <div id="floating" data-status={status}>
        Tooltip
      </div>
    )
  );
}
  • isMountedisMounted is a boolean that determines whether or not the floating element is mounted on the screen, which allows for unmounting animations to play. This replaces the openopen state variable.
  • statusstatus is the status string (StatusStatus).

Above, we apply a data-statusdata-status attribute to the floating element. This can be used to target the transition status in our CSS.

To define an opacity fade CSS transition:

#floating {
  transition-property: opacity;
}
#floating[data-status='open'],
#floating[data-status='close'] {
  transition-duration: 250ms;
}
#floating[data-status='initial'],
#floating[data-status='close'] {
  opacity: 0;
}
#floating {
  transition-property: opacity;
}
#floating[data-status='open'],
#floating[data-status='close'] {
  transition-duration: 250ms;
}
#floating[data-status='initial'],
#floating[data-status='close'] {
  opacity: 0;
}
  • transition-property: opacitytransition-property: opacity is applied to all states. This allows the transition to be interruptible.

The statuses map to the following:

  • 'unmounted''unmounted' indicates the element is unmounted from the screen. No transitions or styles need to be applied in this state.
  • 'initial''initial' indicates the initial styles of the floating element as soon as it has been inserted into the DOM.
  • 'open''open' indicates the floating element is in the open state (1 frame after insertion) and begins transitioning in.
  • 'close''close' indicates the floating element is in the close state and begins transitioning out.

The transition duration must match the durationduration option passed to the hook.

Asymmetric transitions

#floating {
  transition-property: opacity, transform;
}
#floating[data-status='initial'] {
  opacity: 0;
  transform: scale(0);
}
#floating[data-status='open'] {
  opacity: 1;
  transform: scale(1);
  transition-duration: 250ms;
}
#floating[data-status='close'] {
  opacity: 0;
  transform: scale(2);
  transition-duration: 250ms;
}
#floating {
  transition-property: opacity, transform;
}
#floating[data-status='initial'] {
  opacity: 0;
  transform: scale(0);
}
#floating[data-status='open'] {
  opacity: 1;
  transform: scale(1);
  transition-duration: 250ms;
}
#floating[data-status='close'] {
  opacity: 0;
  transform: scale(2);
  transition-duration: 250ms;
}

Placement-aware transitions

const {context, placement} = useFloating();
const {isMounted, status} = useTransitionStatus(context);
 
return (
  isMounted && (
    <div
      id="floating"
      data-placement={placement}
      data-status={status}
    >
      Tooltip
    </div>
  )
);
const {context, placement} = useFloating();
const {isMounted, status} = useTransitionStatus(context);
 
return (
  isMounted && (
    <div
      id="floating"
      data-placement={placement}
      data-status={status}
    >
      Tooltip
    </div>
  )
);
#floating {
  transition-property: opacity, transform;
}
#floating[data-status='open'],
#floating[data-status='close'] {
  transition-duration: 250ms;
}
#floating[data-status='initial'],
#floating[data-status='close'] {
  opacity: 0;
}
#floating[data-status='initial'][data-placement^='top'],
#floating[data-status='close'][data-placement^='top'] {
  transform: translateY(5px);
}
#floating[data-status='initial'][data-placement^='bottom'],
#floating[data-status='close'][data-placement^='bottom'] {
  transform: translateY(-5px);
}
#floating[data-status='initial'][data-placement^='left'],
#floating[data-status='close'][data-placement^='left'] {
  transform: translateX(5px);
}
#floating[data-status='initial'][data-placement^='right'],
#floating[data-status='close'][data-placement^='right'] {
  transform: translateX(-5px);
}
#floating {
  transition-property: opacity, transform;
}
#floating[data-status='open'],
#floating[data-status='close'] {
  transition-duration: 250ms;
}
#floating[data-status='initial'],
#floating[data-status='close'] {
  opacity: 0;
}
#floating[data-status='initial'][data-placement^='top'],
#floating[data-status='close'][data-placement^='top'] {
  transform: translateY(5px);
}
#floating[data-status='initial'][data-placement^='bottom'],
#floating[data-status='close'][data-placement^='bottom'] {
  transform: translateY(-5px);
}
#floating[data-status='initial'][data-placement^='left'],
#floating[data-status='close'][data-placement^='left'] {
  transform: translateX(5px);
}
#floating[data-status='initial'][data-placement^='right'],
#floating[data-status='close'][data-placement^='right'] {
  transform: translateX(-5px);
}

useTransitionStatus Props

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

duration

default: 250250

Specifies the length of the transition in ms.

const {isMounted, status} = useTransitionStatus(context, {
  // Configure both open and close durations:
  duration: 200,
 
  // Or, configure open and close durations separately:
  duration: {
    open: 200,
    close: 100,
  },
});
const {isMounted, status} = useTransitionStatus(context, {
  // Configure both open and close durations:
  duration: 200,
 
  // Or, configure open and close durations separately:
  duration: {
    open: 200,
    close: 100,
  },
});