Radio Card Group — Selectable Option Cards UI
Radio Card Group · Forms · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Radio Card Group — Accessible Selectable Cards Built on Native Radios

When you need a user to pick exactly one option from a few — a pricing plan, a shipping speed, a payment method — large clickable cards convert far better than a cramped native dropdown, because each choice can show a title, price, description, and badge. This snippet builds that selectable-card pattern the right way: on top of real <input type="radio"> elements, so it stays keyboard-navigable, screen-reader friendly, and form-submittable with zero JavaScript required for the core behavior.
Real radios under the hood
Each card is a <label> wrapping a visually hidden <input type="radio"> and a styled .rcg-box. Because they're genuine radios sharing one name, the browser gives you single-selection enforcement, arrow-key navigation between options, focus management, and inclusion in form submission — all for free. The selected styling is driven entirely by the CSS :checked sibling selector (input:checked + .rcg-box), which highlights the border, tints the background, and fills the custom radio dot. This is the critical detail: re-implementing radio behavior with <div>s and JavaScript throws away accessibility that the native element provides automatically.
Selected state you can't miss
A chosen card gets a colored border, a soft background tint, a focus-ring-style shadow, and a filled circular indicator in the corner — four redundant signals so the selection is obvious at a glance and never relies on color alone. The :focus-visible selector adds a stronger ring when a card is reached by keyboard, so keyboard users always see where they are, while mouse users don't get a ring they didn't ask for.
A "most popular" flag without breaking layout
One card carries a "Most popular" badge pinned to its top edge — the standard nudge toward a recommended option. A small CSS rule (.rcg-flag ~ .rcg-tick) nudges the selection indicator down on flagged cards so the badge and the tick never collide, the kind of detail that separates a polished component from one that looks broken on the recommended plan.
JavaScript only for the nice-to-haves
The core selection works with no JavaScript at all — that's the point of building on native radios. The small script only powers conveniences: syncing the submit button's label to the chosen plan ("Continue with Pro") and showing a brief confirmation on submit. Strip the script entirely and the cards still select, navigate, and submit correctly, which is exactly the graceful-degradation property you want in a form control.
Keyboard and screen-reader behavior
The group is wrapped in role="radiogroup" with a label, and because the inputs are real radios, arrow keys move the selection, Tab moves into and out of the group as a single stop, and screen readers announce "Pro, radio button, 2 of 3, selected." None of that needs custom key handling — it's the native behavior preserved by not replacing the radios. This is why the radio-card pattern should always be built on inputs, not reconstructed from scratch.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You do not have to reason through the accessibility tradeoffs here alone. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why the input:checked + .rcg-box sibling selector is what drives all the selected styling instead of a JavaScript class toggle, and what native behaviors (arrow-key navigation, form submission, screen-reader announcements) would be lost if the radios were replaced with plain divs and click handlers. The same assistant can help you optimize it — ask whether the visually-hidden input pattern (position absolute, opacity 0) here has any focus-visible edge cases worth testing across browsers. It's also useful for extending the group: ask it to add a per-card feature comparison list, support a horizontal layout on wide screens instead of stacked cards, or wire the submit handler to a real checkout redirect keyed off the selected plan's value. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a selectable "radio card" group in plain HTML, CSS, and JavaScript, built on real native radio inputs — no custom keyboard handling, no div-based fake radios.
Requirements:
- Each option must be a label element wrapping a visually hidden (not display:none — use position absolute with zero size and opacity 0, so it stays in the accessibility tree and focusable) native input of type radio sharing one name attribute across all options, followed by a styled sibling element containing the card's visible content (a title, a price, and a description).
- All selected-state styling (border color, background tint, box-shadow, and a filled circular indicator) must be driven purely by the CSS :checked sibling combinator on the hidden input — none of it may be set via JavaScript class toggling.
- Add a distinct :focus-visible style on the sibling element so keyboard users see a strong focus ring when tabbing to a card, while mouse clicks do not trigger that same ring.
- One card must display a "Most popular" badge pinned to its top edge, and the layout must ensure that badge never visually overlaps the selected-state indicator circle on that specific card.
- Wrap the group in role="radiogroup" with an accessible label, and confirm that arrow keys move the selection between cards and Tab enters/exits the group as a single stop — purely from using native radio inputs, not custom key handling.
- Add optional JavaScript only for a nice-to-have: keep a submit button's text in sync with the currently selected option's label, and show a brief confirmation state after submit — but the core selection, keyboard navigation, and form submission must all work correctly with that JavaScript removed entirely.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA three-card plan selector renders with Pro pre-selected and marked "Most popular".
- 2Click a cardThe whole card is clickable — selecting it highlights the border, tints the background, and fills the corner indicator.
- 3Use the keyboardTab into the group and use Arrow keys to move between plans — native radio behavior, no custom code.
- 4Watch the submit labelThe button updates to "Continue with [plan]" reflecting the current choice; submitting confirms it briefly.
- 5Edit the optionsAdd or change the option cards in the HTML (and the PLANS map) — each is a label-wrapped radio with your content.
- 6Wire up the choiceOn submit, read the checked radio's value and proceed — start checkout, save the preference, or advance the flow.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Native radios give you single-selection enforcement, arrow-key navigation, focus management, form submission, and correct screen-reader announcements ("2 of 3, selected") automatically. Rebuilding that with divs and JavaScript means re-implementing all of it — and most reimplementations miss keyboard or screen-reader support. Hiding the real radio and styling a sibling keeps every native benefit while giving you full visual control.
Each option is a <label> wrapping an <input type="radio" name="plan" value="..."> and a styled .rcg-box with your title, price, and description. Add or remove these label blocks (and update the PLANS map if you use it for the submit label) — no other code changes, since selection is handled by the shared radio name.
Add the checked attribute to that card's radio input (as the Pro card does here). For no default, leave all unchecked — but a sensible default reduces friction and is recommended for plan pickers where one option is the common choice.
Read form.querySelector('input[name="plan"]:checked').value, either on the submit event (as shown) or on change for live updates. Because they're real radios inside a form, the value also submits automatically with a normal form POST under the radio's name.
In React, hold the selected value in useState and set each radio's checked={value === opt} with an onChange; in Vue, use v-model on the radio group; in Angular, use formControlName or [(ngModel)]. Keep them as real <input type="radio"> elements so you retain native keyboard and accessibility behavior — only the value binding moves into the framework.