Skip to content
design
Components

Radio cards

A choice between a few options, each drawn as a card with a mark, a name and a line about it, with exactly one chosen.

On this page
Describe itBosun drafts the record from plain words.From a templateStart from a tested schedule for your engine.Blank recordFill in every field yourself.
How to start a maintenance record: three cards, each with a mark, a name and a line, and exactly one chosen.
Code
import { RadioCards, type RadioCardOption } from "@bosunkit/components/radio-cards";
import { FilePlus, LayoutTemplate, Sparkles } from "lucide-react";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "How to start a maintenance record: three cards, each with a mark, a name and a line, and exactly one chosen.";

const OPTIONS: readonly RadioCardOption[] = [
  {
    value: "describe",
    label: "Describe it",
    description: "Bosun drafts the record from plain words.",
    icon: <Sparkles size="1.25em" />,
  },
  {
    value: "template",
    label: "From a template",
    description: "Start from a tested schedule for your engine.",
    icon: <LayoutTemplate size="1.25em" />,
  },
  {
    value: "blank",
    label: "Blank record",
    description: "Fill in every field yourself.",
    icon: <FilePlus size="1.25em" />,
  },
];

export default function Showcase(): ReactElement {
  const [value, setValue]: [string, Dispatch<SetStateAction<string>>] = useState<string>("describe");
  return <RadioCards label="How do you want to start?" options={OPTIONS} value={value} onValueChange={setValue} />;
}

Radio cards are a radio group drawn as cards, for a choice where each option needs a sentence: how to start a record, which kind to create, where it goes. The chosen card has the accent on its border, a tint and an accent mark, and a screen reader hears it as checked, so the choice never rests on the color. The arrow keys move the choice, and the group is named by label while each card is named by its own name and line. It is built on Base UI's radio group.

The caller translates every word. A card the crew cannot choose now stays in the group, dimmed and announced as disabled, so they can see it exists. For two to five short options in one row, use the segmented control, which is smaller.

01One choice, said in wordsEach card names what it does and says it in a line, so the choice is made by reading, not by learning what a mark means.
02The choice never rests on colorThe chosen card is also announced as checked, and its mark and border change as well as its tint.
03Touch-sized cardsEvery card is at least a 48 px target, and fewer sit side by side where the box is narrow.

Examples

Two columns

EngineService intervals, filters, belts and the cooling circuit.RiggingStanding and running rigging, sails and furling gear.Safety equipmentNeeds a safety officer to sign it off.
`columns` of 2 for options with longer lines, with one card unavailable: it stays in the group, dimmed, and cannot be chosen.
Code
import { RadioCards } from "@bosunkit/components/radio-cards";
import type { ReactElement } from "react";

export const description: string =
  "`columns` of 2 for options with longer lines, with one card unavailable: it stays in the group, dimmed, and cannot be chosen.";

export default function TwoColumns(): ReactElement {
  return (
    <RadioCards
      label="Where does the record go?"
      columns={2}
      defaultValue="engine"
      options={[
        { value: "engine", label: "Engine", description: "Service intervals, filters, belts and the cooling circuit." },
        { value: "rigging", label: "Rigging", description: "Standing and running rigging, sails and furling gear." },
        {
          value: "safety",
          label: "Safety equipment",
          description: "Needs a safety officer to sign it off.",
          disabled: true,
        },
      ]}
    />
  );
}

Props

PropTypeDefaultDescription
labelrequiredstring–The group's name for a screen reader ("How do you want to start?"), translated by the caller.
optionsrequiredreadonly RadioCardOption[]–The cards, in reading order.
valuestring | undefined–The chosen value, for a caller that holds it; pair it with onValueChange.
defaultValuestring | undefined–The value chosen at first, when the caller does not hold it.
onValueChangeRadioGroupProps["onValueChange"]–Called with the value the crew choose.
columns1 | 2 | 3 | 4 | undefined3How many cards sit side by side where there is room; 3 by default. Fewer sit side by side in a narrow box.
disabledboolean | undefinedfalseWhether every card is unavailable, such as while a choice is being saved.
classNamestring | undefined–Added to the group's classes.
testIdstring | undefined–Becomes data-testid on the group.
refRef<HTMLDivElement> | undefined–The group element.
hintstring | undefined–Supporting instructions announced with the group.
errorstring | undefined–Validation message; marks the group invalid.
namestring | undefined–Submitted field name.
formstring | undefined–ID of an owning form outside the component's ancestors.
requiredboolean | undefined–Requires a choice before native form submission.
readOnlyboolean | undefined–Keeps the value focusable and submitted, but prevents changes.
Do
Describe itBosun drafts the record.BlankFill in every field.
Use cards where each option needs a sentence to be chosen well.
Don't
RoseChart
Don't use cards for short options a segmented control holds in one row: the cards add height and no information.