Radio Card

Bordered selectable card driven by a radio — the radio flavor of Checkbox Card.

Cards

The radio flavor of Checkbox Card.

function RadioCardDemo() {
  return (
    <RadioGroup defaultValue="team" aria-label="Plan" className="grid w-full max-w-lg gap-3">
      <RadioCard value="solo" title="Solo" description="One editor, unlimited projects." />
      <RadioCard value="team" title="Team" description="Shared libraries and review flows." />
    </RadioGroup>
  );
}
function RadioCardDemo() {
  return (
    <RadioGroup defaultValue="team" aria-label="Plan" className="grid w-full max-w-lg gap-3">
      <RadioCard value="solo" title="Solo" description="One editor, unlimited projects." />
      <RadioCard value="team" title="Team" description="Shared libraries and review flows." />
    </RadioGroup>
  );
}

Installation

npx shadcn@latest add https://boardcn.dev/r/radio-card.json
npx shadcn@latest add https://boardcn.dev/r/radio-card.json

npm packages

  • react-aria-components

BoardCN dependencies

The CLI installs these for you — you do not need to add them yourself.

Source

The file the CLI copies into your project.

components/base/radio/radio-card.tsx
"use client";

import type { ReactNode, Ref } from "react";
import { Radio as AriaRadio } from "react-aria-components";
import type { RadioProps as AriaRadioProps } from "react-aria-components";
import { cx } from "@/utils/cx";
import { RadioDot } from "./radio";

export interface RadioCardProps extends Omit<AriaRadioProps, "children"> {
  title: ReactNode;
  description?: ReactNode;
  ref?: Ref<HTMLLabelElement>;
}

export function RadioCard({ className, title, description, ref, ...props }: RadioCardProps) {
  return (
    <AriaRadio
      ref={ref}
      {...props}
      className={(state) =>
        cx(
          "group flex w-full items-center justify-between gap-3 rounded-2lg border border-border-button-default py-3 pr-5 pl-4 select-none",
          "transition-colors duration-150 ease",
          state.isHovered && !state.isDisabled
            ? "bg-background-primary-hover"
            : "bg-background-primary-default",
          state.isDisabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
          typeof className === "function" ? className(state) : className,
        )
      }
    >
      {(state) => (
        <>
          <span className="flex min-w-0 flex-col gap-0.5">
            <span className="truncate text-body-medium text-text-primary">{title}</span>
            {description !== undefined && description !== null && (
              <span className="truncate text-body-regular text-text-secondary">{description}</span>
            )}
          </span>
          <span className="flex shrink-0 items-center py-1">
            <RadioDot size="md" selected={state.isSelected} focusVisible={state.isFocusVisible} />
          </span>
        </>
      )}
    </AriaRadio>
  );
}
"use client";

import type { ReactNode, Ref } from "react";
import { Radio as AriaRadio } from "react-aria-components";
import type { RadioProps as AriaRadioProps } from "react-aria-components";
import { cx } from "@/utils/cx";
import { RadioDot } from "./radio";

export interface RadioCardProps extends Omit<AriaRadioProps, "children"> {
  title: ReactNode;
  description?: ReactNode;
  ref?: Ref<HTMLLabelElement>;
}

export function RadioCard({ className, title, description, ref, ...props }: RadioCardProps) {
  return (
    <AriaRadio
      ref={ref}
      {...props}
      className={(state) =>
        cx(
          "group flex w-full items-center justify-between gap-3 rounded-2lg border border-border-button-default py-3 pr-5 pl-4 select-none",
          "transition-colors duration-150 ease",
          state.isHovered && !state.isDisabled
            ? "bg-background-primary-hover"
            : "bg-background-primary-default",
          state.isDisabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
          typeof className === "function" ? className(state) : className,
        )
      }
    >
      {(state) => (
        <>
          <span className="flex min-w-0 flex-col gap-0.5">
            <span className="truncate text-body-medium text-text-primary">{title}</span>
            {description !== undefined && description !== null && (
              <span className="truncate text-body-regular text-text-secondary">{description}</span>
            )}
          </span>
          <span className="flex shrink-0 items-center py-1">
            <RadioDot size="md" selected={state.isSelected} focusVisible={state.isFocusVisible} />
          </span>
        </>
      )}
    </AriaRadio>
  );
}

Props

Generated from the component's TypeScript types. Standard DOM and React Aria props are omitted.

RadioCard

PropTypeDefaultDescription
titlerequiredReactNode
descriptionReactNode