Skip to content

group

group declares that an element gathers several related controls answered together — a set of radio buttons, a row of checkboxes, an address block. Without it each control announces its own label and nothing more, the heading that says what is being chosen is programmatically unconnected, and a disabled variant styles every control while disabling none.

Status — React: Specified • Web Components: Specified • iOS: Not yet planned • Android: Not yet planned

Roles

Apply the following roles to elements:

RoleTypeElement
groupControlroot of type: container
labelPartElement of type: text or type: slot, or nested instance prop of type: string
descriptionPartElement of type: text or type: slot, or nested instance prop of type: string
errormessagePartElement of type: text or type: slot, or nested instance prop of type: string

label names the group itself rather than pointing at a control, and must render first. The heading is usually a slot, and label on a slot means whatever is slotted here names this group — the naming element wraps the slot rather than replacing anything inside it.

States

The group role is typically applied in conjunction with the following states:

StateEffectClassify?
disabledDisables every control inside, enforced by the platformRecommended
requiredAnnounced as requiredRecommended
invalidAnnounced as invalidRecommended

disabled is the one worth classifying: one declaration disables the whole set without the transform touching a child. There is no readonly — no platform has a group-level equivalent, so set it per control.

Read more about states in specs.

Specs

Component anatomy typically has elements and roles like:

anatomy:
root:
type: container
role: group
header:
type: slot
role: label
children:
type: slot

Figma

Annotate the following layers:

  • root as role:group — on the component node
  • label as role:label on the heading text element, its slot, or a nested instance
  • description / errormessage as their own parts, where the group owns them

Author the heading first, above the controls. The transform lifts it into naming position either way, but a lift means rendered order stops matching layer order, and positional CSS in that subtree breaks.

React

Implementation

<FormGroup header={<Legend>Cabin class</Legend>} name="cabin-class">
<Radio value="economy" label="Economy" />
<Radio value="premium" label="Premium" />
</FormGroup>

Omitting name still yields exclusive radios — it just submits under a generated key.

Before / After

// before
<div className="form-group" data-element="root">
<div className="form-group__header" data-element="header">{p.header}</div>
<div className="form-group__children" data-element="children">{p.children}</div>
</div>
// after
const groupName = p.name ?? React.useId();
<RadioGroupContext.Provider value={{ name: groupName }}>
<fieldset className="form-group" data-element="root" disabled={p.disabled}>
<legend className="form-group__header" data-element="header">{p.header}</legend>
<div className="form-group__children" data-element="children">{p.children}</div>
</fieldset>
</RadioGroupContext.Provider>
  • <fieldset disabled> disables every control inside, enforced by the browser
  • <legend> is announced on focus entry, and HTML requires it to be the first child
  • Context is the only way to reach slotted radios — children are opaque to the component rendering them

Contract

PropTypeTierGenerated body
name?stringSHOULDOverrides the generated group name

No event handler. A group does not respond; the controls inside it do.

Web Components

Same <fieldset> and <legend> in the shadow root. Two differences:

  • No context needed. Slotted children are real descendants of the host, so each radio walks up on connect and reads the group’s name. Re-resolves on reconnect.
  • The host carries [disabled] alongside the inner fieldset, so the stylesheet — written against the host — can still address the state. That is precedence rule 1, not an exception.

iOS

Not yet planned. Intended binding:

TypeSection with a header, descendants’ accessibility merged
AnnouncedGroup name before the first control
disabled.disabled(_:) on the section, propagates to content
GroupingStructural — a Picker owns its options, so no shared name exists

Android

Not yet planned. Intended binding:

TypeContainer with Modifier.semantics(mergeDescendants = true)
AnnouncedContent description from the label part, before the first control
disabledSet per control — does not propagate from a container
GroupingselectableGroup() on the container

Additional details

Naming the controls inside it

Radios are exclusive only when grouped, and only the group knows the set exists — so the group generates a name and publishes it. Each radio prefers its own name prop if given one.

The name is never derived from the label text: it would change with copy edits and localization, collide when two groups share a label, and break deterministic output. A generated name guarantees exclusivity but is a meaningless submission key, so anything that submits wants the consumer to pass name.

Accessible name

The name comes from the label part. Where no label resolves, the group still emits, and warns — an unnamed group is announced as a group with nothing said about what it groups.

See also

  • radio — the control this role most often gathers
  • label — names the group here, names a control everywhere else
  • Precedence — role and states resolving together
  • Roles overview — the vocabulary and how roles are authored