DEV Community

Cover image for shadcn's radio group allows one choice at a time, so pick the default on purpose
Dave Kurian
Dave Kurian

Posted on Originally published at otf-kit.dev

shadcn's radio group allows one choice at a time, so pick the default on purpose

A plan picker needs exactly one answer, and a radio group enforces that for you. The parts you still own are the default the form starts with and the label each option carries.

What the shadcn page covers

The page describes the component as "A set of checkable buttons—known as radio buttons—where no more than one of the buttons can be checked at a time." The install command is pnpm dlx shadcn@latest add radio-group, with tabs for npm, yarn and bun. The usage example imports RadioGroup and RadioGroupItem from @/components/ui/radio-group, and Label from @/components/ui/label. It starts the group with defaultValue="option-one".

The page says to "Use the disabled prop on RadioGroup to disable all items." Its examples cover a default group with comfortable and compact sizes, descriptions, a choice card, a fieldset, a disabled state, an invalid state and a right-to-left layout. For the choice card, the page says to "Use FieldLabel to wrap the entire Field for a clickable card-style selection."

The page does not describe the value and onValueChange props. The rules below, and the code, use them in the usual controlled pattern, and you should check the page's View Code panels before you copy the root. The rules below are ours, and they are labelled as ours.

Build a plan picker

Each option is a RadioGroupItem with a unique value and an id, and a Label points at that id with htmlFor. The page's usage example uses the same pairing, wrapped in a row with flex items-center gap-3:

import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"

const plans = [
  { id: "starter", label: "Starter" },
  { id: "team", label: "Team" },
  { id: "scale", label: "Scale" },
]

export function PlanPicker() {
  return (
    <RadioGroup defaultValue="team">
      {plans.map((plan) => (
        <div key={plan.id} className="flex items-center gap-3">
          <RadioGroupItem value={plan.id} id={plan.id} />
          <Label htmlFor={plan.id}>{plan.label}</Label>
        </div>
      ))}
    </RadioGroup>
  )
}
Enter fullscreen mode Exit fullscreen mode

The id and the htmlFor must match. When they do, clicking the label selects the option, and a screen reader reads the label with the radio. Give each option a short, specific label, such as the plan name, rather than a generic word.

Dex and Luna at a split panel: three unselected dots on one side, a single selected dot on the other

Choose the default on purpose

The default is the choice a reader makes without thinking, so it deserves a decision. Our rule is to pre-select the option that matches the user's real situation, not the most expensive one. A paid plan pre-selected on a signup form is a choice the user did not make, and it costs trust when they notice.

Keep the default in the same place as the value the form submits. If the form reads the value from state, initialize the state with the same default, so the screen and the saved value agree.

Use a card when the choice needs room

A plan with a price, a short list of features and a recommendation needs more than a dot and a word. The page's choice card example wraps the whole field in a FieldLabel, so the entire card is clickable. Use it when each option has a description, and keep the card layout the same across all options so the reader can compare them.

The page also lists a fieldset example. Use a fieldset with a legend when the group has a question of its own, such as "Billing period", so the question is read with the choices.

Disable the whole group

When the group should not change, set disabled on RadioGroup rather than on each item. The page says that prop disables all items, which keeps the group consistent. Say why the group is disabled in the text beside it, so the reader is not left guessing.

Luna taps one round dot on a holographic option card

Read the value on submit

A radio group can never be empty once a default is set, so the submit step rarely needs an error message. Read the value from state, and send it with the rest of the form:

const [plan, setPlan] = React.useState("team")

<RadioGroup value={plan} onValueChange={setPlan}>
  {/* items as above */}
</RadioGroup>

function onSubmit() {
  savePlan(plan) // your save call
}
Enter fullscreen mode Exit fullscreen mode

If your form can start with no choice at all, the page's invalid example is the one to read before you add that state. Check its View Code panel for how the invalid state is set, and do not guess it from this article.

Write the group question

A radio group needs a question above it, even if the question is short. The options answer the question, and the reader needs to see the question before they read the choices. Write it in full, such as "Which plan fits your team?", and place it above the group rather than repeating it in each label.

Keep the labels parallel. Use all plan names, or all time periods, and never a mix. A list that puts "Starter" beside "Monthly" makes readers stop and ask whether the choices compare, and that pause costs more than the extra word in the question.

Test before you ship

Check these on the real form, not only on the component:

  • Tab into the group. Focus should land on the selected option, not on the first one by accident.
  • Press the arrow keys. The selection should move to the next option in the group.
  • Click the label text. The matching option should select.
  • Submit with the default. The saved value should match the selected dot.
  • On a 390px phone, check that every option, its label and the submit button fit without horizontal overflow.

Run the phone check last, after the labels are final. A long plan name can wrap next to its dot and push the next option down the screen.

Where OTF stands

If you want a radio group or a plan picker already built into a product, we cannot point you to one. We checked the SaaS Dashboard, Booking, Fitness and Arcade kit pages for radio, radio group, switch and toggle terms. None of them names a radio group or a switch.

The pricing page lists the Full-stack kits at $99 each and the Everything Bundle at $149, both as one-time payments, and it offers a live demo before you buy. Check each kit's repository for the form controls it ships before you rely on any of them. For a select that offers the same one-of-many choice in a shorter form, see the select guide, and for a toggle in a settings form, see the dialog and sheet guide. If you are choosing a starting point for a product, the pricing page is the place to compare kits side by side: OTF pricing.

Sources

Top comments (0)