Radio Group
A set of radio buttons with arrow-key navigation that works with Angular forms.
import { โฆ } from '@inlaylab/ivorie/radio-group'import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { IV_RADIO_GROUP_IMPORTS } from '@inlaylab/ivorie/radio-group';
@Component({
selector: 'demo-radio-group',
imports: [IV_RADIO_GROUP_IMPORTS],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<iv-radio-group [(value)]="plan" class="w-full max-w-sm">
@for (p of plans; track p.value) {
<label
class="has-[[data-checked]]:border-primary has-[[data-checked]]:ring-primary/20 flex cursor-pointer items-start gap-3 rounded-lg border p-4 has-[[data-checked]]:ring-[3px]"
>
<iv-radio-group-item [value]="p.value" class="mt-1" />
<span class="grid flex-1 gap-1">
<span class="flex items-baseline justify-between gap-2">
<span class="font-medium">{{ p.name }}</span>
<span class="text-muted-foreground text-sm">{{ p.price }}</span>
</span>
<span class="text-muted-foreground text-sm">{{ p.description }}</span>
</span>
</label>
}
</iv-radio-group>
`,
})
export default class RadioGroupDemo {
protected readonly plans = [
{ value: 'starter', name: 'Starter', price: '$0', description: 'For trying things out. Up to 3 projects.' },
{ value: 'pro', name: 'Pro', price: '$12/mo', description: 'For small teams. Unlimited projects, priority support.' },
{ value: 'team', name: 'Team', price: '$49/mo', description: 'For growing teams. Shared workspaces and roles.' },
];
protected readonly plan = signal<string | null>('pro');
}
Installation
npx ivorie add radio-groupimport { โฆ } from '@inlaylab/ivorie/radio-group'A group of native <button role="radio"> items. It supports ngModel, reactive forms and plain two-way binding.
Usage
import { IV_RADIO_GROUP_IMPORTS } from '@inlaylab/ivorie/radio-group';<iv-radio-group [(value)]="plan">
<iv-radio-group-item value="starter" aria-label="Starter" />
<iv-radio-group-item value="pro" aria-label="Pro" />
</iv-radio-group>Examples
With ngModel
Value: starter
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { IV_RADIO_GROUP_IMPORTS } from '@inlaylab/ivorie/radio-group';
@Component({
selector: 'demo-radio-group-ngmodel',
imports: [FormsModule, IV_RADIO_GROUP_IMPORTS],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-col gap-3">
<iv-radio-group [(ngModel)]="plan" class="gap-2">
<iv-radio-group-item value="starter" aria-label="Starter" />
<iv-radio-group-item value="pro" aria-label="Pro" />
<iv-radio-group-item value="team" aria-label="Team" />
</iv-radio-group>
<p class="text-muted-foreground text-sm">Value: {{ plan() }}</p>
</div>
`,
})
export default class RadioGroupNgmodelDemo {
protected readonly plan = signal<string | null>('starter');
}
Reactive forms
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { IV_RADIO_GROUP_IMPORTS } from '@inlaylab/ivorie/radio-group';
@Component({
selector: 'demo-radio-group-reactive-forms',
imports: [ReactiveFormsModule, IV_RADIO_GROUP_IMPORTS],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<form [formGroup]="form" class="flex flex-col gap-3">
<iv-radio-group formControlName="plan" class="gap-2">
<iv-radio-group-item value="starter" aria-label="Starter" />
<iv-radio-group-item value="pro" aria-label="Pro" />
<iv-radio-group-item value="team" aria-label="Team" />
</iv-radio-group>
<p class="text-muted-foreground text-sm">Value: {{ form.value.plan }}</p>
</form>
`,
})
export default class RadioGroupReactiveFormsDemo {
protected readonly form = new FormGroup({ plan: new FormControl('starter') });
}
With labels
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { IV_RADIO_GROUP_IMPORTS } from '@inlaylab/ivorie/radio-group';
@Component({
selector: 'demo-radio-group-with-labels',
imports: [IV_RADIO_GROUP_IMPORTS],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<iv-radio-group [(value)]="plan" class="gap-3">
@for (option of plans; track option.value) {
<label class="flex cursor-pointer items-center gap-2 text-sm">
<iv-radio-group-item [value]="option.value" />
{{ option.label }}
</label>
}
</iv-radio-group>
`,
})
export default class RadioGroupWithLabelsDemo {
protected readonly plans = [
{ value: 'starter', label: 'Starter' },
{ value: 'pro', label: 'Pro' },
{ value: 'team', label: 'Team' },
];
protected readonly plan = signal<string | null>('pro');
}
Disabled group
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { IV_RADIO_GROUP_IMPORTS } from '@inlaylab/ivorie/radio-group';
@Component({
selector: 'demo-radio-group-disabled-group',
imports: [IV_RADIO_GROUP_IMPORTS],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<iv-radio-group value="pro" [disabled]="true" class="gap-3">
<label class="flex items-center gap-2 text-sm">
<iv-radio-group-item value="starter" />
Starter
</label>
<label class="flex items-center gap-2 text-sm">
<iv-radio-group-item value="pro" />
Pro
</label>
</iv-radio-group>
`,
})
export default class RadioGroupDisabledGroupDemo {}
Disabled item
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { IV_RADIO_GROUP_IMPORTS } from '@inlaylab/ivorie/radio-group';
@Component({
selector: 'demo-radio-group-disabled-item',
imports: [IV_RADIO_GROUP_IMPORTS],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<iv-radio-group [(value)]="plan" class="gap-3">
<label class="flex cursor-pointer items-center gap-2 text-sm">
<iv-radio-group-item value="starter" />
Starter
</label>
<label class="flex items-center gap-2 text-sm">
<iv-radio-group-item value="enterprise" [disabled]="true" />
Enterprise (unavailable)
</label>
</iv-radio-group>
`,
})
export default class RadioGroupDisabledItemDemo {
protected readonly plan = signal<string | null>('starter');
}
API
Every part also accepts class.
iv-radio-group
| Name | Type | Default | Description |
|---|---|---|---|
value |
string | null |
null |
Selected value. Bind with [(value)]. |
disabled |
boolean |
false |
Disables all items. Also disabled when the form control is disabled. |
orientation |
'horizontal' | 'vertical' | undefined |
undefined |
Sets aria-orientation. |
Outputs:
| Name | Payload | Description |
|---|---|---|
valueChange |
string | null |
Emits when the user selects an item. |
iv-radio-group-item
Must be a descendant of iv-radio-group. Renders a native button; class goes on that button.
| Name | Type | Default | Description |
|---|---|---|---|
value |
string |
required | Value this item stands for. |
disabled |
boolean |
false |
Disables this item only. |
id |
string | null |
null |
Set on the inner button, so <label for> works. |
aria-label |
string | null |
null |
Set on the inner button. |
aria-labelledby |
string | null |
null |
Set on the inner button. |
Accessibility
- The group has
role="radiogroup". Each item is<button role="radio">witharia-checked. - Only one item is in the tab order (roving tabindex): the checked item, else the first enabled one.
- Arrow Down and Arrow Right select the next enabled item. Arrow Up and Arrow Left select the previous one. Both wrap at the ends and skip disabled items.
Spaceselects the focused item.Enterdoes nothing, as in Radix.- A wrapping
<label>and<label for>both work.
Notes
ngModelandformControlNameneedFormsModuleorReactiveFormsModule.- Each item button has
data-state,data-checked,data-uncheckedanddata-disabled. - Setting the value from the form does not emit
valueChange.