If you are a frontend developer, you probably know that motion is a powerful library for animating your components. One of its standout features is ensuring that elements with animations are not removed from the DOM until their animations are complete. However, the library comes with a size cost: 116.5k (gzipped: 37.5k), which can be significant for simple use cases. If you only need basic animations with AnimatePresence, you can rebuild it without motion.
In this blog, we will learn how to create a simple animation using (motion and AnimatePresence) and then replicate its functionality using native JavaScript and React.
Using motion and AnimatePresence
import { motion, AnimatePresence } from "motion/react";
import { useState } from "react";
function Ball() {
const [visible, setVisible] = useState(true);
return (
<AnimatePresence>
{visible && (
<motion.div
initial={{ opacity: 0, x: -30 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 30 }}
>
⚾
</motion.div>
)}
</AnimatePresence>
);
}
With AniamtePresence
In the With AnimatePresence case, the Show/Hide Button does not shift until the animation is complete. This behavior occurs because AnimatePresence compares the previous childrens with the new children. If it detects removed children, it triggers the "exit" animation before finally removing the elements.
Rebuilding AnimatePresence
Steps:
- Compare the previous
childrenswith the newchildren, and activateanimate__exitanimation - Remove any children marked with
animate__exitclass after the animations are complete - Manage new children added to the DOM
Prerequisites
- What are the differences between
ReactElementandReactNode? - What is
React.Childrenand how to use it?
1. Compare the previous childrens with the new children, and activate animate__exit animation
We’ll maintain a state to track the current children and compare them with new children to detect removals. Removed elements will get the animate__exit class.
import React from "react";
// Minimal types for cloning element
interface CloningElementProps {
className?: string;
style?: { animation?: string }
}
function AnimatePresence({ children }: { children: React.ReactNode }) {
const [childrens, setChildrens] = React.useState<ReactNode[]>(
React.Children.toArray(children)
);
useEffect(() => {
const pre = childrens.map((child) => {
const exists = React.Children.toArray(children).some((curChild) => {
return (
React.isValidElement(child) &&
React.isValidElement(curChild) &&
child.key == curChild.key
);
});
return !exists && React.isValidElement<CloningElementProps>(child)
? cloneElement(child, {
className: (child.props.className ?? "").replace(
"animate__enter",
"animate__exit"
),
})
: child;
});
setChildrens(pre);
}, [children]);
useEffect(() => {
// STEP 2
}, [childrens]);
return childrens;
}
Explanation
- The
useEffectlistens for changes inchildren. - We compare previous
childrenswith newchildrenusing thekeyproperty. - For removed elements, we replace
animate__enterwithanimate__exitto trigger the exit animation.
Hold on
In my case, I designed AnimatePresence to work with simple keyframes. As when animating out,
I'm going to remove the animate__enter class, and add animate__exit class.
Another interesting tip 😊! You can use this cubic bazier cubic-bezier(0.34, 1.56, 0.64, 1), to
make a spring like transition (like in motion)
/* Enter */
.animate__enter {
transform: translateX(-30px);
opacity: 0;
animation: enter 350ms cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
@keyframes enter {
to {
transform: translateX(0);
opacity: 1;
}
}
/* Exit */
.animate__exit {
animation: exit 350ms cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
@keyframes exit {
to {
transform: translateX(30px);
opacity: 0;
}
}2. Remove any children marked with animate__exit class after the animations are complete
useEffect(() => {
// STEP 2
childrens.forEach((child) => {
if (!React.isValidElement < CloningElementProps > child) return;
if (!child.props.className) return;
if (child.props.className.includes("animate__exit")) {
setTimeout(() => {
setChildrens((prevChildrens) => {
return prevChildrens.filter(
(prevChild) =>
React.isValidElement(prevChild) && prevChild.key !== child.key
);
});
}, child.props.style?.transitionDuration ?? 300);
}
});
}, [childrens]);
Tip: If you wish, you could change the timeout duration using something like data-duration
attribute to ensure the removal aligns with the animation length.
3. Manage new children
useEffect(() => {
// Unchaged `pre`
const post = React.Children.toArray(children).filter((child) => {
const exists = childrens.some((curChild) => {
return (
React.isValidElement(child) &&
React.isValidElement(curChild) &&
child.key == curChild.key
);
});
return !exists;
});
setChildrens(pre);
setChildrens([...pre, ...post]);
}, [children]);
Explanation
preidentifies removed elements by comparingchildrenswithchildren.postidentifies new elements by comparingchildrenwithchildrens.- Then merge them.
Finally, you can use this AnimatePresence in simple use cases instead of motion. Let's see
our implementation:
motion
If you find areas for improvement in this approach or have suggestions, we’d love to hear your feedback! 💚