Day 23: Animating the closing of a popup as if button ripple effect wipes it away
Tags: webdev, react, css, animation
Published: 2023-01-08 08:43:33

## TL;DR
A white popup with `mix-blend-mode: lighten` and `color: black` can be wiped away with its child whose `background-color` is `currentColor`.

Suppose you have the following DOM structure for a popup:
```html
<div class="popup" data-closing='false'>
  <span class="ripple"></span>
</div>
```
where the `data-closing` attribute will turn `true` when the user presses the close button of the popup.

Then, the following CSS code
```css
.popup[data-closing='true'] {
  background-color: white;
  color: black;
  mix-blend-mode: lighten;
}
.popup[data-closing='true'] .ripple {
  background-color: currentColor;
  animation: 300ms linear backwards expand 
}
@keyframes expand {
  from {
    transform: scale(0)
  }
  to {
    transform: scale(1)
  }
}
```
creates an impression that the popup is wiped away (as shown in the GIF below), because the `span` element is rendered with the background beneath the popup(!).

This article describes how I apply this technique to the popup for the web app I’m currently developing with React and Styled Components.


![A popup is initially shown above Google Maps. After the user presses the close button at its top-right corner, a ripple erases the popup and reveals the map beneath from top right to bottom left.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/69m32by0otkzazwr8l4n.gif)**The animation this article will implement (a screen capture of [the demo for this article](https://codesandbox.io/s/day-23-xrsoq5))**

## 1. Introduction

### 1.1 Problems
On [**Day 22 of this blog series**](https://dev.to/masakudamatsu/day-22-using-container-transform-pattern-to-animate-the-appearance-of-a-search-box-3g4n), I animated the opening of a search box popup with [the _container transform_ pattern of Material Design](https://m2.material.io/develop/android/theming/motion#container-transform). For closing the search box, then, Material Design guidelines would suggest the reversed version of the _container transform_ pattern: the popup shrinks and morphs back into the button. Such animation reinforces the idea that closing the popup will not destory it but store it inside the container.

For _My Ideal Map_ (the web app I’m building), however, a popup is likened to a cloud floating over the bird’s eye view of streets beneath (that is, the embedded Google Maps). 
![A street map overlaid with a blurry white rectangle featuring the cross mark at its top right](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fc2nhadalham1uut97cm.png)
**An embedded Google Maps overlaid with a cloud-like popup (a screenshot of the prototype version of My Ideal Map)**

We never see clouds in the sky moving in one direction and then in the reversed direction. The reversed _container transform_ animation would therefore break an illusion of the popup as a cloud.

I need another type of animation for closing the popup.

### 1.2 An inspirational UI design work
Searching for an inspiration, I stumbled upon a Dribbble work created by [Rasmussen (2019)](https://dribbble.com/shots/8813498-Close-Tiles). In his work, pressing the close button will _erase_ a card from top-right to bottom-left:
![A square card with the close button at the top-right corner gets erased from top right to bottom left after pressing the close button](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n85c0o8bp2e9tdeleiud.gif)**(A screen capture of animation by [Rasmussen (2019)](https://dribbble.com/shots/8813498-Close-Tiles))**

I think it is a clever UI design: it combines the animation of removing the card with the indication of the close button being pressed.

I immediately thought this animation could be useful to reinfoce the idea of a popup as a cloud. Pressing the top-right corner of a cloud would create the movement of air in the sky, blowing the cloud away.

### 1.3 Implementation with CSS
To implement this animation with CSS and JavaScript, [Tudor (2021)](https://codepen.io/thebabydino/pen/VwmQQwJ) proposes a clever technique. 

First, for the black card (assuming its class selector is `.card`), set its `color` and `mix-blend-mode` properties as follows:
```css
.card {
  color: #fff;
  mix-blend-mode: darken;
}
```
which turns text into “transparent”, revealing the background beneath the card. The `darken` value of `mix-blend-mode` picks the lowest RGB values among all the layers of HTML elements for each pixel. If the text color is `#fff`, that is, the brightest RGB values, the background color beneath the card will be picked as the lowest RGB values in all the pixels, with text turning“transparent” as a result.

Then, create a pseudo element of the card with its `background` property value equal to `currentColor`:
```css
.class::after {
  background: currentColor;
  content: '';
  position: absolute;
	top: 0; right: 0; bottom: 0; left: 0;
}
```
which overlays the black card with a copy of the card that is filled with the background beneath the black card. In other words, the black card appears to be erased away(!). By scaling up this pseudo element from zero to its full size with animation, therefore, we can create a visual effect that appears like erasing the black card.

[Tudor (2021)](https://codepen.io/thebabydino/pen/VwmQQwJ), however, animates the `clip-path` property for scaling up the pseudo element. While Chromium’s browser developer team seems to be working hard to make the `clip-path` animation more performant (see [Kravets 2021](https://developer.chrome.com/blog/hardware-accelerated-animations/)), it is still safe to animate either `transform` or `opacity` property for the best performance (see [Chikuyonok 2016](https://www.smashingmagazine.com/2016/12/gpu-animation-doing-it-right/#animate-transform-and-opacity-properties-only), for example). So we can instead animate the `transform` property to scale up the pseudo element.

### 1.4 Adaptations

Now, the animation by [Rasmussen (2019)](https://dribbble.com/shots/8813498-Close-Tiles) uses a _rectangle_ to erase the card. There is no rectangle in nature, certainly not in the sky. To erase the cloud-like popup, therefore, something _circular_ will look more natural.

Then I thought, how about erasing the popup with the button ripple effect, the ripple created after the pressing of the close button?

That’s what I’m going to implement below, by combining the CSS technique of [Tudor (2021)](https://codepen.io/thebabydino/pen/VwmQQwJ) with the button ripple effect by [Cameron (2020)](https://css-tricks.com/how-to-recreate-the-ripple-effect-of-material-design-buttons/).

## 2. Ripple

### 2.1 Rendering as a React component
Instead of using a pseudo element as in [Tudor (2021)](https://codepen.io/thebabydino/pen/VwmQQwJ), I will use a `span` element as a React component to erase the popup. This is because I need to specify the size and position of the ripple each time the user presses the close button. By rendering the ripple as a React component, we can set its size and position as the component’s prop values.

First, turn the `span` element into a circle and allow it to be positioned freely inside the parent element, with [Styled Components](https://styled-components.com):
```js
// ./styled-components/SpanRipple.js
import styled from 'styled-components';

const shapeRipple = `
  border-radius: 50%;
`;
const positionRipple = `
  position: absolute;
`;
export const SpanRipple = styled.span`
  ${shapeRipple}
  ${positionRipple}
`;
```

Next, render this ripple as a React component. On [**Day 21 of this blog series**](https://dev.to/masakudamatsu/day-21-coding-transition-animation-with-react-from-scratch-244f), I defined the UI state as:
```js
  const [ui, setUi] = useState({
    searchButton: 'open',
    searchBox: 'closed',
  });
```
When the search box was closing, the `ui.searchBox` value would change to `'closing'` while the `ui.searchButton` value would turn to `'opening'`. This was done with the click event handler:
```js
const handleClickCloseButton = () => {
    setUi({
      searchButton: 'opening',
      searchBox: 'closing',
    });
  };
```

Consequently, I can render the ripple when the search box is closing, with the following JSX:
```js
<DivSearchBackground
  data-closing={ui.searchBox === 'closing'}
>
  <CloseButton
    handleClick={handleClickCloseButton}
  />
  {ui.searchBox === 'closing' ? (
    <SpanRipple
      id="ripple"
    />
  ) : null}
</DivSearchBackground>
```
where `DivSearchBackground` is the `div` element styled as the cloud-like popup on [**Day 19**](https://dev.to/masakudamatsu/day-19-styling-a-popup-like-clouds-2m04), and `CloseButton` is the `button` element with the cross (x) icon as its label (see [**Day 18**](https://dev.to/masakudamatsu/day-18-building-a-close-button-with-ripple-effect-as-a-react-component-1n62) for detail).

### 2.2 Styling dynamically
I need to style the ripple dynamically. For one thing, the ripple needs to be as large as the popup whose size can vary when the desktop user changes the browser window size (otherwise, the ripple will not erase the entire popup). For another, the ripple needs to start at the location where the user presses the button.

We can do this by editing the click event handler for the close button as follows:
```js
const handleClickCloseButton = ({ 
  rippleDiameter,      // ADDED
  ripplePositionLeft,  // ADDED
  ripplePositionTop,   // ADDED
}) => {
  setUi({
    searchButton: 'opening',
    searchBox: 'closing',
    rippleDiameter,         // ADDED
    ripplePositionLeft,     // ADDED
    ripplePositionTop,      // ADDED
  });
};
```
The idea is that the `CloseButton` component will obtain the three values of `rippleDiameter` (the diameter of the ripple), `ripplePositionLeft` and `ripplePositionTop` (the x and y coordinates of the top-left corner of the ripple element, respectively) in response to the user’s click of the button. The `handleClickCloseButton` function, passed to the `CloseButton` component as a prop, will then be executed with these three values as its arguments. Then, it will update the UI state with the three values.

Then I use the UI state values to style the ripple dynamically:
```js
<DivSearchBackground
  data-closing={ui.searchBox === 'closing'}
>
  <CloseButton
    handleClick={handleClickCloseButton}
  />
  {ui.searchBox === 'closing' ? (
    <SpanRipple
      id="ripple"
      style={{                       // ADDED
        height: ui.rippleDiameter,   // ADDED
        left: ui.ripplePositionLeft, // ADDED
        top: ui.ripplePositionTop,   // ADDED
        width: ui.rippleDiameter,    // ADDED
      }}                             // ADDED
    />
  ) : null}
  // ADDED UNTIL HERE
</DivSearchBackground>
```
When the UI state for the search box popup becomes `closing`, the `SpanRipple` styled component will be rendered with its `height`, `left`, `top`, and `width` CSS values supplied from the UI state values.

In the file for the `CloseButton` component, then, the `handleClickCloseButton` function is passed as the `handleClick` prop and will be executed inside the button click handler:
```js
// ./components/CloseButton.js
export const CloseButton = ({handleClick}) => {
  const clickHandler = event => {

    // To be inserted here: the calculation of rippleDiameter, ripplePositionLeft, and ripplePositionTop

    handleClick({    
      rippleDiameter,
      ripplePositionLeft,
      ripplePositionTop
    });
  };
  return (
    <button
      onClick={clickHandler}
      type="button"
    >
      <!-- SVG code for cross mark (x) (omitted) -->
    </button>
  )
};
```

### 2.3 Calculating geometry
Now, let’s get into the detail of calculating the size and position of the ripple. 

Inside the handler, I first retrieve the popup element which is the parent of the close button:
```js
const clickHandler = event => {
  const popup = event.currentTarget.offsetParent; 
}
```
where `event.currentTarget` refers to the button element, the element to which the event handler is attached (if we use `event.target` instead, this will refer to the `svg` element that triggers the click event). And the [`offsetParent`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent) property of an HTML element] refers to the popup `div` element because its `position` property is set to be `absolute` (see [**Day 19 of this blog series**](https://dev.to/masakudamatsu/day-19-styling-a-popup-like-clouds-2m04).

I then retrieve the popup element’s geometry with the [`getBoundingClientRect()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect) method:
```js
const clickHandler = event => {
  const popup = event.currentTarget.offsetParent; 

  // ADDED FROM HERE
  const {
    left: popupLeft,
    top: popupTop,
    height: popupHeight,
    width: popupWidth,
  } = popup.getBoundingClientRect();
  // ADDED UNTIL HERE
}
```
where I use the [destructuring assignment](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) to simplify the code.

I want the ripple’s radius to be as large as the popup’s diagonal so that the ripple in its full size completely covers the popup rectangle:
```js
const clickHandler = event => {
  const popup = event.currentTarget.offsetParent; 
  const {
    left: popupLeft,
    top: popupTop,
    height: popupHeight,
    width: popupWidth,
  } = popup.getBoundingClientRect();

  // ADDED FROM HERE
  const popupDiagonalLength = Math.sqrt(
    Math.pow(popupWidth, 2) + Math.pow(popupHeight, 2),
  );
  const rippleRadius = popupDiagonalLength;
  // ADDED UNTIL HERE
}
```
where I use the [Pythagorean Theorem](https://www.calculator.net/pythagorean-theorem-calculator.html) to obtain the length of a diagonal.

For positioning the ripple, we need the location of its center relative to the top-left corner of the popup (so we can use `left` and `top` CSS properties to position the ripple). The location of the user’s click relative to the top-left corner of the _browser window_ is obtained with `event.clientX` and `event.clientY` (see [Akar 2020](https://stackoverflow.com/a/63611994/11847654) for visual explanation). So the ripple’s center coordinate relative to the top-left corner of the _popup_ is obtained as follows:
```js
const clickHandler = event => {
  const popup = event.currentTarget.offsetParent; 
  const {
    left: popupLeft,
    top: popupTop,
    height: popupHeight,
    width: popupWidth,
  } = popup.getBoundingClientRect();
  const popupDiagonalLength = Math.sqrt(
    Math.pow(popupWidth, 2) + Math.pow(popupHeight, 2),
  );
  const rippleRadius = popupDiagonalLength;

  // ADDED FROM HERE
  const rippleCenter = {
    x: event.clientX - popupLeft,
    y: event.clientY - popupTop,
  };
  // ADDED UNTIL HERE
}
```
Now we are ready to set the three arguments for the `handleClick` prop passed to the `CloseButton` component:
```js
// ./components/CloseButton.js
const clickHandler = event => {
  const popup = event.currentTarget.offsetParent; 
  const {
    left: popupLeft,
    top: popupTop,
    height: popupHeight,
    width: popupWidth,
  } = popup.getBoundingClientRect();
  const popupDiagonalLength = Math.sqrt(
    Math.pow(popupWidth, 2) + Math.pow(popupHeight, 2),
  );
  const rippleRadius = popupDiagonalLength;
  const rippleCenter = {
    x: event.clientX - popupLeft,
    y: event.clientY - popupTop,
  };
  // REVISED FROM HERE
  handleClick({
    rippleDiameter: `${Math.round(rippleRadius * 2)}px`,
    ripplePositionLeft: `${Math.round(rippleCenter.x - rippleRadius)}px`,
    ripplePositionTop: `${Math.round(rippleCenter.y - rippleRadius)}px`,
  });
  // REVISED UNTIL HERE
};
```
where the `ripplePositionLeft` and `ripplePositionTop` are obtained by subtracting the ripple radius from the ripple center’s coordinate.

That’s all for sizing and locating the ripple to fully cover the popup.

### 2.4 Making the ripple “transparent”

Next, copy the image of the embedded Google Maps beneath the popup onto the ripple’s surface so that the animated ripple will appear like erasing the popup.
![A hazy-looking white rectangle on top of Google Maps is cut out with a circular arc from its top right](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4n4lnq99am1v8aev236q.png)**The ripple appears like erasing the popup (a screenshot of [the demo for this article](https://codesandbox.io/s/day-23-xrsoq5))**

To do so, I revise the `DivSearchBackground` styled component as follows:
```js
// ./styled-components/DivSearchBackground.js
const revealMapBeneath = `
  &[data-closing='true'] {
    color: black;
    mix-blend-mode: lighten;
  }
  &[data-closing='true'] [id="ripple"] {
    background-color: currentColor;
  }
`;

export const DivSearchBackground = styled.div`
  ...
  ${revealMapBeneath}
`;
```
This is different from [Tudor (2021)](https://codepen.io/thebabydino/pen/VwmQQwJ) does for erasing a _black_ card: 
```css
color: #fff;
mix-blend-mode: darken;
```
But I have a _white_ popup. The `lighten` value of `mix-blend-mode` does the opposite to `darken`: picking the _highest_ RGB values for each pixel. By specifyiing the text color as `black`, then, the `mix-blend-mode: lighten` will pick the RGB values beneath the element, that is, the embedded Google Maps.

### 2.5 Animating the ripple
Finally, I set animation parameters so the ripple will expand from where the user presses the button into the area covering the entire popup. 

As explained in Section 3 of [**Day 21 of this blog series**](https://dev.to/masakudamatsu/day-21-coding-transition-animation-with-react-from-scratch-244f), I find it easy to work with CSS animation if I put all the parameters together in one place as a Javascript object, with the help of [Styled Component's `keyframes` function](https://styled-components.com/docs/basics#animations):
```js
// ./utils/animation.js
import {keyframes} from 'styled-components';
export const animation = {
  openSearchBox: {
    // Code omitted for brevity (see Day 22 of this blog series)
  },
  closeSearchBox: {} // ADDED
};
```

The following parameters will scale the ripple from 0% to its full size for the duration of 300ms with the linear easing:
```js
// ./utils/animation.js
import {keyframes} from 'styled-components';
export const animation = {
  openSearchBox: {
    // Code omitted for brevity
  },
  closeSearchBox: {
    // ADDED FROM HERE
    duration: '300ms',
    easing: 'linear',
    ripple: {
      scale: keyframes`
        from {
          transform: scale(0);
        }
        to {
          transform: scale(1);
        }
      `,
      fillMode: 'backwards'
    },
    // ADDED UNTIL HERE
  },
};
```
where the [`keyframes` helper from Styled Components](https://styled-components.com/docs/basics#animations) allows us to store the `@keyframes` at-rules as a JavaScript variable.

Then I update the `SpanRipple` styled component as follows:
```js
// ./styled-components/SpanRipple.js
import styled, {css} from 'styled-components'; // REVISED
import {animation} from '../utils/animation'; // ADDED

const shapeRipple = `
  border-radius: 50%;f
`;
const positionRipple = `
  position: absolute;
`;

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

export const SpanRipple = styled.span`
  ${shapeRipple}
  ${positionRipple}
  ${animateRipple} /* ADDED */
`;
```
By default, any HTML element has `transform: scale(1)`. To avoid this from appearing before animation starts, the `animation-fill-mode` needs to be set as `backwards`.

For duration, I’ve settled with `300ms` after trial and error. If duration is too short, it is not very noticeable for the popup to be erased by a ripple because animation is too quick. On the other hand, a duration longer than 300ms will prevent the user from moving on to next for too long. The duration of `300ms` strikes the right balance between these two concerns.

For easing, I initially tried the decelerated easing used for opening the popup (see the section entitled “Easing” in [**Day 22 of this blog series**](https://dev.to/masakudamatsu/day-22-using-container-transform-pattern-to-animate-the-appearance-of-a-search-box-3g4n)). But this caused most of the animation to be too quick for the user to notice the ripple erasing the popup. Linear easing mitigates this problem.

## 3. Popup
The code that's been written so far will do most of the job. But there are a few other things to make animation neat, by styling the popup.

### 3.1 Containing the ripple inside the popup
First, I need to contain the ripple inside the popup, by applying `overflow: hidden` to the popup (that is, `DivSearchBackground` styled component):
```js
// ./styled-components/DivSearchBackground.js

...

const containRippleWithin = `
  &[data-closing='true'] {
    overflow: hidden;
  }
`; //

export const DivSearchBackground = styled.div`
  ...
  ${revealMapBeneath}
  ${containRippleWithin} /* ADDED */
`;
```
Without this, the ripple will be overflown beyond the viewport, causing the scroll bars to appear and consequently the layout to shift temporarily.

### 3.2 Handling cross-browser inconsistency
The code now works beautifully with Safari and Firefox. With Chrome, however, the popup won’t be erased completely: the revealed Google Maps beneath the popup appears blurred until the animation is over.

Apparently, Chrome implements `mix-blend-mode` differently from Safari and Firefox, when `background-filter` is also used. On [**Day 19**](https://dev.to/masakudamatsu/day-19-styling-a-popup-like-clouds-2m04), I applied the _glassmorphism_ technique to make the popup background appear like clouds. This technique involves the use of `background-filter: blur(8px)`. When implementing `mix-blend-mode`, Chrome appears to incorporate the `background-filter` result into the calculation while Safari and Firefox seem to ignore it.

To minimize the impact of seeing the blurred image of Google Maps for Chrome users, therefore, I need to make the popup to fade out while being “erased” by the ripple:
```js
// ./utils/animation.js
import {keyframes} from 'styled-components';
export const animation = {
  openSearchBox: {
    ...
  },
  closeSearchBox: {
    duration: '300ms',
    easing: 'linear',
    // ADDED FROM HERE
    popup: {
      opacity: keyframes`
        0% {
          opacity: 1;
        }
        100% {
          opacity: 0;
        }
      `,
      fillMode: 'forwards'
    },
    // ADDED UNTIL HERE
    ripple: {
      scale: keyframes`
        from {
          transform: scale(0);
        }
        to {
          transform: scale(1);
        }
      `,
      fillMode: 'backwards'
    },
  },
};
```
And update the `DivSearchBackground` styled component as follows:
```js
// ./styled-components/DivSearchBackground.js
import styled, {css} from 'styled-components';
import {animation} from '../utils/animation';

// 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 */
`;
```
Remember that `DivSearchBackground` is rendered with the following JSX:
```js
<DivSearchBackground
  data-closing={ui.searchBox === 'closing'}
>
  ...
</DivSearchBackground>
```
So the `[data-closing="true"]` attribute selector only applies during the animation of closing the popup.

### 3.3 Handling the popup edges
So far so good as long as the popup is full-screen or the popup has no noticeable box shadow.

However, for aesthetic reasons, the cloud-like popup for _My Ideal Map_ requires a white box shadow that imitates the edges of clouds (see the section entitled “Edge treatment” in [**Day 19 of this blog series**](https://dev.to/masakudamatsu/day-19-styling-a-popup-like-clouds-2m04)).

Consequently, when the popup only occupies part of the screen, the ripple will not erase the box shadow of the popup. This is because the ripple is a child element of the popup that is styled with `overflow: hidden` (see Section 3.1 above).

To solve this issue, I need a wrapper `div` element that contains the box shadow of the popup. Then, render the ripple as a child of the wrapper element, rather than that of the popup, so that the ripple will cover not only the popup but also its box shadow.

More specifically, I first need to change the JSX code as follows:
```js
<DivSearchBackground.Wrapper                // ADDED
  data-closing={ui.searchBox === 'closing'} // ADDED
>                                          {/* ADDED */}
  <DivSearchBackground
    data-closing={ui.searchBox === 'closing'}
  >
    <CloseButton
      handleClick={handleClickCloseButton}
    />
  </DivSearchBackground>                   {/* MOVED */}
  {ui.searchBox === 'closing' ? (
    <SpanRipple
      id="ripple"
      style={{
        height: ui.rippleDiameter,
        left: ui.ripplePositionLeft,
        top: ui.ripplePositionTop,
        width: ui.rippleDiameter,
      }}
    />
  ) : null}
</DivSearchBackground.Wrapper>             {/* ADDED */}
```
The wrapper is defined as `DivSearchBackground.Wrapper` in the file for `DivSearchBackground`:
```js
// ./styled-components/DivSearchBackground.js
import styled, { css } from "styled-components";

...

export const DivSearchBackground = styled.div`
  /* Omitted for brevity */
`;

// ADDED FROM HERE
DivSearchBackground.Wrapper = styled.div`
  /* To be styled shortly */
`;
// ADDED UNTIL HERE
```
This way, the code itself conveys the idea that this wrapper is inseparable from the popup itself. Also, I don’t have to add another `import` statement: the following line of code
```js
import {DivSearchBackground} from '../styled-components/DivSearchBackground';
```
will import the wrapper component as well.

Now, the wrapper component is styled as follows:
```js
// ./styled-components/DivSearchBackground.js
import styled, { css } from "styled-components";

const placeOverMap = `
  position: absolute;
`;
const setOuterSize = `
  bottom: 10%;
  left: 10%;
  right: 10%;
  top: 10%;
`;
const revealMapBeneath = `
  &[data-closing='true'] {
    color: black;
    mix-blend-mode: lighten;
  }
  &[data-closing='true'] [id="ripple"] {
    background-color: currentColor;
  }
`;
const containRippleWithin = `
  overflow: hidden;
`;

export const DivSearchBackground = styled.div`
  /* Omitted for brevity */
`;

DivSearchBackground.Wrapper = styled.div`
  ${placeOverMap}         /* ADDED */
  ${setOuterSize}         /* ADDED */
  ${revealMapBeneath}     /* ADDED */
  ${containRippleWithin}  /* ADDED */
`;
```
Now the wrapper plays a role of making the ripple copy the image of the embedded Google Maps beneath the popup and containing the ripple inside.

Then, the popup itself is styled as follows:
```js
// ./styled-components/DivSearchBackground.js
import styled, { css } from "styled-components";

...

const setBackground = `
  --blur-radius: 8px;
  background-color: var(--popup-background-color);
  -webkit-backdrop-filter: blur(var(--blur-radius));
  backdrop-filter: blur(var(--blur-radius));
  box-shadow: 0 0 var(--blur-radius) var(--blur-radius) var(--popup-background-color);

  /* The CSS code to support legacy browsers are omitted */
`;

// ADDED FROM HERE
const setInnerSize = `
  bottom: calc(var(--blur-radius) * 2);
  left: calc(var(--blur-radius) * 2);
  position: absolute;
  right: calc(var(--blur-radius) * 2);
  top: calc(var(--blur-radius) * 2);
`;
// ADDED UNTIL HERE

const animateTransitionIn = css`
  /* Omitted for brevity */
`;
const animateTransitionOut = css`
  &[data-closing="true"] {
    /* Omitted for brevity */
  }
`;

export const DivSearchBackground = styled.div`
  ${setBackground}
  ${setInnerSize}         /* ADDED */
  ${animateTransitionIn}
  ${animateTransitionOut}
`;

DivSearchBackground.Wrapper = styled.div`
  /* Omitted for brevity */
`;
```
The popup size is now set so that there will be 16px wide space between the wrapper’s (transparent) border and the the popup’s. It ensures that the entire box shadow (whose blur radius and spread radius of 8px each implies its width of 16px) is included inside the wrapper. (See my article [Kudamatsu (2020)](https://medium.com/web-dev-survey-from-kyoto/css-box-shadow-is-not-to-create-a-shadow-but-to-place-the-duplicate-copy-beneath-11e849d000c0) for how the blur radius and spread radius of `box-shadow` shapes the shadow length).

This way, the ripple will erase the entire popup including its blurry white edges.

## 4. Search button
To animate the reappearance of the button to open the search box popup, apply the `keyframes` to simply fade in:
```js
// ./utils/animation.js
import {keyframes} from 'styled-components';
export const animation = {
  openSearchBox: {
    // omitted for brevity
  },
  closeSearchBox: {
    duration: '300ms',
    easing: 'linear',
    // ADDED FROM HERE
    button: {
      opacity: keyframes`
        0% {
          opacity: 0; 
        }
        100% {
          opacity: 1;
        }
      `,
      fillMode: 'backwards',
    },
    // ADDED UNTIL HERE
    popup: {
      // Omitted for brevity
    },
    ripple: {
      // Omitted for brevity
    }
  }
};
```
```js
// ./styled-components/ButtonCloud.js
import styled, {css} from 'styled-components';
import {animation} from '../utils/animation';

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};

`;

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

## 5. Reduced motion preference
For uses who prefer the _reduced motion_ mode, we simply fade out the popup and fade in the search button, removing the ripple effect.

Once the ripple effect is removed, however, the duration of 300ms is slightly too long. We shorten it to 250ms.

So we replace the duration parameter for the reduced motion preference:
```js
// ./utils/animation.js
...
export const animation = {
  openSearchBox: {
    // omitted for brevity
  },
  closeSearchBox: {
    duration: '300ms',
    easing: 'linear',
    button: {
      // omitted for brevity
    },
    popup: {
      opacity: keyframes`
        0% {
          opacity: 1;
        }
        100% {
          opacity: 0;
        }
      `,
      fillMode: 'forwards'
    },
    ripple: {
      // omitted for brevity
    },
    // ADDED FROM HERE
    reducedMotion: {
      duration: '250ms',
    }
    // ADDED UNTIL HERE
  },
};
```
Accordingly, I revise the styled components as follows. For the ripple, I simply remove the animation and keep the ripple scaled to be zero so that the user will not see it at all: 
```js
// ./styled-components/SpanRipple.js
...
const animateRipple = css`
  animation-duration: ${animation.closeSearchBox.duration};
  animation-fill-mode: ${animation.closeSearchBox.ripple.fillMode};
  animation-name: ${animation.closeSearchBox.ripple.scale};
  animation-timing-function: ${animation.closeSearchBox.easing};
  /* ADDED FROM HERE */
  @media (prefers-reduced-motion: reduce) {
    animation-name: none;
    transform: scale(0);
  }
  /* ADDED UNTIL HERE */
`;
...
```
For the button to open the search box, I override the duration:
```js
// ./styled-components/ButtonCloud.js
...
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 FROM HERE */
  @media (prefers-reduced-motion: reduce) {
    animation-duration: ${animation.closeSearchBox.reducedMotion.duration};
  }
  /* ADDED UNTIL HERE */
`;
...
```
The same applies to the popup:
```js
// ./styled-components/DivSearchBackground.js
...
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 FROM HERE */
    @media (prefers-reduced-motion: reduce) {
      animation-duration: ${animation.closeSearchBox.reducedMotion.duration};
    }
    /* ADDED UNTIL HERE */
  }
`;
// ADDED UNTIL HERE
...
```

## Demo
The entire code is available as [the CodeSandbox demo for this article](https://codesandbox.io/s/day-23-xrsoq5).


## References
Akar, Soner (2020) “[I don’t like and understand things, which can be explained visually, by words](https://stackoverflow.com/a/63611994/11847654)”, _Stack Overflow_, Aug 27, 2020.

Cameron, Bret (2020) “[How to Recreate the Ripple Effect of Material Design Buttons](https://css-tricks.com/how-to-recreate-the-ripple-effect-of-material-design-buttons/)”, _CSS-Tricks_, Oct 12, 2020.

Chikuyonok, Sergey (2016) “[CSS GPU Animation: Doing It Right](https://www.smashingmagazine.com/2016/12/gpu-animation-doing-it-right/#animate-transform-and-opacity-properties-only)”, _Smashing Magazine_, Dec 9, 2016.

Kravets, Una (2021) “[Updates in hardware-accelerated animation capabilities](https://developer.chrome.com/blog/hardware-accelerated-animations/)”, _Chrome Developers_, Feb 22, 2021.

Kudamatsu, Masa (2020) “[CSS box shadow is not to create a shadow…](https://medium.com/web-dev-survey-from-kyoto/css-box-shadow-is-not-to-create-a-shadow-but-to-place-the-duplicate-copy-beneath-11e849d000c0)”, _Web Dev Survey from Kyoto_, Jan 3, 2020.

Rasmussen, Daniel (2019) “[Close Tiles](https://dribbble.com/shots/8813498-Close-Tiles)”, _dribbble_, 2019.

Tudor, Ana (2021) “[Close tiles](https://codepen.io/thebabydino/pen/VwmQQwJ)”, _CodePen_, Feb 26, 2021.