TL;DR
Sometimes it takes a while for web apps to show the result after the user taps a button. One way to tell the user that the app is doing hard work (rather than being frozen) is to flash the tapped button while the user is waiting for the result (see Section 1 for detail).
To implement this feature with React and Styled Components:
- Create a state variable with the
useState()
hook. Set its initial value to beinitial
. Once the button is clicked, set it to beloading
. Then switch it to another value once the result is shown (see Section 2 for detail). - Attach an attribute called
data-loading
to the<button>
element, and toggle its value totrue
when the state variable takes the value ofloading
. Then, use the attribute selector[data-loading="true"]
to style the animation to flash the button. This approach is more performant than usingprops
with Styled Components (see Sections 3 and 4 for detail).
Introduction
This article is a sequel to Day 12 of this blog post series, where I described how I wrote the React code so that tapping a button would show the user's location on embedded Google Maps.
After tapping the button, however, it can take a few seconds until the user sees their location on the map. So it's best to tell the user that the app is working hard to get the user's location data. Otherwise, the user will wonder if tapping the button does anything to the app. Using the phrase coined by UX design guru Norman (2013), we need to bridge the “gulf of evaluation”.
A solution I've chosen is to make the button start flashing after the button is tapped, and then to stop blinking it once the user's location is shown on the map.
This article describes how I've implemented this solution for My Ideal Map App, a web app I'm building to improve the user experience of Google Maps (see Day 1 of this blog series for more detail on My Ideal Map App).
1. Why flashing the button?
1.1 In line with design concept
Flashing light is used to signal something. Lighthouses flash on and off to send a message to ships off shore. Drivers flash their headlights to send a message to other drivers. Somehow, flashing light is associated with transportation.
Showing the user's location on the map is like the user flying up into the sky and looking down below (which is part of the design concept of My Ideal Map App; see Day 2 of this blog series). That's why I use the flight takeoff icon as the button label for showing the user location (see Section 1.3 of Day 8 of this blog series). When I see this button flashing on and off, it somehow feels right. Perhaps because flashing light is associated with transportation in general.
1.2 Why not other solutions?
There are other solutions to indicate that the app is currently working hard. One option is a loading indicator, like an animated hourglass icon. Another option is a temporary banner message shown at the bottom of the screen (i.e., what Google's Material Design calls a “snackbar”). However, My Ideal Map App embeds Google Maps full-screen. Any additional UI element will prevent the user from seeing some parts of the map while they're waiting for their location to be shown. Maybe the user notices something interesting on the map while they're waiting, and want to check that out afterwards. I don't want the user to miss such an opportunity for discovery.
Rather than adding something to the screen, therefore, it's better to animate the button that the user just tapped. It clearly connects the user's action (tapping the button) to the app's response to it.
What kind of animation, then? The web app version of Google Maps uses a rotating circle on the button to tap for showing the user location. To differentiate from Google Maps, therefore, animating the button label is not an option (My Ideal Map App aims to improve Google Maps, not to copycat it).
Which is why I've chosen to animate the entire button, rather than the button label only. And flashing the button echoes the design concept of My Ideal Map App, as described above.
2. How to implement with React
2.1 Settings
I'm using Next.js to build My Ideal Map App, and Next.js relies on React to compose the user interface (UI).
And here's the overall structure of React code for showing the user location after the user taps a button. Read comments inserted to learn what each line of code does (for more detail, see Day 12 of this blog post series):
// Create a button component that takes Google Maps instance as a prop
const LocatorButton = ({mapObject}) => {
// Define the function to run when the user taps the button
const getUserLocation = () => {
// Check if the user's browser supports Geolocation API
if (navigator.geolocation) {
// Obtain user location data from user's device
navigator.geolocation.getCurrentPosition(position => {
// Store user location data
const userLocation = {
lat: position.coords.latitude,
lng: position.coords.longitude,
};
...
// Insert code for marking the user location on the map
...
// Snap the map to the user location
mapObject.setCenter(userLocation);
});
} else {
// Insert code for legacy browsers not supporting Geolocation API
}
};
return (
<button
// run getUserLocation function upon tapping the button
onClick={getUserLocation}
type="button"
>
{/* Insert HTML for button label icon */}
</button>
);
};
Now I'm going to revise the above code to flash the button.
2.2 Defining a state variable
Making a button start flashing is a change in the UI. With React being used to build an app, a change in the UI is implemented with the React state, the change of which triggers the re-rendering of a UI component (and its child components).
So I first define a variable called status
which will store the UI status of the <LocatorButton>
component and a method setStatus
to update the UI status (by changing the value of the status
variable):
import {useState} from 'react'; // ADDED
const LocatorButton = ({mapObject}) => {
const [status, setStatus] = useState('initial'); // ADDED
const getUserLocation = () => {
...
};
...
};
where the initial value of status
is literally set to be initial
.
2.3 Updating the state variable
Then when the user clicks the button, I switch the value of status
to loading
; once the user's location is shown on the map, I switch the value of status
to watching
:
import {useState} from 'react';
const LocatorButton = ({mapObject}) => {
const [status, setStatus] = useState('initial');
const getUserLocation = () => {
if (navigator.geolocation) {
setStatus('loading'); // ADDED
navigator.geolocation.getCurrentPosition(position => {
const userLocation = {
lat: position.coords.latitude,
lng: position.coords.longitude,
};
...
// Insert code for marking the user location on the map
...
mapObject.setCenter(userLocation);
setStatus('watching'); // ADDED
});
} else {
// Insert code for legacy browsers not supporting Geolocation API
}
};
...
};
2.4 Changing the style applied to the button
To make the button flash while the status
takes the value of loading
, I add an attribute called data-loading
to the <button>
element and set its value to whether the expression status === "loading"
is true
or false
:
<button
data-loading={status === "loading"} // ADDED
onClick={getUserLocation}
type="button"
>
{/* Insert HTML for button label icon */}
</button>
Then I will style the button with the data attribute selector (see Sections 3 and 4 below).
You may wonder why I don't use className
instead. That's because I'm using CSS-in-JS (more specifically, Styled Components) to style HTML elements. See Section 4 below for more detail.
3. Define animation
3.1 CSS code
Here's “vanilla” CSS code for flashing the <button>
element while its data-loading
attribute is true
:
@keyframes flashing {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
button[data-loading="true"] {
animation: flashing 1500ms linear infinite;
}
This CSS code is adapted from Fayock (2020). To understand what it means, it's best to start from the animation
property. It sets the duration of animation to be 1.5 seconds (1500ms
) and the speed of animation to be constant (linear
), with animation repeated as long as the data-loading
attribute is true
(infinite
).
The flashing
refers to how the button's style changes during each run of the 1.5 second-long animation. It starts with the opacity of 100%, that is, the button is shown solid. During the first half of the 1.5 seconds of animation, the opacity decreases steadily to 0% so that the button slowly disappears. During the second half of the 1.5 seconds, however, the opacity increases steadily from 0% to 100% so that the button slowly reappears.
Why do I choose the duration of 1.5 seconds and the constant speed of animation? UI designers should be able to explain why they choose particular values of animation duration and speed changes (known as easing). Here's the rationale behind my design decisions.
3.2 Rationale for duration
For duration, I choose 1.5 seconds. Even though more than 0.5 second is considered to be too long for UI animation (Head 2016), even the duration of 1 second feels too fast for this particular case.
I guess the flight takeoff icon makes me imagine the airplane slowly moving on the runway to get ready for takeoff. A fast-flashing button appears incongruent to this imaginary takeoff.
Trying various lengths of duration over 1 second, I find 1.5 seconds to strike the right balance between too fast and too slow.
3.3 Rationale for easing
For easing, I choose linear
. My guideline for choosing the easing pattern is to think of real-life counterparts. Liew (2017) first enlightened me about it. He says:
Imagine yourself throwing a tennis ball into an open field. The ball leaves your hand with the maximum speed. As it moves, it loses energy, it decelerates and eventually comes to a halt. ... Now imagine you’re in a car. It’s not moving right now. When you move the car, it accelerates and goes toward its top speed.
If animation is something equivalent of the movement triggered by human body movement (e.g., animation triggered by the user's swipe of the screen), we should make animation speed starting fast and then slowing down. If it's like the movement initiated by a machine (e.g., animation triggered by pressing a button), animation speed should start slow and then accelerate.
For flashing light, however, there is no movement of physical objects involved. If so, it's natural to keep the animation speed constant. This is also a recommendation by Skytskyi (2018):
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.
So I go with linear motion.
4. How to implement with Styled Components
4.1 Setting up
To use Styled Components to style the <button>
element, I refactor the React code in Section 2 above by replacing <button>
with <Button>
:
...
import {Button} from './Button.js'; // ADDED
const LocatorButton = ({mapObject}) => {
...
return (
<Button // REVISED
data-loading={status === "loading"}
onClick={getUserLocation}
type="button"
>
{/* Insert HTML for button label icon */}
</Button> {/* REVISED */}
);
};
Then define the Button
styled component in a separate file called Button.js
(by separate a file for styling with CSS from the one for behavior with JavaScript, we can immediately tell where to look in the code base for each purpose):
// Button.js
import styled from 'styled-components';
const styleButton = `
/* Insert CSS declarations for styling the button */
`;
export const Button = styled.button`
${styleButton}
`;
Instead of writing CSS declarations directly into the Button
styled component, I first define a variable that contains a string of CSS declarations to achieve one purpose and then refer to it inside the styled component. This way, I can effectively add a “comment” on what each set of CSS declarations achieves (which is often hard to tell from the code itself). I try to avoid inserting the standard comments to the code as much as possible, because I'm sure I will forget updating them when I change the code in the future.
For more detail on how I've styled the button, see Day 7 and Day 8 of this blog series.
4.2 Animating the button
To add the CSS code for animating the button as described in Section 3 above, we first need to use the keyframes
helper function to define how animation proceeds:
import styled, {keyframes} from 'styled-components'; // REVISED
const styleButton = `
/* Insert CSS declarations for styling the button */
`;
// ADDED FROM HERE
const flashing = keyframes`
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
`;
// ADDED UNTIL HERE
export const Button = styled.button`
${styleButton}
`;
Then, set the animation
property with Styled Components's css
helper function:
import styled, {css, keyframes} from 'styled-components'; // REVISED
const styleButton = `
/* Insert CSS declarations for styling the button */
`;
const flashing = keyframes`
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
`;
// ADDED FROM HERE
const flashButton = css`
&[data-loading="true"] {
animation: ${flashing} 1500ms linear infinite;
}
`;
// ADDED UNTIL HERE
export const Button = styled.button`
${styleButton}
${flashButton} /* ADDED */
`;
We need to use the css
helper function; otherwise Styled Components cannot tell what flashing
refers to (see Styled Components documentation).
This way, the button will be flashing only when the data-loading
attribute takes the value of true
, that is, when the app is searching for the user on the map.
In case you've been using Styled Components a lot and wonder why I don't use props
in place of the data attribute selector, it's for performance reasons. See Arvanitakis (2019) for why props
is bad for performance (see also Section 3.4 of Day 8 of this blog series).
Demo
With the code explained in this article (and the previous article), I've uploaded a demo app to Cloudflare Pages. Try to click the button (when asked for permission to use location services, answer yes). You'll see the button flashing until your location is shown on the map.
If you notice something weird, file a bug report by posting a comment to this article. I'll apprecaite your help to improve My Ideal Map App! ;-)
Next step
If My Ideal Map App were a desktop app, it would be good enough to show the user's location each time the user clicks the button. However, the app is also meant to be used with a smartphone while the user is moving around in a city. It's more desirable for the app to keep track of the user location, updating the marker constantly. Next step is to implement such a feature.
Reference
Arvanitakis, Aggelos (2019) “The unseen performance costs of modern CSS-in-JS libraries in React apps”, Web Performance Calendar, Dec 9, 2019.
Fayock, Colby (2020) “Make It Blink HTML Tutorial – How to Use the Blink Tag, with Code Examples”, FreeCodeCamp, Jul 27, 2020.
Head, Val (2016) “How fast should your UI animations be?”, valhead.com, May 5, 2016.
Liew, Zell (2017) “CSS Transitions explained”, zellwk.com, Dec 13, 2017.
Norman, Don (2013) The Design of Everyday Things, revised and expanded edition, New York: Basic Books.
Skytskyi, Taras (2018) “The ultimate guide to proper use of animation in UX”, UX Collective, Sep 5, 2018.
Top comments (0)