Skip to content
Nizivo UI

RadioGroup

One choice from a short list of native radios, in a fieldset with a legend. The shell is server-rendered; one small client part passes the group's name and value to each radio.

Tier: client

Import

import { Radio, RadioGroup } from '@nizivo/react/radio-group';

// Once per app (Getting started), after
// @nizivo/tokens/tokens.css and @nizivo/react/css/base.css and @nizivo/react/css/fieldset.css and @nizivo/react/css/inline-error.css
import '@nizivo/react/css/radio-group.css';

A required RadioGroup keeps the browser's own validation bubble; it does not show an inline message or set aria-invalid until you pass error. Mixing it with Field-wired controls in one native-validated form is covered on Field.

Static or interactive

Client. The fieldset, legend, description and error are server HTML, and the radios' client leaf always ships with them. Native radios that share a name keep arrow keys and one checked radio before hydration. How to choose.

Examples

Basic

Native radios sharing one name: arrow keys move the choice, and the form submits it, with no JS.

Shipping
import type { JSX } from 'react';
import { Radio, RadioGroup } from '@nizivo/react/radio-group';

export function RadioGroupExample(): JSX.Element {
  return (
    <RadioGroup legend="Shipping" name="shipping" defaultValue="standard">
      <Radio value="standard">Standard shipping</Radio>
      <Radio value="express">Express shipping</Radio>
      <Radio value="pickup" disabled>
        Store pickup (no stores yet)
      </Radio>
    </RadioGroup>
  );
}

Horizontal

Horizontal, with a description.

Weight unit
Used on packing slips.
import type { JSX } from 'react';
import { Radio, RadioGroup } from '@nizivo/react/radio-group';

export function RadioGroupHorizontalExample(): JSX.Element {
  return (
    <RadioGroup
      legend="Weight unit"
      description="Used on packing slips."
      name="unit"
      defaultValue="kg"
      orientation="horizontal"
    >
      <Radio value="kg">Kilograms</Radio>
      <Radio value="lb">Pounds</Radio>
    </RadioGroup>
  );
}

Error

Required, with an error from the server: aria-invalid and the message on the group.

Tax category
Choose a tax category.
import type { JSX } from 'react';
import { Radio, RadioGroup } from '@nizivo/react/radio-group';

export function RadioGroupErrorExample(): JSX.Element {
  return (
    <RadioGroup legend="Tax category" name="tax" required error="Choose a tax category.">
      <Radio value="standard">Standard rate</Radio>
      <Radio value="reduced">Reduced rate</Radio>
      <Radio value="exempt">Exempt</Radio>
    </RadioGroup>
  );
}

Disabled

The whole group disabled: the native fieldset disables every radio, with no client code.

Fulfilment
import type { JSX } from 'react';
import { Radio, RadioGroup } from '@nizivo/react/radio-group';

export function RadioGroupDisabledExample(): JSX.Element {
  return (
    <RadioGroup legend="Fulfilment" name="fulfilment" defaultValue="manual" disabled>
      <Radio value="manual">Manual</Radio>
      <Radio value="automatic">Automatic</Radio>
    </RadioGroup>
  );
}

Props

Radio

One option of a RadioGroup. className and style go on the root <label>; every other attribute, ref included, goes on the native <input>. Name, checked state and required come from the group.

Client component Renders data-slot="radio" with the class nz-radio. Props: RadioProps. Source: src/radio-group/radio-group.client.tsx.

NameTypeDefaultDescription
childrenReactNode–

The label, after the circle.

renderRenderProp<RadioState>–

Composition of the root <label>, the hit target.

valuerequiredstring–

Submitted under the group's name when this radio is checked.

  • Also takes every attribute of <input> except checked, children, defaultChecked, name, readOnly, required, size, type, value. (Omit<ComponentPropsWithRef<"input">, "type" | "value" | "name" | "checked" | "defaultChecked" | "required" | "readOnly" | "children" | "size">)

render takes an element, or, in client code, a function of the props and this state (RadioState):

NameTypeDefaultDescription
disabledrequiredboolean–

RadioGroup

A native <fieldset role="radiogroup"> with a <legend> (Fieldset), plus aria-required and aria-invalid, which role group does not allow but radiogroup does (FIELD-CONTRACT.md). required marks the legend and makes every radio required; disabled is the fieldset's own.

Server component Renders data-slot="radio-group" with the class nz-radio-group. Props: RadioGroupProps. Source: src/radio-group/radio-group.tsx.

NameTypeDefaultDescription
childrenReactNode–

Radio elements, possibly with other content between them.

defaultValuestring–

Uncontrolled initial value.

descriptionReactNode–

Help text after the group, announced on entering it through aria-describedby.

From FieldsetProps.

disabledbooleanfalse

Native: disables every control inside, with no client code.

From FieldsetProps.

errorReactNode–

The group's error, rendered through InlineError and joined to aria-describedby; sets data-invalid. Not a live region (§16.5). null, false and '' present no error.

From FieldsetProps.

errorIconReactNode–

Replaces the internal error mark (circle-x). Decorative.

From FieldsetProps.

ids{ description?: string | undefined; error?: string | undefined; }–

Ids replacing the generated ones.

From FieldsetProps.

legendReactNode–

The group's name, a native <legend>.

From FieldsetProps.

namestringa generated name, unique per group

The name every radio submits under.

onValueChange(value: string, details: ChangeDetails<RadioGroupChangeReason>) => void–

Called with the newly checked radio's value, after that radio's own onChange.

orientationOrientation
"horizontal""vertical"
'vertical'

Layout of the radios; native arrow keys move both ways either way.

renderRenderProp<FieldsetState>–

Composition of the root; anything but a <fieldset> loses native disabled.

From FieldsetProps.

requiredbooleanfalse

Draws an aria-hidden mark after the legend; the controls inside carry required.

From FieldsetProps.

valuestring | null–

Controlled: the checked radio's value, or null for none.

  • Also takes every attribute of <fieldset> except children, defaultValue, name, onChange, role; disabled is redeclared above. (Omit<ComponentPropsWithRef<"fieldset">, "children">)

render takes an element, or, in client code, a function of the props and this state (FieldsetState):

NameTypeDefaultDescription
disabledrequiredboolean–
invalidrequiredboolean–

An error message is presented.

requiredrequiredboolean–

Other exports

RadioGroupChangeReason type

import type { RadioGroupChangeReason } from '@nizivo/react/radio-group'

Every change comes from the native change event of the radio that became checked.

type RadioGroupChangeReason = "input"

RadioGroupProps type

import type { RadioGroupProps } from '@nizivo/react/radio-group'

The props of RadioGroup, listed above.

RadioProps type

import type { RadioProps } from '@nizivo/react/radio-group'

The props of Radio, listed above.

RadioState type

import type { RadioState } from '@nizivo/react/radio-group'

What a render function receives for the root <label>.

The render state of Radio, listed above.

Data attributes and slots

Style parts by class (.nz-<slot>) and state by data attribute. Attribute meanings come from contracts/vocabulary.json.

SlotRendered byData attributes
radio
.nz-radio
Radio
root (takes render)
  • data-disabled (present or absent) ← disabled
    Mirrors the disabled attribute
radio-group
.nz-radio-group
RadioGroup
root (takes render)
  • data-orientation: "horizontal""vertical" ← orientation
    Layout axis
radio-input
.nz-radio-input
Radio
inner element (CSS only); interactive, focus: self; forced colours: border, fill on :checked
none
radio-label
.nz-radio-label
Radio
inner element (CSS only)
none

CSS hooks

Set a hook on the component's class or any ancestor to restyle it without specificity games; leave it unset for the default shown.

HookDefaultRead as
--nz-radio-accent--nz-bg-brand
  • --_nz-radio-border on .nz-radio-input:checked
  • --_nz-radio-ring on .nz-radio-input:checked
--nz-radio-gap--nz-space-2
  • column-gap on .nz-radio
--nz-radio-group-gap--nz-space-6
  • column-gap on .nz-radio-group[data-orientation="horizontal"] > .nz-fieldset-content
--nz-radio-size
  • default: 1.25rem
  • block-size on .nz-radio-input
  • inline-size on .nz-radio-input
  • margin-block-start on .nz-radio-input

Layers: nz.components. Classes: .nz-fieldset-content, .nz-fieldset-description, .nz-fieldset-error, .nz-radio, .nz-radio-group, .nz-radio-input, .nz-radio-label.

Tokens this stylesheet reads (18)

--nz-bg-brand, --nz-bg-disabled, --nz-bg-muted, --nz-bg-surface, --nz-border-control, --nz-border-control-hover, --nz-border-critical, --nz-border-subtle, --nz-border-width-default, --nz-duration-fast, --nz-easing-standard, --nz-font-body-m, --nz-radius-full, --nz-space-2, --nz-space-6, --nz-text-disabled, --nz-text-on-brand, --nz-text-primary

Accessibility

These parts are interactive. Each gets the focus outline and touch-action rule from @nizivo/react/css/base.css (the nz.base layer), so the floor survives a restyle:

  • radio-input: draws the focus outline itself on keyboard focus.

In forced-colours mode these states keep a visible system colour: radio-input (border), radio-input:checked (fill).

Pattern: WAI-ARIA Authoring Practices.

Keyboard

KeysAction
TabMoves focus into the group, to the checked radio, or to the first one when none is checked; the next Tab leaves the group.
ArrowDown, ArrowRightChecks the next radio and focuses it, skipping disabled ones.
ArrowUp, ArrowLeftChecks the previous radio and focuses it, skipping disabled ones.
SpaceChecks the focused radio when none is checked.

Source: native <input type="radio"> elements sharing one name (radio-group.client.tsx), which add no key handling; the browser lane (test/browser/toggles.spec.tsx) checks ArrowDown.

Figma

Node ids from design/component-map.json, which nz gen writes from component.json. The links open the Nizivo Figma library, which may ask you to sign in.

On this page