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 style of the native input. Let's explore 2 different pure CSS options we can take.
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.
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.
We now need to replace the control with our own. We will start with the unchecked style, and add to the styles to accomplished the checked look.
Unchecked Radio
We set the display
to inline-block
to allow us to dictate a width and height for the input which we will base on the font size in order for the radio to scale with text as it is grown or shrunk (Listing 2).
Note: Notice the currentColor
value used for the color property. This specifically sets the color value for the input to the color value applied to the parent [3].
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].
Figure 2 shows our current output. We have a style unchecked 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 input is checked we want the color of the input to be that of our accent color. We also want there to be an inner circle within our input. Instead of adding a circle inside, we are going to create the illusion of a circle instead. We give the input a background color and then use box-shadow to create a circle inside the input. We make the color of the circle the same as our background which creates the illusion of transparency. The code applied to checked radios can be found in Listing 4.
Our radio are now styled (Figure 3) but we still need to handle hover and focus.
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.
To apply the effect, we use the pseudo class :hover
and apply a background color and box-shadow similarly to what we did for checked 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.
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-within
to which we will add the new styles.
:focus-visible
works a little bit differently then :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.
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.
We 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.
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 two more things we need to consider.
Reduced Motion
First is that 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).
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 loose 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.
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
See the Pen CSS Radio Buttons by Martine Dowden (@martine-dowden) on CodePen.
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.