Custom Styled CSS Checkbox and Radio Buttons — In-depth intro

Mate Marschalko
8 min readNov 28, 2024

--

In previous posts, we looked at many HTML and CSS features and tricks allowing you to achieve JavaScript level interactivity:

— Animated page scroll with HTML and CSS only

— CSS-only Interactive, Swipeable Image Carousel

— ⏰ CSS-only Accurate, Settable, Animated Analog Clock

— Interactive Image Maps and Closable Popups with HTML and CSS only

— A super-simple, Live CSS Code Editor with no JavaScript

— Advanced CSS-only Beer Counting — Calculate, Increment, Multiply

— Advanced CSS-only Input Fields — Interactive, Animated, Validated

— CSS-only Shopping Basket with a Fully Dynamic Price Calculator

Today, lets take an in-depth look at the HTML checkbox and radio buttons as they will unlock a new level of advanced interactive UIs without using any JavaScript at all!

The checkbox and the radio button

As a web developer, you must have already worked with checkboxes and radio buttons but since they are the main stars of this entire chapter, let’s recap the difference quickly.

Both input types can be ticked or unticked to capture the user’s choice, and when you have multiple checkboxes in a form you can select or unselect as many of them as you want to:

On the other hand, radio buttons only let you select one option maximum. If an option was already selected, the selection will be switched over to the new one:

Adding a checkbox to your HTML document is easy:

<input type="checkbox" />

This simple code will result in a functioning checkbox but there are some optional attributes we can add:

<input
type="checkbox"
name="consent"
value="agreed"
checked
readonly
/>

The name and value attributes are necessary if you want to submit your form and process the selected values later. name will identify the input element to be processed, then your application can extract the value. When working with a text input field, the value of the input is the text entered by the user.

The shorthand checked attribute makes the checkbox pre-checked when the page loads. Then we also have the shorthand readonly attribute to disable the checkbox so the user won’t be able to click on it and modify its state.

When it comes to radio buttons the situation and attributes are very similar, however, a radio button isn’t much use on its own as you want the user to select one from multiple different options.

Fun fact: radio buttons were named after the physical push buttons found on old-fashioned radios, usually found in cars. Their purpose was to select preset stations. When one of the buttons was pressed, other buttons would pop out, leaving the pressed button the only one in.

The original radio button

The demonstrate the behaviour of radio buttons, let’s add a set of them t select a movie genre:

<input type="radio" name="genre" value="action" />
<input type="radio" name="genre" value="comedy" />
<input type="radio" name="genre" value="drama" />
<input type="radio" name="genre" value="horror" checked />

What’s different here from how we work with checkboxes is that the entire set of radio buttons share the same name. This name groups these options together so when we later process the values from the from we can simply request the value from the input element with the name “genre” then the returned value will be from the option the user selected: action, comedy, drama or horror.

The checked attribute works the same way as with the checkbox: you can add it to one of the radio buttons making that the pre-selected value from the group. If the user decides not to modify the settings of the radio buttons, the value of this pre-checked element will be submitted with the form.

Both the checkbox and the radio button change their internal state to “checked” when they are selected. This means we can use the :checked CSS pseudo-class in both cases to modify some styling on the page. The below example will draw a red border around every selected checkbox and radio button on the page:

[type="checkbox"]:checked,
[type="radio"]:checked {
border: 2px solid var( - red);
}

What we haven’t considered yet is that the text from the value attributes are not visible for the user so with the above examples we see nothing, just a random set of inputs with no explanation whatsoever:

In case of the genre selector radio buttons the text from the value attributes could actually work as labels so we might be tempted to output these into an ::after pseudo-element with the attr() function:

/* This will not work */
[type="radio"]::after {
content: attr(value);
}

Unfortunately, this will not work because pseudo-elements (::after, ::before, etc.) can only be defined on container elements as they are rendered within the container itself as a pseudo child element. Input elements can’t contain other elements hence their lack of support for ::before and ::after.

What all this really means is that we need to find another solution to label our <input> elements. Instead of looking for a CSS solution, all we need to do is add some extra HTML. This is not really a problem at all as it will increase the accessibility of our code.

This is something I discussed previously:

Should you add content with the ::before and ::after CSS pseudo-elements?

So the element we need to use is .. you guessed it .. a <label> element.

The text inside the <label> can be anything, in fact we can add any other HTML inside since it is a container. The main consideration with the <label> element is that the browser will not know immediately which input element it belongs to so we need to define this connection. There are two ways to do this: implicitly or explicitly.

Implicitly defining this means that we make the connection obvious by placing the <input> element inside the <label> element:

<label>I agree!
<input type="checkbox" />
</label>

The other way is defining the connection explicitly by linking the two elements with the id and the for attributes:

<input type="checkbox" id="consent" />
<label for="consent">I agree!</label>

The “consent” value in the for attribute tells the browser that this label belongs to an input element with an id of “consent”.

We have definitely increased the user experience of the checkbox with the new label:

But this is not the only benefit of using the label. The label now also functions as a click handle so clicking on it toggles the checkbox. If it fits your needs, it is also allowed to add multiple labels all referencing, and therefore controlling, the same input element.

Once the connection is established we don’t even need the checkbox element to be visible! The label will still toggle the invisible checkbox.

Of course, with the checkbox itself hidden, there’s no visual feedback so we don’t really see if the checkbox is checked or not. We can fix this by adding a background and a border around the label when the hidden checkbox is checked:

[type="checkbox"] {
display: none;
}

[type="checkbox"]:checked + label {
background-color: var( - grey);
border: 2px solid black;
}

And now we can see the status of the hidden checkbox reflected on the label:

We used the adjacent sibling selector (+) which requires the label to come right after the input element in HTML for it to reach it. If there are other elements between the input element and the label, we can use the general sibling selector (~) in combination with a class or an ID:

#consent {
display: none;
}
#consent:checked ~ [for="consent"] {
background-color: var( - grey);
border: 2px solid black;
}

With this, the two elements can have elements wedged between them but they still need to be siblings and the label still needs to come after the input.

With some extra CSS we can push the selected state of this label further and create an animated button that we can toggle on and off by pressing it:

The HTML is pretty much the same we had before with an updated label:

<input type="checkbox" id="custom-toggle" />
<label for="custom-toggle">Toggle Me</label>

And this is the CSS required to achieve the toggling behaviour and specific styling I proposed including smooth transitions:

[type="checkbox"] {
display: none;
}
[type="checkbox"] + label {
display: block;
width: 200px;
height: 100px;
padding: 36px;
text-align: center;
cursor: pointer;
transform: scale(1);
background-color: var( - grey);
box-shadow: 0 0 0 rgba(0, 0, 0, 0);
transition:
box-shadow 300ms,
transform 300ms,
background-color 300ms;
}
[type="checkbox"]:checked + label {
transform: scale(1.1);
background-color: var( - green);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}

Now you can come up with any design or animation you want for your checkbox, you are no longer limited to the default design shipped with the browser. Once the input field is hidden, we can style the label element any way we want to.

The basic idea of the trick we used here to achieve all this was selecting and styling the label element that is connected to a checkbox. But we are not limited to only modifying the styling of the label itself. We can show and hide or change the style of other elements on the page as long as they are siblings with and come after the checkbox.

Believe it or not, based on this simple idea and no JS, I’m going to show you how to build the below examples in the upcoming posts:

  • Interactive popup modal
  • Accordion
  • Tab content switch interface
  • Star rating component

Stay tuned for these!

Source code

You can download the template and example project from my Github:

All you need is HTML and CSS

FYI, this is an example project from my book, All you need is HTML and CSS:

There are many other examples in the book pushing the limits of HTML and CSS including interactive carousels, accordions, calculating, counting, advanced input validation, state management, dismissible modal windows, and reacting to mouse and keyboard inputs. There’s even an animated pure CSS clock (analog and digital) and a fully working star rating widget!

--

--

Mate Marschalko
Mate Marschalko

Written by Mate Marschalko

Senior Creative Developer, Generative AI, Electronics with over 15 years experience | JavaScript, HTML, CSS

No responses yet