RadioGroup presents a set of mutually-exclusive options where exactly one can be selected. Per the component-selection table in AGENTS.md, use it for ≤4 mutually-exclusive options; for switching between views in one context use Tabs, and for a compact segmented choice use .ods-segmented instead.
Installation
HTML — already included if you loaded the 3 stylesheets — see the Installation guide.
React
npm install @operant/ui
@operant/ui is currently a monorepo workspace package, not yet published to the public npm registry.
Usage
HTML
<div class="ods-radio-group" role="radiogroup" aria-label="Run status">
<label class="ods-radio-label" for="status-draft">
<input class="ods-radio" type="radio" name="status" id="status-draft" checked>
Draft
</label>
<label class="ods-radio-label" for="status-review">
<input class="ods-radio" type="radio" name="status" id="status-review">
In review
</label>
<label class="ods-radio-label" for="status-approved">
<input class="ods-radio" type="radio" name="status" id="status-approved">
Approved
</label>
</div>
Note: .ods-radio is styled with appearance: none conventions elsewhere in this system, and pairs a small dot indicator (.ods-radio-indicator) that Radix renders as a separate element on [data-state="checked"]. In plain HTML with a native <input type="radio">, the checked dot is drawn by the border/background rules directly — no extra indicator element is required.
React
import { RadioGroup, RadioGroupItemField } from "@operant/ui";
export default function Example() {
return (
<RadioGroup aria-label="Run status" defaultValue="draft">
<RadioGroupItemField value="draft" label="Draft" />
<RadioGroupItemField value="review" label="In review" />
<RadioGroupItemField value="approved" label="Approved" />
</RadioGroup>
);
}
Examples
Column (default)
The default orientation="column" stacks options vertically with --space-6 gap, matching .ods-radio-group's default flex-direction: column.
HTML
<div class="ods-radio-group" role="radiogroup" aria-label="Run status">
<label class="ods-radio-label" for="c-draft"><input class="ods-radio" type="radio" name="c-status" id="c-draft" checked>Draft</label>
<label class="ods-radio-label" for="c-review"><input class="ods-radio" type="radio" name="c-status" id="c-review">In review</label>
<label class="ods-radio-label" for="c-approved"><input class="ods-radio" type="radio" name="c-status" id="c-approved">Approved</label>
</div>
React
<RadioGroup aria-label="Run status" defaultValue="draft">
<RadioGroupItemField value="draft" label="Draft" />
<RadioGroupItemField value="review" label="In review" />
<RadioGroupItemField value="approved" label="Approved" />
</RadioGroup>
Row
Pass orientation="row" (React) or add the .-row modifier (HTML) to lay options out horizontally with --space-12 gap. On the React side this also switches Radix's internal orientation to "horizontal" so arrow-key navigation follows the same axis as the visual layout.
HTML
<div class="ods-radio-group -row" role="radiogroup" aria-label="Run status">
<label class="ods-radio-label" for="r-draft"><input class="ods-radio" type="radio" name="r-status" id="r-draft" checked>Draft</label>
<label class="ods-radio-label" for="r-review"><input class="ods-radio" type="radio" name="r-status" id="r-review">In review</label>
<label class="ods-radio-label" for="r-approved"><input class="ods-radio" type="radio" name="r-status" id="r-approved">Approved</label>
</div>
React
<RadioGroup aria-label="Run status" defaultValue="draft" orientation="row">
<RadioGroupItemField value="draft" label="Draft" />
<RadioGroupItemField value="review" label="In review" />
<RadioGroupItemField value="approved" label="Approved" />
</RadioGroup>
With a disabled item
Individual items can be disabled independently of the group. .ods-radio:disabled drops opacity to 50% and removes pointer events.
HTML
<div class="ods-radio-group" role="radiogroup" aria-label="Run status">
<label class="ods-radio-label" for="d-draft"><input class="ods-radio" type="radio" name="d-status" id="d-draft" checked>Draft</label>
<label class="ods-radio-label" for="d-review"><input class="ods-radio" type="radio" name="d-status" id="d-review" disabled>In review</label>
<label class="ods-radio-label" for="d-approved"><input class="ods-radio" type="radio" name="d-status" id="d-approved">Approved</label>
</div>
React
<RadioGroup aria-label="Run status" defaultValue="draft">
<RadioGroupItemField value="draft" label="Draft" />
<RadioGroupItemField value="review" label="In review" disabled />
<RadioGroupItemField value="approved" label="Approved" />
</RadioGroup>
Bare items (no label)
For cases where the visible label lives elsewhere (e.g. a table cell), render RadioGroupItem directly and supply an aria-label.
HTML
<div class="ods-radio-group -row" role="radiogroup" aria-label="Run status">
<input class="ods-radio" type="radio" name="b-status" aria-label="Draft" checked>
<input class="ods-radio" type="radio" name="b-status" aria-label="In review">
</div>
React
<RadioGroup aria-label="Run status" defaultValue="draft" orientation="row">
<RadioGroupItem value="draft" aria-label="Draft" />
<RadioGroupItem value="review" aria-label="In review" />
</RadioGroup>
API Reference
<RadioGroup>
Wraps Radix RadioGroupPrimitive.Root.
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "column" | "row" | "column" | Visual layout. Maps to the .-row modifier and to Radix's orientation ("vertical"/"horizontal") so arrow-key navigation matches the visual axis. |
value | string | — | Controlled selected value. |
defaultValue | string | — | Uncontrolled initial value. |
onValueChange | (value: string) => void | — | Called when the selection changes. |
disabled | boolean | false | Disables every item in the group. |
className | string | — | Merged with .ods-radio-group. |
<RadioGroupItem>
Wraps Radix RadioGroupPrimitive.Item + Indicator. Renders .ods-radio with an inner .ods-radio-indicator (only visible on [data-state="checked"]).
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Required. The value this item represents. |
disabled | boolean | false | Disables this single item. |
id | string | — | Forwarded to the underlying element; pair with a <label htmlFor>. |
<RadioGroupItemField>
Convenience wrapper pairing RadioGroupItem with an .ods-radio-label, auto-generating an id/htmlFor pair via useId() when none is given.
| Prop | Type | Default | Description |
|---|---|---|---|
label | ReactNode | — | Required. Label content rendered next to the control. |
| ...rest | RadioGroupItemProps | — | All RadioGroupItem props (value, disabled, id, etc.). |
HTML-side classes: .ods-radio-group (container, .-row modifier for horizontal layout), .ods-radio (the control itself — a hairline-bordered circle that gains a 2px --color-primary border on [data-state="checked"]), .ods-radio-indicator (the 8px filled dot), .ods-radio-label (label + control row, shared with .ods-checkbox-label).
Accessibility
- Renders with the native ARIA
radiogroup/radioroles via Radix, so screen readers announce the group label and each option's checked state correctly. - Full keyboard support: arrow keys move selection along the group's orientation axis,
Tabmoves in/out of the group as a single stop (roving tabindex). RadioGrouprequires an accessible name — passaria-label(as in every example above) or associate a visible<label>/heading viaaria-labelledby.- Focus is visible via
:focus-visible→--shadow-focuson the currently-focused radio; disabled items are skipped during keyboard navigation. - Each
RadioGroupItemFieldwires its ownid/htmlForpair automatically, so clicking the label text toggles the radio — no manual wiring needed.