Which method styles a Tailwind radio button best: accent color, appearance-none or the forms plugin?
Use accent-* when all you need is a brand color. Go with appearance-none when the checked indicator has to look custom, and with @tailwindcss/forms when a whole project needs one consistent reset. Underneath all of them sits the same native input type=”radio”, styled with utility classes instead of a separate stylesheet.
Most projects settle on one method. Mixing two usually ends in a conflict.
| Method | What you control | Setup cost | Main limit |
|---|
| accent-* | Color of the native dot and ring | One class | Shape and border stay browser-made |
| appearance-none | Every visual part | Border, size and dot built by hand | You own focus and forced-colors behavior |
| @tailwindcss/forms | Border, size and checked color | One plugin | accent-* stops working |
Using the accent color utility
Chrome and Edge 93 and Firefox 92 were the first versions to support accent-color, according to browser compatibility data. Safari tinted controls from 15.4 but only counts as full support from Safari 26.2.
That covers every current desktop engine. MDN still labels the property as limited availability rather than Baseline as of October 2026, so test the browsers your audience actually uses.
Tailwind generates accent-* from your palette, so accent-indigo-500 works out of the box, and hover:accent-pink-500 recolors the control on hover (Tailwind docs). In v4, a token such as –color-brand declared inside @theme becomes accent-brand.
The browser picks a contrasting dot color on its own, except in Safari before 26.2 and in Chrome for Android, which skip that contrast step per the same data. Keyboard and screen reader behavior stay native. To resize the control, use size-5 or size-6 (size-* arrived in Tailwind v3.4).
Building from scratch with appearance-none
appearance-none removes the browser’s circle, so the shape has to come back through size-5, rounded-full and a border.
The checked dot can be a sibling span that fades in with peer-checked:opacity-100, or you can skip the dot and thicken the border instead. Checked color comes from checked:border-indigo-600 plus a matching dot color.
Don’t forget focus. focus-visible:ring-2 with an offset keeps keyboard users aware of where they are.
Material Tailwind’s HTML radio docs take this route. A peer input sits first, followed by an absolutely centered span that goes from opacity-0 to peer-checked:opacity-100.
It’s the longest route and the only one with total control. If a design calls for a square box or a star, this is where you end up.
Adding the forms plugin in Tailwind v3 and v4
The plugin is a reset, not a component kit.
Every input type radio gets restyled globally, and a form-radio class covers elements that need the look without the tag. Inputs, selects and textareas get the same reset, which matters when you are styling the rest of your Tailwind forms.
In Tailwind v4, add @plugin “@tailwindcss/forms”; to the main stylesheet after the tailwindcss import, and pass strategy: “base” or “class” inside a block to limit what the plugin generates. Tailwind v3 takes require(‘@tailwindcss/forms’) in the plugins array of tailwind.config.js, with the same strategy option.
Checked color follows text-* utilities, as the README shows with text-pink-500 on a checkbox. Installed globally, the plugin replaces the native look, so accent-* stops tinting radios and text-* takes over.
Version 0.5.7 (November 2023) switched checkboxes and radios back to their normal appearance in forced-colors mode, per the plugin changelog.
How do you style radio button states like checked, focus-visible and disabled?
Prefix the utility with the state you want to target. Variants stack, so dark:checked:border-indigo-400 works exactly as written.
On the input itself, checked:border-indigo-600 sets the selected border, and a slightly darker border under hover: gives pointer feedback. ring-2 and ring-offset-2 behind focus-visible: show up for keyboard focus and stay off for mouse clicks.
Locked options get opacity-50 and cursor-not-allowed under disabled:, plus peer-disabled: on the label so the text dims along with the control. Set the disabled attribute on the single input you want to lock. The rest of the group keeps working.
Pick state colors with color contrast in mind. WCAG success criterion 1.4.11 asks for at least 3:1 contrast between a control’s states and boundaries and the colors next to them (W3C).
A border-gray-300 ring on a white page sits far below that. Disabled controls are exempt from the rule, so dimming them is fine.
In Tailwind v4 the dark: variant follows prefers-color-scheme by default. For a manual toggle, declare @custom-variant dark (&:where(.dark, .dark *)); in your CSS.
In v3 the same job belongs to darkMode: ‘selector’ in tailwind.config.js, which replaced the class strategy in v3.4.1 (Tailwind docs). A toggle that does nothing to your radios usually means this switch is missing.
How do you build custom radio cards and pills with peer-checked and has-checked?
Hide the native input with sr-only, then style its label from the input’s state. peer-checked: works when the input comes before the label as a sibling, while has-checked: works from a wrapping label and has no ordering rule.
I’d use peer-checked for pills and simple tiles, and has-checked for cards with text, badges, icons and other bits that all change on selection.
Any card layout from your Tailwind card components can become a radio card this way, because only the label’s classes change.
The peer-checked pattern
Here’s the build for one card.
- Put the input first and the label second, as siblings. The peer marker only reaches later siblings (Tailwind docs).
- Give the input sr-only and peer, a shared name, its own value and an id that the label’s for attribute points to.
- Style the label as the card: border, rounded-lg, p-4, cursor-pointer.
- Add peer-checked:border-indigo-600 and peer-checked:ring-2 to the label.
- Add peer-focus-visible:ring-2 so keyboard focus shows on the card.
- Show a check icon from another sibling, positioned absolutely: hidden by default, peer-checked:block when selected.
Wrap each input and label pair in its own element, a div or an li. Without that wrapper, one checked input styles the labels of every later option too.
The has-checked pattern
Tailwind v3.4 (December 19, 2023) introduced has-* variants for the :has() pseudo-class. The release post shows a payment-method label that recolors itself with has-[:checked].
The current docs write the same thing as has-checked:bg-indigo-50 and add dark:has-checked:bg-indigo-950 for dark mode.
One label wraps the input and all the card content. The release-post example keeps the native radio visible and tints it with accent-indigo-500, which makes it a blend of two methods. The current docs example styles the visible input with checked:border-indigo-500 instead.
There’s also peer-has-checked:, which hides or shows an element elsewhere in the layout when a peer’s descendant is checked.
Nothing to order here. Move the input anywhere inside the label and the styles still apply.
How do you make a custom Tailwind radio group accessible?
Wrap the options in a fieldset with a legend, give every input the same name, tie each label to its input, and keep the input focusable. Native radios then supply the radio role and arrow-key navigation with no extra attributes.
- A fieldset with a legend that names the group
- One shared name attribute across all inputs
- A label for attribute matching each input id
- sr-only instead of hidden, so the input stays in the tab order and the accessibility tree
- A visible focus ring on the card through peer-focus-visible
Skipping labels is common. WebAIM’s 2026 Million report (February 2026, one million home pages) found 33.1% of form inputs not properly labeled.
Radio labels are one piece of the broader practice of building accessible forms, which also covers errors, required fields, instructions and the rest.
Tab moves focus into the group and lands on the checked radio, or on the first one when nothing is checked. Arrow keys then move focus and check the next option, wrapping from last to first (W3C WAI-ARIA Authoring Practices Guide).
With the input hidden through sr-only, the focus ring has to live on the card, since the input itself is invisible. Test with Tab and the arrow keys before anything else, because a pill that looks right but never shows focus fails real users.
Native inputs already carry the radio role, so layering ARIA attributes on them adds nothing. WebAIM’s 2026 data points the same direction: pages using ARIA averaged 59.1 detected errors against 42 for pages without it, a correlation WebAIM does not pin on ARIA alone.
How do you control a Tailwind radio group in React and Vue?
Keep the native input and let the framework hold the selected value. React binds each input with checked and onChange, Vue with v-model, and the CSS :checked state follows both, so peer-checked and has-checked styling keeps working unchanged.
In React, store the selection in one state variable and set checked on each input by comparing that variable with the input’s value. The value attribute is what the form submits, and defaultChecked covers the uncontrolled case (React docs).
Vue works the other way around. Point v-model on every input at the same ref and give each one a value attribute, and v-model reads the checked property and listens for change events (Vue docs).
- React treats a radio with a boolean checked prop as controlled and will not let it switch back to uncontrolled
- checked without onChange gives a read-only input and a console warning
- Vue ignores the initial checked attribute, so the starting selection lives in JavaScript state
- Vue’s own radio examples omit name, which keeps state correct but skips native grouping, so add one
Prefer CSS state over state-driven classes for radio cards. Fewer moving parts, same result.
When a design does need conditional classes, write complete strings such as border-indigo-600. Fragments like border-${color}-600 never reach the stylesheet, because Tailwind reads source files as plain text.
If the project is not wired up yet, setting up Tailwind CSS in a React project takes a Vite plugin and one CSS import.
Which component library offers a ready-made Tailwind radio group?
For a plain styled single-choice list, native inputs with peer-checked still beat every library below. Headless UI earns its place when you need object values and custom indicators. shadcn/ui suits teams that want copy-in React components, and daisyUI or Flowbite give you class-based markup with no logic.
| Library | Approach | Styling hook | Framework |
|---|
| Headless UI | Unstyled components | data-checked, data-disabled | React, Vue (the Vue docs are v1) |
| shadcn/ui | Copy-in components | Classes on RadioGroupItem | React |
| daisyUI | Component classes | radio, radio-primary | Any |
| Flowbite | Tailwind classes on a native input | checked:, peer-checked: | Any |
Headless UI’s Radio renders as a span with the radio role, not a native input, so peer-checked has nothing to target. Style it with data-checked: and group-data-checked: instead, as the docs show.
It also binds objects as values, with a by prop for comparing them, and the name prop renders a hidden input so the group submits with a native form (Headless UI docs, v2.1). Tailwind Plus sold predesigned radio groups built on it, and its Catalyst kit wraps it with Fieldset, Legend and Description components. Tailwind Labs closed Tailwind Plus to new sign-ups on September 9, 2026, the day it announced it was joining Shopify, so only existing customers can still get them.
shadcn/ui is a different story as of October 2026. Its Radio Group docs offer three primitives, Base UI, React Aria and Radix UI, installed through the shadcn CLI. Older write-ups that call it a Radix wrapper describe the earlier default.
Libraries like these package accessible UI components, so focus handling and arrow-key movement get written once instead of per project.
daisyUI 5.7.50 documents 8 color classes (radio-neutral through radio-error) and 5 sizes (radio-xs to radio-xl) on a plain input type radio, and warns that each set needs its own name. Flowbite’s v4.0 radio docs write the appearance-none pattern directly as Tailwind classes, with checked:border-brand and a focus ring.
Reach for a library when you need object values or a non-native indicator, or when the project already has a design system built on one. Radios are a small part of that choice, and UI component library examples for selects, dialogs, tabs and so on show whether a kit earns its dependency.
When does a Tailwind radio button style break?
The forms plugin switching off accent-* and a hidden input dropping out of the keyboard order cause most of the damage. A v3 snippet running on v4 defaults causes the rest. Each failure has a quick check and a fix.
Forms plugin and accent color conflict
Install the plugin globally and accent-* does nothing to radios.
The plugin replaces the native look with its own styles, so no native control is left for accent-color to tint. A 2022 iThome tutorial reports the same result, then adds checked:bg-none to remove the plugin’s white dot image and recolors the control with an outline in the checked state.
The simplest way out is to set the checked color with text-* utilities and drop accent-* for radios. Or switch the plugin to the class strategy, so only inputs carrying form-radio get the reset and plain radios keep accent-* working (plugin README).
Hidden inputs and lost keyboard focus
Flowbite’s current radio docs build their card layout with hidden peer on the input (v4.0 docs, retrieved October 2026).
hidden is display: none, and MDN states that it removes an element from the accessibility tree. The radio then disappears for screen readers and cannot take Tab focus.
Tailwind’s own forced-color-adjust docs use sr-only on the radios in a color swatch picker instead, wrapped in forced-color-adjust-none so the swatch colors survive forced colors mode.
Focus rings fail in a second way. Tailwind’s docs describe ring utilities as a solid box-shadow, and MDN notes that forced colors mode sets box-shadow to none.
Pair peer-focus-visible:ring-2 with peer-focus-visible:outline-hidden. In v4, outline-hidden keeps an invisible outline that still shows in forced colors mode, while the new outline-none removes it, so a v3 snippet with focus:outline-none loses its indicator after migration (Tailwind upgrade guide).
Tab through the group with the keyboard first. Then emulate forced-colors: active in developer tools, as Tailwind’s docs suggest, and finish with a screen reader pass over the legend and each label. All of it belongs on your web accessibility checklist for any custom radio.
Version-specific setup differences
Tailwind v4.0 targets Safari 16.4, Chrome 111 and Firefox 128 and does not work in older browsers (Tailwind upgrade guide).
Treat that as a cross-browser compatibility decision before upgrading, because v3.4 stays the documented option for older browsers.
| Behavior | Tailwind v3 | Tailwind v4 |
|---|
| Plugin loading | plugins array in tailwind.config.js | @plugin directive in CSS |
| Default border color | gray-200 | currentColor |
| Default ring | 3px, blue-500 | 1px, currentColor |
| outline-none | Invisible outline, kept in forced colors | Removes the outline, use outline-hidden |
A radio card that says border with no color turns text-colored on v4. One more change hits cards too: hover: styles now apply only when the primary input device supports hover, so touch users never see them.
Sources disagree on when @tailwindcss/forms became v4-ready. Some third-party migration guides list 0.5.7 as compatible, while the plugin changelog records alpha v4 install support in 0.5.8 (August 2024) and beta support in 0.5.10 (January 2025).
Trust the changelog and install the latest release. As of October 2026 that is 0.5.11, dated December 2025.
Tailwind Radio Button FAQ
Should you use a radio button or a checkbox for a yes or no choice?
Use radio buttons whenever only one option can be selected. Vue’s docs add a practical reason: browsers leave unchecked boxes out of form submissions, so radio inputs guarantee that one of the two values arrives.
How do you put Tailwind radio buttons in a row?
Wrap the inputs and labels in a flex container with a gap, such as flex gap-4. Flowbite’s horizontal list group stacks the options on small screens and switches to a row from the sm breakpoint with sm:flex.
How do you preselect a radio option?
Add the checked attribute to one input in a native group. In React, use defaultChecked on an uncontrolled input, and in Headless UI pass defaultValue to the group.
Only one option per group can start checked.
Why do two radio groups on the same page change each other?
They share a name attribute, so the browser merges them into one group and checking an option in one set clears the other. Give each set its own unique name, as daisyUI’s radio docs warn.
Can users uncheck a radio button once it is selected?
No. Clicking a checked radio leaves it checked, and only another option in the group, a form reset or a script clears the selection.
For an optional field, add a “None” radio or switch to a checkbox.
Which Styling Method to Ship First
Start with accent-* on the native input. A Tailwind radio button that only changes color adds no custom CSS to test.
Move up only when the design demands a different shape, first to peer-checked or has-checked cards with an sr-only input, and past that to a headless library if you need object values.
Each step adds markup that keyboard and forced colors checks must cover, plus a screen reader pass, so the cheapest working option wins.
The cost of accent-* is design range. It tints the control but leaves its shape to the browser.
Verified October 29, 2026: Tailwind’s upgrade guide says the team is exploring a v4 compatibility mode for older browsers, and a release would end the advice to stay on v3.4 for them.