Native HTML controls work great up until you need to apply custom styling. It’s pretty difficult to style controls like selects, checkboxes, and radio buttons. That’s why people usually end up ditching the native controls and write their own controls in HTML/CSS/JS.

Recreating your own controls on the surface seems simple, but it is more complicated than you think. You now have to ensure that keyboard users & all forms of assistive tech users can still activate that custom control. It is now up to you to manage the current state of that control.

Using just JavaScript to recreate the control isn’t enough. This is because we have to fill in the Accessibility Tree for this custom element. For example, if you’re recreating a checkbox from a span this would not be enough:

To a user who is clicking on the span, this would seem to work out fine, JavaScript is doing its job. What if you try to activate the checkbox by pressing enter or spacebar? That won’t work because you can’t tab to the “checkbox”. That’s an easy fix, all we have to do is add a tabindex to the span:

<span id="checkbox" tabindex="0"></span>

Now we can tab to the “checkbox”, but pressing the space bar or enter doesn’t activate it. This means those who use your app with a keyboard won’t be able to use this checkbox.

If you turn on a screen reader and navigate to that checkbox, it doesn’t tell you any information about what element it might be. It just says “group”. Pressing enter or spacebar toggles the checkbox but the screen reader is never updated.

To fill in the gaps of our span checkbox we can change the role of the element to checkbox and add a keydown handler to catch enter or spacebar presses.

Now if we tab to our checkbox we can press enter or spacebar and toggle the state! If you turn on a screen reader you will hear our checkbox be announced as a checkbox.

You might notice the screen reader also gives you a lot more information about how to interact with this element. This is because semantics matter a lot to assistive tech (AT).

If you activate the checkbox with the screen reader the state change isn’t announced. This is because we still haven’t finished filling in the accessibility tree. We have to use the aria-checkedattribute to signal the checkbox state change to AT.

AT now will be able to tell what the current state of the checkbox is.

Now the checkbox can be toggled with AT, keyboard presses, and clicking. We’re done, right? Not quite! Form controls have special behavior with labels. For checkboxes clicking the label should focus on the linked checkbox and toggle it.

Let’s add that functionality with help from aria-labelledby. aria-labelledby allows us to label one element by linking it to the content of another. We’re going to use it to link the content of a label to our custom checkbox.

If you turn on a screen reader it will now read the label when the checkbox is focused. And if you click the label it should focus the checkbox and toggle it like a normal checkbox would.

Whew, that’s a lot!

That’s a lot of work to recreate something that seems as simple as a checkbox. And we didn’t cover the cases for linking your checkbox to a label by using the for attribute or nesting the checkbox inside of a label. You can see why accessibility is difficult — you have to recreate a lot of behavior you normally get for free. It’s not as simple as toggling an is-checked class.

Introducing Checkbox.js

To help simplify creating checkboxes that give you full freedom of styling (and whatever else), I’ve created a pure JavaScript library called Checkbox.js . It can turn basically any HTML element into a functional and accessible checkbox.

Checkbox.js also enables other features a checkbox usually has like linking a label by using the forattribute:

<label for="checkbox">Label</label>
<span id="checkbox">
<script>
let checkbox = document.getElementById('checkbox')
new Checkbox(checkbox);
</script>

And nesting a checkbox inside of the label will also work with Checkbox.js too:

<label>
Label
<span id="checkbox">
</label>
<script>
let checkbox = document.getElementById('checkbox')
new Checkbox(checkbox);
</script>

Checkbox.js is framework agnostic so you can use this anywhere there’s a DOM and JavaScript. For example a simple react component using Checkbox.js might look like:

import React, { Component } from 'react';
import Checkbox from 'checkboxjs';
class ReactCheckbox extends Component {
componentDidMount() {
this.checkbox = new Checkbox(this.refs.checkbox, {
isChecked: this.props.isChecked,
label: this.props.label
});
}
render() {
return (
<span ref="checkbox" className="checkbox" id={this.props.id}></span>
);
}
}
export default ReactCheckbox;

And you might use it like:

<label for="checkbox">My Label</label>
<ReactCheckbox isChecked={true} id="checkbox" />

You can checkout the full API & documentation here and the GitHub repo here.