Lets users pick exactly one option from a small set that stays visible. Renders as a simple list or as selectable cards with icons, and supports descriptions, horizontal layout, validation and native form submission.
description on a Radio adds secondary text under the label. Use it when the labels alone don't explain the difference between options.
Visibility
import { Radio, RadioGroup } from "@/components/ui/radio-group";export default function RadioGroupDescription() { return ( <RadioGroup label="Visibility" defaultValue="private" className="max-w-xs"> <Radio value="public" description="Anyone on the internet can see this project." > Public </Radio> <Radio value="internal" description="Everyone in your organization can see it." > Internal </Radio> <Radio value="private" description="Only people you invite can see it."> Private </Radio> </RadioGroup> );}
isDisabled on a Radio makes just that option unavailable while keeping it visible; explain why in the group's description. isDisabled on the group disables every option. isReadOnly shows the value but ignores changes, and keeps the group focusable.
RegionTokyo is at capacity. Try again next week.
Billing cycle
Data residency
import { Radio, RadioGroup } from "@/components/ui/radio-group";export default function RadioGroupDisabled() { return ( <div className="flex flex-col gap-8"> <RadioGroup label="Region" description="Tokyo is at capacity. Try again next week." defaultValue="fra" > <Radio value="iad">Washington, D.C. (iad1)</Radio> <Radio value="fra">Frankfurt (fra1)</Radio> <Radio value="hnd" isDisabled> Tokyo (hnd1) </Radio> </RadioGroup> <RadioGroup label="Billing cycle" isReadOnly defaultValue="yearly"> <Radio value="monthly">Monthly</Radio> <Radio value="yearly">Yearly</Radio> </RadioGroup> <RadioGroup label="Data residency" isDisabled defaultValue="eu"> <Radio value="us">United States</Radio> <Radio value="eu">European Union</Radio> </RadioGroup> </div> );}
RadioCard is a larger target with a leading icon, a title and a description. The whole card is the label, and the selected card gets a brand-colored border.
Plan
import { BuildingIcon, RocketIcon, UserIcon } from "lucide-react";import { RadioCard, RadioGroup } from "@/components/ui/radio-group";export default function RadioGroupCards() { return ( <RadioGroup label="Plan" defaultValue="team" className="w-full max-w-sm"> <RadioCard value="personal" icon={<UserIcon />} title="Personal" description="For individuals. Up to 3 projects." /> <RadioCard value="team" icon={<RocketIcon />} title="Team" description="$12 per seat. Unlimited projects." /> <RadioCard value="enterprise" icon={<BuildingIcon />} title="Enterprise" description="SSO, audit logs and dedicated support." /> </RadioGroup> );}
Control the group to reveal a related field when a specific option is picked, like "Something else". Place the input after the group, not inside a radio's label.
Why are you cancelling?
import { useState } from "react";import { Radio, RadioGroup } from "@/components/ui/radio-group";import { TextField } from "@/components/ui/text-field";export default function RadioGroupWithInput() { const [reason, setReason] = useState("price"); return ( <div className="flex w-full max-w-xs flex-col gap-3"> <RadioGroup label="Why are you cancelling?" value={reason} onChange={setReason} > <Radio value="price">It's too expensive</Radio> <Radio value="features">It's missing features I need</Radio> <Radio value="switch">I'm switching to another product</Radio> <Radio value="other">Something else</Radio> </RadioGroup> {reason === "other" && ( <TextField aria-label="Tell us more" placeholder="Tell us more…" autoFocus className="pl-6" /> )} </div> );}
isRequired blocks form submission until an option is selected and adds a * to the label. With native validation, the errorMessage appears after the first submit attempt.
import { Form } from "react-aria-components";import { Button } from "@/components/ui/button";import { Radio, RadioGroup } from "@/components/ui/radio-group";export default function RadioGroupValidation() { return ( <Form className="flex w-full max-w-xs flex-col gap-5" onSubmit={(e) => e.preventDefault()} > <RadioGroup label="Team size" isRequired errorMessage="Select your team size to continue." > <Radio value="1">Just me</Radio> <Radio value="2-10">2–10 people</Radio> <Radio value="11-50">11–50 people</Radio> <Radio value="51+">More than 50</Radio> </RadioGroup> <Button type="submit" className="self-start"> Continue </Button> </Form> );}
validate receives the selected value and returns an error string, or null when valid. With validationBehavior="aria" the message updates live as the selection changes.
Shipping to a P.O. box
import { Radio, RadioGroup } from "@/components/ui/radio-group";export default function RadioGroupCustomValidation() { return ( <RadioGroup label="Shipping to a P.O. box" defaultValue="standard" validationBehavior="aria" validate={(value) => value === "overnight" ? "Overnight delivery isn't available for P.O. boxes." : null } > <Radio value="standard" description="5–7 business days · Free"> Standard </Radio> <Radio value="express" description="2–3 business days · $9.00"> Express </Radio> <Radio value="overnight" description="Next business day · $24.00"> Overnight </Radio> </RadioGroup> );}
RadioCard accepts the same React Aria props as Radio, except children: its content comes from title, description and icon. See React Aria's Radio props.