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 buttonsUsing 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);
}
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;
}
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);
}
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.
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);
}
Our radios are now styled, (Figure 3) but we still need to handle hover and focus.
accent-color
applied to radio buttonsHover
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.
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);
}
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;
}
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.
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;
}
Figure 6 shows the transition from state to state as the user interacts with the radio buttons.
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; }
}
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.
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;
}
}
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
- Mozilla. “accent-color.” MDN, 4 April 2022, https://developer.mozilla.org/en-US/docs/Web/CSS/accent-color. Accessed 1 May 2022.
- Deveria, Alexis. “accent-color.” CanIUse, https://caniuse.com/?search=accent-color. Accessed 1 May 2022.
- Coyier, Chris. “currentColor.” CSS-Tricks, 17 March 2011, https://css-tricks.com/currentcolor/. Accessed 1 May 2022.
- W3C. “Web Content Accessibility Guidelines (WCAG) 2.1.” W3C, 5 June 2018, https://www.w3.org/TR/WCAG21/#focus-visible. Accessed 2 May 2022.
- Mozilla. “:focus-visible.” MDN, https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible. Accessed 2 May 2022.
- Mozilla. “forced-colors.” MDN, 18 February 2022, https://developer.mozilla.org/en-US/docs/Web/CSS/@media/forced-colors. Accessed 2 May 2022.
- 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.