forty-cdk
llms.txt

Primitives

Radio Group

A set of radio buttons where only one option can be selected, with arrow-key navigation.

forty-cdk/radio-group WAI-ARIA APG

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

Shipping method

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

T-shirt size

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.

Shipping method

API

ForRadioGroup

PropertyTypeDescription
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 attributeValues
data-orientationhorizontal | vertical
data-disabledpresent | absent
data-readonlypresent | absent

ForRadio

PropertyTypeDescription
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 attributeValues
data-statechecked | unchecked
data-disabledpresent | absent
data-readonlypresent | absent
data-orientationhorizontal | 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 attributeValues
data-statechecked | unchecked
data-orientationhorizontal | 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>) or aria-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; orientation is a visual / aria-orientation hint 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-readonly belongs on the group, not the radios. The radiogroup root carries it; each radio only gets the data-readonly styling hook, because WAI-ARIA does not support aria-readonly on role="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).