Radio Group
A single-choice group — sizes, orientation, invalid state, accessible, in React (@bpdm/ui) and Angular (@bpdm/ng).
The Radio Group lets users pick exactly one option from a set (use a checkbox
group for multiple). Built on Radix — controlled (value + onValueChange) or
uncontrolled (defaultValue). In Angular it's <bpdm-radio-group> with
<bpdm-radio> items, driven by [(value)], [(ngModel)], or reactive forms.
Usage
Pair each RadioGroupItem with a <label> linked by id so clicking the text
selects the option.
import { RadioGroup, RadioGroupItem } from '@bpdm/ui/radio-group';
export function PlanPicker() {
return (
<RadioGroup defaultValue="pro">
<div className="flex items-center gap-2.5">
<RadioGroupItem value="free" id="free" />
<label htmlFor="free">Free</label>
</div>
<div className="flex items-center gap-2.5">
<RadioGroupItem value="pro" id="pro" />
<label htmlFor="pro">Pro</label>
</div>
<div className="flex items-center gap-2.5">
<RadioGroupItem value="enterprise" id="enterprise" />
<label htmlFor="enterprise">Enterprise</label>
</div>
</RadioGroup>
);
}import { Component } from '@angular/core';
import { BpdmRadioGroup, BpdmRadio } from '@bpdm/ng';
@Component({
selector: 'plan-picker',
imports: [BpdmRadioGroup, BpdmRadio],
templateUrl: './radio-usage.html',
})
export class PlanPicker {}<bpdm-radio-group [(value)]="plan">
<label class="flex items-center gap-2.5">
<bpdm-radio value="free" /> Free
</label>
<label class="flex items-center gap-2.5">
<bpdm-radio value="pro" /> Pro
</label>
<label class="flex items-center gap-2.5">
<bpdm-radio value="enterprise" /> Enterprise
</label>
</bpdm-radio-group>Orientation
Vertical by default; set orientation="horizontal" to lay items in a row.
import { RadioGroup, RadioGroupItem } from '@bpdm/ui/radio-group';
export function HorizontalPlan() {
return (
<RadioGroup defaultValue="pro" orientation="horizontal">
<label className="flex items-center gap-2"><RadioGroupItem value="free" /> Free</label>
<label className="flex items-center gap-2"><RadioGroupItem value="pro" /> Pro</label>
<label className="flex items-center gap-2"><RadioGroupItem value="enterprise" /> Enterprise</label>
</RadioGroup>
);
}import { Component } from '@angular/core';
import { BpdmRadioGroup, BpdmRadio } from '@bpdm/ng';
@Component({
selector: 'app-horizontal-plan',
standalone: true,
imports: [BpdmRadioGroup, BpdmRadio],
template: `
<bpdm-radio-group [(value)]="plan" orientation="horizontal">
<label class="flex items-center gap-2"><bpdm-radio value="free" /> Free</label>
<label class="flex items-center gap-2"><bpdm-radio value="pro" /> Pro</label>
<label class="flex items-center gap-2"><bpdm-radio value="enterprise" /> Enterprise</label>
</bpdm-radio-group>
`,
})
export class HorizontalPlanComponent {
plan = 'pro';
}Sizes
The item size prop offers sm, md (default), and lg.
import { RadioGroup, RadioGroupItem } from '@bpdm/ui/radio-group';
export function Sizes() {
return (
<RadioGroup defaultValue="md" orientation="horizontal" aria-label="Size">
<RadioGroupItem value="sm" size="sm" aria-label="Small" />
<RadioGroupItem value="md" size="md" aria-label="Medium" />
<RadioGroupItem value="lg" size="lg" aria-label="Large" />
</RadioGroup>
);
}import { Component } from '@angular/core';
import { BpdmRadioGroup, BpdmRadio } from '@bpdm/ng';
@Component({
selector: 'app-sizes',
standalone: true,
imports: [BpdmRadioGroup, BpdmRadio],
template: `
<bpdm-radio-group [(value)]="size" orientation="horizontal" aria-label="Size">
<bpdm-radio value="sm" size="sm" aria-label="Small" />
<bpdm-radio value="md" size="md" aria-label="Medium" />
<bpdm-radio value="lg" size="lg" aria-label="Large" />
</bpdm-radio-group>
`,
})
export class SizesComponent {
size = 'md';
}Disabled
Set disabled on the group to disable every option at once.
import { RadioGroup, RadioGroupItem } from '@bpdm/ui/radio-group';
export function DisabledPlan() {
return (
<RadioGroup defaultValue="pro" disabled>
<label className="flex items-center gap-2"><RadioGroupItem value="free" /> Free</label>
<label className="flex items-center gap-2"><RadioGroupItem value="pro" /> Pro</label>
<label className="flex items-center gap-2"><RadioGroupItem value="enterprise" /> Enterprise</label>
</RadioGroup>
);
}import { Component } from '@angular/core';
import { BpdmRadioGroup, BpdmRadio } from '@bpdm/ng';
@Component({
selector: 'app-disabled-plan',
standalone: true,
imports: [BpdmRadioGroup, BpdmRadio],
template: `
<bpdm-radio-group [(value)]="plan" disabled>
<label class="flex items-center gap-2"><bpdm-radio value="free" /> Free</label>
<label class="flex items-center gap-2"><bpdm-radio value="pro" /> Pro</label>
<label class="flex items-center gap-2"><bpdm-radio value="enterprise" /> Enterprise</label>
</bpdm-radio-group>
`,
})
export class DisabledPlanComponent {
plan = 'pro';
}Invalid
Set aria-invalid on the items to show the error state — the controls turn
destructive-coloured and screen readers announce the invalid state.
import { RadioGroup, RadioGroupItem } from '@bpdm/ui/radio-group';
export function InvalidPlan() {
return (
<div className="flex flex-col gap-1.5">
<RadioGroup aria-label="Plan" aria-describedby="plan-err">
<label className="flex items-center gap-2"><RadioGroupItem value="free" aria-invalid /> Free</label>
<label className="flex items-center gap-2"><RadioGroupItem value="pro" aria-invalid /> Pro</label>
</RadioGroup>
<p id="plan-err" className="text-xs text-destructive">Select a plan to continue.</p>
</div>
);
}import { Component } from '@angular/core';
import { BpdmRadioGroup, BpdmRadio } from '@bpdm/ng';
@Component({
selector: 'app-invalid-plan',
standalone: true,
imports: [BpdmRadioGroup, BpdmRadio],
template: `
<div class="flex flex-col gap-1.5">
<bpdm-radio-group [(value)]="plan" aria-label="Plan" aria-describedby="plan-err">
<label class="flex items-center gap-2"><bpdm-radio value="free" aria-invalid="true" /> Free</label>
<label class="flex items-center gap-2"><bpdm-radio value="pro" aria-invalid="true" /> Pro</label>
</bpdm-radio-group>
<p id="plan-err" class="text-xs text-destructive">Select a plan to continue.</p>
</div>
`,
})
export class InvalidPlanComponent {
plan = '';
}Controlled value
Drive the selection yourself with value + onValueChange (React) or two-way
[(value)] / [(ngModel)] (Angular).
Selected: pro
import { useState } from 'react';
import { RadioGroup, RadioGroupItem } from '@bpdm/ui/radio-group';
export function Controlled() {
const [value, setValue] = useState('pro');
return (
<RadioGroup value={value} onValueChange={setValue} orientation="horizontal">
<label className="flex items-center gap-2"><RadioGroupItem value="free" /> Free</label>
<label className="flex items-center gap-2"><RadioGroupItem value="pro" /> Pro</label>
<label className="flex items-center gap-2"><RadioGroupItem value="enterprise" /> Enterprise</label>
</RadioGroup>
);
}import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BpdmRadioGroup, BpdmRadio } from '@bpdm/ng';
@Component({
selector: 'app-controlled-plan',
standalone: true,
imports: [FormsModule, BpdmRadioGroup, BpdmRadio],
// two-way [(value)] shown here; [(ngModel)] / reactive forms work the same way
template: `
<bpdm-radio-group [(value)]="plan">
<label class="flex items-center gap-2.5"><bpdm-radio value="free" /> Free</label>
<label class="flex items-center gap-2.5"><bpdm-radio value="pro" /> Pro</label>
<label class="flex items-center gap-2.5"><bpdm-radio value="enterprise" /> Enterprise</label>
</bpdm-radio-group>
<p class="mt-2 text-sm text-muted-foreground">Selected: {{ plan }}</p>
`,
})
export class ControlledPlanComponent {
plan = 'pro';
}API
RadioGroup / <bpdm-radio-group>
| Prop | Type | Default | Description |
|---|---|---|---|
value / defaultValue | string | — | Controlled / uncontrolled selection. |
onValueChange | (value: string) => void | — | React — fires on change. Angular uses [(value)] / [(ngModel)]. |
orientation | vertical | horizontal | vertical | Layout direction. |
disabled | boolean | false | Disable the whole group. |
name | string | — | Form field name (React). |
RadioGroupItem / <bpdm-radio>
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Required. The item's value. |
size | sm | md | lg | md | Control size. |
disabled | boolean | false | Disable just this option (skipped by arrow-key navigation). |
aria-invalid | boolean | — | Error state. |
id | string | — | Control id (label association / testing), forwarded to the radio. |
aria-label / aria-labelledby / aria-describedby | string | — | Accessible name / description, forwarded to the radio. |
In React, all native Radix RadioGroup props (name, dir, aria-*, data-*, …)
are spread and refs are forwarded. In Angular, orientation / disabled are
@Inputs on <bpdm-radio-group> (a ControlValueAccessor), and value / size /
disabled / aria-invalid / id / aria-label / aria-labelledby /
aria-describedby on <bpdm-radio>.
Accessibility
- A proper
role="radiogroup"ofrole="radio"items with roving tabindex — the group is a single tab stop, and arrow keys move focus and selection between options (wrapping, skipping disabled ones); Space selects. Implemented in both frameworks (React via Radix, Angular natively). - Give each item an accessible name — link a
<label>byid(htmlForin React,forin Angular), wrap the control in the label, or passaria-label/aria-labelledby(forwarded to the control in both frameworks). - Give the group an accessible name with
aria-label/aria-labelledbywhen there's no visible group heading. - Set
aria-invalidon items that fail validation; bpdm styles them destructive-red and assistive tech announces the state. The selected dot is decorative.
Internationalization
- The radio group renders no text of its own — pass translated accessible names
(item
aria-label/ associated<label>, and a grouparia-label) and translated help/error viaaria-describedby. There's nothing hard-coded to override. - RTL-safe: the controls are symmetric circles with no physical spacing, so the group
mirrors under
dir="rtl", and the arrow keys follow the visual order — ArrowLeft advances in a right-to-left layout. In Angular this is automatic from the ambientdir; in React passdir="rtl"to<RadioGroup>(or use a RadixDirectionProvider). See the Internationalization guide.