RadioGroup
One choice from a set: native radios sharing a name, with arrow-key movement.
Basic
value is two-way. Arrow keys move and check; Tab enters on the checked radio.
Code
<RadioGroup value={state.density} attrs={{ "aria-label": "Density" }}>
<Label><RadioGroupItem value="default" /> Default</Label>
<Label><RadioGroupItem value="comfortable" /> Comfortable</Label>
<Label><RadioGroupItem value="compact" /> Compact</Label>
</RadioGroup>Usage
import { RadioGroup, RadioGroupItem } from "@elements/ui/radio-group";
<RadioGroup value={form.density} attrs={{ "aria-label": "Density" }}>
<Label><RadioGroupItem value="default" /> Default</Label>
<Label><RadioGroupItem value="compact" /> Compact</Label>
</RadioGroup>
<RadioGroup value={form.plan} options={PLANS} orientation="horizontal" />Examples
With descriptions
Each item is labelled and described by id.
For individuals and small teams.
For growing businesses.
For large teams and enterprises.
Code
<RadioGroup value={state.plan} attrs={{ "aria-label": "Plan" }}>
<div class="option">
<RadioGroupItem value="plus" attrs={{ id: "plus", "aria-describedby": "plus-hint" }} />
<div>
<Label htmlFor="plus">Plus</Label>
<p class="hint" id="plus-hint">For individuals and small teams.</p>
</div>
</div>
...
</RadioGroup>Options as data, horizontal
options renders a labelled item each; orientation lays them in a row.
Code
const SIZES = [{ value: "s", label: "Small" }, ...];
<RadioGroup value={state.size} options={SIZES} orientation="horizontal" attrs={{ "aria-label": "Size" }} />No wrap, disabled item
loop off stops at the ends; arrows skip the disabled Quarterly and Lifetime.
Code
<RadioGroup value={state.edge} options={PLANS} loop={false} />Disabled group
disabled on the group disables every item.
Read only
readOnly keeps the choice under a click, Space or an arrow. The radios stay focusable and arrows still move focus. readOnly on one RadioGroupItem locks only that item. form joins the second group to a form it sits outside.
Code
<RadioGroup value="b" readOnly attrs={{ "aria-label": "Region" }}
options={[{ value: "a", label: "United States" }, { value: "b", label: "European Union" }]} />
<RadioGroup value={state.storage} name="storage" form="notify-form" attrs={{ "aria-label": "Storage" }}>
<Label><RadioGroupItem value="standard" /> Standard</Label>
<Label><RadioGroupItem value="archive" readOnly /> Archive (locked)</Label>
</RadioGroup>Required in a form
Nothing chosen blocks the submit. invalid marks every item.
Code
<form onsubmit={() => save(state)}>
<RadioGroup value={state.notify} name="notify" required invalid={state.invalid}>
...
</RadioGroup>
<Button type="submit" variant="primary">Save</Button>
<Button type="reset">Reset</Button>
</form>Required item
required on one RadioGroupItem makes the browser require a choice, as native radios sharing a name do.
Code
<RadioGroup value={state.contact} name="contact">
<Label><RadioGroupItem value="email" required /> Email</Label>
<Label><RadioGroupItem value="phone" /> Phone</Label>
</RadioGroup>No name
Without a name the radios still group, one tab stop and one choice, but the form submits nothing for them. The bound value carries the choice.
Code
<form>
<RadioGroup value={state.pace} attrs={{ "aria-label": "Pace" }}>
<Label><RadioGroupItem value="steady" /> Steady</Label>
<Label><RadioGroupItem value="fast" /> Fast</Label>
</RadioGroup>
</form>Fieldset
A FieldSet and legend name the group; each item is a horizontal Field.
Code
<FieldSet>
<FieldLegend variant="label">Subscription Plan</FieldLegend>
<FieldDescription>Yearly and lifetime plans offer significant savings.</FieldDescription>
<RadioGroup value={state.subscription} name="subscription">
<Field orientation="horizontal" label="Monthly ($9.99/month)" reserveError={false}>
<RadioGroupItem value="monthly" />
</Field>
...
</RadioGroup>
</FieldSet>Choice cards
A FieldLabel wrapping a Field makes the whole card choose the item.
Code
<RadioGroup value={state.tier} attrs={{ "aria-label": "Plan" }}>
<FieldLabel>
<Field orientation="horizontal" reserveError={false}>
<FieldContent>
<FieldTitle>Plus</FieldTitle>
<FieldDescription>For individuals and small teams.</FieldDescription>
</FieldContent>
<RadioGroupItem value="plus" />
</Field>
</FieldLabel>
...
</RadioGroup>Rating
The APG rating example: one radio per star. Arrows change the rating and every star up to it fills.
Code
<RadioGroup value={state.rating} orientation="horizontal" attrs={{ "aria-label": "Rating" }}>
<label e:for={n of [1, 2, 3, 4, 5]} class="star">
<RadioGroupItem value={String(n)}
attrs={{ "aria-label": `${n} stars`, "data-filled": Number(state.rating) >= n ? "" : undefined }} />
<Icon name="star" />
</label>
</RadioGroup>Cancel a change
onValueChange runs before the choice changes and gets details second; details.cancel() keeps the checked radio, for a click, Space or an arrow.
Code
<RadioGroup value={guard.value} onValueChange={(value, details) => {
if (!guard.allowed) details.cancel();
}}>
<Label><RadioGroupItem value="free" /> Free</Label>
<Label><RadioGroupItem value="pro" /> Pro</Label>
<Label><RadioGroupItem value="team" /> Team</Label>
</RadioGroup>Right to left
ArrowLeft moves to the next radio in a right-to-left group.