Button Series - Checkbox Best Practices

Author: Pedro Balotelli

Subscribe

Button Series - Checkbox Best Practices

Published: March 2nd, 2021

Last update: March 2nd, 2021

11 min read

Today I decided to create an example of how to create the basic checkbox, radio and select inputs in the most efficient and fluent way. There are many ways to create these elements, but you should always consider whether the chosen option is the way that offers the best performance in the same time maintining all it's functionalities and accessibility.

I will create each of these elements one by one and share the steps with you. Let's start with the checkbox element.

Checkbox

Checkbox is mostly used in cases where the user has a lot of options to choose. Let's say that you're about to reserve a hotel for your vacation. You open hotels.com of whatever and start looking about the hotels from the area. You probably want your room to have a bathtub or balcony or that the resort should have their own pool for its visitors.

They will for sure provide you a list of checkboxes to tap according to your requirements.

Default

As a default the checkbox looks like this:

Notice! There are few inline styles like margins already applied.

And the code as follows:

<div>
  <div className="checkbox1" >
    <input type="checkbox" id='testBox1' />
    <label htmlFor='testBox1'>Click me</label>
  </div>
  <div className="checkbox1">
    <input type="checkbox" id='testBox2' />
    <label htmlFor='testBox2'>Click me</label>
  </div>
</div>
.checkbox {
  margin-right: 1rem;
  > input {
    margin-right: 0.5rem;
  }
}

So let's talk about the structure a little bit. Every checkbox should be wrapped inside a container as they will always have the input (checkbox) part and the label.

Each of the checkboxes should also have their unique id to be used in both input and label parts. This will automatically make the tap area larger as the labels htmlFor="" part makes it unique to the input field if they're both having the same id and hence the label click will effect the input as well.

When we start to customize the checkbox look we can't really just override the default styles but instead we will make the default input invisible and then create completely new elements that appear in the place of the default checkbox.

The beginning

I've built my own "checkbox" -component the way that it offers plenty of appearances and functionalities. My <Choice/> -component can work as a

  1. checkbox,
  2. check token,
  3. radio button or
  4. even as a toggle.

As I mentioned the default styles should be hidden in the first place and it is usually done one of these two ways:

  1. appearance: none
  2. visibility: hidden

It is worth to considering the pros and cons if using visibility: hidden as it will remove those abilities from the component. I'd personally prefer the appearance: none method as it will preserve all the checkbox accessibility options like :active and :focus when clicking the element.

If using opacity instead of just separately setting colors for border and background in different states, you should also use background: none and border: none as well, so the styling will get completely naked with all devices and states overriding the user agent styles.

I'd also use position: absolute so the element won't come to our way.

If you're following this solution the CSS should now look like this:

/* In your checkbox.scss file */
/* Hide the default appearance */

.checkbox {
  margin-right: 1rem;
  > input {
    margin-right: 0.5rem;
    position: absolute;    appearance: none;    -moz-appearance: none;    -webkit-appearance: none;    border: none;    background: none;  }
}

There are two ways to refer to the input element and as we are now building a component that will be used as a part of a parent <Input/> component AND which will cover all the four options mentioned earlier, I prefer pointing the class attributes straight to the inputs without the input[type=""] entries.

This is the look after appearance: none settings.

Let's then continue with a tiny adjustment to the html code and convert each label as follows:

<div className="checkbox">
  <input type="checkbox" id="id" />
  <label htmlFor="id">
    <span>      <svg width="12px" height="10px" className="svg-check">        <svg viewBox="0 0 12 10">          <polyline points="1.5 6 4.5 9 10.5 1" />        </svg>      </svg>    </span>    <span>Click me</span>  </label>
</div>

And the CSS:

.checkbox {
  margin-right: 1rem;
  --checkbox-size: 24px;
  --checkbox-padding: 0.3rem;
  --border-width: 2px;
  > input {
    position: absolute;
    appearance: none;
    -moz-appearance: none;
    -webkit-appearance: none;
    border: none;
    background: none;
  }
  > label {
    > span:first-of-type {
      position: relative;
      float: left;
      background: white;
      margin-right: 0.5rem;
      height: var(--checkbox-size);
      width: var(--checkbox-size);
      border-radius: 3px;
      border: var(--border-width) solid black;
    }
    > span:last-of-type {
      line-height: var(--checkbox-size);
    }
  }
}

.svg-check {
  fill: none;
  stroke: black;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 16px;
  stroke-dashoffset: 0px;
  transition: all 0.2s ease-out;
  width: 100%;
  height: 100%;
  padding: 20%;
  position: relative;
}

What we did there - we replaced the label with two children. The first children represents the checkmark that will be drawn to the checkbox on checked state and the second span represents the label text.

Why SVG? Because SVG lines are easy and efficient to animate with CSS transitions.

We're not functioning yet

As you can see the checkmark can be animated just with changing the stroke-dashoffset value. If we set the stroke-dasharray to over the length of the stroke and same time give stroke-offset that same 16px length value, our stroke will be drawn off.

Now. If we want to show the stroke, we set the stroke-dashoffset to 0. So let's make these small adjustments:

.checkbox {
  ...
  > input:checked + label {
    > span:first-of-type {
      > svg {
        stroke-dashoffset: 0;
      }
    }
  }
}
.svg-check {
  ...
  stroke-dashoffset: 16px;
}

Example

Left: Fork the slider below | Right: Click

How about the styles

Let's add some necessary styles like disabled styles utilizing the input[type="checkbox"] default checked attribute.

Checkboxes in different states.

And the CSS look like this at the moment:

.checkbox {
  display: flex;
  margin: 0 1rem 0.5rem 0;
  --checkbox-size: 24px;
  --checkbox-padding: 0.3rem;
  --border-width: 2px;
  > input {
    position: absolute;
    appearance: none;
    -moz-appearance: none;
    -webkit-appearance: none;
    &:checked {
      & + label {
        > span:first-of-type {
          background: orange;
          > svg {
            stroke-dashoffset: 0px;
          }
        }
      }
      &:disabled + label {
        > span:first-of-type {
          background: rgb(160, 160, 160);
        }
      }
    }
    &:disabled {
      & + label {
        &:hover {
          cursor: not-allowed;
        }
        > span {
          opacity: 0.4;
        }
      }
    }
  }
  > label {
    cursor: pointer;
    > span:first-of-type {
      position: relative;
      float: left;
      background: white;
      margin-right: 0.5rem;
      height: var(--checkbox-size);
      width: var(--checkbox-size);
      border-radius: 3px;
      border: var(--border-width) solid black;
      transition: background-color 0.2s ease;
    }
    > span:last-of-type {
      line-height: var(--checkbox-size);
    }
  }
}

There is a lot of other improvements what we could do for the animations and styles but I will keep the example as simple as possible.

How do we then use these same definitions to change the same checkbox to function as a radio? Lets give each of the checkboxes equal name="" attributes and change type="checkbox" to type="radio".

Radio buttons without circle styling.
<div className="checkbox">
  <input type="radio" id="testBox13" name="radio1" />
  <label htmlFor="testBox13">
    <span>
      <svg width="12px" height="10px" className="svg-check">
        <svg viewBox="0 0 12 10">
          <polyline points="1.5 6 4.5 9 10.5 1" />
        </svg>
      </svg>
    </span>
    <span>Click me</span>
  </label>
</div>
<div className="checkbox1">
  <input type="radio" id="testBox14" name="radio1" />
  <label htmlFor="testBox14">
    <span>
      <svg width="12px" height="10px" className="svg-check">
        <svg viewBox="0 0 12 10">
          <polyline points="1.5 6 4.5 9 10.5 1" />
        </svg>
      </svg>
    </span>
    <span>Click me</span>
  </label>
</div>

As we can see, the only difference between radio and checkbox is the name attribute, different type and usually round appearance.

So let's handle the radio button style with input[type='radio'] inside the checkbox styles:

.checkbox {
  ...
  input[type='radio'] {    & + label {      > span:first-of-type {        border-radius: 50%;      }    }  }}
Radio buttons in different states.

That's pretty much it. With just one definition in CSS and two minor changes in HTML we've converted our checkbox to radio button. Feel free to adjust the styles as you desire.

In the next article I will dive into the radio-button setup and start off the <Choice/> component construction so you will get the idea how to pass the type="radio" as a props to the parent container which will then give the attribute to all its children changing their styles and functionality.

Extra: My setup for the <Choice/> component and all its variations.

Change options to see how the list below changes.

Selected: 0

We are all working with just one prop!

This is where the magic happens

Try checking us as well.

If you enjoyed this article and please share and feel free to subscribe to hear about the latest posts first.