Radio Cards
Card-style single-select. Each card highlights when selected, ideal for plan pickers.
Examples
Plan picker
A controlled single-select with a highlighted card and a live readout.
Current plan: Pro
import { RadioCards, RadioCardsItem } from "@codefast/ui/radio-cards";
import { useState } from "react";
const PLANS = [
{ value: "free", label: "Free", price: "$0 / mo", description: "For personal projects" },
{ value: "pro", label: "Pro", price: "$12 / mo", description: "For professional use" },
{ value: "team", label: "Team", price: "$49 / mo", description: "For growing teams" },
];
export function RadioCardsPlans() {
const [plan, setPlan] = useState("pro");
const current = PLANS.find((item) => item.value === plan);
return (
<div className="w-full max-w-xs space-y-3">
<RadioCards className="grid gap-2" value={plan} onValueChange={setPlan}>
{PLANS.map(({ value, label, price, description }) => (
<RadioCardsItem key={value} value={value}>
<div className="flex flex-col gap-0.5 text-start">
<span className="text-sm font-medium">
{label} — {price}
</span>
<span className="text-xs text-ui-muted">{description}</span>
</div>
</RadioCardsItem>
))}
</RadioCards>
<p className="text-center text-xs text-ui-muted">
Current plan: <span className="font-medium text-ui-fg">{current?.label}</span>
</p>
</div>
);
}
Billing interval
Lay cards out in a grid; mix in a Badge to flag the better deal.
import { Badge } from "@codefast/ui/badge";
import { RadioCards, RadioCardsItem } from "@codefast/ui/radio-cards";
import { useState } from "react";
export function RadioCardsInterval() {
const [interval, setInterval] = useState("yearly");
return (
<RadioCards className="grid w-full max-w-sm grid-cols-2 gap-2" value={interval} onValueChange={setInterval}>
<RadioCardsItem value="monthly">
<div className="flex flex-col gap-0.5 text-start">
<span className="text-sm font-medium">Monthly</span>
<span className="text-xs text-ui-muted">$12 billed each month</span>
</div>
</RadioCardsItem>
<RadioCardsItem value="yearly">
<div className="flex flex-col gap-0.5 text-start">
<span className="flex items-center gap-2 text-sm font-medium">
Yearly
<Badge variant="secondary">−20%</Badge>
</span>
<span className="text-xs text-ui-muted">$115 billed once a year</span>
</div>
</RadioCardsItem>
</RadioCards>
);
}
Icon cards
Compact cards with an icon per option.
import { RadioCards, RadioCardsItem } from "@codefast/ui/radio-cards";
import { CreditCardIcon, LandmarkIcon, WalletIcon } from "lucide-react";
import { useState } from "react";
const METHODS = [
{ value: "card", label: "Card", icon: CreditCardIcon },
{ value: "wallet", label: "Wallet", icon: WalletIcon },
{ value: "bank", label: "Bank", icon: LandmarkIcon },
];
export function RadioCardsPayment() {
const [method, setMethod] = useState("card");
return (
<RadioCards className="grid w-full max-w-sm grid-cols-3 gap-2" value={method} onValueChange={setMethod}>
{METHODS.map(({ value, label, icon: Icon }) => (
<RadioCardsItem key={value} value={value}>
<div className="flex flex-col items-center gap-1.5">
<Icon className="size-5" />
<span className="text-xs font-medium">{label}</span>
</div>
</RadioCardsItem>
))}
</RadioCards>
);
}
Usage
The minimal import and composition — see Examples below for styled, real-world variants.
import { RadioCards, RadioCardsItem } from "@codefast/ui/radio-cards";
export function RadioCardsUsage() {
return (
<RadioCards className="grid grid-cols-2 gap-3" defaultValue="starter">
<RadioCardsItem value="starter">Starter</RadioCardsItem>
<RadioCardsItem value="pro">Pro</RadioCardsItem>
</RadioCards>
);
}
Anatomy
How the parts nest — every slot the component exposes, in composition order.
Features
- Each RadioCardsItem already renders its own Label wrapping the radio and its children — the whole card is clickable, no separate <Label htmlFor> needed.
- Built on the same roving-tabindex radiogroup as Radio Group, styled as selectable cards instead of dot-and-label rows.
API reference
Props for each part of the component. All native element props are also forwarded.
RadioCards
Single-select group. Lay out children with grid/flex classes.
valuestringThe controlled selected value.
defaultValuestringThe selected value when initially rendered (uncontrolled).
onValueChange(value: string) => voidCalled when the selected value changes.
disabledbooleanDisables the whole group.
Default
false
RadioCardsItem
valuestringValue selected when this card is chosen (required).
disabledbooleanMakes a single card non-selectable.
Default
false
Accessibility
Built to be keyboard-navigable and screen-reader friendly out of the box.
| Key | Function |
|---|---|
| Tab | Moves focus into the group. |
| Arrow+Down | Selects the next card. |
| Arrow+Up | Selects the previous card. |
- Built on the radio-group pattern — exactly one card is selected at a time.
- The whole card is the hit target, not just a small control.
- Keep card content concise so the selected state stays scannable.
Guidelines
Conventions that keep usage consistent across an app.
Do
- Use for picking one option from a small set of rich choices (plans, methods).
- Show price or key detail directly on each card.
Don’t
- Don’t use for multi-select — that’s Checkbox Cards.
- Don’t overload a card with multiple actions.
Explore further
Ready to integrate?
Follow the Getting Started guide to install @codefast/ui, or browse the full component gallery.