Skip to main content

RadioGroup

The RadioGroup component is a fully headless and accessible UI primitive that enables selection of one option from a predefined list. It's inspired by HeadlessUI RadioGroup, but built natively for Angular.

It lets you build custom radio-style controls where users can select one option from a predefined list.

Unlike native <input type="radio">, this component gives you full control over structure, behavior, and styling — without sacrificing accessibility or keyboard support.

When to use it

Use RadioGroup when you need a single-select UI component that is:

  • Fully customizable in layout and appearance
  • Accessible with screen readers and keyboard navigation
  • Controlled via Angular binding ([(modelValue)])
  • Compatible with reactive or template-driven forms

Common use cases:

  • Language, gender, or theme selectors
  • Shipping or payment options
  • Rating levels

Anatomy

A complete RadioGroup is composed of:

  • RadioGroup – The root container that manages selection
  • RadioGroupLabel – A label element tied to the group for screen readers
  • RadioGroupOption – Each selectable item, focusable and keyboard-accessible
  • RadioGroupDescription (optional) – Assistive description linked to the group or individual options

Features

  • ✅ Accessible by default (ARIA attributes)
  • ✅ Tailwind-friendly (no styles imposed)
  • ✅ Multiple selectors supported:
    • <RadioGroup>
    • <div ngxRadioGroup>
    • <ngx-headlessui-radiogroup>
  • ✅ Full control via #templateRefs making complex logic a breeze
  • ✅ Supports multiple instances per page

Installation

RadioGroup ships as part of the @ngx-headless/ui by default. Install if you haven't already.

npm install @ngx-headless/ui

Import the components directly:

import {
RadioGroupComponent,
RadioGroupDescriptionComponent,
RadioGroupLabelComponent,
RadioGroupOptionComponent,
} from "@ngx-headless/ui";

Usage Examples

Basic Example

languages = [
{ id: "en", name: "English" },
{ id: "fr", name: "French" },
{ id: "ur", name: "Urdu" },
];

selectedLanguage = this.languages[0];
<RadioGroup [(modelValue)]="selectedLanguage" class="flex flex-col gap-4">
<RadioGroupLabel class="text-sm font-medium text-gray-700">
Select a language
</RadioGroupLabel>

<RadioGroupDescription class="text-sm text-gray-500">
This sets your preferred language across the app.
</RadioGroupDescription>

<RadioGroupOption
*ngFor="let lang of languages"
[value]="lang"
class="cursor-pointer px-4 py-2 border rounded-md hover:bg-gray-50"
[class.bg-blue-600]="selectedLanguage === lang"
[class.text-white]="selectedLanguage === lang"
>
{{ lang.name }}
</RadioGroupOption>
</RadioGroup>

Usage Example with #templateRefs

//yourtemplate.component.html

<RadioGroup #radioGroup="ngxRadioGroup" [(modelValue)]="selectedLanguage" class="flex flex-col gap-4">
<RadioGroupLabel class="text-sm font-medium text-gray-700">
Select a language
</RadioGroupLabel>

<RadioGroupDescription class="text-sm text-gray-500">
This sets your preferred language across the app.
</RadioGroupDescription>

<RadioGroupOption
#opt="ngxRadioGroupOption"
*ngFor="let lang of languages"
[value]="lang"
class="cursor-pointer px-4 py-2 border rounded-md hover:bg-gray-50"
[class.bg-blue-600]="selectedLanguage === lang"
[class.text-white]="selectedLanguage === lang"
>
{{ lang.name }}
</RadioGroupOption>
</RadioGroup>

//yourcomponent.component.ts

...

@ViewChild('radioGroup') radioGroup!: RadioGroupComponent;
// ☝️ access to RadioGroupWrapper via #templateRefs

@ViewChildren('opt') options!: QueryList<RadioGroupOptionComponent>;
// ☝️ access to All RadioGroupOptions via #templateRefs

// 🧠 NOTE: You can also use @ViewChild RadioGroupOptionComponent without QueryList<> to capture a single instance

plans = [
{
name: 'Startup',
ram: '12GB',
cpus: '6 CPUs',
disk: '160 GB SSD disk',
},
...
]

selectedPlan = null;
// you can keep modelValue empty or pre-assign a default value like: selectedPlan = plans[0];

clear() {
// now you have access to entire radioGroup instance and all its methods.
this.radioGroup.clear();
}

Accessibility

This component handles:

  • role="radiogroup" on the root
  • role="radio" and aria-checked on each option
  • Keyboard navigation: , , , to move focus
  • aria-labelledby and aria-describedby linkage for assistive technologies
  • Focus ring and tabindex management

Animations

RadioGroup and all it's components can be animated freely using Angular’s built-in animation system.

Angular supports entry/exit transitions using @trigger bindings and the :enter/:leave lifecycle hooks.

👉 See Angular Animations Guide

Component API

RadioGroup

InputTypeDescription
modelValueTThe current selected value
defaultValueT?Optional default selection (used only if no modelValue)
classstringUtility classes applied to the outer group wrapper
OutputTypeDescription
modelValueChangeEventEmitter<T>Emits when selection is updated
MethodReturnsDescription
select(value: T)voidSets the selected value
clear()voidClears the current selection
toggle(value: T)voidSelects or clears the value
getSelected()T | nullGets the currently selected value
isSelected(value: T)booleanChecks if a given value is selected

RadioGroupOption

InputTypeDescription
valueTThe value this option represents
classstringUtility classes for styling
HostBindingTypeDescription
selectedbooleanTrue if this option is currently selected
aria-checkedbooleanARIA state for screen reader compatibility
tabindexnumberControls focus order and keyboard behavior
role'radio'Identifies the element as a radio option

RadioGroupLabel

InputTypeDescription
classstringUtility classes for styling
BehaviorDescription
aria-labelledbyApplied to the nearest radiogroup
idAuto-generated and used for linkage

RadioGroupDescription

InputTypeDescription
classstringUtility classes for styling
BehaviorDescription
aria-describedbyApplied to nearest radiogroup or radio option
idAuto-generated and used for linkage

Best Practices

  • Keep your option labels short and clear
  • Use consistent padding and spacing to visually group options
  • Avoid hiding options conditionally; use *ngIf cautiously
  • Add a RadioGroupDescription for clarity when options aren't self-explanatory

See Also