Radio Group
One of these. Every option on screen, each with room to explain itself.
import { RadioGroup, Radio } from "@dede-style/ui/radio", then <RadioGroup label="Delivery" value={v} onChange={setV}> holding a Radio per option. It is controlled — the selected value lives with whoever owns it — and it renders a real <fieldset> with the label as its <legend>.
The options are one control. They share a name, so the browser treats them as a single question with a single answer: choosing one clears the others, Tab lands on the chosen option and leaves the set in one stop, and the arrow keys move the choice. None of that is written here — it arrives with <input type="radio">, and the component’s job is not to break it.
The label is the legend. A screen reader hears “Delivery” before the first option, so “Standard” is heard as an answer to something rather than as a word on its own. That is what a <fieldset> gives that a heading above a list of radios does not, and it is why the label is required.
Without descriptions
The row tightens honestly when there is no second line.
A description is a second line, not a reserved one. An option without one is a single line of label beside its circle, and the rows sit at the same 12px rhythm a Sleeve of checkboxes takes — nothing holds space for words that were not written.
Three short words with nothing to explain is also the case where a Segmented would do, and often better: it is one line rather than three. This group earns its rows when the options need reading, or when there are more than four; see the last band.
Disabled
One option set aside, and a whole question set aside, are two different props.
isDisabled on a Radio says this option cannot be chosen; on the RadioGroup it says the question is not being asked. The first is the usual case — gift boxing is not offered for this item, and the row stays in the list so the reader knows it exists. The second is rarer and reads differently: every row dims, and the legend with them, so what is set aside is the set.
The group does it with one attribute, disabled on the <fieldset>, which the browser carries to every control inside — nothing is told per option. A disabled option is skipped by the arrow keys as well as the pointer, and a chosen option in a disabled group stays chosen: the form still knows the answer, the reader just cannot change it.
When the label wraps
The circle holds to the first line, however many follow.
The family’s one geometric claim, kept by the row rather than by the circle. A Radio stands in the same two columns a Checkbox does, so the offset that holds a box to the first line — one line of the label’s own type, less the control, halved — holds a circle to it without a second rule. The description sits beneath the words, not beneath the control.
When not a Radio Group
Three components choose one of N, and the choice between them is about the options, not the control. Segmented for two to four short words with nothing to explain — a text alignment, a unit — where one line is better than four rows and the choice takes effect at once. Select for many options, or a long list a reader will search rather than read, where hiding them behind a click is the point. A Radio Group when the options need explaining before they can be chosen, or must all be visible at once to be compared — a delivery speed, a plan, a payment method. It costs a row per option, and that row is where the description goes.
And a set of options that are independent — tick two, tick none — is not a question with one answer at all. That is a Checkbox Group, the same fieldset with less on it.