DEV Community

Adam Butterfield
Adam Butterfield

Posted on

Custom Styling Radio Buttons: The Modern Way

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 or span) 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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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%
  );
}
Enter fullscreen mode Exit fullscreen mode

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%
  );
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)