Primitives
Radio Group
A set of radio buttons where only one option can be selected, with arrow-key navigation.
Move between the radios with the arrow keys: selection follows focus, the group keeps one tab stop, and the checked radio carries data-state="checked".
Headless implementation with selection-on-focus, wrap-around arrow navigation, and FormValueControl<string | null> integration for Angular Signal Forms.
Anatomy
<div forRadioGroup [(value)]="value" orientation="vertical" aria-labelledby="rg-label">
<span id="rg-label">Shipping method</span>
<button type="button" forRadio value="standard">
<span forRadioIndicator></span>
Standard
</button>
<button type="button" forRadio value="express">
<span forRadioIndicator></span>
Express
</button>
</div>
Examples
Horizontal orientation
orientation='horizontal' reflects data-orientation and switches arrow navigation to ArrowLeft / ArrowRight (swapped in RTL).
Signal Forms
forRadioGroup implements FormValueControl<string | null>, so [formField] binds the selected value into the form and surfaces validity back. This field is required: Tab through without choosing and the group reflects data-invalid / data-touched once focus leaves it.
API
ForRadioGroup
| Property | Type | Description |
|---|---|---|
value | model | Two-way bindable. The selected radio's value; null = none selected. Required by FormValueControl<string | null>.Default: null |
orientation | input | Layout hint reflected as aria-orientation / data-orientation. Does not restrict arrow navigation: all four cursors navigate in either orientation.Default: 'vertical' |
dir | input | Swaps ArrowLeft / ArrowRight. Default: 'ltr' |
disabled / readonly / required / invalid / pending | input | Reflected as aria-* / data-*. disabled blocks all interaction; readonly blocks selection changes but arrows still move focus.Default: — |
loop | input | When true (default), arrow nav wraps around past the first / last enabled radio. Set to false for a non-wrapping group.Default: true |
name | input | For form association. Default: — |
errors | input | Wired by [formField].Default: — |
touched | model | Set to true when focus leaves the group entirely.Default: — |
| Data attribute | Values | |
|---|---|---|
data-orientation | horizontal | vertical | |
data-disabled | present | absent | |
data-readonly | present | absent |
ForRadio
| Property | Type | Description |
|---|---|---|
value | input.required | This radio's identifier. Must be unique within the group and non-empty. Default: — |
disabled | input | Disables this radio independently of the group. Disabled radios are skipped during arrow navigation. Default: — |
| Data attribute | Values | |
|---|---|---|
data-state | checked | unchecked | |
data-disabled | present | absent | |
data-readonly | present | absent | |
data-orientation | horizontal | vertical |
A disabled radio reflects aria-disabled="true" + data-disabled="" (no native disabled, per APG). It is announced but non-selectable, and skipped during arrow nav. Tabindex is 0 for the selected radio (or, when no radio is selected, the first enabled one) and -1 for the rest.
A read-only group reflects data-readonly="" on every radio for styling. aria-readonly is deliberately not emitted on role="radio" (WAI-ARIA supports that property on radiogroup, not on radio), so the read-only announcement lives on the [forRadioGroup] root, which reflects both aria-readonly="true" and data-readonly="".
ForRadioIndicator
Optional slot inside a ForRadio. Apply on the element you want to show only while the radio is selected (typically a filled dot). Mirrors the parent radio's data-state so you can hide the unchecked state with [data-state="unchecked"] { display: none }.
| Data attribute | Values | |
|---|---|---|
data-state | checked | unchecked | |
data-orientation | horizontal | vertical |
Keyboard
- Tab moves focus into / out of the group; lands on the selected radio (or the first enabled one if nothing is selected).
- Space / Enter select the focused radio (Space is APG; Enter comes from the underlying
<button>and is harmless). - ArrowDown / ArrowUp / ArrowRight / ArrowLeft: all four cursors move focus AND change selection ("selection on focus"), wrapping at the ends, in either orientation (per the WAI-ARIA Radio Group pattern). RTL swaps Left/Right.
- Home / End jump to the first / last enabled radio (and select it).
- Disabled radios are skipped.
- Under
readonly, arrow keys and Home / End still move focus so the options can be traversed, but selection never changes.
Accessibility
Implements the WAI-ARIA Radio Group pattern.
- Provide a group label. Use
aria-labelledby(pointing to a heading or<span>) oraria-label. Without one, screen readers cannot announce the group's purpose. - Selection-on-focus is the APG-mandated behavior for standard radio groups (toolbars use a different model). Be aware that arrow navigation immediately changes the form value.
- All four arrow keys navigate regardless of
orientation, matching the WAI-ARIA Radio Group pattern and native HTML radios;orientationis a visual /aria-orientationhint only. role="radio"on a<button>is the most accessible host: it gets keyboard activation and SR-friendly semantics. Other host elements lose those defaults.aria-readonlybelongs on the group, not the radios. Theradiogrouproot carries it; each radio only gets thedata-readonlystyling hook, because WAI-ARIA does not supportaria-readonlyonrole="radio".
Styling
forty-cdk ships no styles: put your own class on each piece and key your CSS off the data-* attributes listed under API, not off the for* selectors (Styling forty-cdk explains why).
.rg-dot {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid currentColor;
border-radius: 50%;
}
.rg-option[data-state='checked'] .rg-dot {
background: radial-gradient(circle, currentColor 40%, transparent 45%);
}
.rg-option:not([data-disabled]):hover {
cursor: pointer;
}
Wrapping in a design system
Wrapping form primitives documents both supported wrapper patterns: hostDirectives with the exported FOR_RADIO_GROUP_HOST_DIRECTIVE_INPUTS / FOR_RADIO_GROUP_HOST_DIRECTIVE_OUTPUTS name tuples, and subclassing (which re-provides FOR_RADIO_GROUP_CONTEXT with useExisting pointing at the subclass).