RadioGroup
A "choose one" compound control. A role="radiogroup" container names the set via
aria-labelledby; each option is a real <input type="radio"> sharing a single name — so the
browser enforces single-select and native form submission — visually hidden behind a styled circle.
Pass the options as an array; per-option disabled is supported.
For a short pick shown as one compact row of buttons (Light / Dark / Auto), use
SegmentedControl: the same radio semantics, styled as segments.
The examples are organized by layer: the class reference is the standalone
@oriui/css layer, and the Framework API is the @oriui/vue component. Every
example is live — flip its code between HTML (the standalone classes, also your htmx / Astro / Svelte /
plain-HTML usage), Vue; HTML is the default.
Classes
The group is a block class plus single-class token utilities — one class repoints one token, no base class needed. The Vue props in Framework API map 1:1 to these.
| Class | Type | Description |
|---|---|---|
ori-radio-group | Block | Required base class on the container; carries role=radiogroup. |
ori-color_* | Color | primary · secondary · success · warning · danger · info · surface |
ori-font-size_* | Size | xs · sm · md · lg · xl · xxl — scales all option labels. |
ori-radio-group_horizontal | Layout | Lays options out in a row (flex-wrap) instead of a column. |
ori-radio-group__label | Part | Group label element; referenced by aria-labelledby. |
ori-radio-group__options | Part | Options wrapper; flex column (or row when _horizontal). |
ori-radio | Part | Wrapping |
ori-radio__input · ori-radio__circle · ori-radio__label | Part | Hidden native input / styled circle indicator / visible label text. |
disabled · aria-required | State | Real attributes on the native input / container, not extra classes. |
À la carte: the classes above ship in @oriui/css/components/radio.css. .ori-radio-group lives in
radio.css, not a file of its own. Import a foundation (@oriui/css/base.css or @oriui/css/tokens.css)
first — the token utilities (ori-color_*, ori-size-radius_*, …) live there, not in the component file. The
full bundle @oriui/css is the default and already carries both; see à-la-carte imports.
Anatomy
div.ori-radio-group [role="radiogroup", aria-labelledby]
div.ori-radio-group__label ← group heading (optional)
div.ori-radio-group__options
label.ori-radio ← one per option
input.ori-radio__input ← visually hidden real radio
span.ori-radio__circle ← styled indicator (aria-hidden)
span.ori-radio__label ← visible label text
Basic
<div class="ori-radio-group ori-color_primary ori-font-size_md" role="radiogroup" aria-labelledby="plan-label">
<div id="plan-label" class="ori-radio-group__label">Choose a plan</div>
<div class="ori-radio-group__options">
<label class="ori-radio">
<input class="ori-radio__input" type="radio" name="plan" value="free" />
<span class="ori-radio__circle" aria-hidden="true"></span>
<span class="ori-radio__label">Free</span>
</label>
<!-- one <label class="ori-radio"> per option, all sharing the same name -->
</div>
</div>
Colors
Every semantic role. The accent color is shared by the checked circle and the focus ring.
<!-- swap the color: ori-color_primary → _success / _danger / _warning / _info -->
<div class="ori-radio-group ori-color_success ori-font-size_md" role="radiogroup">…</div>
Sizes
xs → xxl. The size class controls font-size, which scales the circle and the gap uniformly
via em units.
<!-- swap the size: ori-font-size_md → _xs / _sm / _lg / _xl / _xxl -->
<div class="ori-radio-group ori-color_primary ori-font-size_lg" role="radiogroup">…</div>
Horizontal
orientation="horizontal" lays the options out in a wrapping row instead of a column — useful for short label sets.
<!-- add ori-radio-group_horizontal to the container -->
<div
class="ori-radio-group ori-radio-group_horizontal ori-color_primary ori-font-size_md"
role="radiogroup"
aria-labelledby="notify-label"
>
<div id="notify-label" class="ori-radio-group__label">Notifications</div>
<div class="ori-radio-group__options">
<label class="ori-radio">…</label>
<label class="ori-radio">…</label>
<label class="ori-radio">…</label>
</div>
</div>
Disabled — whole group
Pass disabled to lock all options at once.
<div class="ori-radio-group ori-color_primary ori-font-size_md" role="radiogroup">
<div class="ori-radio-group__label">Plan (locked)</div>
<div class="ori-radio-group__options">
<!-- each input gets the disabled attribute; its label dims through it -->
<label class="ori-radio">
<input class="ori-radio__input" type="radio" name="plan" value="free" disabled />
<span class="ori-radio__circle" aria-hidden="true"></span>
<span class="ori-radio__label">Free</span>
</label>
</div>
</div>
Disabled — per option
Set disabled: true on individual items in the options array to lock only those choices.
<!-- only the disabled option's input gets the attribute; its label dims through it -->
<label class="ori-radio">
<input class="ori-radio__input" type="radio" name="plan" value="enterprise" disabled />
<span class="ori-radio__circle" aria-hidden="true"></span>
<span class="ori-radio__label">Enterprise (contact us)</span>
</label>
Required
required sets aria-required="true" on the group and the required attribute on each input.
<div
class="ori-radio-group ori-color_primary ori-font-size_md"
role="radiogroup"
aria-labelledby="contact-label"
aria-required="true"
>
<div id="contact-label" class="ori-radio-group__label">Preferred contact</div>
<div class="ori-radio-group__options">
<label class="ori-radio">
<input class="ori-radio__input" type="radio" name="contact" value="email" required />
…
</label>
</div>
</div>
Common patterns
Color, size, horizontal layout, and per-option disabled in a compact panel.
<div
class="ori-radio-group ori-radio-group_horizontal ori-color_secondary ori-font-size_sm"
role="radiogroup"
aria-labelledby="theme-label"
>
<div id="theme-label" class="ori-radio-group__label">Theme</div>
<div class="ori-radio-group__options">
<label class="ori-radio">
<input class="ori-radio__input" type="radio" name="theme" value="light" />
<span class="ori-radio__circle" aria-hidden="true"></span>
<span class="ori-radio__label">Light</span>
</label>
<!-- … Dark, System -->
</div>
</div>
Accessibility
The accessibility contract holds across every layer — the standalone classes and the Vue component render the same attributes and keyboard behavior.
- The root element carries
role="radiogroup". Whenlabelis provided,aria-labelledbypoints at the rendered label element; without alabel, passaria-labelas a fallthrough attribute. - Every option is a real
<input type="radio">sharing onename(auto-generated viauseIdwhen omitted). The browser natively enforces single-select, arrow-key navigation between options in the group, and form submission — no JS needed. - The visual
ori-radio__circleisaria-hidden="true"; the accessible name comes from the adjacentori-radio__labeltext, wired through the<label>wrapper. disabled(group-level or per-option) sets the nativedisabledattribute on the<input>.requiredsets both the nativerequiredattribute on each input andaria-required="true"on the group container.- Focus is always visible via
:focus-visibleon the hidden<input>, reflected onto theori-radio__circlewith a 2 pxoutlinein the group's accent color. Safari drops:focus-visiblefrom a radio an arrow key focused, so the component setsdata-ori-keyboardon the group while the keyboard is in use and the ring follows:focusunder it; in plain HTML, set that attribute yourself on keydown and remove it on pointerdown.
| Key | Action |
|---|---|
Tab | Moves focus into / out of the radio group. |
Arrow keys | Cycles between options within the group (native). In RTL the left and right arrows swap, except in Safari, which keeps the physical arrows (Browser support). |
Space | Selects the focused option (native). |
Framework API
The props, events, and slots of the Vue component. The standalone CSS layer has no component API — its surface is the classes above.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
color | ThemeColor | 'primary' | Accent color for the checked circle and focus ring. |
disabled | boolean | false | Disables all options; per-option disable is also supported via options[].disabled. |
label | string | — | Group heading. When present the container is aria-labelledby this element. |
name | string | — | Shared name for all radio inputs. Auto-generated via useId when omitted. |
options | RadioOption[] | [] | Array of { label, value, disabled? } objects — the full options-array API. |
orientation | Orientation | 'vertical' | 'horizontal' lays options out in a wrapping row (ori-radio-group_horizontal); 'vertical' stacks them. |
required | boolean | false | Sets aria-required="true" on the group and required on each input. |
size | ActionSize | 'md' | Font size scale (xs–xxl); the circle and gap scale with it via em units. |
Orientation: 'horizontal' | 'vertical'
RadioOption is exported — import type { RadioOption } from '@oriui/vue':
import type { RadioOption } from '@oriui/vue'
const options: RadioOption[] = [
{ label: 'Email', value: 'email' },
{ label: 'SMS', value: 'sms', disabled: true }
]
Events & attributes
v-model is the public state binding — prop modelValue holds the currently selected value, and
the component emits update:modelValue when the user picks another option. The name attribute is
shared across all native inputs; omit it to let the component auto-generate a unique id via useId.
Extra attributes (e.g. aria-label, class, data-*) are bound to the root div.ori-radio-group, the
role="radiogroup" element. An unlabeled group should
pass aria-label this way:
<OriRadioGroup v-model="val" aria-label="Sort order" :options="sortOptions" />
Slots
| Slot | Props | Description |
|---|---|---|
option | { option } | Scoped, rendered once per option to customize its label markup — card-style or leading-icon radios. option is the { label, value, disabled? } entry; falls back to option.label. |