Components / forms

RadioGroup

One of two components that look like they need a binding and do not.

Rendered from the package · open on its own

Summary

A one-of-many choice where an option is always selected.

APG patternRadio Group
Built onnative elements
Statusstable
Since0.1.0
<RadioGroup id="visibility" name="visibility" legend="Who can see this portfolio?"
  description="You can change this at any time." defaultValue="public" options={…} />

Anatomy

A <fieldset data-se="radio-group"> with a <legend>, holding native radios.

PartElementRequired
description<p>no
error<p>no
legend<legend>yes
options<div>yes

Properties

PropertyValuesDefaultWhat it does
namestringrequiredShared by every input — this is what gives the group arrow keys for free.
legendReact.ReactNoderequiredThe question. Rendered as a <legend> so it is announced with each option.
descriptionReact.ReactNodenoneA line under the legend, before the options. Announced with the group.
optionsRadioOption[]requiredThe choices, in display order. One is always selected, so there is no empty state.
valuestringnoneThe selected value. Passing this makes the group controlled.
defaultValuestringnoneWhich option starts selected. Falls back to the first enabled one, because a radio group with nothing selected has no honest resting state.
onChange(value: string) => voidnoneCalled with the newly selected value.
orientationvertical · horizontalverticalStacks the options or lays them in a row.
inToolbarbooleannoneSet when the group sits inside a Toolbar. APG gives it a different arrow contract there: Right and Left move within the group but hand off to the toolbar at the ends, while Down and Up stay inside and wrap. The group takes over roving focus, so it costs the toolbar one tab stop, not three.
errorReact.ReactNodenoneShown and wired to aria-describedby. Never rely on color alone.
idstringrequiredRequired — used to build the option, description and error ids.

States

_None. Rest is the only condition it has._

Behavior

It needs no script. Arrow keys, roving focus and the one-of-many constraint come from the platform once every input shares a name. The onKeyDown returns immediately unless the group sits in a toolbar, where the APG gives it a different arrow contract.

Accessibility

The legend is the question, and it is announced with every option. A heading above a plain div is not, which leaves the reader hearing Only you with no idea what it answers. That is the whole reason this is a fieldset rather than a styled stack of Choice.

One tab stop for the group. Proving it needs a real Tab press from outside the group — every radio reports tabIndex === 0, and focusing an unchecked one makes it the stop, so the property cannot answer the question.

Keyboard

KeyResult
TabEnters the group once and leaves it once — the whole group is one stop
Arrow keysMove to the next or previous radio and select it
SpaceSelects the focused radio

Roles, states and properties

AttributeSet by
aria-describedbyset at render
aria-invalidset at render
aria-labelledbyset at render
rolealways radiogroup

Focus

Focus enters on the checked radio, or the first when none is checked. Proving it is one stop needs a real Tab from outside the group — every radio reports tabIndex === 0, and focusing an unchecked one makes it the stop, so no property can answer the question.

Screen reader

Not recorded as verified. Stage 4 of the readiness ladder — driving each component with VoiceOver and NVDA — is unstarted across the package, and no component should claim otherwise until docs/journal/ records the session that did it.

Contrast and target size

npm run check:contrast audits every color pair this component uses across all four skins, light and dark, and fails the build on a shortfall. Two waivers are on the record in docs/audit/DEFERRED.md.

It sets no target size of its own; anything interactive inside it brings its own.

Tested

  • test/apg/batch-c.test.jsx
  • test/apg/radio.test.jsx

Known gaps

  • 1 upstream ref — an SVG star rating built from g[role="radio"]
  • 2 upstream refs — an aria-activedescendant radio group; ours uses native inputs
  • 1 upstream ref — APG's own page or markup, not the pattern

Appearance

TokenUsed for
--se-leading-bodyline-height
--se-leading-uiline-height
--se-negativecolor
--se-space-1gap, margin-block-start
--se-space-2margin-block-end, margin-block-start
--se-space-4gap
--se-textcolor
--se-text-mutedcolor
--se-text-smfont-size
--se-weight-mediumfont-weight

Motion

None beyond the control's own.

Content

The legend is a question the options answer. Who can see this portfolio? Each option is the answer in the reader's words — Anyone with the link, not Public (unlisted).

Usage

An option is always selected. If none may be, the question is really a checkbox.

When not to use it. For several checkboxes answering one question — that is CheckboxGroup. For a single on/off setting — that is Choice.

It renders its own options, and that is the point. The options array becomes Choice controls this component owns — the fieldset, the legend and the wiring between them are the reason it exists, and they are the parts most often got wrong by hand. Reach for Choice directly only for a lone control that answers no group's question.

When not to use it. For any of many, and it also carries the tri-state parent that reports mixed — that is CheckboxGroup.

Notes

One of two components that look like they need a binding and do not.