React tooltips with framer motion
By: Simon Kerr
Tags: Framer Motion, JavaScript, Frontend, Tooltips, React

What is framer motion?

Framer motion is a motion library for react, and it makes some pretty cool stuff possible in a relatively simple way. You can animate single elements, or orchestrate animation between elements. You can animate the presence and disappearance of elements. You can also specify how the animation takes place, either based on duration with an easing mechanism, or time-based with factors like dampening and spring.

Concepts

The basic idea of the tooltip is to have markers anywhere on the page. These are linked to tooltips. so that when a marker is clicked, the linked tooltip appears. Only one tooltip can appear at once on screen and if one is already open, clicking another one will cause the open one to close first.

The markers and tooltips are connected via react context. A provider gives access to descendant components to determine which tooltip to show/hide. The markers hook into this provider to set the currently selected tooltip. The tooltips hook into the provider to determine whether to show themselves or not.

To make sure that the tooltips always appear over any other content, the tooltip content is rendered using a portal, to target a div that exists outside of the main app div.

This is my go-to styling choice for most react apps these days. The flexibility it provides as well as seamlessly working with framer motion are two big advantages. For framer motion, it also allows styled components to be created from its own motion enabled components, and then accepts dynamic styling props.

For this solution, a number of react hooks get used, including useState to set the tooltip position on screen, useRef to get access to the tooltips underlying DOM element, and useEffect and useLayoutEffect to handle how components get rendered.

This is where the actual animation happens. Without it, the tooltips work but they don’t look as nice. With framer motion, we can create tooltips that animate when they come into and leave the DOM. This is done by the use of the ‘AnimatePresence’ component. Here’s a snippet of the tooltip code.

const Component = ( <AnimatePresence> {active && ( <Tooltip ref={ref} position={position} {...motionProps} > {children} </Tooltip> )} </AnimatePresence> ); return createPortal(Component, document.getElementById("tooltips"));

The ‘AnimatePresence’ component wraps the child component and handles the animation when it enters or exits the DOM. How the animation functions is handled by the ‘motionProps’ prop, shown below…

const variants = { active: { opacity: 1, y: 0, transition: { duration: 0.4, ease: "easeOut", }, }, inactive: { opacity: 0, y: -40, transition: { duration: 0.3, }, }, }; export const motionProps = { variants, initial: "inactive", animate: "active", exit: "inactive", };

So one way to handle enter/exit animation is by using variants and then specifying which variant to use for the various stages (initial, animate, exit). The above code is saying that for initial and exit animations the element should shift up by 40px, fade out, and that it should take 0.3 seconds. When it’s active, it should move 40px down, fade in, and it should take 0.4 seconds. It’s also got easing set on it so the tooltip slows down slightly as it reaches the destination.

I’m no UX expert but I did have a good read of an excellent article on proper use of animation in UX. A couple of points I got was that entry animation in this case should take slightly longer than exit animation, and that entry animation looks more natural when it slows down as it reaches completion.

Conclusion

There’s obviously a ton of other stuff you can do with framer motion, I’ve just scratched the surface, but it seems to me to be an intuitive and simple way to solve quite common problems. I’m going to be exploring other aspects of framer motion in subsequent posts, so check back later if you’re interested.

Check out the sample repo that shows the full code (caveat, its a work in progress 😉). Hope you enjoy it.