Skip to main content

Radios

Overview​

The Radios component wraps multiple Radio components in a Bulma-styled group.

Use it for lists of mutually exclusive choices, such as RSVP or selection lists.


Import​

import { Radios, Radio } from '@allxsmith/bestax-bulma';

Usage​

Grouped Radios (All Disabled Example)​

This example demonstrates the Radios component wrapping multiple Radio children, all with the same name prop for mutual exclusivity. The disabled prop is set on each Radio to render them as non-interactive. Use this pattern for lists of mutually exclusive options, such as RSVP or selection lists.

<Radios>
  <Radio name="event" disabled>
    {' '}
    Attend{' '}
  </Radio>
  <Radio name="event" disabled>
    {' '}
    Decline{' '}
  </Radio>
  <Radio name="event" disabled>
    {' '}
    Tentative{' '}
  </Radio>
</Radios>


Context-Aware Rendering​

The Radios component is context-aware: it detects whether it is already inside a Field or Control and adjusts its rendering accordingly. This means you can use it standalone with a label prop (it wraps itself in Field+Control), inside a Field (it skips its own Field), or inside both Field and Control (it renders only the raw radio group).

Default (with label)​

The simplest usage — the component automatically renders its own Field and Control wrappers.

<Radios label="RSVP">
  <Radio name="rsvp"> Attend </Radio>
  <Radio name="rsvp"> Decline </Radio>
  <Radio name="rsvp"> Tentative </Radio>
</Radios>


With Field Wrapper​

When you need manual control over the Field layout, wrap the component in Field. The component detects it's inside a Field and skips rendering its own, and a labeled Field that holds the Radios directly names the group through aria-labelledby, with no extra wiring.

<Field label="RSVP">
  <Radios>
    <Radio name="rsvp-field"> Attend </Radio>
    <Radio name="rsvp-field"> Decline </Radio>
    <Radio name="rsvp-field"> Tentative </Radio>
  </Radios>
</Field>


In a Horizontal Form​

as="fieldset" renders the Radios' own Field as a native <fieldset>, which keeps role="radiogroup", with the label as its <legend>. With horizontal, the legend is the label column, and it names the group, so there are no ids to wire.

<Radios as="fieldset" horizontal label="RSVP">
  <Radio name="rsvp-fieldset"> Attend </Radio>
  <Radio name="rsvp-fieldset"> Decline </Radio>
  <Radio name="rsvp-fieldset"> Tentative </Radio>
</Radios>

To compose the row from a horizontal Field yourself, as the next example does, the label sits on the outer Field and the Radios in an inner one, which starts its own scope, so point the group at the label by hand: labelProps={{ id, htmlFor: undefined }} on that Field and a matching aria-labelledby on the Radios. Nothing in the inner Field takes the label's for, so the label drops it once the page mounts and labelProps={{ id }} works too. The htmlFor: undefined keeps the for out of server-rendered HTML as well.


With Field and Control Wrappers​

For full manual control, wrap in both Field and Control. The component detects both and renders only the raw radio group.

function example() {
  return (
    <Field
      horizontal
      label="RSVP"
      labelProps={{ id: 'rsvp-label-control', htmlFor: undefined }}
    >
      <Field.Body>
        <Field>
          <Control>
            <Radios aria-labelledby="rsvp-label-control">
              <Radio name="rsvp3"> Attend </Radio>
              <Radio name="rsvp3"> Decline </Radio>
              <Radio name="rsvp3"> Tentative </Radio>
            </Radios>
          </Control>
        </Field>
      </Field.Body>
    </Field>
  );
}

Inside a Control with no Field around it, Radios renders no Field of its own either, unless you give it label, message, horizontal or fieldClassName. Those need a Field, so with any of them it keeps its own Field inside the Control and warns in development. Wrap the Control in a Field, as above, and set the label, horizontal and class name on that Field instead.


Compound (dot-notation) usage​

Radio is also available as Radios.Radio, so a radio group can be composed from the single Radios import.

<Radios name="color" label="Favorite color">
  <Radios.Radio value="red">Red</Radios.Radio>
  <Radios.Radio value="green">Green</Radios.Radio>
</Radios>


Group State​

Radios can manage selection state for the entire group, matching the pattern used by MUI's RadioGroup, Radix's RadioGroup, and React Aria's RadioGroup. Three usage modes:

  1. Name-only — pass just name. Each child Radio manages its own checked state via defaultChecked or checked. Backwards compatible with existing usage.
  2. Controlled — pass value and onChange. The parent owns selection; each Radio derives checked from group.value === my.value.
  3. Uncontrolled — pass defaultValue (and optionally onChange). The group manages internal state; onChange fires on selection change.

Local props (checked, onChange) on individual Radio children always win over the group context — useful for opt-out scenarios.

Controlled​

function ControlledRadios() {
  const [color, setColor] = React.useState('red');
  return (
    <div>
      <Radios name="color" value={color} onChange={setColor}>
        <Radio value="red">Red</Radio>
        <Radio value="green">Green</Radio>
        <Radio value="blue">Blue</Radio>
      </Radios>
      <p style={{ marginTop: '1rem' }}>
        Selected: <strong>{color}</strong>
      </p>
    </div>
  );
}

Uncontrolled​

function UncontrolledRadios() {
  const [last, setLast] = React.useState('');
  return (
    <div>
      <Radios name="size" defaultValue="md" onChange={setLast}>
        <Radio value="sm">Small</Radio>
        <Radio value="md">Medium</Radio>
        <Radio value="lg">Large</Radio>
      </Radios>
      {last && (
        <p style={{ marginTop: '1rem' }}>
          Last selected: <strong>{last}</strong>
        </p>
      )}
    </div>
  );
}


Form Submission​

Radios is HTML-form-compatible. Pass a name prop on the group and every child Radio inherits it via React context (works at any nesting depth — including custom wrapper components, fragments, and conditionals). The selected value submits as name=value in FormData, like a native radio group.

PropDescription
nameForm field name shared by every child Radio. Children with their own name prop keep theirs (explicit > implicit).
function RadiosFormDemo() {
  const [submitted, setSubmitted] = React.useState('');
  return (
    <form
      onSubmit={e => {
        e.preventDefault();
        const fd = new FormData(e.currentTarget);
        setSubmitted(JSON.stringify(Array.from(fd.entries()), null, 2));
      }}
    >
      <Radios name="color">
        <Radio value="red" defaultChecked>
          Red
        </Radio>
        <Radio value="green">Green</Radio>
        <Radio value="blue">Blue</Radio>
      </Radios>
      <div style={{ marginTop: '1rem' }}>
        <button type="submit" className="button is-primary">
          Submit
        </button>
      </div>
      {submitted && <pre style={{ marginTop: '1rem' }}>{submitted}</pre>}
    </form>
  );
}

The same name propagation works through any wrapper of your own. Here a Plan component wraps a RadioCard, the radio drawn as a card that's picked wherever you click it:

function RadiosWrappedFormDemo() {
  const [submitted, setSubmitted] = React.useState('');
  const Plan = ({ value, price }: { value: string; price: string }) => (
    <RadioCard value={value} title={value} description={price} />
  );
  return (
    <form
      onSubmit={e => {
        e.preventDefault();
        const fd = new FormData(e.currentTarget);
        setSubmitted(JSON.stringify(Array.from(fd.entries()), null, 2));
      }}
    >
      <Radios name="plan" defaultValue="Pro">
        <Plan value="Basic" price="$9 a month" />
        <Plan value="Pro" price="$29 a month" />
        <Plan value="Enterprise" price="Talk to us" />
      </Radios>
      <Button type="submit" color="primary">
        Submit
      </Button>
      {submitted && <pre className="mt-4">{submitted}</pre>}
    </form>
  );
}


Accessibility​

  • By default the group is rendered as a <div class="radios" role="radiogroup"> containing labeled radio buttons.
  • The label prop names the whole group: the rendered <label> gets an id (yours via labelProps.id, or a generated one) and the group points at it with aria-labelledby.
  • Inside a labeled Field, that Field's label names the group the same way.
  • With as="fieldset", the group is a native <fieldset> with role="radiogroup", named by its <legend> where it renders its own Field, or by an outer Field's label through aria-labelledby inside one.
  • An aria-label or aria-labelledby you give the group wins over either label, whether that label sits on the Radios or on a Field around them.
  • Each child should be a Radio for proper labeling and accessibility.
  • Use the same name for all radios in a group to ensure mutual exclusivity.

  • Radio: Individual radio button.
  • Field: For labeled/grouped form fields.

Additional Resources​


Props​

PropTypeDefaultDescription
labelReact.ReactNode—Field label naming the whole group. Automatically associated via aria-labelledby on the role="radiogroup" wrapper — uses your labelProps.id when provided, otherwise a generated one. Dropped inside an outer Field, whose own label names the group instead through aria-labelledby when that Field generates a target id (not grouped/hasAddons, no explicit labelProps.htmlFor). An aria-label or aria-labelledby you set on the group wins over either label, which still renders but no longer names it. An aria-labelledby key counts even when undefined, since it is spread over the group's own.
labelPropsReact.LabelHTMLAttributes<HTMLLabelElement> & { [key: string]: unknown; }—Props for the label element. An explicit id here is used as the aria-labelledby target instead of a generated one; any htmlFor is ignored (a group label names the group, never a single control).
classNamestring—Additional CSS classes to apply.
namestring—Form field name shared by every Radio in the group (via context).
valuestring—Currently-selected value (controlled mode).
defaultValuestring—Initial selected value (uncontrolled mode).
onChange(value: string) => void—Fires when the selection changes.
childrenReact.ReactNode—Radio elements to render in the group.
as'div' | 'fieldset''div'The element the group renders as. With fieldset it's a native <fieldset> that keeps role="radiogroup". Where the group renders a Field of its own, that Field is the <fieldset> and the label is its <legend>, in the label column when horizontal, so the legend names the group with no ids to wire. Your other attributes then go on the <fieldset>, while className and the helper props stay on the box that lays out the radios. Inside an outer Field there's no legend: the group element itself is the <fieldset>, named by that Field's label as before. div, the default, renders the group as it always has.
labelSize'small' | 'normal' | 'medium' | 'large'—Size for the label (used in horizontal layouts).
horizontalbooleanfalseHorizontal field layout.
messageReact.ReactNode—Help/validation message below the input.
messageColor'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger'—Bulma color for the message.
fieldClassNamestring—Additional CSS classes for the Field wrapper.
...Bulma helper props—See Helper Props

Subcomponents:

  • Radios.Radio: The Radio component provides a Bulma-styled radio button input with flexible labels and helper classes.