Styling checkboxes and radio buttons with CSS

There are two approaches to styling checkboxes and radio buttons with CSS. For a simple way to match the colour of the standard controls to your site’s design, you can use the accent-color property. For complete control over the design of checkboxes and radio buttons, you can use the appearance property to disable their standard styling and implement your own.

Setting an accent colour

The accent-color property sets the colour used by checkboxes and radio buttons, as well as range fields and progress indicators. The accent colour is inherited, so only needs to be set at the root of the document:

1
2
3
:root {
  accent-color: #696;
}

These checkboxes and radio buttons are styled using accent-color:

Checkboxes:
Radio buttons:

A fully custom design

The design of checkboxes and radio buttons styled using the accent-color property varies between browsers and operating systems, and may not work well with the rest of your site’s design. As an alternative, you can use the appearance property to disable the standard styling and implement a fully custom design, as these checkboxes and radio buttons do:

Checkboxes:
Radio buttons:

Creating the boxes

Firstly we style the default state of the controls (enabled, but not active, focused, or checked):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
input:where([type="checkbox"], [type="radio"]) {
  -webkit-appearance: none;
  appearance: none;
  width: 22px;
  height: 22px;
  vertical-align: top;
  margin: calc(0.75em - 11px) 0.25rem 0 0;
  border: 2px solid #ddd;
  border-radius: 4px;
  background: #fff no-repeat center center;
}

input[type="radio"] {
  border-radius: 50%;
}

Lines 2 and 3 disable the standard styling by setting the appearance property to none (the -webkit-appearance property is needed for Safari).

Lines 4 and 5 set the size of the controls. While we would usually use relative units to scale user interface controls based on the text size, for checkboxes and radio buttons this can lead to controls that are too small or excessively large, so we instead use a reasonable fixed size. Note that we are assuming the use of border box sizing, so these dimensions include the border width.

Lines 6 and 7 align the controls with their label text. The vertical-align property isn’t precise enough on its own, so we combine it with a margin on the top of half the line height (here 1.5em) minus half the widget height, which centres the controls within the line. We also add some margin on the right to create more space between the control and its label.

Lines 8, 9, and 14 set the borders of the controls. Checkboxes are given slightly rounded corners, while radio buttons are made circular. Line 10 sets the background, including the positioning of the images that we will add later.

Active and focused states

Next we style the active and focused states, which are essential for visitors who use the keyboard to interact with sites:

17
18
19
20
input:where([type="checkbox"], [type="radio"]):where(:active:not(:disabled), :focus) {
  border-color: #696;
  outline: none;
}

The change in border colour clearly indicates the active or focused element, so we can disable the default outline. Note that we add :not(:disabled) to the :active selector as otherwise disabled controls would show as active while the mouse is held down on them.

Disabled state

Next we style the disabled state by greying out the controls:

22
23
24
input:where([type="checkbox"], [type="radio"]):disabled {
  background: #eee;
}

Checked state

Finally we style the checked state using background images:

26
27
28
29
30
31
32
input[type="checkbox"]:checked {
  background-image: url('checkbox.svg');
}

input[type="radio"]:checked {
  background-image: url('radio.svg');
}

You can download checkbox.svg (148 bytes) and radio.svg (137 bytes) for use on your own site.

The finished code

Combining all of the above leads to the finished code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
input:where([type="checkbox"], [type="radio"]) {
  -webkit-appearance: none;
  appearance: none;
  width: 22px;
  height: 22px;
  vertical-align: top;
  margin: calc(0.75em - 11px) 0.25rem 0 0;
  border: 2px solid #ddd;
  border-radius: 4px;
  background: #fff no-repeat center center;
}

input[type="radio"] {
  border-radius: 50%;
}

input:where([type="checkbox"], [type="radio"]):where(:active:not(:disabled), :focus) {
  border-color: #696;
  outline: none;
}

input:where([type="checkbox"], [type="radio"]):disabled {
  background: #eee;
}

input[type="checkbox"]:checked {
  background-image: url('checkbox.svg');
}

input[type="radio"]:checked {
  background-image: url('radio.svg');
}

Sharing styling with other input controls

Other input controls usually share elements of their design — such as borders and active, focus, and disabled styling — with checkboxes and radio buttons:

In this case, the shared styling can be set for all controls (lines 1 to 18), reducing the amount of additional styling needed for checkboxes and radio buttons (lines 20 to 39):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
input,
textarea,
select {
  border: 2px solid #ddd;
  border-radius: 4px;
  background: #fff no-repeat center center;
  color: #000;
}

:is(input, textarea, select):where(:active:not(:disabled), :focus) {
  border-color: #696;
  outline: none;
}

:is(input, textarea, select):disabled {
  background: #eee;
  color: #000;
}

input:where([type="checkbox"], [type="radio"]) {
  -webkit-appearance: none;
  appearance: none;
  width: 22px;
  height: 22px;
  vertical-align: top;
  margin: calc(0.75em - 11px) 0.25rem 0 0;
}

input[type="radio"] {
  border-radius: 50%;
}

input[type="checkbox"]:checked {
  background-image: url('checkbox.svg');
}

input[type="radio"]:checked {
  background-image: url('radio.svg');
}