Wizardry

Build an Animated Modal with React

December 07, 2017

Julian
Written by Julian, a Javascript spellslinger.

Cast a Twitter follow

Cover

This is how I built an animated modal based on a prototype made by Karan Shah.

There is a live, editable snapshot of every commit that I made, this way you get to see how code evolved over time. I built the UI using React, layed out elements using Flexbox and CSS Grids, and animated them using CSS transitions.

This is the process I followed: First I made a static version of the modal and after that I added animations. The initial step of the static version is building the trip summary page. Then I created the opened state of the modal, which contains menu links. And after that I built the closed state. Finally I connected the two states using CSS transitions.

Step 1 - Trip summary

Show title

As you can see, the page title is just an <h1 /> so there is not much to say about it. Let’s move on.

Show trip price

The container uses flexbox to evenly distribute space among its children, whereas the children use flexbox to vertically align their text.

The trip image placeholder uses flexBasis to set its width. I like to use flexBasis instead of width or height because the browser gives it precedence over those values.

Display departure price

I used flex and justifyContent: "space-between" to push Departure and Edit info to the sides.

Display departure date

As you can see, I created a <Dot /> component to represent the little vertical dots in the mockup. I try to create an abstraction only when it’s really obvious that it’s needed, and in this case I think it’s worth it. This is why I haven’t created a <Title />, <Container /> or <Box /> component so far. These little dots are going to be repeated a lot, so they deserve to be a stand-alone element.

Display grey dots

See, I told you we were going to use these dots all over the place. They are inside a flex container that centers them over the y axis with flexDirection: "column" and alignItems: "center".

Show departure details

Departure dates get arranged using display: grid because its items are layed out in two dimensions, both horizontally and vertically. Each part has three rows and four columns, with some items covering more than one row or column.

To create this kind of layout, where items may span more than one row or column, I love using gridTemplateAreas because it’s really visual. This line gridTemplateAreas: "a b c d" "a e f d" "a g g g", defines three areas (a,b,c and d). It concisely states the following:

  • There are three rows
  • Each row has four columns
  • Area a is in the first column of every row
  • Area b is in the second column of the first row
  • Area c is in the third column of the first row
  • Area d is in the fourth column of the first and second row
  • Area g is in the second, third and fourth column of the third row

To complete this layout we need to correlate elements with areas using gridArea and determine width of rows and columns using gridTemplateColumns and gridTemplateRows.

Trip summary is completed now, let’s build the menu links that appear when the modal is opened.

Step 2 - Opened modal

Show menu items

Our App component is starting to look nice. It has <TripSummary /> and <Menu />, linking them together with a state variable called isMenuOpened.

class App extends Component {
  state = { isMenuOpened: true };
  render() {
    return (
      <div
        style={{
          fontFamily: "sans-serif"
        }}
      >
        <TripSummary
          onModalButtonClick={() => this.setState({ isMenuOpened: true })}
        />
        <Menu isOpen={this.state.isMenuOpened} />
      </div>
    );
  }
}

If you look inside <Menu /> we layout the menu items in one axis with display: grid. We do this because gridGap lets us easily define spacing between items.

You may also have noticed the big violet circle that serves as the modal background.

<div
  style={{
    position: "absolute",
    top: 0,
    left: 0,
    backgroundColor: darkViolet,
    width: "calc(80vh*2)",
    height: "calc(80vh*2)",
    transform: "translate(-50%,-50%)",
    borderRadius: "100%"
  }}
/>

A nice trick inside that component is setting transform: "translate(-50%,-50%)" so we can use left and top to position it’s center instead of its top left corner.

Show circles

All of the circles that form the modal are layed out using good old position: absolute. No grid or flex magic over here.

Step 3 - Closed modal

Toggle modal on click

When the modal is closed we hide all items based on the isOpen property:

  • Menu items get hidden by changing their opacity
  • Profile picture change their right property
  • Close button sneaks out with a bottom change
  • Background circles shrink their width and height

This modal is working right now, but it needs some animations to bring it to life. We are going to animate it step by step using CSS transitions.

Step 4 - Animate

Transition menu items opacity

Animating menu links opacity is as simple as adding transitionProperty: "opacity", transitionDuration: ".5s", to them.

Transition background circles’ size

This is starting to look nicer now that all circles are expanding. We achieve this expansion with transform: 'translate(-50%,-50%) ${isOpen ? "scale(1)" : "scale(0)"}'. We could have also transitioned width and height instead of scale, but scale is better because browsers have better performance animating transform.

Transition position of close button and profile picture

Close button and profile picture move differently than the background circles. Background circles move in a linear fashion, whereas close button and profile picture go a bit beyond their final position and then bounce back until they reach it.

We achieve this movement by setting a transition timing function transitionTimingFunction: isOpen ? "cubic-bezier(0.5, 1, 0.3, 1.3)" : "cubic-bezier(0.5, -1, 0.3, 1.3)". It is a function that describes how fast a value changes during an animation. I played a bit with parameters in cubic-bezier.com to end up with these functions.

Change circle’s original position

I realized that the main violet background circle should start right above the modal button, instead of starting from the page’s top left corner. This overlapping creates a nice effect in which the button appears to grow when you click it.

Stagger menu items

Menu items should appear one after the other, not simultaneously. This gradual animation is called stagger and to achieve this effect with CSS transitions we need transitionDelay and a little bit of really basic math.

The complete animation lasts a second, but the menu items animate for half a second, and we have seven menu items, so with this equation we can know the animation delay of each item: animationDelay = (animationDuration / numberOfItems) * elementIndex.

  • First element: animationDelay = (0.5s / 7) * 0 = 0s
  • Second element: animationDelay = (0.5s / 7) * 1 = 1/7s = 0.07s
  • Third element: animationDelay = (0.5s / 7) * 2 = 1/7s = 0.14s
  • Fourth element: animationDelay = (0.5s / 7) * 3 = 1/7s = 0.21s
  • Fifth element: animationDelay = (0.5s / 7) * 4 = 1/7s = 0.28s
  • Sixth element: animationDelay = (0.5s / 7) * 5 = 1/7s = 0.35s
  • Seventh element: animationDelay = (0.5s / 7) * 6 = 1/7s = 0.42s

Map over menu items

One of the main benefits of using style tags (or any other css in js solution) instead of css files is that we can use variables and loops. Using an array and its map function we improved the hard coded solution we had in the last step.

{menuItems.map((menuItem, index) => (
  <div
    style={{
      zIndex: 1
    }}
    key={menuItem}
  >
    <a
      style={{
        cursor: "pointer",
        opacity: isOpen ? 1 : 0,
        transitionProperty: "opacity",
        transitionDuration: ".5s",
        transitionDelay: isOpen
          ? `${index * 0.5 / menuItems.length}s`
          : `${0.5 -
              index * 0.5 / menuItems.length +
              0.5 / menuItems.length}s`,
        zIndex: 1,
        color: white
      }}
    >
      {menuItem}
    </a>
  </div>
))}

Refactor magic numbers into transition duration

There were a lot of hardcoded values like 0.5s and 1s, so I refactored them into a transitionDuration property. Now we can control the menu duration like this: <Menu transitionDuration={1} />.

Transition menu items y position

Menu items should move upwards as they appear, like in the mockup, so I animated them with transform: translateY(${isOpen ? 0 : 20}px) and transitionProperty: "transform".

Shorten transition duration

The animation felt sluggish, right? A shorter duration will make it snappier, so I added transitionDuration={.75} to <Menu />.

Use glamor css prop instead of style prop

Changed all style props for a css prop by using glamor. This is a small change that brings huge benefits in terms of performance and browser support.

It’s a performance improvement because glamor creates a css class that will apply the styles we pass to css prop. This reduces the generated html size and also lets us keep using the great css dev tools on modern browsers.

It improves browser support because glamor adds browser prefixes where needed. Elements with display: grid and display: flex benefit greatly from these.

Improve animations performance on mobile

Time for the final performance improvement. Animations were really slow on mobile devices and it turns out that’s because we are animating huge circles simultaneously. This forces a browser repaint on every new frame, which is not good. We can avoid this repaints by moving the huge circles into separate layers and let the GPU handle them. How can we achieve this magic? Using willChange: transform.

You need to know that willChange: transform improved speed in our case, but it can cause issues like huge memory consumption. You should understand how the browser works before you use it, and only use it if you really need to. These two articles make a great job at explaining the pros and cons:

Conclusion

It was a fun ride, huh? I love to open and close this modal for no reason other than watching every element appear on the screen in different ways.

We learned a lot about React and CSS standards by building this animated modal, namely:

  • Flexbox

    • Laying out elements on a single axis
    • Centering
  • Grid

    • Laying out elements on two dimensions using gridTemplateAreas
    • Consistent spacing using gridGap
  • CSS transitions

    • Animating opacity and transform using transitionProperty and transitionDuration
    • Achieving a bounce like transition using transitionTimingFunction
    • Stagger elements using a bit of math and transitionDelay
  • Performance

    • Reducing generated HTML and adding vendor prefixes using glamor css property
    • Improving animation speed using willChange: transform

I hope you liked following along. Happy programming for you!


Want to learn GraphQL and React? Check out GraphQL College