oriUI

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.

ClassTypeDescription
ori-radio-groupBlockRequired base class on the container; carries role=radiogroup.
ori-color_*Colorprimary · secondary · success · warning · danger · info · surface
ori-font-size_*Sizexs · sm · md · lg · xl · xxl — scales all option labels.
ori-radio-group_horizontalLayoutLays options out in a row (flex-wrap) instead of a column.
ori-radio-group__labelPartGroup label element; referenced by aria-labelledby.
ori-radio-group__optionsPartOptions wrapper; flex column (or row when _horizontal).
ori-radioPartWrapping
ori-radio__input · ori-radio__circle · ori-radio__labelPartHidden native input / styled circle indicator / visible label text.
disabled · aria-requiredStateReal 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

text
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

Choose a plan
html
<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>
vue
<OriRadioGroup
    v-model="plan"
    label="Choose a plan"
    :options="[
        { label: 'Free', value: 'free' },
        { label: 'Pro', value: 'pro' },
        { label: 'Team', value: 'team' }
    ]"
/>

Colors

Every semantic role. The accent color is shared by the checked circle and the focus ring.

Primary
Success
Danger
html
<!-- 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>
vue
<OriRadioGroup v-model="val" label="Primary" color="primary" :options="opts" />
<OriRadioGroup v-model="val" label="Success" color="success" :options="opts" />
<OriRadioGroup v-model="val" label="Danger" color="danger" :options="opts" />

Sizes

xs → xxl. The size class controls font-size, which scales the circle and the gap uniformly via em units.

xs
sm
md (default)
lg
html
<!-- 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>
vue
<OriRadioGroup v-model="val" label="xs" size="xs" :options="opts" />
<OriRadioGroup v-model="val" label="md (default)" size="md" :options="opts" />
<OriRadioGroup v-model="val" label="lg" size="lg" :options="opts" />

Horizontal

orientation="horizontal" lays the options out in a wrapping row instead of a column — useful for short label sets.

Notifications
html
<!-- 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>
vue
<OriRadioGroup v-model="notify" label="Notifications" orientation="horizontal" :options="notifyOptions" />

Disabled — whole group

Pass disabled to lock all options at once.

Plan (locked)
html
<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>
vue
<OriRadioGroup v-model="plan" label="Plan (locked)" disabled :options="planOptions" />

Disabled — per option

Set disabled: true on individual items in the options array to lock only those choices.

Choose a plan
html
<!-- 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>
vue
<OriRadioGroup
    v-model="plan"
    label="Choose a plan"
    :options="[
        { label: 'Free', value: 'free' },
        { label: 'Pro', value: 'pro' },
        { label: 'Enterprise (contact us)', value: 'enterprise', disabled: true }
    ]"
/>

Required

required sets aria-required="true" on the group and the required attribute on each input.

Preferred contact
html
<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>
vue
<OriRadioGroup v-model="contact" label="Preferred contact" required :options="contactOptions" />

Common patterns

Color, size, horizontal layout, and per-option disabled in a compact panel.

Theme
html
<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>
vue
<OriRadioGroup
    v-model="theme"
    label="Theme"
    orientation="horizontal"
    color="secondary"
    size="sm"
    :options="[
        { label: 'Light', value: 'light' },
        { label: 'Dark', value: 'dark' },
        { label: 'System', value: 'system' }
    ]"
/>

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". When label is provided, aria-labelledby points at the rendered label element; without a label, pass aria-label as a fallthrough attribute.
  • Every option is a real <input type="radio"> sharing one name (auto-generated via useId when 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__circle is aria-hidden="true"; the accessible name comes from the adjacent ori-radio__label text, wired through the <label> wrapper.
  • disabled (group-level or per-option) sets the native disabled attribute on the <input>.
  • required sets both the native required attribute on each input and aria-required="true" on the group container.
  • Focus is always visible via :focus-visible on the hidden <input>, reflected onto the ori-radio__circle with a 2 px outline in the group's accent color. Safari drops :focus-visible from a radio an arrow key focused, so the component sets data-ori-keyboard on the group while the keyboard is in use and the ring follows :focus under it; in plain HTML, set that attribute yourself on keydown and remove it on pointerdown.
KeyAction
TabMoves focus into / out of the radio group.
Arrow keysCycles between options within the group (native). In RTL the left and right arrows swap, except in Safari, which keeps the physical arrows (Browser support).
SpaceSelects 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

PropTypeDefaultDescription
colorThemeColor'primary'Accent color for the checked circle and focus ring.
disabledbooleanfalseDisables all options; per-option disable is also supported via options[].disabled.
labelstring—Group heading. When present the container is aria-labelledby this element.
namestring—Shared name for all radio inputs. Auto-generated via useId when omitted.
optionsRadioOption[][]Array of { label, value, disabled? } objects — the full options-array API.
orientationOrientation'vertical''horizontal' lays options out in a wrapping row (ori-radio-group_horizontal); 'vertical' stacks them.
requiredbooleanfalseSets aria-required="true" on the group and required on each input.
sizeActionSize'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':

ts
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:

vue
<OriRadioGroup v-model="val" aria-label="Sort order" :options="sortOptions" />

Slots

SlotPropsDescription
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.