This seems like an obvious statement; styling should be done with CSS. But unfortunately in the case of checkboxes, there's basically no support for directly styling the checkbox.
Because of that, I've seen several JavaScript implementations to change between displaying an unchecked box and a checked box based on state in order to fake a styled checkbox.
But let's not go so quickly to JS to solve a fundamentally styling problem! It can be done with just CSS, we just need to shift our thinking a little.
The goals
- Style the checkbox
- Style the label
- Require no additional DOM nodes or configuration (including icons/images)
- Require 0 JavaScript
The results
The strategy
Since we cannot style the checkbox input directly with something like checkbox-style: awesome;
, we'll need to take a different approach:
- Hide the actual checkbox input
- Show a styled element that looks like an empty checkbox when the input is unchecked
- Show a styled element that looks like a checked checkbox when the input is checked
How to get there
The CSS selectors used
Type selector
type
- selects all elements of the giventype
(e.g.input
will select all<input ... />
nodes)Attribute selector
[attribute="value"]
- selects an element withattribute
where its value is exactlyvalue
Psuedo-class
:checked
- selects checkbox/radio input types oroption
s in aselect
that are selected/checked/on/activePsuedo-element
::before
- styleable element that doesn't actually exist in the DOM; considered the first child of the selected elementUniversal selector
*
- selects anything/everythingChild combinator
>
- combines two selectors; narrowing the selection on the right-hand side to only those elements that are direct descendants of elements selected by the left-hand side.Adjacent sibling combinator
+
- combines two selectors; narrowing the selection on the right-hand side to only those elements that are the siblings immediately after the elements on the left-hand side
Important CSS styles used
content
- used in the::before
psuedo-element to set its contentdisplay
- specificallynone
to hide elements andinline-block
to make our otherwise inline checkbox able to have a consistent width and heightwidth
/height
- does what you think: sets width and height of the elementcolor
- sets the color of the texttext-align
/vertical-align
- used for adjusting the position of our check/checkbox to its labelborder
styles - How we'll form and color the checkboxbackground
- sets the background color (used to fill in the checkbox when it is checked)
Starting out: the HTML
Let's set up our checkbox as a child of its label
element with a sibling of a span
of the label text:
<label>
<input type="checkbox" name="key" value="value" />
<span>I am a checkbox</span>
</label>
This structure allows clicking on the label text (I am a checkbox
) to toggle the checkbox without needing for
or unique id
attributes. Placing the text in a span
directly after the input
will allow us to select it in CSS.
First step: hide the unstyleable checkbox
Going back to our strategy: since we can't do anything with the native checkbox, we'll have to hide it and do our own thing.
label > input[type="checkbox"] {
display: none;
}
We'll select the checkbox (input[type="checkbox"]
) and make sure it's labelled the way we need it to be (label >
). Then just display: none
to get it off our screens.
Second step: make our own checkbox
Making an empty square is easy with CSS, just put a border around an element with no content and set its width and height. Let's also put it on the ::before
psuedo-element to avoid adding unnecessary HTML.
label > input[type="checkbox"] + *::before {
content: "";
display: inline-block;
vertical-align: bottom;
width: 1rem;
height: 1rem;
border-radius: 10%;
border-style: solid;
border-width: 0.1rem;
border-color: gray;
}
Building from the selection in the previous section; we add + *
to select any element as long as it is the direct subsequent sibling to the checkbox of interest and then ::before
to select the said psuedo-element for making the box.
Last step: make our checkbox change when checked
So far, even though the checkbox has been working, it doesn't look like it's working. Let's change that by making the checkbox checked when it is :checked
. Also, for the proof-of-concept, let's change styles on the label text itself (while I'll only change the text color, you can imagine changing any style to build any sort of check/uncheck select/unselect interface).
label > input[type="checkbox"]:checked + *::before {
content: "✓";
color: white;
text-align: center;
background: teal;
border-color: teal;
}
label > input[type="checkbox"]:checked + * {
color: teal;
}
The important part here being :checked
is placed after the input[type="checkbox"]
(since that is the element that is checked or not). And fortunately for us, there's a checkmark character that we can set as the content of our checkbox: ✓
.
Bonus points: extend this to radio buttons
As a challenge to you, reader, this same strategy applies to making a styled radio button. Using this as a guide, are you able to transform this for the type="radio"
input? (or just follow me as that will be the next installment in this series)
Wrapping up and side notes
Thanks for reading! I like sharing and finding things like this where common web design patterns can be done without a massive JavaScript library or framework bogging the page down. Give me some suggestions below on patterns you'd like to see me break-down in CSS/HTML-only for this series.
While this example had "no unnecessary DOM"; it is also perfectly valid to include an additional span
(or two) to hold svg/font-awesome icons for more precise/exotic checkbox designs. Then :checked
should be used to alternatively display: none
and display: inline-block
the icons and +
will need to become ~
to select all the siblings.
Top comments (7)
Hi Nathan,
nice and clean, I like that.
But without "-webkit-appearance: none;" it doesn't work on iOS?
Iill find a solution for that. My question:
How to style longer text? If I had a longer text,
No icons or unnecessary DOM CSS-only fully stylable checkbox CSS-only fully stylable checkbox CSS-only fully stylable checkbox CSS-only fully stylable checkbox CSS-only fully stylable checkbox CSS-only fully stylable checkbox CSS-only fully stylable checkbox CSS-only fully stylable checkbox
it flows around the checkbox which is ugly. If I set
input[type="checkbox"] + * {display: inline-flex;}
It's a nice textblock, completly on the right ahnd of the checkbox. But then (only) the unchecked checkbox gets much to small. If checked it gets in its correct size. Didn't understand this.
Oh, and I must mention, that for reasons of the CMS-Plugin, I do nat have a or other tag available to directly style it. Its just sitting plain inside the -tag.
I can't figure out, how to style the uncheked box properly. Any Ideas?
Thanks!
Best regards from Germany
Tim
Hi Tim,
Try adding an
input[type="checkbox"] + *::before { flex-shrink: 0; }
to make sure the checkbox doesn't get shrunk as part of flex attempting to squeeze so much content into its container. (I've updated my code pen to add these two changes so you can see it in action)Hello Nathan,
thanks! Clear and simpel. Never heard about flex-shrink...
Do you have an explanation why I get shown 2 checkboxes on iOS-Devices? Fist ist appearance-stylend second by own css. So /display: none/ seems to be ignored. But also /-webkit-appearance: none/ doesn't work.
This happens not in OS X or WIN PCs only iOS (iPad, iPhone)...
Best regards
Tim
Tim, I'm not sure; there must be some vendor specific style to hide the checkbox on iOS/Safari. I don't have an iOS device to test this on; if you do have a solution I'd love to incorporate it into this example for everyone to learn from.
If I get your example run (without span-tag), I'll surely find a solution and will let you know.
Hi Nathan,
it works perfectly when the multi-lined text ist within its own tag (in your example ). But my form-plugin doesn't cover the text, so it just stays plain between input and label. In this case your solution doesn't work (just delete the span-tag in your example and see what happens).
Any ideas?
Best regards
Tim