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:
<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
.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.
The animation this article will implement (a screen capture of the demo for this article)
1. Introduction
1.1 Problems
On Day 22 of this blog series, I animated the opening of a search box popup with the container transform pattern of Material Design. 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).
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). In his work, pressing the close button will erase a card from top-right to bottom-left:
(A screen capture of animation by Rasmussen (2019))
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) 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:
.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
:
.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), 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), it is still safe to animate either transform
or opacity
property for the best performance (see Chikuyonok 2016, 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) 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) with the button ripple effect by Cameron (2020).
2. Ripple
2.1 Rendering as a React component
Instead of using a pseudo element as in Tudor (2021), 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:
// ./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, I defined the UI state as:
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:
const handleClickCloseButton = () => {
setUi({
searchButton: 'opening',
searchBox: 'closing',
});
};
Consequently, I can render the ripple when the search box is closing, with the following JSX:
<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, and CloseButton
is the button
element with the cross (x) icon as its label (see Day 18 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:
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:
<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:
// ./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:
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
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.
I then retrieve the popup element’s geometry with the getBoundingClientRect()
method:
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 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:
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 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 for visual explanation). So the ripple’s center coordinate relative to the top-left corner of the popup is obtained as follows:
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:
// ./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.
The ripple appears like erasing the popup (a screenshot of the demo for this article)
To do so, I revise the DivSearchBackground
styled component as follows:
// ./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) does for erasing a black card:
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, 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:
// ./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:
// ./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 allows us to store the @keyframes
at-rules as a JavaScript variable.
Then I update the SpanRipple
styled component as follows:
// ./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). 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):
// ./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, 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:
// ./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:
// ./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:
<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).
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:
<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
:
// ./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
import {DivSearchBackground} from '../styled-components/DivSearchBackground';
will import the wrapper component as well.
Now, the wrapper component is styled as follows:
// ./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:
// ./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) 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:
// ./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
}
}
};
// ./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:
// ./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:
// ./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:
// ./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:
// ./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.
References
Akar, Soner (2020) “I don’t like and understand things, which can be explained visually, by words”, Stack Overflow, Aug 27, 2020.
Cameron, Bret (2020) “How to Recreate the Ripple Effect of Material Design Buttons”, CSS-Tricks, Oct 12, 2020.
Chikuyonok, Sergey (2016) “CSS GPU Animation: Doing It Right”, Smashing Magazine, Dec 9, 2016.
Kravets, Una (2021) “Updates in hardware-accelerated animation capabilities”, Chrome Developers, Feb 22, 2021.
Kudamatsu, Masa (2020) “CSS box shadow is not to create a shadow…”, Web Dev Survey from Kyoto, Jan 3, 2020.
Rasmussen, Daniel (2019) “Close Tiles”, dribbble, 2019.
Tudor, Ana (2021) “Close tiles”, CodePen, Feb 26, 2021.
Top comments (0)