Minimal React Transition Hook (
I often find myself in the need of a very basic open/close transition in React for components like dialogs, side sheets, or dropdowns. The goto solution for a while seemed to be
React Transition Group, which I never understood how to use properly. An excellent solution for animations is
react-spring, but I'd consider it an overkill for a basic CSS powered open/close transition (but it's great for animations in something like an image viewer).
This is why I’ve ended up writing my own hook:
It is used like this:
; ; if !isVisible return <div> ... </div>
Isn’t that easy to understand and reason about just from reading the usage? Here is a complete example using the hook: Demo, Source.
The hook itself is just ~50 lines long (excluding the typings and doc comments) and so simple, it easily fits into this post:
This is exactly what I wanted. Simple, small, no fancy magic - just using basic
Let’s dissect its inner workings top-down.
Typically, when a component is closed, it is just not rendered anymore. This does not work well with a close transition, because it is necessary to keep the component in the DOM until the close transition finished. This is why the hook takes the desired state (visible or not;
isOpen in the usage example above, and
desiredState in the code above) as an input, and returns whether you should still render the component or not (
isVisible in the example usage above, and
currentState in the code below).
When the hook is first used, it determines what the initial state is and also provides an option to skip the enter transition if it starts being visible right away. It also sets its initial transition state (
transition), which is either
entered, if the component is already visible, or
null if it is not.
When either the current or desired states change, it updates the active transition accordingly:
- Not visible right now (
currentState === false), but should be shown (
desiredState === true): Render the component and set
entering(usually something like 0% opacity, or moved outside of the screen) as the active transition.
- Visible right now (
currentState === true), but should not be shown anymore (
desiredState === false): Set active transition to
exiting(often the same as
entering, so something like 0% opacity, …) and keep the component for now.
For the open transition, the transition cannot be set to
entered right away. It is always necessary to render the component with
entering first so that there is a starting point for the transition to be based on. Example:
- Render with
0%opacity, and once that is reflected in the DOM,
- Set the opacity to
100%for the transition to start.
This is what the second
useEffect is for.
useEffect cannot be integrated into the first one, because there needs to be a DOM update before the state changes of the second
useEffect are applied. By separating them, the state changes from the first effect are reflected in the DOM before the whole hook is called again and applies the changes from the second effect. The second effect is thereby simply reacting on the changes from the first
useEffect and kicks of the transition by moving from
entered, or from
It is necessary to know when the close transition finished so that the component can be removed from the DOM. This is achieved by a simple
onTransitionEnd event handler. Once fired, it sets the current state to
false and resets the transition to
That’s all there is to it.
Finally, as a small bonus, an advanced example of how to use it for a Radix UI Dialog-based side sheet:
; ; ; ; ;