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

Usually a transition should open at the default property, for example opacity: 1opacity: 1 or no transform. 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],
  }),
});

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