DEV Community

Cover image for Day 18: Building a close button with ripple effect as a React component
Masa Kudamatsu
Masa Kudamatsu

Posted on • Originally published at Medium

Day 18: Building a close button with ripple effect as a React component

TL;DR

This article goes through the steps to build a close button as a React component.

You will also learn

  1. HTML coding best practices for close buttons (Section 1)
  2. How to style an SVG icon button (Section 3)
  3. Best practices for styling the button focus state (Section 4)
  4. How to display the ripple animation after clicking the button (Section 5)

The result is available as a CodeSandbox demo.

Introduction

The web app I’m developing features a view of Google Maps embedded in full-screen. For the user to search a place on the map, to save a searched place, to see the detail of a saved place, and to change the user setting, the app needs a popup window to provide information and relevant buttons to the user.

One of those buttons is the close button, that little cross icon at the top-right corner of a popup. Since it will be used for all these popups, it is best to build it as a reusable component.

It turns out that making a close button component involves quite a bit of web dev techniques. In this article, I’d like to share what I have learned out of my experience of creating a close button as a React component.

1. HTML

When I start composing a React component, the first thing I do is to forget about React. Instead I solely focus on HTML. What HTML code do I want the React component to render? That’s the question I ask myself.

A little investigation into the best practices of coding HTML for the close button led me to discover two excellent articles on the subject: Stanton (2020) and Matuzovic (2020). Below is what I digest from them.

1.1 SVG Icon

Using the character as an “icon” for the close button is not a good idea in terms of accessibility: for screen reader users, it does not mean “close something”. Matuzovic (2020) writes:

✕ doesn’t represent close or crossed out, it’s the multiplication sign, like in 2 ✕ (times) 2. Don’t use it for close buttons.

Instead, the best approach is to use an SVG icon, which itself has no literal meaning. For my purpose, using SVG is also beneficial in terms of visual consistency: since I’m developing a web app that heavily features Google Maps (see Day 1 of this blog series), an SVG icon taken from Google‘s Material Icons will achieve visual consistency in my web app design.

Material Icons’s “Close” icon has the following SVG code:

<svg
  xmlns="http://www.w3.org/2000/svg"
  height="24px"
  viewBox="0 0 24 24"
  width="24px"
  fill="#000000"
>
  <path d="M0 0h24v24H0V0z" fill="none" />
  <path
    d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"
  />
</svg>
Enter fullscreen mode Exit fullscreen mode

What I actually need is the viewBox attribute value and the second <path> element.

  • The xmlns attribute is unnecessary if the SVG code is embedded in HTML (see MDN Web Docs on <svg>).
  • The height and width attributes are redundant when we use CSS to set the size (see Bellamy-Royds 2015).
  • So is the fill attribute (we can use CSS).
  • And I always wonder what the first <path> element is for, whenever I download Material Icons SVG images (please comment to this article if you have an idea).

So the first attempt to code HTML for the close button is something like this:

<button type="button">
  <svg viewBox="0 0 24 24">
    <path
      d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"
    />
  </svg>
</button>
Enter fullscreen mode Exit fullscreen mode

1.2 Button label for screen reader users

However, the above HTML code would confuse screen reader users: the button has no label. To let them know what the button does, we need to add aria-label:

<button aria-label="Close" type="button"> <!-- REVISED -->
  <svg aria-hidden="true" viewBox="0 0 24 24"> <!-- REVISED -->
    <path
      d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"
    />
  </svg>
</button>
Enter fullscreen mode Exit fullscreen mode

Here I apply the lesson learned on Day 16 of this blog series: “Icon buttons should be labelled with aria-label” (see the link for detail). It also happens to be what Stanton (2020) concluses as the best practice for close buttons.

You might wonder why the <svg> element is hidden for screen readers with aria-hidden="true". Once the button is labeled with aria-label, screen reader users do not need to know whether there is an SVG icon on the button or not. So we can remove it for them.

1.3 “Close what?”

One additional consideration comes from the following passage from Matuzovic (2020):

Sometimes it makes sense to use more descriptive labels like “Close dialog”, “Close gallery”, or “Close ad”.

So we need some flexibility in the value of aria-label, to make it reusable in different contexts. That means the aria-label value needs to be a prop of the React component.

2. React component

To reuse the above HTML structure whenever we render a close button, we can create a Reaxt component. Here‘s how I go about it.

2.1 ARIA attributes as props

To allow aria-label to differ, I make it as a prop:

export const CloseButton = ({ ariaLabel }) => {
  return (
    <button aria-label={ariaLabel} type="button">
      <svg aria-hidden="true" viewBox="0 0 24 24">
        <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
      </svg>
    </button>
  );
};
Enter fullscreen mode Exit fullscreen mode

To make it explicit that aria-label is always required, I add PropTypes:

import PropTypes from "prop-types"; // ADDED

export const CloseButton = ({ ariaLabel }) => {
  return (
    <button
      aria-controls={ariaControls}
      aria-expanded={ariaExpanded}
      aria-label={ariaLabel}
      type="button"
    >
      <svg aria-hidden="true" viewBox="0 0 24 24">
        <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
      </svg>
    </button>
  );
};

// ADDED FROM HERE
CloseButton.propTypes = {
  ariaLabel: PropTypes.string.isRequired,
};
// ADDED UNTIL HERE
Enter fullscreen mode Exit fullscreen mode

So, in case I forget giving the ariaLabel prop to the CloseButton component, React will complain in the JavaScript console. (Which indeed happened when I was creating a demo for this article with CodeSandbox.)

Maybe it’s time for me to start using TypeScript, though.

2.2 Click handler

Of course, we need to attach a click event handler to the <button> element so that the pressing of it will close something.

For this purpose, I add handleClick prop:

import PropTypes from "prop-types";

export const CloseButton = ({
  ariaLabel,
  handleClick, // ADDED
}) => {
  return (
    <button
      aria-label={ariaLabel}
      onClick={handleClick} // ADDED
      type="button"
    >
      <svg aria-hidden="true" viewBox="0 0 24 24">
        <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
      </svg>
    </button>
  );
};

CloseButton.propTypes = {
  ariaLabel: PropTypes.string.isRequired,
  clickHandler: PropTypes.string.isRequired, // ADDED
};
Enter fullscreen mode Exit fullscreen mode

Again, I add the prop type so React will complain if I forget adding the clickHandler prop.

Now, let’s move on to style the close button.

3. CSS

3.1 Using Styled Components

To style React components, I always use Styled Components:

import PropTypes from 'prop-types';

import { Button } from '../styled-components/Button'; // ADDED

export const CloseButton = ({
  ariaLabel,
  handleClick
}) => {
  return (
    <Button // REVISED
      aria-label={ariaLabel}
      onClick={handleClick}
      type="button"
    >
      <svg aria-hidden="true" viewBox="0 0 24 24">
        <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
      </svg>
    </Button> {/* REVISED */}
  );
};

CloseButton.propTypes = {
  ariaLabel: PropTypes.string.isRequired,
  clickHandler: PropTypes.string.isRequired,
};
Enter fullscreen mode Exit fullscreen mode

where the Button styled component is defined in a separate file:

// styled-components/Button.js

import styled from "styled-components";

export const Button = styled.button`
  /* CSS declarations to be inserted */
`;
Enter fullscreen mode Exit fullscreen mode

In this file, let’s add CSS declarations to style the close button step by step.

3.2 Reset the default button style

The button element has a particular set of default style. I want to reset it first:

// styled-components/Button.js

import styled from "styled-components";

// ADDED FROM HERE
const resetStyle = `
  background-color: rgba(255, 255, 255, 0);
  border: none;
`;
// ADDED UNTIL HERE

export const Button = styled.button`
  ${resetStyle} /* ADDED */
`;
Enter fullscreen mode Exit fullscreen mode

The kind of the close button that I’m after is minimally styled, just a cross mark icon without borders or solid background color.

For transparency, I always use the transparent white (i.e., rgba(255, 255, 255, 0)) rather than the named color keyword of transparent ever since I learned about cross-browser inconsistency: Safari interprets transparent as the transparent black (see Coyier 2017).

And I use a variable resetStyle to define a set of CSS declarations for code readability. This is what I like about CSS-in-JS.

3.3 Set clickable area

Next, let‘s shape the close button.

Although there is no border, when the keyboard user focuses it with the Tab key, I want it to show a circular focus ring, rather than a square one. The border-radius: 50%; does the job.

For the size of the button, Google recommends the 48x48 pixel area as a minimum touch target size, because:

“The 48x48 pixel area corresponds to around 9mm, which is about the size of a person’s finger pad area.” (Gash et al. 2020)

So the diameter of the button is set to be 48px.

Therefore, we have:

// styled-components/Button.js

import styled from 'styled-components';

...

// ADDED FROM HERE
const setClickableArea = `
  --minimum-target-size: 48px;
  border-radius: 50%;
  height: var(--minimum-target-size);
  width: var(--minimum-target-size);
`;
// ADDED UNTIL HERE

export const Button = styled.button`
  ${resetStyle}
  ${setClickableArea} /* ADDED */
`;
Enter fullscreen mode Exit fullscreen mode

Using a CSS variable name (--minimum-target-size), I can embed the logic behind the numbers into the CSS code.

3.4 Center-align the icon

Now we need to position the SVG icon inside the clickable area. I want it to be center-aligned both vertically and horizontally. So I use flexbox:

// styled-components/Button.js

import styled from 'styled-components';

...

// ADDED FROM HERE
const centerAlignIcon = `
  align-items: center;
  display: flex;
  justify-content: center;
`;
// ADDED UNTIL HERE

export const Button = styled.button`
  ${resetStyle}
  ${setClickableArea}
  ${centerAlignIcon} /* ADDED */
`;
Enter fullscreen mode Exit fullscreen mode

3.5 Style the icon

Now let’s style the icon:

// styled-components/Button.js

import styled from 'styled-components';

...

// ADDED FROM HERE
const styleIcon = `
  & svg {
    fill: var(--button-label-color-default);
    height: calc(var(--minimum-target-size) * 0.75);
    width: calc(var(--minimum-target-size) * 0.75);
  }
`;
// ADDED UNTIL HERE

export const Button = styled.button`
  ${resetStyle}
  ${setClickableArea}
  ${centerAlignIcon}
  ${styleIcon} /* ADDED */
`;
Enter fullscreen mode Exit fullscreen mode

To specify the color of an SVG icon, we need the fill property. I use a CSS variable so it will switch between light and dark modes (see Dodds 2020 for how).

The SVG icon’s height and width are set to be three-quarters of the minimum target size of 48px, which makes the close button look decent when the focus ring appears around it. Rather than hard-coding 36px, using the calc() CSS function and the CSS variable of --minimum-target-size makes clear the logic behind the choice of 36px.

That’s all for the default style.

Notice that I don’t include any CSS code for positioning the close button at the top-right corner of a dialog. That’s the job for the CSS code of a dialog element (<div role="dialog>). We will see an example of it in the future article of this blog series.

4. Focus state style

4.1 Consistency with cloud buttons

On Day 7 of this blog series (see Section 8), I styled the focus ring for cloud-shaped buttons in a shade of blue rgb(69,159,189) and made it glow with drop-shadow(0 0 5px rgb(69,159,189)). Also, I made the button label's color change to rgb(3, 3, 3).

To achieve visual consistency, I want the close button to have the same focus ring. So the first attempt is something like this:

// styled-components/Button.js
import styled from 'styled-components';

...

// ADDED FROM HERE
const glowFocusRing = `
  border: 1px solid rgb(69, 159, 189);
  box-shadow: 0 0 5px rgb(69, 159, 189);
`;
const darkenButtonLabel = `
  fill: rgb(3, 3, 3);
`;
const supportHighContrastMode = `
  outline: 1px solid transparent;
`
const styleFocusState = `
  &:focus {
    ${glowFocusRing}
    ${supportHighContrastMode}
  }
  &:focus svg {
    ${darkenButtonLabel}
  }
`;
// ADDED UNTIL HERE

export const Button = styled.button`
  ${resetStyle}
  ${setClickableArea}
  ${centerAlignIcon}
  ${styleIcon}
  ${styleFocusState} /* ADDED */
`;
Enter fullscreen mode Exit fullscreen mode

The above code gives us a focus ring like this:

The cross icon surrounded with a glowing sky blue ring

4.2 Fallback for Windows High Contrast Mode

The above CSS code includes the declaration of outline: 1px solid transparent. This is for Windows High Contrast Mode, which removes the color of border and box shadow and instead renders any outline in solid white (even if it’s transparent). It’s becoming the norm (as far as I see) to include a transparent outline when the custom focus style involves border and box shadow. See Higley (2020) for detail.

An added bonus is that it also removes the default focus ring by overriding the outline property.

4.3 Remove focus style for touch device users and mouse users

The focus state style for a button is unnecessary for smartphone/tablet users and mouse users. We only need to style the focus state for keyboard users.

However, for all the browsers except Safari, the :focus pseudo selector gets activated when the button is clicked (as pointed out by Underhill 2021). So there will be a flash of the focus style when touch device users tap the button (or when mouse users click the button). That’s not desirable.

A solution is the :focus-visible pseudo selector which gets activated only when the button is focused with keyboard strokes. Underhill (2021) provides an excellent overview of :focus-visible.

But there is an issue of handling those “legacy” browsers (including the versions of Safari released before March 2022) that do not support :focus-visible. Consequently, we also need to use the :focus pseudo selector so that keyboard users with old devices won’t be ignored.

Lauke (2018) proposes to define the focus state style with :focus for legacy browsers and then to remove the focus state style with the following pseudo selector:

:focus:not(:focus-visible)
Enter fullscreen mode Exit fullscreen mode

which refers to the clicking of a button by smartphone, tablet and mouse users.

Following this technique, I revise the previous code for styling the focus state as follows:

// styled-components/Button.js
import styled from 'styled-components';

...

const glowFocusRing = `
  border: 1px solid rgb(69, 159, 189);
  box-shadow: 0 0 5px rgb(69, 159, 189);
`;
const darkenButtonLabel = `
  fill: rgb(3, 3, 3);
`;
const supportHighContrastMode = `
  outline: 1px solid transparent;
`;

// ADDED FROM HERE
const removeFocusRing = `
  border: none;
  box-shadow: none;
`;
const resetButtonLabelColor = `
  fill: rgb(90, 90, 90);
`;
// ADDED UNTIL HERE

const styleFocusState = `
  &:focus {
    ${glowFocusRing}
    ${supportHighContrastMode}
  }
  &:focus svg {
    ${darkenButtonLabel}
  }

  /* ADDED FROM HERE */
  &:focus:not(:focus-visible) {
    ${removeFocusRing}
  }
  &:focus:not(:focus-visible) svg {
    ${resetButtonLabelColor}
  }
  /* ADDED UNTIL HERE */
`;

export const Button = styled.button`
  ${resetStyle}
  ${setClickableArea}
  ${centerAlignIcon}
  ${styleIcon}
  ${styleFocusState}
`;
Enter fullscreen mode Exit fullscreen mode

where rgb(90, 90, 90) is the default color for button icons.

5. Active state style

The active state (or what Material Design calls the “pressed” state) is critical to tell the user whether the button is pressed or not. If the button doesn‘t visually respond to the user’s action, the user might think the app is not working, especially when the network connection is slow or the user’s device is slow (like my iPad Mini 2 bought in 2015).

For some reason, many web designers and web developers are not keen on styling the button active state. Lorenz (2019) showcases the CodePen demos of “Top 50 CSS Buttons”. When I click these buttons, very few of them clealry indicate that the button is indeed pressed.

5.1 Ripple effect

Today the most popular active state style is probably the ripple effect, popularized by Google with its Material Design.

And I follow this practice because, due to the popularity, the user can immediately tell what ripple animation means. Strictly speaking, it is not fully consistent with the design concept of my web app (see Day 2 of this blog series). For creating the minimum viable product, however, the ripple effect is good enough, and relatively easy to implement.

5.2 Adding the ripple to the DOM

Cameron (2020) provides a React code snippet for the ripple effect. Adapting this snippet, I first write down a function called createRipple:

// /utils/createRipple.js

export function createRipple(event) {
  const button = event.currentTarget;

  const diameter = Math.max(button.clientWidth, button.clientHeight);
  const radius = diameter / 2;

  const circle = document.createElement('span');

  circle.style.width = circle.style.height = `${diameter}px`;
  circle.style.left = `${event.offsetX - radius}px`;
  circle.style.top = `${event.offsetY - radius}px`;

  circle.classList.add('ripple');

  circle.addEventListener('animationend', () => button.removeChild(circle));

  button.appendChild(circle);
}
Enter fullscreen mode Exit fullscreen mode

The core idea is to create a temporary <span> element as a child of the <button> element, to shape it as a circle, and to set its left and top CSS properties to be dependent on where the user clicks on the button (event.offsetX and event.offsetY). For more detail, see Cameron (2020).

Note that the above code is revised from the original snippet by Cameron (2020). I have incorporated a couple of comments to the article:

5.3 Style the ripple

The above code attaches a class ripple to the <span> element. This class is used to style the ripple:

// /styled-components/Button.js
import styled, {css, keyframes} from 'styled-components'; // REVISED

...

// ADDED FROM HERE
const ripple = keyframes`
  to {
      transform: scale(4);
      opacity: 0;
    }
`;
const styleActiveState = css`
  overflow: hidden;
  position: relative;
  & .ripple {
    animation: ${ripple} 600ms linear;
    background-color: var(--ripple-color);
    border-radius: 50%;
    position: absolute;
    transform: scale(0);
  }
`;
// ADDED UNTIL HERE

export const Button = styled.button`
  ${resetStyle}
  ${setClickableArea}
  ${centerAlignIcon}
  ${styleIcon}
  ${styleFocusState}
  ${styleActiveState} /* ADDED */
`;
Enter fullscreen mode Exit fullscreen mode

At the top of the code, I import the css and keyframes helper functions, which is necessary to use CSS animation with Styled Components. The keyframes is for defining animation, and the css is required whenever CSS declarations include the animation keyframe (see Styled Components Docs for detail).

The animation defined with keyframes involves transform and opacity properties. The <span> element is animated to scale from zero to four with transform: scale(). It’s not clear why four. But I follow Cameron (2020), who must have reached this magnic number by trial and error.

And opacity is animated from 1 (default) to 0, imitating the vanishing water ripples.

Animation duration is 600ms, which is another magic number from Cameron (2020). Animation easing is linear: water ripples move at a constant speed according to this Reddit discussion.

The color of the ripple is set with --ripple-color, which is defined (elsewhere in the code base) as rgba(3, 3, 3, 0.33) for light mode and rgba(255, 255, 255, 0.4) for dark mode. According to the Material Design guideline, the (initial) opacity of the ripple should be higher the darker the background.

Finally, the <button> element itself needs to be styled with

overflow: hidden;
position: relative;
Enter fullscreen mode Exit fullscreen mode

so that the <span> element’s position is contained inside the button and that the ripple disappears when it hits the edges of the button.

5.4 Create the ripple on the click event

Then execute the createRipple function when the user clicks the button (but before executing the close button’s functionality given by the handleClick prop):

import PropTypes from 'prop-types';

import { Button } from '../styled-components/Button';

import { createRipple } from '../utils/createRipple'; // ADDED

export const CloseButton = ({
  ariaLabel,
  handleClick,
}) => {
  // ADDED FROM HERE
  const clickHandler = (event) => {
    createRipple(event);
    handleClick();
  };
  // ADDED UNTIL HERE

  return (
    <Button
      aria-label={ariaLabel}
      onClick={clickHandler} // REVISED
      type="button"
    >
      <svg aria-hidden="true" viewBox="0 0 24 24">
        <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
      </svg>
    </Button>
  );
};

CloseButton.propTypes = {
  ariaLabel: PropTypes.string.isRequired,
  clickHandler: PropTypes.string.isRequired,
};
Enter fullscreen mode Exit fullscreen mode

The snippet by Cameron (2020) ends here. But I realize that there is one thing missing: handling the button click with a keyboard.

5.5 Handle keyboard users

The above code won’t create the ripple effect when keyboard users press the button with the Enter key.

Let’s assume that the button is pressed at its center in this case. So the createRipple function is revised as follows:

// /utils/createRipple.js

export function createRipple(event) {
  const button = event.currentTarget;

  const diameter = Math.max(button.clientWidth, button.clientHeight);
  const radius = diameter / 2;

  const circle = document.createElement('span');

  circle.style.width = circle.style.height = `${diameter}px`;

  // REVISED FROM HERE
  if (event.key === 'Enter') {
    circle.style.left = 0;
    circle.style.top = 0;
  } else {
    circle.style.left = `${event.offsetX - radius}px`;
    circle.style.top = `${event.offsetY - radius}px`;
  } 
  // REVISED UNTIL HERE

  circle.classList.add('ripple');

  circle.addEventListener('animationend', () => button.removeChild(circle));

  button.appendChild(circle);
}
Enter fullscreen mode Exit fullscreen mode

Also the CloseButton component needs to be revised as follows:

import PropTypes from 'prop-types';

import { Button } from '../styled-components/Button';

import { createRipple } from '../utils/createRipple';

export const CloseButton = ({
  ariaLabel,
  handleClick,
}) => {
  const clickHandler = (event) => {
    createRipple(event);
    handleClick();
  };

  // ADDED FROM HERE
  const keydownHandler = (event) => {
    if (event.key === 'Enter') {
      event.preventDefault(); // otherwise click event will be fired as well
      createRipple(event);
      handleClick();
    }
  };
  // ADDED UNTIL HERE

  return (
    <Button
      aria-label={ariaLabel}
      onClick={clickHandler}
      onKeyDown={keydownHandler} // ADDED
      type="button"
    >
      <svg aria-hidden="true" viewBox="0 0 24 24">
        <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
      </svg>
    </Button>
  );
};

CloseButton.propTypes = {
  ariaLabel: PropTypes.string.isRequired,
  clickHandler: PropTypes.string.isRequired,
};
Enter fullscreen mode Exit fullscreen mode

Demo

Here’s a CodeSandbox demo of what the code in this article achieves. Try to click the close button on the top right. It won’t close anything, but it does give you the UI feedback of the ripple effect!

References

Bellamy-Royds, Amelia (2015) “How to Scale SVG”, CSS-Tricks, Jan 6, 2015.

Cameron, Bret (2020) “How to Recreate the Ripple Effect of Material Design Buttons”, CSS-Tricks, Oct 12, 2020.

Coyier, Chris (2017) “A Thing To Know about Gradients and ‘Transparent Black’”, CSS-Tricks, Jan 10, 2017.

Dodds, Kent C. (2020) “Use CSS Variables instead of React Context”, Epic React, Oct 2020.

Gash, Dave, Meggin Kearney, Rachel Andrew, Rob Dodson, and Patrick H. Lauke (2020) “Accessible tap targets”, web.dev, Mar 31, 2020.

Gonzalez, Angelo (2020) “Awesome work! For anyone trying to apply this to absolute positioned elements...”, comment to CSS-Tricks, Oct 14, 2020.

Higley, Sarah (2020) “Quick Tips for High Contrast Mode”, sarahmhigley.com, May 26, 2020.

Jonas (2020) “Thanks for this nice article! One thing I did differently is...”, comment to CSS-Tricks, Oct 14, 2020.

Lauke, Patrick H. (2018) “:focus-visible and backwards compatibility”, TPGi, Mar 23, 2018.

Lorenz (2019) “Top 50 CSS Buttons (+ animations)”, Dev Community, Jun 9, 2019.

Matuzovic, Manuel (2020) “#20 HTMHell special: close buttons”, HTMHell, May 23, 2020.

Stanton, Benjy (2020) “Accessible close buttons”, benjystanton.co.uk, Apr 30, 2020.

Underhill, Martin (2021) “Refining focus styles with focus-visible”, tempertemper, May 25, 2021.

Top comments (0)