Styling Radio Buttons A CSS Solution

Radio inputs are one of those input types that are notoriously difficult to style because we don't have a whole lot of control over the native input. Let's explore 2 different pure CSS options we can use to achieve the desired results.

Accent Color Property

The first option, which only recently became available to us is to set a color value to the accent-color property [1] for the input. Now added to the latest versions of the major browsers (Chrome, Edge, Firefox and Safari),[2] the accent-color takes a color and applies it to the control. Figure 1 shows 2 sets of radio buttons, the first has an accent-property value of auto, making the selected radio blue, the default color for my system. The second set has an accent-color value of #96055b so the selected radio is burgundy.

accent-color applied to radio buttons

Using accent-color is a great way to make the control match the rest of our application quickly, but if we want to change anything other than the color, we need to replace the native display with our own.

Custom Radio

The first thing we need to do in order to replace the native control display with our own, is hide the default display using the appearance property and reset the styles as seen in Listing 1.

The appearance property will remove the native control styles of the input field. It does not hide it, or remove it from the DOM, it only removes the styles specific to the control. If we were to apply it to a checkbox, it would hide the square. We still have an input field on the page ready to be styled.

input[type="radio"] {
  /* hide the native control */
  appearance: none;
  margin: 0 .5rem 0 0;

  /* remove IOS background */
  background-color: var(--background);
}
Hide radio native control

We now need to replace the control with our own. We will start by defining the unchecked style and then add the styles necessary to accomplish the checked look. It's important to note that we use color variables throughout this example to ensure we can support various themes, including light and dark modes on the page.

Unchecked Radio

We set the display to inline-block to allow us to dictate a width and height for the input. We will base this on the font size in order for the radio to scale with text as it is grown or shrunk (Listing 2).

Note: The currentColor value used for the color property specifically sets the color value for the input to the color property value of the parent [3].

input[type="radio"] {
  ...
  display: inline-block;
  width: 1em;
  height: 1em;

  /* inherit the font from the parent so that we are sure the font-family, size ... match */
  font: inherit;
  color: currentColor;
}
Setting up the size and font

We now style the field to look like an unchecked ratio button using borders and border radius (Listing 3).

Note: Although we could have used inherit above to set the color, if we want the border to be the color of the text, but don't know the color of the text, we can't inherit. We can however use currentColor to set the value. The computed value will be the color property value of the parent element [3].

input[type="radio"] {
  ...
  border: 0.1em solid currentColor;

  /* Makes the element a circle */
  border-radius: 50%;

  /* Adjust the position relative to the text */
  transform: translateY(.125rem);
}
Setting up the size and font

Figure 2 shows our current output. We have a styled radio, however we can't tell which one is checked. We now need to handle the styles for the selected input.

Styled radio buttons. Label preceded by circle which is the same color as the label (black). Diameter is slightly larger than a capital letter for the text.

Unchecked radio buttons

Checked Radio

When the radio is checked we want the color of the input field to match our accent color. We need to change the color of the border, fill the inner disc of the circle with color, and provide a visual separation between the inner disc and the outer border for a nice visual effect. We give the input a background color to show the selection disc, then we use box-shadow to create the illusion of a ring inside the input. We make the color of this separating ring the same as our page background to create the illusion of transparency. The code applied to checked radios can be found in Listing 4.

input[type="radio"]:checked {
  /* change our border color to burgundy */
  border-color: var(--accent);

  /* makes the input burgundy */
  background-color: var(--accent);

  /* creates a "transparent" ring inside of the input */
  box-shadow: inset -.005rem -.005rem 0 .1rem var(--background);
}
Checked Radio

Our radios are now styled, (Figure 3) but we still need to handle hover and focus.

Checked radio has a burgundy outline and inner burgundy circle.

accent-color applied to radio buttons

Hover

Adding a hover effect will help our users understand that the element can be interacted with, and confirm which element the user is about to select. We are going to add a gray inner circle to the element when it is being hovered as seen in Figure 4.

The outline of the input being hovered is black, same as the text, but the inner circle is gray.

Hover effect applied to "Water" option

To apply the effect, we use the pseudo class :hover and apply a background color and box-shadow similarly to what we did for the checked radio, but using a gray color rather than the accent color. Listing 5 shows the full CSS rule for the hover.

We only apply the hover to non checked inputs because we do not want to visually lose the concept of which one is currently already checked.

input[type="radio"]:not(:checked):hover {
  background-color: rgba(0, 0, 0, .36);
  box-shadow: inset -.005rem -.005rem 0 .1rem var(--background);
}
Hover effect

Even though we only applied the hover style to the input element itself, hovering over the text also produces the desired effect. This is because both the input and the text are contained within a label element.

With hover handled, we will now handle styles to be applied when the element is focused.

Focus

Adding a visual effect when an element is in focus is imperative to accessibility and a requirement in the Web Content Accessibility Guideline (WCAG) standards. Success Criterion 2.4.7 Focus Visible states:

Any keyboard operable user interface has a mode of operation where the keyboard focus indicator is visible [4].

By default the browser will add an outline around the element. We are going to style that outline to match our style and color. We will then offset the outline away from the element to make it more obvious when present. Like hover, we will use a pseudo class to target the element, but this time we will use :focus-visible to which we will add the new styles.

:focus-visible works a little bit differently than :focus as it only shows when the browser detects that the element is being navigated to or interacted with via keyboard rather than mouse [5]. Listing 6 shows this concept applied in code.

input[type="radio"]:focus-visible {
  outline: dotted 2px var(--accent);
  outline-offset: 3px;
}
Focus Effect

When we click on the radio value we would like to select, the focus outline does not appear. However, when we navigate to the selection via the keyboard, the outline appears as shown in Figure 5.

The second option, which is being focused, has a burgundy dotted outline.

Focus effect applied to "Tea" option

Now that we've added hover and focus effects, let's animate the change to make the visual transition between states smoother (Listing 7). On the input itself we will dictate which properties will be transitioned, the speed at which the transition happens, and the amount of time it will take to perform the transition.

input[type="radio"] {
  ...
  transition-property: background-color, border-color, outline, outline-offset;
  transition-timing-function: ease-in;
  transition-duration: 200ms;
}
Transition

Figure 6 shows the transition from state to state as the user interacts with the radio buttons.

As the user hovers, clicks, or keyboard navigates to each element, the change is applied smoothly over a 200 ms time period rather than abruptly.

Applied transition

We aren't quite done yet. We have 2 more things we need to consider.

Reduced Motion

Since we applied an animation, we need to make sure to consider the user's preferences and suppress the animation for users who have prefers-reduced-motion set to reduce. One of the reasons a user may choose this setting is because on-screen motion may render them ill. For accessibility, and a good user experience, it is imperative that we respect the user's choice. To accomplish this end, we use a media query to target the setting and set the transition duration to 0, which is the equivalent to removing it entirely (Listing 8).

@media (prefers-reduced-motion: reduce) {
  * { transition-duration: 0ms; }
}
Reduced Motion

Forced Colors

Forced colors is a setting which enforces a user-chosen limited color palette to the page [6]. It also affects some of the CSS properties we have applied to our input field, most notably box-shadow and background-color. To make sure the checked input is still differentiated from its unchecked counterpart, we add a background color of ActiveText to the input when checked. We lose the inner ring (as seen in Figure 7), but this ensures that the checked input is distinct.

ActiveText references the system value for selected text as defined by the CSS Color Module Level 4 [7]. The color that will display will therefore depend upon the user's system settings.

The output is almost entirely black and white, with only the focus outline in burgundy. The checked input circle is entirely black, the same color as the text.

Output when forced colors is applied

To only apply this change to users who have choose to use forced-colors we use a media query as shown in Listing 9.

@media (forced-colors: active) {
  input[type="radio"]:checked {
    background-color: ActiveText;
  }
}
Forced Colors

With this last piece completed we have custom styled radio inputs that are accessible and respect our user's settings.

The code can be found below or on codepen at https://codepen.io/martine-dowden/pen/oNENMgw

Happy Coding!

References

  1. Mozilla. “accent-color.” MDN, 4 April 2022, https://developer.mozilla.org/en-US/docs/Web/CSS/accent-color. Accessed 1 May 2022.
  2. Deveria, Alexis. “accent-color.” CanIUse, https://caniuse.com/?search=accent-color. Accessed 1 May 2022.
  3. Coyier, Chris. “currentColor.” CSS-Tricks, 17 March 2011, https://css-tricks.com/currentcolor/. Accessed 1 May 2022.
  4. W3C. “Web Content Accessibility Guidelines (WCAG) 2.1.” W3C, 5 June 2018, https://www.w3.org/TR/WCAG21/#focus-visible. Accessed 2 May 2022.
  5. Mozilla. “:focus-visible.” MDN, https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible. Accessed 2 May 2022.
  6. Mozilla. “forced-colors.” MDN, 18 February 2022, https://developer.mozilla.org/en-US/docs/Web/CSS/@media/forced-colors. Accessed 2 May 2022.
  7. W3C. “CSS Color Module Level 4.” W3C, 28 April 2022, https://www.w3.org/TR/css-color-4/#css-system-colors. Accessed 2 May 2022.

Consulting

Our expertise helps your team ramp up on new technologies or practices, or to fill short-term skills gaps.

Read more about Consulting with Andromeda