README
Retransition
A library that helps you create smooth transitions in your react app.
| English | Russian |
Features
- Automatic transition/animation end detection
- List animations with FLIP technique
- React strict mode compatible
- Small size (<2.8kb minified gzipped)
- TypeScript support out of the box
Motivation
I decided to create this package because often used react-transition-group
but I didn't like some things about it. For instance, you have to pass timeout
or provide your custom addEndListener
. Also, you don't have a "move" transition
for <TransitionGroup />
and it's not strict mode compatible.
Installation
npm:
npm i retransition
or yarn:
yarn add retransition
Note that this library uses hooks, so you need to have react
and react-dom
16.8.0 or higher.
Getting started
Basic example
import React, { useState } from "react";
import { Transition } from "retransition";
import "./index.css";
const App = () => {
const [visible, setVisible] = useState(false);
return (
<>
<button onClick={() => setVisible(v => !v)}>Toggle</button>
<Transition visible={visible} name="fade">
<div style={{ height: 200, width: 200, background: "black" }}></div>
</Transition>
</>
);
};
.fade-leave-to,
.fade-enter-from {
opacity: 0;
transform: translateX(300px);
}
.fade-enter-active,
.fade-leave-active {
transition: all 500ms ease;
}
.fade-leave-from,
.fade-enter-to {
opacity: 1;
transform: translateX(0);
}
Let's dive deeper into this example and understand what happens under the hood.
When you change the visible
prop of the <Transition>
component, it will show or hide the child element according to it. But it won't do it immediately. Entering and Leaving will be done in 3 steps:
- At the step first element would be inserted into the DOM if it's enter transition.
fade-(enter|leave)-from
andfade-(enter|leave)-active
classes would be added. - On the next frame (once the browser was able to rerender the screen and apply new styles) we remove
fade-(enter|leave)-from
class and addfade-(enter|leave)-to
class. If classes are written correctly, this should trigger a transition. - Once the transition is finished, we remove
fade-(enter|leave)-active
andfade-(enter|leave)-to
classes. The element would be removed from the DOM if it's aleave
transition.
CSS Animation
Although CSS transitions are more common and simpler, there are some situations where they don't give you enough control. That's why this library also supports CSS animations.
import React, { useState } from "react";
import { Transition } from "retransition";
import "./index.css";
const App = () => {
const [visible, setVisible] = useState(false);
return (
<>
<button onClick={() => setVisible(v => !v)}>Toggle</button>
<Transition visible={visible} name="fade-animation">
<div style={{ height: 200, width: 200, background: "black" }}></div>
</Transition>
</>
);
};
.fade-animation-enter-active {
animation: rotate-in 500ms ease;
}
.fade-animation-leave-active {
animation: rotate-in 500ms ease reverse;
}
@keyframes rotate-in {
0% {
transform: scale(0) rotate(360deg);
}
70% {
transform: scale(1.3) rotate(-108deg);
}
100% {
transform: scale(1) rotate(0);
}
}
Transition type
Sometimes you may want to have transition
and animation
on the same element. This library will pick the one that has a longer duration to detect the end of the transition. But this may not be the case in some situations. For instance, if you have animation on initial render and transition on mouseover. In these cases, you have to manually pass about which type <Transition>
component should care about.
<Transition type="animation" {...props}>
{/* ... */}
</Transition>
Unmounting
Be default your child element/component will be unmounted on leave. But if you want it to be hidden with display: none
, you could pass umount
prop as false
.
<Transition name="fade" visible={visible} unmount={false}>
<div>I'm always in the DOM</div>
</Transition>
Transitions on Initial Render
By default, your transition won't run on the initial render. If you want to change it, pass appear
prop.
<Transition name="fade" visible={visible} appear>
{/* ... */}
</Transition>
Note that you could just pass
appear
without any value and it'd be equivalent toappear={true}
This will result in having enter transition on initial render. But if you want custom transition (different classes and events) for the initial render you could pass customAppear
. In this case ${name}-appear-from
, ${name}-appear-active
and ${name}-appear-to
classes would be generated.
<Transition name="fade" visible={visible} appear customAppear>
{/* ... */}
</Transition>
Custom classes
If you don't want your classes to be generated from the name, you could pass your classes through props. They'll override generated classes.
<Transition
name="fade"
enterFromClass="class-1"
enterActiveClass="class-2"
enterToClass="class-3"
leaveFromClass="class-4"
leaveActiveClass="class-5"
leaveToClass="class-6"
appearFromClass="class-7"
appearActiveClass="class-8"
appearToClass="class-9"
>
{/* ... */}
</Transition>
JavaScript Events
<Transition>
provides javascript events for each phase of a transition.
<Transition
name="fade"
visible={visible}
onBeforeEnter={onBeforeEnter}
onEnter={onEnter}
onAfterEnter={onAfterEnter}
onBeforeLeave={onBeforeLeave}
onLeave={onLeave}
onAfterLeave={onAfterLeave}
// only works with `customAppear`
onBeforeAppear={onBeforeAppear}
onAppear={onAppear}
onAfterAppear={onAfterAppear}
>
{/* ... */}
</Transition>
List Transitions
We've been working with single elements so far. But what if you want to animate enter/leave of list items?. That's where you should use <TransitionGroup>
. It's like a state machine that detects an item addition/removal and passes correct props to <Transition>
component.
import React, { useState } from "react";
import { Transition, TransitionGroup } from "retransition";
import "./index.css";
const getRandomIndex = length => Math.floor(Math.random() * length);
const initialNumbers = new Array(10)
.fill(null)
.map((_, i) => ({ value: i, index: Math.random() }));
const App = () => {
const [numbers, setNumbers] = useState(initialNumbers);
const add = () => {
const index = getRandomIndex(numbers.length);
const newNum = {
value: numbers.length,
index: Math.random(),
};
const newValue = [
...numbers.slice(0, index),
newNum,
...numbers.slice(index),
];
setNumbers(newValue);
};
const remove = () => {
const index = getRandomIndex(numbers.length);
const newValue = numbers.filter((_, idx) => idx !== index);
setNumbers(newValue);
};
return (
<>
<button onClick={add} style={{ marginRight: 5 }}>
Add
</button>
<button onClick={remove} style={{ marginRight: 5 }}>
Remove
</button>
<div>
<TransitionGroup name="fade">
{numbers.map(n => (
<Transition key={n.index}>
<div style={{ padding: 5 }}>{n.value}</div>
</Transition>
))}
</TransitionGroup>
</div>
</>
);
};
.fade-leave-to,
.fade-enter-from {
transform: translateX(200px);
opacity: 0;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 500ms ease, transform 500ms ease;
}
.fade-leave-active {
/*
note that we add absolute position to leaving element
so other elements change their position and trigger move transition
*/
position: absolute;
}
.fade-leave-from,
.fade-enter-to {
transform: translateX(0);
opacity: 1;
}
Note that you can pass a
name
to your<TransitionGroup>
and it will be used also for its children<Transition>
components.
List Move Transition
There is one problem with our previous example. When the item gets added/removed, other ones just snap into their new position. Let's see how we can fix it.
<TransitionGroup>
adds ${name}-move
class to its children whenever they change their position. Let's tweak our previous example a little bit and see what we can do with it.
import React, { useState } from "react";
import { Transition, TransitionGroup } from "retransition";
+import { shuffle } from "lodash-es";
import "./index.css";
const getRandomIndex = length => Math.floor(Math.random() * length);
const initialNumbers = new Array(10)
.fill(null)
.map((_, i) => ({ value: i, index: Math.random() }));
const App = () => {
const [numbers, setNumbers] = useState(initialNumbers);
const add = () => {
const index = getRandomIndex(numbers.length);
const newNum = {
value: numbers.length,
index: Math.random(),
};
const newValue = [
...numbers.slice(0, index),
newNum,
...numbers.slice(index),
];
setNumbers(newValue);
};
const remove = () => {
const index = getRandomIndex(numbers.length);
const newValue = numbers.filter((_, idx) => idx !== index);
setNumbers(newValue);
};
+ const reorder = () => {
+ setNumbers(n => shuffle(n));
+ };
return (
<>
<button onClick={add} style={{ marginRight: 5 }}>
Add
</button>
<button onClick={remove} style={{ marginRight: 5 }}>
Remove
</button>
+ <button onClick={reorder} style={{ marginRight: 5 }}>
+ Shuffle
+ </button>
<div>
<TransitionGroup name="fade">
{numbers.map(n => (
<Transition key={n.index}>
<div style={{ padding: 5 }}>{n.value}</div>
</Transition>
))}
</TransitionGroup>
</div>
</>
);
};
.fade-leave-to,
.fade-enter-from {
transform: translateX(200px);
opacity: 0;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 500ms ease, transform 500ms ease;
}
.fade-leave-active {
/*
note that we add absolute position to leaving element
so other elements change their position and trigger move transition
*/
position: absolute;
}
.fade-leave-from,
.fade-enter-to {
transform: translateX(0);
opacity: 1;
}
+.fade-move {
+ transition: transform 500ms ease;
+}
Custom move class
You could pass your move class if you don't want to use a generated one. Just pass a moveClass
prop.
<Transition moveClass="my-move-class">
{/* ... */}
</Transition>
Important note about move transition
When you use <TransitionGroup>
, it assumes that you want to have a move transition. As a result, moveClass
will be added to children, whenever they change their position. But if the styles for it don't have a transition, it won't get removed (unlike with the <Transition>
component, which removes classes if the transition/animation is not defined). Because <TransitionGroup>
doesn't check whether each child has transition or not, since it could be a performance bottleneck (for large lists). Therefore if you don't plan to have a move transition and don't want unnecessary classes on your elements - pass moveTransition={false}
.
<TransitionGroup moveTransition={false}>
{/* ... */}
</TransitionGroup>
Cool example
With <TransitionGroup/>
's move
class you could make really cool animations. Checkout, for instance, this example:
import React, { useState } from "react";
import { Transition, TransitionGroup } from "retransition";
import { shuffle } from "lodash-es";
import "./index.css";
const makeArr = () => {
return Array(81)
.fill(null)
.map((_, index) => ({
id: "