I love Radix Primitives! They allow you to skip implementing all the boring details of a UI component and focus on its styling. And a lot of times, styling involves animations too.
The easiest way to add animations is to use CSS animations. Radix adds different data-
attributes to the elements, so you can target elements in a particular state. Radix even allows you to animate an element's exit: it will wait for the CSS animation to finish before actually removing the element from the page.
But I usually do animations with Framer Motion. Radix works well with it but requires a bit of tinkering with wrapping components in motion
and placing forceMount
on the correct elements to make everything work together. And I always forgot how to do it properly, so this article is a short guide for both you and me :)
Simple animations
When you want to animate an element's entrance or state change (e.g. from disabled to enabled) with Framer Motion, you need to wrap the Radix component in motion. After this, you can use it as your normal motion.something
component. For example, the progress bar:
Alternatively, you could use the asChild
prop, provided by Radix.
And with that, you can now use Framer Motion features like animate
prop, gestures and layout animations with your Radix components.
Exit animations
When it comes to components that render some of their elements conditionally (for example, a tooltip), it gets a bit more complicated. You can still use enter and state animations the same way as in the previous chapter, but exit animations are more tricky to wire correctly.
By default, Radix manages a component's state (open/closed) by itself. This makes our lives easier by removing boilerplate from our components, but at the same time, it's not really compatible with AnimatePresence
from Framer Motion. Unmounted elements need to be direct children of <AnimatePresence>
for exit animations to work correctly.
Fortunately, Radix provides an option to give control over the state back to us. Let's take a <Tooltip>
as an example. Here is the usual structure of the Radix Tooltip:
Tooltip content is rendered by the RadixTooltip.Content
element, which in itself is inside a portal. Radix decides on its own when to actually render portal and tooltip content as a response to user action (like hovering over a trigger element). So, how do we take control of our <Tooltip>
component?
To do this, we need to introduce a new state variable open
and pass it to <RadixTooltip.Root>
. And we need to provide a onOpenChange
callback to update state when required. This way, Radix still gets to decide when a tooltip should be shown and when hidden, but the state now lives inside our component, and we can use it to conditionally render elements.
Now we need to tell Radix that we'll be handling the conditional rendering of the tooltip's content. We do this by passing forceMount
property to <RadixTooltip.Portal>
and <RadixTooltip.Content>
elements. And finally, we need to conditionally render content inside <AnimatePresence>
.
Poof! Your tooltip now supports exit animations. But the same approaches will work with other Radix primitives as well. Now that you are equipped with knowledge, go add some crispy animations to your Radix components!