CSS Radio Button Styles: 20 Free Custom Designs 2026
The Problem with Default Radio Buttons
Browser-default radio buttons are styled differently on every operating system. On macOS they look one way; on Windows 10 another; on Android Chrome they render differently again. This inconsistency makes it impossible to deliver a cohesive visual experience without custom styling.
Beyond inconsistency, default radio buttons are typically 12–14px — below the WCAG 2.5.5 recommended touch target size of 44×44px. On mobile, this makes radio selections frustrating and error-prone.
The CSS-Only Custom Radio Button Technique
The modern approach hides the native element while preserving its accessibility semantics, then draws a replacement using ::before/::after pseudo-elements.
HTML Structure
<label class="radio">
<input type="radio" name="plan" value="starter">
<span class="radio__control"></span>
<span class="radio__label">Starter Plan</span>
</label>
The actual <input type="radio"> stays in the DOM — it provides keyboard handling, form submission, and accessibility semantics automatically. We only hide its visual appearance.
CSS — Hiding the Native, Drawing a Custom Control
.radio {
display: flex;
align-items: center;
gap: 0.625rem;
cursor: pointer;
user-select: none;
}
/* Hide native input visually but keep it accessible */
.radio input[type="radio"] {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
/* Custom radio circle */
.radio__control {
width: 20px;
height: 20px;
border-radius: 50%;
border: 2px solid rgba(255,255,255,0.25);
background: rgba(255,255,255,0.04);
position: relative;
flex-shrink: 0;
transition: border-color 0.2s, background 0.2s;
}
/* Inner dot — hidden by default */
.radio__control::after {
content: '';
position: absolute;
top: 50%; left: 50%;
width: 8px; height: 8px;
border-radius: 50%;
background: #ef4444;
transform: translate(-50%, -50%) scale(0);
transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* Checked state */
.radio input[type="radio"]:checked + .radio__control {
border-color: #ef4444;
background: rgba(239,68,68,0.12);
}
.radio input[type="radio"]:checked + .radio__control::after {
transform: translate(-50%, -50%) scale(1);
}
/* Focus ring — keyboard users only */
.radio input[type="radio"]:focus-visible + .radio__control {
outline: 2px solid #ef4444;
outline-offset: 3px;
}
The spring easing (cubic-bezier(0.34, 1.56, 0.64, 1)) gives the selection a satisfying, bouncy feel. The :focus-visible pseudo-class shows the focus ring only for keyboard navigation, not for mouse clicks — keeping the UI clean for mouse users while remaining accessible.
Style Variations
Card-Style Radio (Pricing Plan Selector)
Wrapping the radio inside a card creates a larger, more obvious selection target — ideal for pricing plan or feature option selectors:
.radio-card {
display: flex;
align-items: flex-start;
gap: 1rem;
padding: 1.25rem;
border: 1px solid rgba(255,255,255,0.1);
border-radius: 14px;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
}
.radio-card:has(input[type="radio"]:checked) {
border-color: #ef4444;
background: rgba(239,68,68,0.06);
}
The :has() relational pseudo-class (Chrome 105+, Safari 15.4+, Firefox 121+) lets the parent card respond to the child input's state without JavaScript.
Grouping Radio Buttons
Always group related radio buttons with <fieldset> and <legend>. This is required for screen readers to correctly announce which group each option belongs to:
<fieldset>
<legend>Select your plan</legend>
<label class="radio">
<input type="radio" name="plan" value="starter">
<span class="radio__control"></span>
<span class="radio__label">Starter — Free</span>
</label>
<label class="radio">
<input type="radio" name="plan" value="pro">
<span class="radio__control"></span>
<span class="radio__label">Pro — $12/mo</span>
</label>
</fieldset>
Accessibility Considerations
- Never use
display: noneto hide the input. Useopacity: 0—display: noneremoves it from the accessibility tree, destroying keyboard navigation and screen reader support. - Use
:focus-visiblenot:focusfor custom focus rings. This hides the ring on mouse click but shows it for keyboard users. - Touch target size: at least 44×44px. Even if the visual radio control is 20px, the
<label>should expand via padding to meet the touch target requirement. - Always use
<fieldset>/<legend>to group options. - Respect
prefers-reduced-motion: replace the bounce transition with an instant state change.
Browser Support
- Core technique (opacity: 0 + adjacent sibling): all modern browsers and IE11
:focus-visible: Chrome 86+, Firefox 85+, Safari 15.4+:has()for card radios: Chrome 105+, Safari 15.4+, Firefox 121+
For older browser support, omit the :has() card highlight and use a JavaScript class toggle as a progressive enhancement.
ProofMatcher Radio Button Components
ProofMatcher provides 20 copy-paste radio button designs at /components/category/radio-buttons — from minimal dark-mode dots to card-style plan selectors and animated pill radios. For related form controls, see the checkbox components, which use the same underlying technique.
For a deeper dive into toggle patterns, the CSS Toggle Switch 2026 guide covers the same approach applied to on/off switches.
Radio Buttons vs Checkboxes vs Toggles vs Selects
Choosing the right control matters as much as styling it. Each one communicates a different kind of choice.
- Radio buttons are for choosing exactly one option from a small set, usually between two and six options, where seeing every choice at once helps the decision. Shipping speed and pricing plans are classic examples.
- Checkboxes are for selecting any number of options, including none, or for a single yes or no agreement such as accepting terms.
- Toggle switches are for settings that take effect immediately, like turning notifications on. If the change only applies after pressing Save, use a checkbox instead.
- Select menus suit long lists, such as countries, where showing every option as a radio button would take too much space.
Keyboard Behaviour and Grouping
Native radio buttons come with keyboard behaviour that custom designs must preserve. Radios that share the same name form a group: the Tab key moves focus into the group and out again as a single stop, and the arrow keys move the selection between options. This is why the technique above visually hides the native input rather than removing it. Replace the inputs with div elements and all of this behaviour disappears.
Wrap each group in a <fieldset> with a <legend> that asks the question, such as "Choose a billing period". Screen readers announce the legend when focus enters the group, so users understand what the options refer to. To make a choice mandatory, add the required attribute to one radio in the group, and the browser will prevent submission until an option is selected.
Finally, make sure the whole label is clickable, not only the small circle. Wrapping the input and its text in a <label> element creates a large target that is much easier to tap on mobile, and it is the foundation of the card-style radio design shown earlier.
Testing Your Custom Radio Buttons
Before you ship a custom design, test it the way real users will. Use only the keyboard to move through the group and change the selection. Zoom the page to 200 percent and check that the circles and labels still line up. Turn on Windows high contrast mode or forced colours and confirm the selected state is still visible, which usually means drawing the checked indicator with a border or outline rather than only a background colour. Finally, test with a screen reader such as VoiceOver or NVDA to hear that the legend, each label, and the checked state are announced correctly. A few minutes of testing catches problems that no amount of visual review will reveal.
Frequently Asked Questions
Why use opacity: 0 instead of display: none?
display: none removes the element from the accessibility tree — screen readers can't find it and keyboard users can't navigate to it. opacity: 0 hides it visually but keeps it accessible and focusable.
Can I animate the radio without JavaScript?
Yes. The approach above uses only CSS transition and transform — no JavaScript needed.
How do I pre-select a radio button?
Add the checked attribute: <input type="radio" checked>. In React, use defaultChecked for uncontrolled or checked with onChange for controlled inputs.
Can I use this in React?
Yes. Replace class with className, use htmlFor instead of for, and manage checked state with useState or a form library like React Hook Form.
Conclusion
Custom CSS radio buttons replace inconsistent, undersized browser defaults with on-brand controls that work well on all devices and pass accessibility requirements — with no JavaScript and no external dependencies.
Browse all 20 radio button designs at /components/category/radio-buttons — copy the code and adapt it to your project in minutes.