Radio Card Group — Selectable Option Cards UI

Radio Card Group · Forms · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Built on native radio inputs
Real <input type="radio"> elements give single-selection, keyboard nav, and form submission for free.
Pure-CSS selected styling
The :checked sibling selector drives all selected visuals — no JavaScript needed for the core behavior.
Redundant selection signals
A chosen card shows border, background tint, shadow, and a filled indicator — never relying on color alone.
Keyboard focus ring
:focus-visible adds a strong ring for keyboard users without showing one to mouse users.
"Most popular" badge
A pinned flag nudges toward a recommended option, with a CSS rule preventing it from colliding with the indicator.
Whole-card click target
The entire card is the label, so clicking anywhere in it selects the option — a large, forgiving hit area.
Graceful degradation
Strip the JavaScript and the cards still select, navigate, and submit — the script only adds label sync and confirmation.
Screen-reader friendly
role="radiogroup" plus native radios announce the option, its position, and selected state automatically.

About this UI Snippet

Radio Card Group — Accessible Selectable Cards Built on Native Radios

Screenshot of the Radio Card Group snippet rendered live

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:

text
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

  1. 1
    Paste HTML, CSS, and JSA three-card plan selector renders with Pro pre-selected and marked "Most popular".
  2. 2
    Click a cardThe whole card is clickable — selecting it highlights the border, tints the background, and fills the corner indicator.
  3. 3
    Use the keyboardTab into the group and use Arrow keys to move between plans — native radio behavior, no custom code.
  4. 4
    Watch the submit labelThe button updates to "Continue with [plan]" reflecting the current choice; submitting confirms it briefly.
  5. 5
    Edit the optionsAdd or change the option cards in the HTML (and the PLANS map) — each is a label-wrapped radio with your content.
  6. 6
    Wire 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

Pricing and plan selection
Let users choose a subscription tier as large comparable cards — pair with a pricing toggle for monthly/annual.
Checkout options
Pick a shipping speed or payment method as cards, alongside a promo code input and order summary.
Onboarding preferences
Have new users select a use case, role, or goal from a few descriptive cards.
Survey and quiz answers
Present single-choice questions as cards with more context than a bare radio list.
Configurator and product options
Choose a model, size tier, or service level where each option benefits from a title, price, and description.
Learning accessible custom controls
A reference for styling native radios without losing accessibility — compare with a segmented control for a compact single-choice toggle.

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.