DEV Community

Cover image for Day 13: Flashing tapped button while user is waiting (with React and Styled Components)
Masa Kudamatsu
Masa Kudamatsu

Posted on • Originally published at Medium

Day 13: Flashing tapped button while user is waiting (with React and Styled Components)

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:

  1. Create a state variable with the useState() hook. Set its initial value to be initial. Once the button is clicked, set it to be loading. Then switch it to another value once the result is shown (see Section 2 for detail).
  2. Attach an attribute called data-loading to the <button> element, and toggle its value to true when the state variable takes the value of loading. Then, use the attribute selector [data-loading="true"] to style the animation to flash the button. This approach is more performant than using props 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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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 = () => {
    ...
  };
  ...
};
Enter fullscreen mode Exit fullscreen mode

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     
    }
  };
  ...
};
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 */}
  );
};
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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}
`;
Enter fullscreen mode Exit fullscreen mode

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 */
`;
Enter fullscreen mode Exit fullscreen mode

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)