Your browser doesn't support some of the latest features used to build this site. For the best experience you might want to look at upgrading your browser.

Skip to main content

Custom checkbox and radio button form elements

Create some great looking progressively enhanced custom checkboxes and radio button form elements using HTML, CSS and inline SVG.

Setting up the markup

Let’s begin by setting up the markup for our checkboxes and radio buttons; this way we know exactly what we have to work with.

Checkbox markup

Firstly we’ll set up the checkbox as it has some slightly more complex markup than the radio button.

<div class="a-checkbox m-form-group__checkbox">
    <input class="a-checkbox__input" id="checkbox-1" name="checkbox-1" type="checkbox" value="Label">
    <label class="a-checkbox__label" for="checkbox-1">
        <span class="a-checkbox__box">
            <svg class="a-icon a-icon--stroke a-icon--tick a-checkbox__icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
                <polyline points="1 9.26644699 6.70753323 13.4134757 15 2" />
            </svg>
        </span>
        Label
    </label>
</div>

Hopefully, there isn’t too much here that looks confusing but let’s go through it quickly:

  • We have an input element with a few attributes on it
  • Followed by a label element with a for attribute that has the same value as the input’s id attribute. This is important as this will check the checkbox when the label is clicked.
  • Then we have the <span class="a-checkbox__box"> and the svg which we’ll use for our custom checkbox
  • Lastly, we have the label text

If you’re new to inline SVG, the polyline element is our tick icon. Each number in the points attribute is a coordinate inside the SVG.

If you know what you’re doing with SVG’s you can write these points yourself. If not; you can use tools like Sketch and Illustrator to create the icon and export it as an SVG.

Radio button markup

Next, let’s take a look at the markup for a radio button.

<div class="a-radio m-form-group__radio">
    <input class="a-radio__input" id="radio-1" name="radio-1" type="radio" value="Label">
    <label class="a-radio__label" for="radio-1">
        <span class="a-radio__box">
            <span class="a-radio__icon"></span>
        </span>
        Label
    </label>
</div>

The markup here is almost identical to the checkbox but instead of type="checkbox" we’re using type="radio" and instead of the svg, we’re just going to use a span element for the icon.

Adding some style

In order to progressively enhance our checkboxes and radio buttons, we’re going to start with our default styling and layer styles on top for newer browsers.

This way, people with older browsers can still use our default form elements and people with newer browsers will get our custom elements. Win-win for everyone.

Checkbox styling

As mentioned, let’s start by styling up our default checkbox (this is what people using older browsers will see).

.a-checkbox__input {
    float: left;
    margin: 0 0.625em 0 0;
    width: auto;
}

.a-checkbox__label {
    display: inline-block;
}

.a-checkbox__box {
    display: none;
}

As you can see nothing too complicated going on here, we’re simply:

  • Floating the input field left and adding some margin to the right so it sits inline with the label
  • Making the label inline-block so it sits next to the input
  • Lastly hiding the markup for our custom checkbox
Default checkbox styling

Enhancing our checkbox

In order to enhance our checkbox, we need to use some sort of feature detection that’ll only get applied if the browser passes some rules.

We have a few different options here:

For our checkboxes and radio buttons, we’re going to use CSS feature queries.

In our case, we aren’t using any CSS features that wouldn’t be supported if the browser supports CSS feature queries.

So in short, if the browser doesn’t support CSS feature queries then the user will get the default styling.

Feature query browser support
Styling the icons

Ok, let’s start styling our icons. This is some global styling I have set up to be used with an inline SVG icon system which we’ll be using for our checkboxes tick icon.

.a-icon {
    color: #000000;
    vertical-align: top;
}

.a-icon--stroke {
    fill: none;
    stroke: currentColor;
    stroke-linecap: butt; /* butt, round, square */
    stroke-linejoin: miter; /* bevel, miter, round */
    stroke-width: 2;
}

If you haven’t used an inline SVG icon system before it’s pretty great, let’s go through what’s going on here.

  • vertical-align: top - removes inner spacing that gets added to the svg
  • fill: none - removes the fill colour from the svg’s children elements
  • stroke: currentColor - sets the stroke colour on the svg’s children elements to the same colour as the closest parent element that has a color property set on it (in our case .a-icon)
  • stroke-linecaep: butt - makes the start and end points in the svg sqaure
  • stroke-linejoin: miter - makes any points between the start and end points in the svg square
  • stroke-width: 2 - sets the stroke width on the svg’s children elements

As you can see inline SVG makes for a pretty awesome icon system.

By simply changing the stroke-linecap and stroke-linejoin to round; all your line icons would have rounded points.

And if you want your icons to be thinner or thicker, just change the stroke-width value.

Hiding the default checkbox

Now that we have our icons sorted out, let’s take a look at styling the checkbox. First of all, we’re going to hide our default checkbox input.

In this case, we can’t just set it to display: none, as we’ll lose all the native functionality of the input (which we need to make this work).

Instead, we’re going to use the CSS below to visually hide the input element. This means the input will still behave as if it was visible on the screen.

@supports (transition: stroke-dashoffset) {
    .a-checkbox__input {
        border: 0;
        clip: rect(0 0 0 0);
        height: 1px;
        margin: -1px;
        overflow: hidden;
        padding: 0;
        position: absolute;
        width: 1px;
    }
}

If you’ve never used CSS feature queries before that’s what the @supports code is here.

Nothing inside this block will get executed if the browser doesn’t support the declaration inside the parentheses or CSS feature queries itself.

Styling the custom checkbox

Next, we’ll style up our custom checkbox.

The following block of code creates the box that’ll surround our icon, nothing too complex here.

@supports (transition: stroke-dashoffset) {
    .a-checkbox__box {
        border: 0.125em solid #dcdcdc;
        display: inline-block;
        height: 1.875em;
        margin-right: 0.625em;
        position: relative;
        transition: border-color 0.3s cubic-bezier(0.5, 0.61, 0.355, 1), box-shadow 0.3s cubic-bezier(0.5, 0.61, 0.355, 1);
        vertical-align: middle;
        width: 1.875em;
    }
}

Now, let’s look at styling up the checkbox tick icon.

@supports (transition: stroke-dashoffset) {
    .a-checkbox__icon {
        height: 1em;
        left: 50%;
        position: absolute;
        stroke-dasharray: 22;
        stroke-dashoffset: 22;
        top: 50%;
        -webkit-transform: translate(-50%, -50%);
                transform: translate(-50%, -50%);
        transition: stroke-dashoffset 0.3s cubic-bezier(0.5, 0.61, 0.355, 1);
        width: 1em;
    }
}

Again nothing too complicated going on here, we’re setting the icon to position: absolute and aligning it to the center of the box.

What you may not have seen before is stroke-dasharray and stroke-dashoffset.

If you’ve never seen these properties before, here are some great articles on CSS Tricks:

In short, we’re trying to make a dash so long that it covers the entire length of the shape.

Once we’ve done that, we offset it so that the shape disappears (which makes our unchecked state).

If you want to find out how long a path is you can run the following in your browser console (replacing the selector with yours):

document.querySelector('polyline').getTotalLength();
Checking the checkbox

Now we want to show the tick icon when someone has checked the checkbox.

.a-checkbox__input:checked + .a-checkbox__label .a-checkbox__icon {
    stroke-dashoffset: 0;
}

If this CSS selector looks a bit weird to you, let me talk through what it’s doing:

  • .a-checkbox__input:checked - is our visually hidden checkbox input element (this gets the checked pseudo selector when the label is clicked because the labels for attribute is the same as the inputs id attribute)
  • + .a-checkbox__label .a-checkbox__icon - is then finding the label element that directly follows the checkbox input element in the DOM and then finds our custom checkbox icon inside and sets stroke-dashoffset: 0 to make the icon visible again

Browsers actually read selectors from right to left and not left to right. But explaining it from left to right is easier .

Adding focus to the checkbox

The last thing to do is add a focus state to our custom checkbox.

This is so keyboard users who rely on a focus state know where they are on the page when tabbing through (and it also benefits none keyboard users).

We can do this the same way that we did the checked state, but instead of using the :checked pseudo selector we can use the :focus pseudo selector and add our focus state to the box (not the icon).

.a-checkbox__input:focus + .a-checkbox__label .a-checkbox__box {
    box-shadow: 0 0 0 0.25em rgba(49, 204, 137, 0.5);
    border-color: #31cc89;
}
Bringing it all together
.a-icon {
    color: #000000;
    vertical-align: top;
}

.a-icon--stroke {
    fill: none;
    stroke: currentColor;
    stroke-linecap: butt; /* butt, round, square */
    stroke-linejoin: miter; /* bevel, miter, round */
    stroke-width: 2;
}

.a-checkbox__input {
    float: left;
    margin: 0 0.625em 0 0;
    width: auto;
}

@supports (transition: stroke-dashoffset) {
    .a-checkbox__input {
        border: 0;
        clip: rect(0 0 0 0);
        height: 1px;
        margin: -1px;
        overflow: hidden;
        padding: 0;
        position: absolute;
        width: 1px;
    }
}

.a-checkbox__label {
    display: inline-block;
}

.a-checkbox__box {
    display: none;
}

@supports (transition: stroke-dashoffset) {
    .a-checkbox__box {
        border: 0.125em solid #dcdcdc;
        display: inline-block;
        height: 1.875em;
        margin-right: 0.625em;
        position: relative;
        transition: border-color 0.3s cubic-bezier(0.5, 0.61, 0.355, 1), box-shadow 0.3s cubic-bezier(0.5, 0.61, 0.355, 1);
        vertical-align: middle;
        width: 1.875em;
    }
}

@supports (transition: stroke-dashoffset) {
    .a-checkbox__icon {
        height: 1em;
        left: 50%;
        position: absolute;
        stroke-dasharray: 22;
        stroke-dashoffset: 22;
        top: 50%;
        -webkit-transform: translate(-50%, -50%);
                transform: translate(-50%, -50%);
        transition: stroke-dashoffset 0.3s cubic-bezier(0.5, 0.61, 0.355, 1);
        width: 1em;
    }
}

@supports (transition: stroke-dashoffset) {
    .a-checkbox__input:checked + .a-checkbox__label .a-checkbox__icon {
        stroke-dashoffset: 0;
    }
}

@supports (transition: stroke-dashoffset) {
    .a-checkbox__input:focus + .a-checkbox__label .a-checkbox__box {
        box-shadow: 0 0 0 0.25em rgba(49, 204, 137, 0.5);
        border-color: #31cc89;
    }
}

Radio styling

And here is the CSS for the radio button. It’s very similar to the checkbox styling but instead of transitioning the SVG polyline element we’re transitioning the opacity on our span used for the icon.

.a-radio__input {
    float: left;
    margin: 0 0.625em 0 0;
    width: auto;
}

@supports (transition: stroke-dashoffset) {
    .a-radio__input {
        border: 0;
        clip: rect(0 0 0 0);
        height: 1px;
        margin: -1px;
        overflow: hidden;
        padding: 0;
        position: absolute;
        width: 1px;
    }
}

.a-radio__label {
    display: inline-block;
}

.a-radio__box {
    display: none;
}

@supports (transition: stroke-dashoffset) {
    .a-radio__box {
        border: 0.125em solid #dcdcdc;
        border-radius: 100%;
        display: inline-block;
        height: 1.875em;
        margin-right: 0.625em;
        position: relative;
        transition: border-color 0.3s cubic-bezier(0.5, 0.61, 0.355, 1), box-shadow 0.3s cubic-bezier(0.5, 0.61, 0.355, 1);
        vertical-align: middle;
        width: 1.875em;
    }
}

@supports (transition: stroke-dashoffset) {
    .a-radio__icon {
        background-color: currentColor;
        border-radius: 100%;
        height: 0.875em;
        left: 50%;
        opacity: 0;
        position: absolute;
        top: 50%;
        -webkit-transform: translate(-50%, -50%);
                transform: translate(-50%, -50%);
        transition: opacity 0.3s cubic-bezier(0.5, 0.61, 0.355, 1);
        width: 0.875em;
    }
}

@supports (transition: stroke-dashoffset) {
    .a-radio__input:checked + .a-radio__label .a-radio__icon {
        opacity: 1;
    }
}

@supports (transition: stroke-dashoffset) {
    .a-radio__input:focus + .a-radio__label .a-radio__box {
        box-shadow: 0 0 0 0.25em rgba(49, 204, 137, 0.5);
        border-color: #31cc89;
    }
}

Fin

There you have it, progressively enhanced custom checkbox and radio button form elements.

See the Pen Custom checkbox and radio button form elements (SVG and CSS animation) by Rob Simpson 👨🏻‍💻 (@robsimpson) on CodePen.

If you’ve enjoyed this post I’d really appreciate it if you shared it around online .

Equally, if you have any questions don’t hesitate to drop me a message on social media .

Thank you, your answers have been sent
These will help me create more relevant content that you’re looking for.
Which topics would you like to see more of?
Thank you, your answers have been sent
These will help me create more relevant content that you’re looking for.
Always stay up to date
What would you like to recieve?
Please confirm that you’d like to hear from me

I’ll only use the information you provide on this form to get touch about the topics you’ve chosen above. For more information on how I use and protect your data, please view my privacy policy.

Related tutorials