codefast/ui

Command Palette

Search for a command to run...

Source
Form

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

33 lines
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.

27 lines
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.

26 lines
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.

10 lines
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.

RadioCards
├── RadioCardsItem
└── RadioCardsItem

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.

valuestring

The controlled selected value.

defaultValuestring

The selected value when initially rendered (uncontrolled).

onValueChange(value: string) => void

Called when the selected value changes.

disabledboolean

Disables the whole group.

Defaultfalse

RadioCardsItem

valuestring

Value selected when this card is chosen (required).

disabledboolean

Makes a single card non-selectable.

Defaultfalse

Accessibility

Built to be keyboard-navigable and screen-reader friendly out of the box.

KeyFunction
TabMoves focus into the group.
Arrow+DownSelects the next card.
Arrow+UpSelects 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.