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 arounduseTransitionStatus
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 style
style
prop for a floating element.
This hook is a standalone hook that accepts the
context
context
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>
)
);
}
isMounted
isMounted
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 theopen
open
state variable.styles
styles
is an object of inline transition styles (React.CSSProperties
React.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: 250
250
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: 1
opacity: 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;
}
side
side
represents a physical side — with the vast majority of transitions, you’ll likely only need to be concerned about the side.placement
placement
represents the whole placement string in cases where you want to also change the transition based on the alignment.
close
default: undefined
undefined
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: undefined
undefined
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: undefined
undefined
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 transformOrigin
transformOrigin
is at the tip
of the arrow. The arrow
middleware provides data to achieve
this.
useTransitionStatus
This hook provides a status
status
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
context
context
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>
)
);
}
isMounted
isMounted
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 theopen
open
state variable.status
status
is the status string (Status
Status
).
Above, we apply a data-status
data-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: opacity
transition-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 duration
duration
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: 250
250
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,
},
});