Subscription Tier Card Stack — Radio-Driven Plan Picker with Recommended Highlight

Subscription Tier Card Stack — Recommended Highlight · Cards · Plain HTML, CSS & JS · Live preview

CategoryCards

What's included

Features

Built on a native radio group — mutual exclusivity, keyboard support and form submission all work with zero custom JS
Whole-card clickability via label-wrapping, no manual click handlers for the selection mechanic
Recommended tier visually elevated with combined transform (translateY + scale) plus shadow, not color alone
Floating "Recommended" ribbon badge for an unambiguous text label alongside the visual emphasis
:checked ~ sibling selector drives the Select pill's solid/checkmark state purely in CSS
:focus-visible ring on the hidden radio styled onto its label for full keyboard accessibility
Minimal JavaScript — one change listener updates a plain-language selection summary
Works as a real submittable form field out of the box if wrapped in a <form>

About this UI Snippet

Subscription Tier Card Stack — A Radio Group That Looks Like a Pricing Table

Screenshot of the Subscription Tier Card Stack — Recommended Highlight snippet rendered live

Pricing tier cards are usually built as clickable <div>s with a JavaScript click handler toggling a "selected" class by hand. This snippet builds the same visual pattern on top of a real native radio group instead — three <label> elements, each wrapping a hidden <input type="radio" name="tier">, so the browser's own radio semantics (mutual exclusivity, keyboard operability, form submission) do all of the selection-state work, and the only JavaScript left is a single change listener that updates a summary line.

The whole card is the label, the input just tracks state

Each .tier-card *is* a <label>, and the radio input sits inside it, visually hidden via opacity: 0 but still present and focusable. Clicking anywhere on the card — the price, the feature list, the "Select" pill — toggles that card's radio because native <label>-wrapping makes the entire element a valid click target for its child input, with zero click-handler code required for the selection mechanic itself.

How the "Select" pill reflects checked state without JS

.pick-indicator is a plain sibling <span> styled through .tier-radio:checked ~ .pick-indicator — when the radio becomes checked, this sibling selector switches the pill's background to solid indigo and appends a checkmark via content: ' ✓' on a ::after. Because this is pure CSS state, it works correctly even before any JavaScript has loaded or if JavaScript fails entirely — the radio group is a genuinely functional (if visually plain) form control on its own.

Why the recommended card is elevated, not just colored differently

The middle "Growth" card gets a combined transform: translateY(-8px) scale(1.03) plus a colored border and drop shadow, physically lifting it above its neighbors in the stack — a much stronger visual signal than color alone, especially useful for readers with color vision deficiencies who might not register a colored border as strongly as a person with typical color vision would. A small .rec-ribbon badge floats above the card's top edge for an explicit, unambiguous "Recommended" label alongside the visual elevation.

What the JavaScript actually does

The only script in this snippet listens for the group's change event and writes a plain-language summary ("Selected plan: Growth — $29/mo") to the page — a good stand-in for updating a checkout total, an order summary sidebar, or a "Continue" button's label elsewhere on a real pricing or upgrade page.

Step by step

How to Use

  1. 1
    Keep all three inputs sharing name="tier"The shared name attribute is what makes the three cards mutually exclusive as a native radio group.
  2. 2
    Move the .recommended class and rec-ribbon to any cardBoth are independent of which radio is checked by default — you can highlight a different tier than the one pre-selected.
  3. 3
    Add or remove feature <li> items freelyEach card's .tier-feats list is independent; keep counts roughly aligned for a cleaner visual comparison.
  4. 4
    Update the labels map in the JS panelThe labels object maps each radio's value to the summary text shown after selection — keep it in sync with your card prices.
  5. 5
    Read the selection on form submitBecause these are real radios, wrap the stack in a <form> and read FormData's "tier" value directly — no custom state management needed.

Real-world uses

Common Use Cases

SAAS
SaaS Signup / Upgrade Flows
Let a user pick a plan as a real form field before continuing to a checkout or account-upgrade step.
PRICING
Marketing Pricing Pages
A three-tier pricing section with a clear default recommendation, submittable directly into a signup form.
ONBOARD
Onboarding Plan Selection Step
One step of a multi-step onboarding wizard where selecting a tier advances the flow.
AB
A/B Testing Which Tier to Recommend
Swap the .recommended class between cards to test which default recommendation converts best.

Got questions?

Frequently Asked Questions

The entire card is a <label> wrapping the hidden radio input, so clicking anywhere inside it — the price, the feature list, or the pill — toggles the selection via native label-to-input association.

Radios give you correct mutual exclusivity, keyboard operability (Tab plus arrow keys within the group), and native form submission for free, and remain functional even if the page's JavaScript fails to load — a button-based version would need to reimplement all of that by hand.

The pre-selected radio (checked attribute) and the .recommended styling class are independent — you can pre-check one card while visually elevating a different one, though keeping them aligned is the more common and less confusing pattern.

Yes — wrap the .stack element in a <form> and submit it normally; a FormData read or a standard POST will include the tier field with whichever radio's value is checked, exactly like any other native radio group.

The .tier-radio:checked ~ .pick-indicator CSS selector switches the pill's background, text color, and adds a checkmark via a ::after pseudo-element the moment the radio becomes checked — purely a CSS sibling-selector reaction with no script involved.

Only the plain-text "Selected plan: …" summary line beneath the cards — the visual selection state of the cards themselves is handled entirely by CSS reacting to the native radio's :checked state.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI assistant to explain why building the selected-tier state on a native radio group is more robust than a JavaScript-managed "selected" class, and what specifically would break (keyboard navigation, form submission, no-JS fallback) if the cards were plain divs with a click handler instead. It's also worth asking for a version with a monthly/annual billing toggle that updates all three prices simultaneously, or one that supports four or more tiers in a horizontally scrollable row.

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 three-tier subscription plan picker in HTML, CSS and vanilla JavaScript where the middle tier is visually highlighted as "Recommended" — no external libraries.

Requirements:
- Implement the three plan cards as <label> elements each wrapping a hidden <input type="radio"> sharing the same name attribute, so selection is a real, native, mutually-exclusive radio group requiring no custom JavaScript for the selection mechanic itself.
- The entire card area (price, description, feature list) must be clickable to select that plan, relying on native label-to-input association rather than a manual click listener.
- Visually elevate the "Recommended" card above its neighbors using a combination of a transform (e.g. slight scale and upward translation) and an accent border/shadow, plus a floating ribbon badge with the word "Recommended" — not color alone.
- Use a CSS :checked sibling selector to switch a "Select" indicator pill to a solid, checkmarked state on whichever card is currently selected, with no JavaScript involved in that particular visual change.
- Give the hidden radio input a visible :focus-visible outline styled onto its label sibling for keyboard accessibility.
- Add one small JavaScript listener that responds to the group's change event and displays a plain-language summary of the currently selected plan and price elsewhere on the page.

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.

Source Code

<div class="demo">
  <div class="stack" id="stack">
    <label class="tier-card" data-tier="starter">
      <input type="radio" name="tier" value="starter" class="tier-radio" />
      <div class="tier-top">
        <span class="tier-name">Starter</span>
        <span class="tier-price">$9<small>/mo</small></span>
      </div>
      <p class="tier-desc">For solo builders getting started</p>
      <ul class="tier-feats">
        <li>3 projects</li>
        <li>1 GB storage</li>
        <li>Community support</li>
      </ul>
      <span class="pick-indicator">Select</span>
    </label>

    <label class="tier-card recommended" data-tier="growth">
      <input type="radio" name="tier" value="growth" class="tier-radio" checked />
      <span class="rec-ribbon">Recommended</span>
      <div class="tier-top">
        <span class="tier-name">Growth</span>
        <span class="tier-price">$29<small>/mo</small></span>
      </div>
      <p class="tier-desc">For growing teams that need more room</p>
      <ul class="tier-feats">
        <li>Unlimited projects</li>
        <li>50 GB storage</li>
        <li>Priority support</li>
        <li>Team collaboration</li>
      </ul>
      <span class="pick-indicator">Select</span>
    </label>

    <label class="tier-card" data-tier="scale">
      <input type="radio" name="tier" value="scale" class="tier-radio" />
      <div class="tier-top">
        <span class="tier-name">Scale</span>
        <span class="tier-price">$79<small>/mo</small></span>
      </div>
      <p class="tier-desc">For organizations with advanced needs</p>
      <ul class="tier-feats">
        <li>Unlimited everything</li>
        <li>500 GB storage</li>
        <li>Dedicated support</li>
        <li>SSO &amp; audit logs</li>
      </ul>
      <span class="pick-indicator">Select</span>
    </label>
  </div>
  <p class="picked-line">Selected plan: <strong id="pickedLabel">Growth — $29/mo</strong></p>
</div>