TL;DR:
I made a thing. Maybe you think it's cool? See code here.
For people who read, please continue.
Radio buttons have been around for quite some time, and as a staple of UI, developers often want to style them to better match the theme or style of their applications. The traditional way to customize radio buttons usually involves extra HTML elements or wrapping the radio input with a label, coupled with the use of :before
and :after
pseudo-elements. However, with the advancements in modern CSS, we can now style them with just the input
element alone.
The Traditional Approach
In the past, custom styling radio buttons usually involved:
- Additional HTML elements (like
div
orspan
) as overlays. - Utilizing the sibling selector combined with
label
elements. - Leveraging
:before
and:after
pseudo-elements to create custom designs.
While these methods are tried and true, they can be cumbersome and add unnecessary weight to the DOM.
The Pseudo-Element Limitation on Input Elements
An often overlooked, but crucial detail when styling form elements, particularly input
, is their limitation with :before
and :after
pseudo-elements. Historically, these pseudo-elements have been used extensively to add decorative content or visual cues to elements. However, input
elements are "replaced elements", and as per the CSS specification, replaced elements and their pseudo-elements do not have a visual box and thus cannot be styled or receive generated content using :before
and :after
(see here and here).
This limitation often led designers and developers to resort to workarounds, like wrapping input
elements inside other elements or using sibling selectors combined with label
elements. While these methods work, they introduce unnecessary complexity and can bloat the DOM.
By leaning into modern CSS, as I outline below, we sidestep the need for these workarounds entirely. We embrace a more streamlined approach, focusing on the input element itself, and doing away with the need for extra HTML or pseudo-elements.
Embracing Modern CSS
Modern CSS has introduced several properties and selectors that make styling more straightforward, cleaner, and maintainable.
Let's break down the new approach, step by step:
1. Define Color Variables
Using the :root
selector, we can define global CSS variables. This makes it easier to change the color scheme without having to search and replace values throughout the CSS.
Of course it's up to you with how creative you want to be with the colors. But at minimum you will need 4 different colors:
- Border color
- Checked color
- Hover color
- Disabled background color
It's pretty common these days to two tones of blue and two tones of gray (as I did here).
:root {
--radio-border-color: #8b8c89;
--radio-checked-color: #274c77;
--radio-hover-color: #a3cef1;
--radio-disabled-bg-color: #d9d9d9;
}
2. Base Styling
The core styling for our radio button. We use the appearance
property set to none
to remove the default appearance of the radio button.
input[type="radio"] {
box-sizing: border-box;
width: 20px;
height: 20px;
margin: 6px;
padding: 0;
border: 2px solid var(--radio-border-color);
border-radius: 50%;
appearance: none;
background-color: transparent;
outline: none;
}
Of course the appearance
property isn't exactly new. In fact, it actually has quite a rich history...
Side quest: A Brief History of the appearance property
Early Days & Vendor Prefixes (Late 1990s - Early 2000s):
In the late 1990s and early 2000s, web browsers had implemented proprietary properties to allow for the customization of form controls. That era was marked by vendor prefixes:
-
-webkit-appearance
was for WebKit browsers (like Chrome and Safari) -
-moz-appearance
was for Firefox
Discouraged Use (Mid-2000s):
By the mid-2000s, as more developers had begun to explore the web's capabilities, the non-standard nature of the appearance property had led to its discouraged use. The cross-browser inconsistency had meant that web developers often had to rely on hacks and workarounds.
Standardization Efforts (Late 2000s - Early 2010s):
By the late 2000s, the CSS Working Group had started discussions about the appearance property's potential standardization. It had been considered for inclusion in the CSS UI (User Interface) specification drafts. The property was formally introduced in the CSS Basic User Interface Module Level 3 (CSS3 UI) around 2012.
Modern Usage (Mid-2010s - Present):
From the mid-2010s onwards, browsers had started aligning their implementations with the CSS3 UI specification. The use of vendor prefixes had started to decline, with many modern browsers having begun to support the unprefixed appearance property. By 2019, the widespread support for appearance: none had solidified its position as a reliable tool for web developers.
Now back to your regularly scheduled article.
3. Styling the Checked State
The :not()
pseudo-class combined with :checked
is used to style radio buttons when they're checked, but not disabled. I'm a big fan of using pseudo-classes like this to be really explicit when these styles are applied, instead of letting things cascade and just overwriting them farther down the stylesheet.
The radial-gradient
creates the appearance of a selected inner circle. Instead of reaching for SVGs (I'm looking at you Material UI), or using wrappers and whatever else, we can draw that circle with just straight up CSS!
input[type="radio"]:not(:disabled):checked {
border-color: var(--radio-checked-color);
background-color: var(--radio-checked-color);
background-clip: content-box;
padding: 2px;
background-image: radial-gradient(
circle,
var(--radio-checked-color) 0%,
var(--radio-checked-color) 50%,
transparent 60%,
transparent 100%
);
}
4. Styling for Disabled State
For the disabled state, we only need to add a background color, and cover the case when a radio can be checked && disabled.
input[type="radio"]:disabled {
background-color: var(--radio-disabled-bg-color);
}
input[type="radio"]:disabled:checked {
background-image: radial-gradient(
circle,
var(--radio-border-color) 0%,
var(--radio-border-color) 50%,
transparent 50%,
transparent 100%
);
}
That covers the basic states of the input. Now time to move on to the interactions.
5. Enhancing Hover Effects
The @media (hover: hover)
media query ensures that hover effects are applied only on devices that support hovering, like desktops. This detail is easy to overlook, especially if you haven't worked with CSS recently.
@media (hover: hover) {
input[type="radio"]:not(:disabled):hover {
background-color: var(--radio-hover-color);
outline: 6px solid var(--radio-hover-color);
}
}
6. Focus Indication with :focus-visible
The :focus-visible
pseudo-class is a modern approach to style the focus state only when it's relevant (like when using keyboard navigation). No longer do you have to annoy users (and of course get annoyed yourself) by showing a focus ring to users who don't need it.
input[type="radio"]:focus-visible {
background-color: var(--radio-hover-color);
outline: 6px solid var(--radio-hover-color);
}
In the current code, both hover/focus have the same style. This may not be the case though for you. Always make sure to follow accessibility best practices when picking colors and width of hover/focus ring effects.
7. Adding the Finishing Touches
Refinements can make a big difference in user experience. There's a couple things we can add to polish this thing up a bit:
Smooth Transitions:
A slight transition can make state changes feel more natural.
input[type="radio"] {
box-sizing: border-box;
width: 20px;
height: 20px;
margin: 6px;
padding: 0;
border: 2px solid var(--radio-border-color);
border-radius: 50%;
appearance: none;
background-color: transparent;
outline: none;
+ transition: outline 0.1s;
}
Subtle Scaling:
A hint of scaling can indicate interaction, provided it's not overdone.
@media (hover: hover) {
input[type="radio"]:not(:disabled):hover {
background-color: var(--radio-hover-color);
outline: 6px solid var(--radio-hover-color);
+ transform: scale(1.05);
}
}
input[type="radio"]:focus-visible {
background-color: var(--radio-hover-color);
outline: 6px solid var(--radio-hover-color);
+ transform: scale(1.05);
}
Honoring User Preferences:
With the prefers-reduced-motion media query, we ensure that our design respects users who prefer reduced motion.
@media (prefers-reduced-motion: reduce) {
input[type="radio"] {
transition: none;
}
input[type="radio"]:focus-visible {
transform: scale(1);
}
}
@media (prefers-reduced-motion: reduce) and (hover: hover) {
input[type="radio"]:not(:disabled):hover {
transform: scale(1);
}
}
And that's about it for the code.
You can find the full code here (and here if you use scss or some preprocessor with nested styles, but it's only 4 lines shorter, so who needs it anyways?).
Accessibility Considerations for Custom Radio Button Styling
In the world of web design, aesthetics often take center stage. However, it's super important that our designs are not just visually appealing but also accessible to all, including persons with disabilities. Ensuring that your custom-styled elements remain user-friendly and compliant with accessibility standards is both a responsibility and a mark of excellent web design.
There are several important things to think about if you want to use my approach, or use any other custom radio button implementation.
Contrast
Ensure the colors you choose meet the minimum contrast ratios set by the Web Content Accessibility Guidelines (WCAG). Tools like the WebAIM Color Contrast Checker can help evaluate your color choices.
Label Associations
Wrap the input with a label to increase the clickable/tappable area, making it easier for everyone, especially users with motor impairments. Ensure the label contains descriptive text that clearly indicates the choice the radio button represents.
<label>
<input type="radio" name="choice" value="option1" />
Descriptive Option Text
</label>
Focus Styles
The custom radio button uses :focus-visible
to show a distinct style when the radio is focused. It’s crucial to ensure that the focus style is clearly visible to aid keyboard-only users in navigating and making selections.
Size Matters
Aim for a minimum size of 44x44 pixels for the clickable/tappable area, as recommended by the WCAG. This size is not only beneficial for users with motor impairments but also improves the experience for mobile users. The current size in these styles is only 20x20 pixels, so you will want to have the label
wrapping the input
at least 44x44 pixels.
Be Sensible with Animations
While subtle transitions and animations (like scaling) can enhance user experience, avoid overly complex or distracting animations. Some users are sensitive to motion. Always include a provision for users who have set a preference for reduced motion.
Semantic HTML
Ensure that the HTML structure is semantic. While styling is important, maintaining the semantic integrity of elements like input
and label
ensures assistive technologies can interpret and convey the content effectively. Of course you can always aria-hidden="true"
to unnecessary markup, but more code === more maintenance overhead && more bytes you're shipping to users' browsers, so it's something to think about.
ARIA Attributes
While native HTML elements, when used correctly, often don't require additional ARIA attributes, always be on the lookout for scenarios where they might enhance accessibility. For instance, if you have a group of radio buttons, the group could be wrapped in a fieldset
with a legend
describing the set.
Testing
Of course, it's always best to test your designs with real users, including those who rely on assistive technologies. Manual testing, while time-consuming, often surfaces issues automated tools might miss.
Parting words
Modern CSS offers a cleaner, more streamlined approach to customizing radio buttons without the need for extra HTML or convoluted CSS. Embracing these techniques allows for efficient, maintainable, and user-friendly designs.
Always think about WHY you want to create a custom radio button in the first place. If the reason is to offer a better experience to users, then be sure to think about ALL users; think about accessibility. By focusing on accessibility from the outset, designers and developers can ensure a more inclusive web experience for all users, regardless of their abilities or how they access content.
And if improving user experience is really your goal, make sure to keep things as performant as you can. I don't think we're really optimizing early here or anything. It's a simple matter of less code means less bugs, less bytes sent down the wire, and hopefully after reading this, less css in js in your application or design system.
Top comments (0)