Building a Complex UI Animation in React, Simply

Let’s use React , styled-components , and react-flip-toolkit to make our own version of the animated navigation menu on the Stripe homepage. It’s an impressive menu with some slick animation effects and the combination of these three tools can make it relatively easy to recreate.

This is an intermediate-level walkthrough that assumes familiarity with React and basic animation concepts. Our React guide is a good place to start.

Here’s what we’re aiming to make:

See the Pen React Stripe Menu by Alex (@aholachek ) on CodePen .

View Repo

Breaking down the animation

First, let’s break down the animation into different parts so we can more easily reproduce it. You might want to check out the finished product in slow motion (use the toggles) so you can catch all the details.

  1. The white dropdown container updates both its size and position.
  2. The gray background in the bottom half of the dropdown container transitions its height.
  3. As the dropdown container moves, the previous contents fade out and slightly to the opposite direction, as if the dropdown is leaving them behind, while the new contents slide into view.

There are a few useful guiding rules to keep in mind as we embark on reproducing this animation in React. Where possible, let’s keep things simple by having the browser manage layout. We’ll do this by keeping elements in the regular DOM flow instead of using absolute positioning and manual calculation. Rather than having a single dropdown component that we have to relocate every time a user’s mouse position changes, we’ll render a single dropdown in the appropriate navbar section.

We’ll use the FLIP technique to create the illusion that the three separate dropdown components are actually a single, moving component.

Scaffolding out the UI with styled-components

To start with, we’ll build an unanimated navbar component that simply takes a configuration object of titles and dropdown components and renders a navbar menu. The component will show and hide the relevant dropdown on mouseover.

We’ll build the UI components using styled-components . Not only are they a convenient way to build a modular UI, but they have a great API for adding configurable CSS keyframe animations . It turns out CSS animations and React play really nicely together, so we’ll be using CSS keyframes to add many of the animations later on.

With the components assembled without any animations, we’ve created something that looks like this:

See the Pen React Stripe Menu Before Animation by Alex (@aholachek ) on CodePen .

Notice that the gray background at the bottom of the menu is missing. It’s the only element that we’re going to have to take out of the regular DOM flow and absolutely position, so we’ll ignore it for now.

Animating our dropdown with the FLIP technique

We’re going to be using the react-flip-toolkit library to help us animate the dropdown’s size and position. This is a library I put together to make advanced and complex transitions easier and configurable.

It provides us with two components: a top-level component, and a component to wrap any children we want to animate.

First, let’s set up the Flipper wrapper component in the render function of AnimatedNavbar :

// currentIndex is the index of the hovered dropdown

    {, index) => {
      // render navbar items here

Next, in our DropdownContainer component, we’ll wrap elements that need to be animated in their own Flipped components, making sure to give them each a unique flipdId prop:


We’re animating the component and the component separately so that the overflow:hidden style on the component doesn’t interfere with the rendering of the component.

Now we’ve got a working FLIP animation, but there’s still one problem: the contents of the dropdown appear weirdly stretched over the course of the animation:

See the Pen React Stripe Menu — Error #1: no scale adjustment by Alex (@aholachek ) on CodePen .

This unwanted effect occurs because scale transforms apply to children. If you apply a scaleY(2) to a div with some text inside, you will be scaling up the text and distorting it as well.

We can solve this problem by wrapping the children in a Flipped component with an inverseFlipId that references the parent component’s flipId (in this case "dropdown") to request that parent transforms be cancelled out for children. Because we still want translate transforms to affect the children, we also pass the scale prop to limit the cancellation to just scale changes.


Whew. All that work and we’ve created something that looks like this:

See the Pen React Stripe Menu — Simple FLIP by Alex (@aholachek ) on CodePen .

It’s all in the details

It’s getting closer, but we still have to attend to the small details the make the animation look great: the subtle rotation animation when the dropdown appears and disappears, the cross-fade of previous and current dropdown children, and that silky-smooth gray background height animation.

Configurable CSS keyframe animations with styled components

Styled-components, which we’ve been using to build up the UI for this demo, offer a super convenient way to create configurable keyframe animations. We’ll use this functionality for both the dropdown enter animation and the cross-fade of the contents. We can pass in some basic information about the desired animation — whether the contents are animating in or out, and the direction the user’s mouse has moved — and automatically get the appropriate animation applied. Here, for example, is the code for the crossfade animation in the component:

const getFadeContainerKeyFrame = ({ animatingOut, direction }) => {
  if (!direction) return;
  return keyframes`
  from {
    transform: translateX(${
      animatingOut ? 0 : direction === "left" ? 20 : -20
  to {
    transform: translateX(${
      !animatingOut ? 0 : direction === "left" ? -20 : 20
    opacity: ${animatingOut ? 0 : 1};

const FadeContainer = styled.div`
  animation-name: ${getFadeContainerKeyFrame};
  animation-duration: ${props => props.duration * 0.5}ms;
  animation-fill-mode: forwards;
  position: ${props => (props.animatingOut ? "absolute" : "relative")};
  opacity: ${props => (props.direction && !props.animatingOut ? 0 : 1)};
  animation-timing-function: linear;
  top: 0;
  left: 0;

Each time the user hovers a new item, we’ll provide not only the current dropdown, but the previous dropdown as children to the DropdownContainer component, along with information about which direction the user’s mouse has moved. The DropdownContainer component will then wrap both its children in a new component, FadeContents, that will use the keyframe animation code above to add the appropriate transition.

Here’s a link to the full code for the FadeContents component.

The dropdown’s enter/exit animation will function very similarly.

The final touch: A fluid background animation

Finally, let’s add the gray background animation. To keep this animation crisp, we need to diverge from our previous strategy of keeping normal DOM nesting and letting the browser handle layout, and perform some manual positioning calculations instead. We’ll also need to interact directly with the DOM. In short, it’s going to get a little messy.

Here’s a visual representation of our basic approach:

See the Pen React Stripe Menu — Animated Background by Alex (@aholachek ) on CodePen .

We’ll absolutely position a gray div at the top of the DropdownContainer. In the componentDidMount lifecycle function of DropdownContainer, we’ll update the translateY transform of the gray background. If the dropdown container component only has a single child (which means the user has only hovered a single dropdown so far), we’ll set the gray div’s translateY to the height of the first dropdown section. If there are two children, including a previous dropdown, we’ll instead set the initial translateY to the height of the previous dropdown’s first section, and then animate the translateY to the height of the current dropdown’s first section. Here’s the function the gets called in componentDidMount:

const updateAltBackground = ({
}) => {
  const prevHeight = getFirstDropdownSectionHeight(prevDropdown)
  const currentHeight = getFirstDropdownSectionHeight(currentDropdown)
  // we'll use this function when we want a change 
  // to happen immediately, without CSS transitions
  const immediateSetTranslateY = (el, translateY) => { = `translateY(${translateY}px)` = "transform 0s"
    requestAnimationFrame(() => ( = ""))

  if (prevHeight) {
    // transition the grey ("alt") background from its previous height
    // to its current height
    immediateSetTranslateY(altBackground, prevHeight)
    requestAnimationFrame(() => { = `translateY(${currentHeight}px)`
  } else {
    // immediately set the background to the appropriate height
    // since we don't have a stored value
    immediateSetTranslateY(altBackground, currentHeight)

This approach requires DropdownContainer to use a ref and reach inside its children to take DOM measurements in the getFirstDropdownSectionHeight function, which feels sloppy. If you have any ideas for alternative implementations, please let me know in the comments!

Wrapping up

Hopefully this article has helped clarify some techniques you can use the next time you build an animation in React. There are normally multiple ways of achieving any effect, but often it makes sense to start with the simplest possible implementation — basic components with some CSS transitions or keyframe animations — and scale up the complexity from there when necessary. In our case, that meant including an additional library, react-flip-toolkit, so we didn’t have to worry about manually transitioning the position of the dropdown component across the screen. To fully recreate the animation, we did have to write a fair amount of code. But by breaking down this animation into separate parts and tackling them one-by-one, we managed to replicate a pretty cool UI effect in React.

The post Building a Complex UI Animation in React, Simply appeared first on CSS-Tricks .

Original Article From CSS Tricks

Leave a Reply

Your email address will not be published. Required fields are marked *