# Radio Card Plans — React Checkbox Component

> Plan cards backed by native radios: one shared selection ring glides from card to card with a spring while the radio dot pops in.

- Category: [Checkboxes & Radios](https://pixelpapa.com/components/checkboxes.md)
- Tags: radio, card, plans, layout, pricing
- Dependencies: motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/checkboxes/radio-card-plans

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/radio-card-plans.json"
```

The file is written to `components/pixelpapa/radio-card-plans.tsx`. To install manually, run `npm install motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { motion } from "motion/react";
import { useId, useState } from "react";
import { cn } from "@/lib/utils";

type Plan = { id: string; name: string; blurb: string; price: string; badge?: string };

type PlanRadioCardsProps = {
  /** Accessible name for the group. */
  label: string;
  plans: Plan[];
  value?: string;
  defaultValue?: string;
  onValueChange?: (id: string) => void;
  className?: string;
};

export function PlanRadioCards({ label, plans, value, defaultValue, onValueChange, className }: PlanRadioCardsProps) {
  const [internal, setInternal] = useState(defaultValue ?? plans[0]?.id);
  const current = value ?? internal;
  const name = useId();

  return (
    <div role="radiogroup" aria-label={label} className={cn("flex w-full flex-col gap-2.5", className)}>
      {plans.map((plan) => {
        const active = plan.id === current;
        return (
          <label
            key={plan.id}
            className="group relative flex cursor-pointer items-center gap-3.5 rounded-2xl border border-border bg-card p-4 transition-colors hover:bg-muted/40 has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-ring"
          >
            {/* Native radios with a shared name give arrow-key navigation for free. */}
            <input
              type="radio"
              name={name}
              value={plan.id}
              checked={active}
              onChange={() => {
                setInternal(plan.id);
                onValueChange?.(plan.id);
              }}
              className="sr-only"
            />
            {active && (
              <motion.span
                aria-hidden
                layoutId={`radio-card-plans-ring-${name}`}
                className="absolute -inset-px rounded-2xl border-2 border-primary bg-primary/[0.05]"
                transition={{ type: "spring", stiffness: 420, damping: 32 }}
              />
            )}
            <span
              aria-hidden
              className={cn(
                "relative flex size-5 shrink-0 items-center justify-center rounded-full border-[1.5px] transition-colors",
                active ? "border-primary" : "border-input group-hover:border-primary/50",
              )}
            >
              <motion.span
                initial={false}
                animate={{ scale: active ? 1 : 0 }}
                transition={{ type: "spring", stiffness: 520, damping: 20 }}
                className="size-2.5 rounded-full bg-primary"
              />
            </span>
            <span className="relative min-w-0 flex-1">
              <span className="flex items-center gap-2 text-sm font-semibold text-card-foreground">
                {plan.name}
                {plan.badge && (
                  <span className="rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-semibold tracking-wide text-primary uppercase">
                    {plan.badge}
                  </span>
                )}
              </span>
              <span className="block truncate text-xs text-muted-foreground">{plan.blurb}</span>
            </span>
            <span className="relative text-right">
              <span className="block text-base font-semibold text-card-foreground tabular-nums">{plan.price}</span>
              <span className="block text-[11px] text-muted-foreground">per month</span>
            </span>
          </label>
        );
      })}
    </div>
  );
}

const plans: Plan[] = [
  { id: "hobby", name: "Hobby", blurb: "3 projects · community support", price: "$0" },
  { id: "pro", name: "Pro", blurb: "Unlimited projects · 100 GB · priority support", price: "$24", badge: "Popular" },
  { id: "team", name: "Team", blurb: "Everything in Pro · SSO · audit log", price: "$79" },
];

export default function RadioCardPlansDemo() {
  const [plan, setPlan] = useState("pro");
  const chosen = plans.find((p) => p.id === plan) ?? plans[0];

  return (
    <div className="w-full max-w-md">
      <PlanRadioCards label="Plan" plans={plans} value={plan} onValueChange={setPlan} />
      <button
        type="button"
        className="mt-4 h-11 w-full rounded-full bg-primary text-sm font-medium text-primary-foreground transition-[scale,background-color] hover:bg-primary/90 active:scale-[0.98]"
      >
        Continue with {chosen.name}
      </button>
    </div>
  );
}
```

## AI prompt

```text
Add the "Radio Card Plans" component from PixelPapa UI to this project.

What it is: Plan cards backed by native radios: one shared selection ring glides from card to card with a spring while the radio dot pops in.
Category: Checkboxes & Radios. Depends on: motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/radio-card-plans.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/radio-card-plans.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/radio-card-plans" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More checkboxes & radios

- [Checkbox Card Select](https://pixelpapa.com/components/checkboxes/checkbox-card-select.md) (Pro): Multi-select cards: a chosen card tints, its icon tile inverts and a check bubble springs into the corner while the running total rolls.
- [Checkbox Draw Check](https://pixelpapa.com/components/checkboxes/checkbox-draw-check.md): A checkbox whose fill springs out from the centre while the tick is drawn as a single pen stroke, then erased in reverse when cleared.
- [Checkbox Indeterminate Tree](https://pixelpapa.com/components/checkboxes/checkbox-indeterminate-tree.md) (Pro): A nested checkbox tree where parents reflect their children: the mark morphs between a dash and a check, and branches fold open with animated height.
- [Checkbox Todo Strikethrough](https://pixelpapa.com/components/checkboxes/checkbox-todo-strikethrough.md): A to-do list where ticking an item draws a line through its text from left to right, then the finished item sinks to the bottom with a layout spring.
- [Radio Segmented Dots](https://pixelpapa.com/components/checkboxes/radio-segmented-dots.md) (Pro): A minimal radio group of dots on a hairline: a ringed marker bounces between them on a spring and the chosen option's name rolls in beside the label.
