Custom Styling Checkboxes: The Modern Way
Tags: css, webdev, designsystem, a11y
Published: 2023-09-12 21:40:56

TL;DR:

I made another thing. Maybe you think it's cool? [See code here](https://gist.github.com/adbutterfield/990f05a17eb47419094515af59c28b09).

_For people who read, please continue._

Also, in case you missed it, I got [a write up about custom styling radio buttons too](https://dev.to/adbutterfield/custom-styling-radio-buttons-the-modern-way-the-butterfield-way-emk)! 

Since radio buttons and checkboxes are kind of the siblings of the input world, a lot of what I already wrote about radio buttons applies here as well. They share [the same traditional approach](https://dev.to/adbutterfield/custom-styling-radio-buttons-the-modern-way-the-butterfield-way-emk#the-traditional-approach) for styling, they share [the same limitation for using pseudo-elements](https://dev.to/adbutterfield/custom-styling-radio-buttons-the-modern-way-the-butterfield-way-emk#the-pseudoelement-limitation-on-input-elements), and they share [the same accessibility concerns](https://dev.to/adbutterfield/custom-styling-radio-buttons-the-modern-way-the-butterfield-way-emk#accessibility-considerations-for-custom-radio-button-styling). 

So really, if you didn't read my article about radio buttons, you might want to do that first.

## Let's get to the code! 

My approach for styling checkboxes uses more or less the same approach as for radio buttons.

Let's break it down step by step:

### 1. Define Color Variables

Using the `:root` selector, we can define global CSS variables. This makes it easier to change the color scheme without having to search and replace values throughout the CSS.

Of course it's up to you with how creative you want to be with the colors. But at minimum you will need 4 different colors:

- Border color
- Checked color
- Hover color
- Disabled background color

It's pretty common these days to two tones of blue and two tones of gray (as I did here).

```css
:root {
  --checkbox-border-color: #8b8c89;
  --checkbox-checked-color: #274c77;
  --checkbox-hover-color: #a3cef1;
  --checkbox-disabled-bg-color: #d9d9d9;
}
```

I actually used the same colors as for radio buttons. So in your design system or application, you will want to rename the variables and use them consistently across both types of inputs.

### 2. Base Styling

The core styling for our checkboxes. Same as with radio buttons, we use the `appearance` property set to `none` to remove the default appearance of the checkbox. (More info on `appearance` in [my other article](https://dev.to/adbutterfield/custom-styling-radio-buttons-the-modern-way-the-butterfield-way-emk#side-quest-a-brief-history-of-the-appearance-property).)

```css
input[type="checkbox"] {
  box-sizing: border-box;
  width: 20px;
  height: 20px;
  margin: 6px;
  padding: 0;
  border: 2px solid var(--checkbox-border-color);
  appearance: none;
  background-color: transparent;
  outline: none;
  transition: outline 0.1s;
}
```

### 3. Styling the Checked State

The `:not()` pseudo-class combined with `:checked` is used to style checkboxes when they're checked, but not disabled. I'm a big fan of using pseudo-classes like this to be really explicit when these styles are applied, instead of letting things cascade and just overwriting them farther down the stylesheet.

For radio buttons, we used `radial-gradient` to create the appearance of a selected inner circle. For checkboxes, we can't use the same approach. It's pretty easy to use `linear-gradient` to draw an X mark for the checked state. It might be possible to draw a check mark as well, but I decided to use `background-image` with an inlined svg instead.

Both the disabled and non-disabled states share some styles, which is why we have the first block. With the second block only applying to `:not(:disabled):checked`.

```css
input[type="checkbox"]:checked {
  background-size: cover;
  padding: 2px;
}

input[type="checkbox"]:not(:disabled):checked {
  border-color: var(--checkbox-checked-color);
  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 32 32" xml:space="preserve"><path style="fill: %23274c77" d="M11.941,28.877l-11.941-11.942l5.695-5.696l6.246,6.246l14.364-14.364L32,8.818"/></svg>');
}
```

So why use an inline svg? Of course we all know that a svg scales much better than a plain old image, which could be important because the check mark is so small and we want it to look nice and sharp. We will also need to use the same check mark for the disabled state, but with a different color. And of course we can change the color of a svg. If using images, we would need 2 separate ones with different colors. Then finally, we want to inline the svg so we don't get a flash of unstyled content while your application has to go fetch the asset if it was on a remote resource.

One important thing to notice about the svg. For setting the color, you need to URL encode the color variable, which is why you see `%23274c77` (of course "%23" === "#"). Unfortunately we can't use the CSS variables, there's an issue with using CSS variables within `data:` URIs. CSS variables won't be evaluated within the context of a `data:` URI because they are processed within the CSS of your stylesheet, and `data:` URIs are typically not part of that processing. Kind of a bummer...

### 4. Styling for Disabled State

For the disabled state, we only need to add a background color, and cover the case when a checkbox can be checked && disabled. Here we use the same svg, but with a different `fill` color.

```css
input[type="checkbox"]:disabled {
  background-color: var(--checkbox-disabled-bg-color);
}

input[type="checkbox"]:disabled:checked {
  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 32 32" xml:space="preserve"><path style="fill: %238b8c89" d="M11.941,28.877l-11.941-11.942l5.695-5.696l6.246,6.246l14.364-14.364L32,8.818"/></svg>');
}
```

That covers the basic states of the input. Now time to move on to the interactions.

### 5. Enhancing Hover Effects

The `@media (hover: hover)` media query ensures that hover effects are applied only on devices that support hovering, like desktops. This detail is easy to overlook, especially if you haven't worked with CSS recently.

```css
@media (hover: hover) {
  input[type="checkbox"]:not(:disabled):hover {
    background-color: var(--checkbox-hover-color);
    outline: 6px solid var(--checkbox-hover-color);
    transform: scale(1.05);
  }
}
```

### 6. Focus Indication with :focus-visible

The `:focus-visible` pseudo-class is a modern approach to style the focus state only when it's relevant (like when using keyboard navigation). No longer do you have to annoy users (and of course get annoyed yourself) by showing a focus ring to users who don't need it.

```css
input[type="checkbox"]:focus-visible {
  outline: 6px solid var(--checkbox-hover-color);
  transform: scale(1.05);
}
```

In the current code, both hover/focus have the same style. This may not be the case though for you. Always make sure to follow accessibility best practices when picking colors and width of hover/focus ring effects.

### 7. Adding the Finishing Touches

I already included the same [smooth transitions](https://dev.to/adbutterfield/custom-styling-radio-buttons-the-modern-way-the-butterfield-way-emk#smooth-transitions), [subtle scaling](https://dev.to/adbutterfield/custom-styling-radio-buttons-the-modern-way-the-butterfield-way-emk#subtle-scaling) in the checkbox code that I added to the radio buttons. Wwe also want to [honor user preferences](https://dev.to/adbutterfield/custom-styling-radio-buttons-the-modern-way-the-butterfield-way-emk#honoring-user-preferences) the same way we did with radio buttons. You'll see that in the final code, but I won't explain that again here for checkboxes.

And that's about it for the code.

You can find the [full code here](https://gist.github.com/adbutterfield/990f05a17eb47419094515af59c28b09).

Make sure you also read about [the accessibility concerns](https://dev.to/adbutterfield/custom-styling-radio-buttons-the-modern-way-the-butterfield-way-emk#accessibility-considerations-for-custom-radio-button-styling) before applying this or any custom input in your application.

## Parting Words

(Totally just copy/pasting this from my other article. Very anti-DRY of me...)

Modern CSS offers a cleaner, more streamlined approach to customizing checkboxes without the need for extra HTML or convoluted CSS. Embracing these techniques allows for efficient, maintainable, and user-friendly designs.

Always think about WHY you want to create a custom checkbox in the first place. If the reason is to offer a better experience to users, then be sure to think about ALL users; think about accessibility. By focusing on accessibility from the outset, designers and developers can ensure a more inclusive web experience for all users, regardless of their abilities or how they access content.

And if improving user experience is really your goal, make sure to keep things as performant as you can. I don't think we're really optimizing early here or anything. It's a simple matter of less code means less bugs, less bytes sent down the wire, and hopefully after reading this, less css in js in your application or design system.