Custom Checkbox in React (Animated and Accessible)
Tags: react, a11y, css, html
Published: 2021-10-21 18:06:48

In this article, you're going to learn how to create a custom animated (yet accessible) checkbox. The technique that you'll learn is also applicable for styling radio inputs.

![Kapture 2021-10-05 at 22.30.00](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4hj7jxfy5erbfwkqac0o.gif)

(You can find the video version of this article on YouTube)
{% youtube 2Sf8E9CUavA %}

# How NOT to create a custom checkbox?
Let's first have a look at some possible (but wrong) approaches to creating custom checkboxes and explore their drawbacks.

As you can't really use CSS (as you'd normally do for styling form elements like buttons or text inputs) to style your checkboxes, you might be tempted to do something like this:
```jsx
// ❌ Do NOT do this. (Bad a11y + hard to integrate with form libraries)
function Checkbox() {
  const [isChecked, setIsChecked] = useState(false)

  return (
    <div
      className={`checkbox ${isChecked ? 'checkbox--active' : ''}`}
      onClick={() => { setIsChecked(!isChecked) }}
    />
  )
}

// + provide styles for .checkbox .checkbox--active classes
```

There are several problems with this approach.

**1) It's bad for for the accessibility**
If your user happens to be using a screen reader, there is no way that the screen reader can recognise that your `div` is actually a checkbox (let alone recognise if the checkbox is checked or not).

**2) It breaks the native form behaviour**
The `div` element doesn't emit `change` events so it's harder to integrate it with form libraries. Moreover, the "form data" on the `div` element aren't sent to the server upon form submission.

You could fix this by using `aria-role="checkbox"` on the `div` element, other `aria-*` attributes and a lot of JavaScript.

However, there is a simpler way...

# How to create a custom checkbox?
First, we'll have a look at how we'll approach it conceptually so that we have a "big picture" of the implementation.

We're going to use three different HTML elements for creating a custom checkbox. A `label`, an `input[type="checkbox"]`, and `span` (or `svg` or whatever you'd like 😉).

The `input[type"checkbox"]` is going to be *visually* hidden (but still accessible for screen readers), and we're gonna use the `label` element as a parent element so that clicking anywhere in the checkbox triggers the `change` event on the `input[type="checkbox"]`.
> Using label as a parent element is valid HTML as per https://www.w3.org/TR/html401/interact/forms.html#edef-LABEL

We'll use `aria-hidden="true"` on the custom (`span` or `svg`) checkbox so that it's hidden for screen readers as its purpose is only "decorative". We're also going to toggle `checkbox--active` class on it so that we can style it differently for "checked" and "unchecked" states.

With that said, let's write some JSX
```jsx
import { useState } from "react";

function Checkbox() {
  const [isChecked, setIsChecked] = useState(false);

  return (
    <label>
      <input
        type="checkbox"
        onChange={() => {
          setIsChecked(!isChecked);
        }}
      />
      <span
        className={`checkbox ${isChecked ? "checkbox--active" : ""}`}
        // This element is purely decorative so
        // we hide it for screen readers
        aria-hidden="true"
      />
      Don't you dare to check me!
    </label>
  );
}
```

To visually hide the native checkbox, create (and import) a new CSS file with the following:
```css
/* taken from https://css-tricks.com/inclusively-hidden/ */
input[type="checkbox"] {
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}
```
> To learn more about visually hiding things in CSS, [visit this blog post on CSS Tricks](https://css-tricks.com/inclusively-hidden/)

If you now hit save and open the browser, you'll see something like this:
![Screenshot 2021-10-05 at 00.12.14](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o8xnysd01qpyuh737qxh.png)

The native checkbox *is* hidden but we still need to style our custom one.

Let's do it in the next section!

## Styling Our Custom Checkbox
Let's first include some styles for our custom checkbox:
```css
.checkbox {
  display: inline-block; // set to `inline-block` as `inline elements ignore `height` and `width`
  height: 20px;
  width: 20px;
  background: #fff;
  border: 2px #ddd solid;
  margin-right: 4px;
}

.checkbox--active {
  border-color: purple;
  background: purple;
}
```

This is going to result in something like this:
![Kapture 2021-10-05 at 00.34.41](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ncjsg2m0dl6nvdt9j7n0.gif)

While it reacts to our input, it's missing something–a checkmark indicating if the checkbox is checked or not. Let's turn our `span` into an `svg` and add a checkmark.
```jsx
// ...

function Checkbox() {
  // ...

  return (
    <label>
      {/* ... */}
      <svg
        className={`checkbox ${isChecked ? "checkbox--active" : ""}`}
        // This element is purely decorative so
        // we hide it for screen readers
        aria-hidden="true"
        viewBox="0 0 15 11"
        fill="none"
      >
        <path
          d="M1 4.5L5 9L14 1"
          strokeWidth="2"
          stroke={isChecked ? "#fff" : "none"} // only show the checkmark when `isCheck` is `true`
        />
      </svg>
      Don't you dare to check me!
    </label>
  );
}
```

(You can find the source code for this section on [CodeSandbox](https://codesandbox.io/s/react-checkbox-2-xbdje?file=/src/App.js))

# Animating Checkbox using React Spring
In this section, we'll make the checkbox even prettier while maintaining its accessibility.

![Kapture 2021-10-05 at 22.30.00](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ro39rgngtrepklmoouyh.gif)
 

We'll be using React Spring library for the animations. You might be able to pull this animation off just with plain CSS but as we'll be animating the SVG Path and we'll need JavaScript to measure its length to make the animation possible, library like React Spring will come in handy.
> I've created a whole [series on animating SVGs with React Spring](https://dev.to/tomdohnal/series/10252). Be sure to check it out if you want to learn about it in more detail!

First, let's tackle the easier bit–animating the background and border colours.

After adding `react-spring` as a dependency using your favourite package manager, let's import `animated` and `useSpring` from the library and turn `svg` into `animated.svg` and `path` into `animated.path` so that they're set and ready to be animated.
```jsx
// ...
import { animated, useSpring } from "react-spring";

function Checkbox() {
  return (
    <label>
      {/* ... */}
      <animated.svg /* ... */>
        <animated.path /* ... *//>
      </animated.svg>
      {/* ... */}
    </label>
  );
}
```

Once we're done, we'll use the `useSpring` hook to animate `backgroundColor` and `borderColor` attributes. This is going to be analogical to toggling the values of those properties by using the `checkbox--active` CSS class.
```jsx
// ...

function Checkbox() {
  // ...
  const checkboxAnimationStyle = useSpring({
    backgroundColor: isChecked ? "#808" : "#fff",
    borderColor: isChecked ? "#808" : "#ddd"
  });

  return (
    <label>
      {/* ... */}
      <animated.svg
        style={checkboxAnimationStyle}
        /* ... */
      >
        {/* ... */}
      </animated.svg>
      {/* ... */}
    </label>
  );
}
```

Finally, we'll remove the `checkbox--active` class from our CSS file as it's no longer needed.

## Animating the Checkmark
>We'll be using a famous technique for animating SVG Paths  (using `strokeDashoffset` and `strokeDasharray`) in this tutorial.  I created a [whole blog post explaining this subject in much more detail](https://dev.to/tomdohnal/react-svg-animation-with-react-spring-3-3c91).

To animate the checkmark, we first need to measure (and store) its length. We'll use `useState(...)` to store its length, pass a callback to the `ref` property of our SVG Path, and call `ref.getTotalLength()` to measure its length.
```jsx
// ...

function Checkbox() {
  // ...
  const [checkmarkLength, setCheckmarkLength] = useState(null);

  return (
    <label>
      {/* ... */}
      <animated.svg /* ... */>
        <animated.path
          {/* ... */}
          ref={(ref) => {
            if (ref) {
              setCheckmarkLength(ref.getTotalLength());
            }
          }}
        />
      </animated.svg>
      {/* ... */}
    </label>
  );
}
```

Now that we've got the length of the path, we can set the `strokeDasharray` to `checkmarkLength` and use `useSpring` to animate the `strokeDashoffset` between `0` and `checkmarkLength`. And we'll set the stroke to `#fff` no matter of the `isActive` state value.
> If you're not use how this works, see [my blog post](https://dev.to/tomdohnal/react-svg-animation-with-react-spring-3-3c91) where I explain this very technique in detail.
```jsx
// ...

function Checkbox() {
  // ...

  const checkmarkAnimationStyle = useSpring({
    x: isChecked ? 0 : checkmarkLength
  });

  return (
    <label>
      {/* ... */}
      <animated.svg /* ... */>
        <animated.path
          // ...
          stroke="#fff"
          strokeDasharray={checkmarkLength}
          strokeDashoffset={checkmarkAnimationStyle.x}
        />
      </animated.svg>
      Don't you dare to check me!
    </label>
  );
}

export default Checkbox;
```

If you now try your code out, you'll see that it's working quite okay!
![Kapture 2021-10-05 at 22.31.34](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xybog4zg4lwhp0ids1zi.gif)
 

While our animation is working quite smoothly, I think we can still add a little bit of spice to take it to the next level.

First, let's tweak the `config` of the `useSpring` hook. Let's import the `config` variable from React Spring which includes some predefined configs and use `config: config.gentle` in our `useSpring(...)` calls. This is going to give our animations a little bit more of a playful feel.
```jsx
// ...
import { /* ... */ config } from "react-spring";

function Checkbox() {
  // ...
  const checkboxAnimationStyle = useSpring({
    // ...
    config: config.gentle
  });

  // ...

  const checkmarkAnimationStyle = useSpring({
    // ...
    config: config.gentle
  });

  // ...
}
``` 

Next, if you look at the animation really closely, you'll notice that the checkmark animation only appears for a brief moment. That's because the checkmark is white for the whole duration of the animation while the background is animating from white to purple. So during the time when the background is white, the checkmark is barely visible (as it's white on a white background).

We can tackle this by using the `useChain` hook from React Spring. This hook enables us to trigger the `useSpring(...)` animations one after another. In our case, we'll use it to delay the checkmark animation a bit so that it only starts animating when the background of the checkbox is already mostly purple. We'll do the opposite when animating in the other direction.

Let's import `useChain` along with `useSpringRef` from `react-spring`. Then, we'll use the `useSpringRef` hook to create references to our `useSpring` calls which we'll then pass into the `useChain` function:
```jsx
// ...
import {
  // ...
  useSpringRef,
  useChain
} from "react-spring";

function Checkbox() {
  // ...
  const checkboxAnimationRef = useSpringRef();
  const checkboxAnimationStyle = useSpring({
    // ...
    ref: checkboxAnimationRef
  });

  // ...

  const checkmarkAnimationRef = useSpringRef();
  const checkmarkAnimationStyle = useSpring({
    // ...
    ref: checkmarkAnimationRef
  });

  useChain(
    isChecked
      ? [checkboxAnimationRef, checkmarkAnimationRef]
      : [checkmarkAnimationRef, checkboxAnimationRef],
    [0, 0.1] // -> delay by 0.1 seconds
  );

  // ...
}
```

If we now play the animation, it looks bonkers!
![Kapture 2021-10-05 at 22.30.00](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/syzr5s25l021niz30wlm.gif)

You can find the source code for the whole tutorial on [CodeSandbox](https://codesandbox.io/s/react-checkbox-3-xz9ol?file=/src/App.js)