Animated UI with react-transition-group
We will build this animation on slider value change using React and build the leaf animation with react-transition-group
library.
Checkout out the demo - Link
There are two parts to this animation-
- The branch growing upward.
- The leaf appearing and disappearing.
Branch Animation
The branch is a div
whose growth is controlled by changing the height property. We initialize a ref branch
and use it
to change the height of the branch based on the prop yearIndex
. This prop has been passed from the Slider
component, and it's the index of the label array - [0, 3, 5, 8, 10]
.
This is the basic structure of the Tree component that illustrates branch functionality-
import { useEffect,useState, useRef } from 'react'
import { OFFSET } from '../utils/constant'
const Tree = ({ yearIndex }) => {
const branch = useRef(null)
useEffect(() => {
branch.current.style.height = `${(yearIndex * OFFSET) + 45}px`
}, [yearIndex])
return (
<div className="tree-wrapper">
<div className="tree">
<div ref={branch} className="branch">
{/*
.....
*/}
</div>
<svg className="pot">
....
</svg>
</div>
</div>
)
}
The constant OFFSET
is dependent on the size of the leaf and the static value 45
is the initial
height of the branch. The branch grows upward since the position has been set absolute
with bottom value.
.branch {
position: absolute;
bottom: 50px; /* placing it above the pot */
left: 30px;
transition: 0.4s height ease-out;
will-change: height;
}
Leaf Animation
As the slider value increases, we add two new leaves in the branch on the left and right. The leaf at the top remains fixed.
const Tree = ({ yearIndex }) => {
....
const createArray = (n) => Array.from({ length: n + 1 })
const [leaves, setLeaves] = useState(createArray(yearIndex))
return (
<div className="tree-wrapper">
<div className="tree">
<div ref={branch} className="branch">
{leaves.map((_, l) => (
<Leaf
key={l}
index={l}
/>
))}
</div>
<svg className="pot">
....
</svg>
</div>
</div>
)
}
We create an array based on the value of yearIndex
and generate leaves based on that. The Leaf
component generates two leaves on each side in each loop.
const Leaf = ({ index }) => {
const directions = ['left', 'right']
return (
<>
{directions.map((direction, d) => (
<div
key={d}
style={{transform: `${setLeafPosition(index, direction)}`}}
className={`leaf ${direction}`}
>
<LeafStyle />
</div>
))}
</>
)
}
LeafStyle
component contains the icon for leaf. We are using only one icon, and based on the direction,
we are setting the position and angle of the leaf.
const setLeafPosition = (index, direction) => {
const isLeft = direction === 'left'
const deg = isLeft ? 75 : 5
const yPosition = index * OFFSET
return `translate(0, ${-yPosition}px) rotate(${deg}deg)`
}
The position of each leaf has also been set as absolute. The left leaf has been raised a little bit from right to give an illusion of alternate ordering.
.leaf {
position: absolute;
bottom: 0;
transition: 0.8s transform ease;
&.right {
left: 0;
bottom: 2px;
transform-origin: bottom left; /* To scale from the corner */
}
&.left {
right: 0;
bottom: -8px;
transform-origin: top right;
}
}
Now, to add the appearance animation of leaf, we need to import the react-transition-group
package in Leaf
component.
...
import { CSSTransition } from 'react-transition-group'
...
const Leaf = ({ index }) => {
const [isEntered, setIsEntered] = useState(false)
const nodeRef = useRef(null)
....
useEffect(() => {
// Trigger the animation after the initial render
setIsEntered(true)
}, [])
return (
<CSSTransition
in={isEntered}
nodeRef={nodeRef}
timeout={0}
>
{(state) => (
<div ref={nodeRef}>
{directions.map((direction, d) => (
<div
key={d}
style={{
transform: `${setLeafPosition(index, direction)} scale(${
state === 'entered' ? 1 : 0
})`,
}}
className={`leaf ${direction}`}
>
<LeafStyle />
</div>
))}
</div>
)}
</CSSTransition>
)
}
We wrap the leaf div with CSSTransition
group and control the appearance of leaf based on the scale
transform. The scale is set to 1 based on the isEntered
prop, which is set to true on component mounts. The animation
works perfectly when new leaves are added, but there is no animation when leaves are removed.
To fix the disappearance animation issue, we set a timeout to delay the new array assignment process in Tree
component.
const Tree = ({ yearIndex }) => {
....
useEffect(() => {
...
let timer = setTimeout(() => {
setLeaves(createArray(yearIndex));
// if the previous value is greater than current one we set delay
}, leaves.length > yearIndex + 1 ? 1500 : 0)
return () => {
clearTimeout(timer)
}
}, [yearIndex, leaves])
return (
...
<Leaf index={l} key={l} visible={l < yearIndex + 1} />
...
)
}
We also set a prop visible
in Leaf
component which will clear the leaf if the Slider value has been
decreased. So, when slider value is less than before, the visible
prop becomes false
and since the array
assignment is set to delay in this scenario, the disappearance animation now works and the array is reassigned after the delay.
...
const Leaf = ({ index, visible }) => {
....
return (
<CSSTransition
in={isEntered && visible}
...
>
....
</CSSTransition>
)
}
That's mostly the basic idea of how this animation has been built. If you want to see the full code, you can checkout out here