Day 21: Animating transitions for a React app without external libraries
Tags: webdev, react, css, animation
Published: 2023-01-02 01:20:55

## TL;DR

With four steps of coding, we can animate transitions for a React app without relying on external libraries.

First, use four state values for UI change: `closed`, `opening`, `open`, and `closing`. 

Second, render a component when the state value is either `opening`, `open`, or `closing`. In other words:
```js
{state !== "closed" ? (
  <div />
) : null}
```

Third, trigger the exiting animation by adding a data attribute to the exiting component:
```js
{state !== "closed" ? (
  <div data-closing={state === "closing"} />
) : null}
```
With the following CSS selectors, entering and exiting animation start when the state turns `opening` and `closing`, respectively:
```css
div {
  animation: /* Set entering animation parameters here */
}
div[data-closing="true"] {
  animation: /* Set exiting animation parameters here */
}
```

Finally, to switch the UI state from `opening` to `open`, or from `closing` to `closed`, use the `onAnimationEnd` prop:
```js
{state !== "closed" ? (
  <div 
    data-closing={state === "closing"}
    onAnimationEnd={handleAnimationEnd} // ADDED
  />
) : null}
```
```js
const handleAnimationEnd = () => {
  if (state === 'opening') {
    setState('open');
  }
  if (state === 'closing') {
    setState('closed');
  }
}
```
In the text below, I use an example of opening and closing a search box popup for more detailed description. Here is [the CodeSandbox demo for this article](https://codesandbox.io/s/day-21-animating-transition-with-react-23gkpg?file=/src/components/Search.js).

## 1. Motivation
Implementing transition animation with React is a bit tricky. Entering animation is fine. We can simply use CSS animation, which will be triggered when a React component gets mounted. Difficulty comes with exiting animation. When a component gets dismounted, we have no opportunity to apply a set of CSS declarations for animation to that component.

There are a bunch of animation libraries to overcome this difficulty. The most famous is probably [React Transition Group](https://reactcommunity.org/react-transition-group/). There are also [Framer Motion](https://www.framer.com/motion/), [React Spring](https://www.react-spring.dev), and [Transition Hook](https://github.com/iamyoki/transition-hook#%EF%B8%8F-transition-hook).

To my eyes, all these libraries make the code for React components less intuitive than necessary. I'd rather code from scratch on my own to keep the code base easy to maintain.

Below I describe how I did so for _My Ideal Map_, a web app I’m building. As a specific example, this article aims to animate the opening and closing of a search box popup. But the same logic can be applied to other cases of transition animation.

For the ease of exposition, I stick to a simple transition animation of fade-in and fade-out (which I use for the reduced motion mode) so that the reader will not get distracted by the complexity of animation per se.

![When the user presses a search button at the top-right corner of the screen, a popup with a search box fades in to the view. Then, when the user presses the close button in the popup, the popup fades out while the search button fade in to the view.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8xip91shdjfuldbhp1kn.gif) **Fade-in and fade-out transitions that this article will implement with React**

## 2. UI state management with React

Imagine that an app does not show a search box by default, to save the screen space. Instead, it shows a search button the pressing of which will open the search box.

### 2.1 Initial state
I set the UI state as an object called `ui` with two properties, `searchButton` and `searchBox`. The initial values of these two properties are `open` and `closed`, respectively.
```js
const [ui, setUi] = useState({
  searchButton: 'open',
  searchBox: 'closed',
});
```

### 2.2 Opening search box
When the user presses the search button, the following event handler will be executed to change the UI state to `closing` and `opening`:
```js
const handleClickSearchButton = () => {
  setUi({
    searchButton: 'closing',
    searchBox: 'opening',
  });
};
```
This handler is going to be attached to the search button element as the `onClick` prop value (see below for detail).

Then, when the transition animation ends, which triggers [the `animationend` event](https://developer.mozilla.org/en-US/docs/Web/API/Element/animationend_event), the following event handler will be executed to set the UI state to be `closed` and `open`:
```js
const handleAnimationEnd = () => {
  setUi({
    searchButton: 'closed',
    searchBox: 'open',
  });
```
This handler is going to be attached to the `form` element, the parent of both the search button and the search box popup, as the `onAnimationEnd` prop value (see Section 2.6 below).

### 2.3 Closing search box
When the close button in the search box popup is pressed, the following event handler will set the UI state to be `opening` and `closing`:
```js
const handleClickCloseButton = () => {
  setUi({
    searchButton: 'opening',
    searchBox: 'closing',
  });
};
```
This handler is going to be attached to the close button element as the `onClick` prop value (see Section 2.5 below).

When the transition animation ends, the event handler will set the UI state to be the initial values. This is done by revising the `handleAnimationEnd` function as follows:
```js
  const handleAnimationEnd = () => {
    if (ui.searchButton === 'closing') { // ADDED
      setUi({
        searchButton: 'closed',
        searchBox: 'open',
      });
    } // ADDED
    // ADDED FROM HERE
    if (ui.searchBox === 'closing') {
      setUi({
        searchButton: 'open',
        searchBox: 'closed',
      });
    }
    // ADDED UNTIL HERE
  };
```
Since the `animationend` event is fired both when the search button completely disappears and when the search box popup completely disappears, I need to check which of these two events occurs. This is why I use the following two conditions:
```js
if (ui.searchButton === 'closing') {}
```
```js
if (ui.searchBox === 'closing') {}
```

### 2.4 Rendering search button
Now, we want to render the search button unless its UI state is `closed`:
```js
{ui.searchButton !== 'closed' ? (
  <ButtonCloud
    aria-expanded="false"
    aria-label="Search a place on the map"
    onClick={handleClickSearchButton}
    type="button"
  >
    <!-- SVG code for magnifying glass icon (omitted) -->
  </ButtonCloud>
) : null}
```
where the `ButtonCloud` component refers to the search button element. Its CSS code for animation will be set with Styled Components in Section 3 below.

Note that the `handleClickSearchButton` function, defined in Section 2.2 above, is attached as the `onClick` prop value.

### 2.5 Rendering search box
We also want to render the search box unless its UI state is `closed`:
```js
{ui.searchBox !== 'closed' ? (
  <DivSearchBackground
    data-testid="div-search-background"
  >
    <CloseButton
      ariaLabel="Close search box"
      handleClick={handleClickCloseButton}
    />
    <SearchBox closeSearchBox={closeSearchBox} id="searchbox" />
  </DivSearchBackground>
) : null}
```
where `DivSearchBackground`, `CloseButton`, and `SearchBox` refer to the popup, the close button, and the search box, respectively. Among these, `DivSearchBackground` will be styled with Styled Components for animation in Section 3 below.

Note that the `handleClickCloseButton` function, defined in Section 2.3 above, is attached to `CloseButton` as its `handleClick` prop (see [**Day 18 of this blog series**](https://dev.to/masakudamatsu/day-18-building-a-close-button-with-ripple-effect-as-a-react-component-1n62) how the `CloseButton` component is composed).

### 2.6 “Animaitonend” event handler
To listen to the `animationend` event, its event handler is attached to the `<form role="search">` element that contains both the search button and the search box popup as its child elements. The entire JSX is now as follows:
```js
<form 
  onAnimationEnd={handleAnimationEnd}
  role="search" 
>
  {ui.searchButton !== 'closed' ? (
    <ButtonCloud
      aria-expanded="false"
      aria-label="Search a place on the map"
      onClick={handleClickSearchButton}
      type="button"
    >
      <!-- SVG code for magnifying glass icon (omitted) -->
    </ButtonCloud>
  ) : null}
  {ui.searchBox !== 'closed' ? (
    <DivSearchBackground
      data-testid="div-search-background"
    >
      <CloseButton
        ariaLabel="Close search box"
        handleClick={handleClickCloseButton}
      />
      <SearchBox closeSearchBox={closeSearchBox} id="searchbox" />
    </DivSearchBackground>
  ) : null}
</form>
```
As the event bubbles up in the DOM tree, both the end of animation for the search button (`ButtonCloud`) and for the search box popup (`DivSearchBackground`) will be caught by their parent element, the `<form role="search">` element.

### 2.7 For styling the exit animation
Finally, to animate the _disappearance_ of the search button and the search box popup, the `data-closing` attribute is added to them to trigger CSS animation:
```js
return (
  <form role="search" onAnimationEnd={handleAnimationEnd}>
    {ui.searchButton !== 'closed' ? (
      <ButtonCloud
        aria-expanded="false"
        aria-label="Search a place on the map"
        data-closing={ui.searchButton === 'closing'} // ADDED
        onClick={handleClickSearchButton}
        type="button"
      >
        <!-- SVG code for magnifying glass icon (omitted) -->
      </ButtonCloud>
    ) : null}
    {ui.searchBox !== 'closed' ? (
      <FocusLock>
        <DivSearchBackground
          data-closing={ui.searchBox === 'closing'} // ADDED
          data-testid="div-search-background"
        >
          <CloseButton
            ariaLabel="Close search box"
            handleClick={handleClickCloseButton}
          />
          <SearchBox closeSearchBox={closeSearchBox} id="searchbox" />
        </DivSearchBackground>
      </FocusLock>
    ) : null}
  </form>
)
```
This way, we can use the attribute selector
```css
[data-closing="true"] {
  animation: ...
}
```
to style the exit animation (see Section 3 below for more detail).

We’re done with the coding for React components. Now it’s time to style transition animation with CSS.

## 3. Setting animation parameters

### 3.1 Opening search box
For the search box to enter, we want it to fade in with the duration of 300ms and the linear easing.

The choice of 300ms is inspired from Material Design. Jonas Naimark, a Material Design team member, states as follows:
> Since nav transitions usually occupy most of the screen, a long duration of 300ms is a good rule of thumb. — [Naimark (2018)](https://medium.com/google-design/motion-design-doesnt-have-to-be-hard-33089196e6c2).

The linear easing is typically used for animation that doesn’t involve any movement:
> Linear motion can, for example, be used only when the object changes its color or transparency. Generally speaking, we can use it for the states when an object does not change its position. — [Skytskyi (2018)](https://uxdesign.cc/the-ultimate-guide-to-proper-use-of-animation-in-ux-10bd98614fa9)

The fade-in animation can be achieved with the following `keyframes` and the `backwards` value of `animation-fill-mode` property:
```css
@keyframes fade-in {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}
```
```css
animation-fill-mode: backwards;
animation-name: fade-in;
```
The `backwards` value of `animation-fill-mode` prevents the flash of the default style before animation starts. Every HTML element has the opacity of 1 by default. If animation starts with the opacity value less than 1, we need `animation-fill-mode: backwards`.

At the same time as the search box enters, we want the search button to disappear with fade-out animation. To match the timing of animation, I use the same duration and easing values.

The fade-out animation can be defined as follows:
```css
@keyframes fade-out {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}
```
```css
animation-fill-mode: forwards;
animation-name: fade-in;
```
By default, an HTML element’s default style (including `opacity: 1`) will appear after animation is over. To avoid the flash of an element after fade-out animation, therefore, we need the `forwards` value of `aniamtion-fill-mode`.

To summarize the animation parameters I have set so far, let’s define a JavaScript object called `animation`:
```js
// ./utils/animation.js
import { keyframes } from "styled-components";
export const animation = {
  openSearchBox: {
    duration: "300ms",
    easing: "linear",
    button: {
      opacity: keyframes`
        from {
          opacity: 0;
        }
        to {
          opacity: 1;
        }
     `,
      fillMode: "backwards",
    },
    popup: {
      opacity: keyframes`
        from {
          opacity: 0;
        }
        to {
          opacity: 1;
        }
     `,
      fillMode: "backwards",
    },
  },
};
```
Styled Components allow us to store `keyframes` as a variable with its `keyframes` helper (see [Styled Components docs for detail](https://styled-components.com/docs/basics#animations)).

To apply these parameters to the `DivSearchBackground` styled component for the search box popup, we code as follows:
```js
// ./styled-components/DivSearchBackground.js
import styled, {css} from 'styled-components';

...

const animateTransitionIn = css`
  animation-duration: ${animation.openSearchBox.duration};
  animation-fill-mode: ${animation.openSearchBox.popup.fillMode};
  animation-name: ${animation.openSearchBox.popup.opacity};
  animation-timing-function: ${animation.openSearchBox.easing};
`;

export const DivSearchBackground = styled.div`
  ...
  ${animateTransitionIn}
`;
```
where the `css` helper is necessary to refer to the `keyframes` for the `animation-name` property.

To apply the animation parameters to the `ButtonCloud` styled component for the search button, we code as follows:
```js
// ./styled-components/ButtonCloud.js
import styled, {css} from 'styled-components';

...

const animateTransitionOut = css`
  &[data-closing="true"] {
    animation-duration: ${animation.openSearchBox.duration};
    animation-fill-mode: ${animation.openSearchBox.button.fillMode};
    animation-name: ${animation.openSearchBox.button.opacity};
    animation-timing-function: ${animation.openSearchBox.easing};
  }
`;
export const ButtonCloud = styled.button`
  ...
  ${animateTransitionOut}
`;
```
Note that I use the `[data-closing="true"]` attribute selector to define animation. This will apply only when the React state value for `ui.searchButton` is `"closing"` (see Section 2.7 above).

I believe this way of setting the parameters of transition animation is easier to maintain than using CSS variables with which (1) the nested structure of variables is infeasible and (2) the content of `@keyframes` cannot be stored. (Let me know if you have a different opinion.)

With the help of Styled Components (or other CSS-in-JS solutions), just going through one set of code, we can be reminded of how animation parameters were set by someone else or by ourselves in the past.

### 3.2 Closing search box
To close the search box, I want the fade-out animation with the duration of 250ms and the linear easing.

Duration is 50ms shorter than for opening the search box. According to the Material Design guidelines:
> Transitions that close, dismiss, or collapse an element use shorter durations. Exit transitions may be faster because they require less attention than the user’s next task. — [Google (undated)](https://m2.material.io/design/motion/speed.html#controlling-speed)

For the search button to appear, I want the fade-in animation with the same duration and easing.

So we update the animation parameter object as follows:
```js
// ./utils/animation.js
import { keyframes } from "styled-components";
export const animation = {
  openSearchBox: {
    ...
  },
  // ADDED FROM HERE
  closeSearchBox: {
    duration: "250ms",
    easing: "linear",
    button: {
      opacity: keyframes`
        from {
          opacity: 0;
        }
        to {
          opacity: 1;
        }
     `,
      fillMode: "backwards"
    },
    popup: {
      opacity: keyframes`
        from {
          opacity: 1;
        }
        to {
          opacity: 0;
        }
      `,
      fillMode: "forwards"
    },
  },
  // ADDED UNTIL HERE
};
```
These parameters are applied to the two styled components as follows:
```js
// ./styled-components/DivSearchBackground.js
import styled, {css} from 'styled-components';

...

// ADDED FROM HERE
const animateTransitionOut = css`
  &[data-closing="true"] {
    animation-duration: ${animation.closeSearchBox.duration};
    animation-fill-mode: ${animation.closeSearchBox.popup.fillMode};
    animation-name: ${animation.closeSearchBox.popup.opacity};
    animation-timing-function: ${animation.closeSearchBox.easing};
  }
`;
// ADDED UNTIL HERE

export const DivSearchBackground = styled.div`
  ...
  ${animateTransitionIn}
  ${animateTransitionOut} /* ADDED */
`;
```
```js
// ./styled-components/ButtonCloud.js
import styled, {css} from 'styled-components';

...

// ADDED FROM HERE
const animateTransitionIn = css`
  animation-duration: ${animation.closeSearchBox.duration};
  animation-fill-mode: ${animation.closeSearchBox.button.fillMode};
  animation-name: ${animation.closeSearchBox.button.opacity};
  animation-timing-function: ${animation.closeSearchBox.easing};
`;
// ADDED UNTIL HERE

export const ButtonCloud = styled.button`
  ...
  ${animateTransitionIn} /* ADDED*/
  ${animateTransitionOut}
 ;
```

### 3.3 More elaborate transition animation
For _My Ideal Map_, the web app I’m building, I use more elaborate transition animation for the search box popup. That’ll be the topic of next two posts of this blog series.

However, the above simple fade-in and fade-out animation will be used for the reduced motion mode. 

## 4. Demo
The entire code is available in [the CodeSandbox for this blog post](https://codesandbox.io/s/day-21-animating-transition-with-react-23gkpg?file=/src/components/Search.js).

Compare this demo to [the one without transition animation](https://codesandbox.io/s/day-20-toggling-search-box-styled-component-ver-c3ieku). I believe it feels nice and tender with transition animation.

## References
Google (undated) “[Speed](https://m2.material.io/design/motion/speed.html)”, _Material Design_, undated.

Naimark, Jonas (2018) “[Motion Design Doesn’t Have to be Hard](https://medium.com/google-design/motion-design-doesnt-have-to-be-hard-33089196e6c2)”, _Google Design_, Sep 27, 2018.

Skytskyi, Taras (2018) “[The ultimate guide to proper use of animation in UX](https://uxdesign.cc/the-ultimate-guide-to-proper-use-of-animation-in-ux-10bd98614fa9)”, _UX Collective_, Sep 5, 2018.