Radio Group

Developer Experience (DX)

How developers use the Radio Group, with one consistent, typed API that works with your forms however you build them.

Next, let's look at how developers use the Radio Group. It has one API, with the same names as every other component, it's typed end to end, and it works with your forms however you build them.

import { useState } from "react";
import { Radio, RadioGroup } from "@/components/RadioGroup";

export function Delivery() {
  const [delivery, setDelivery] = useState("standard");

  return (
    <RadioGroup
      name="delivery"
      legend="Delivery"
      value={delivery}
      onChange={(event) => setDelivery(event.target.value)}
    >
      <Radio value="standard">Standard</Radio>
      <Radio value="express">Express</Radio>
    </RadioGroup>
  );
}

Consistent

Once you've learned the Input's API, you already know most of the Radio Group's. These names mean the same thing everywhere in Open Components:

NameKindWhat it means
sizePropxs, sm, md, lg or xl, with the same heights everywhere
disabledPropThe component can't be used, here the whole group or one radio
requiredPropIts form can't be sent until it has a value
namePropThe name its form sends its value under
valuePropThe value, which you bind the way your framework binds a form control's

We recommend size="md" as the default. The Radio Group also leaves a few props out, on purpose:

  • There's no variant or color. A radio has one checked look, from the primary colour, and its one other coloured state, invalid, comes from aria-invalid, so its colour and its state can't disagree.
  • There's no options prop. The radios are children, so each one can have its own label, its own disabled and its own attributes, and the code reads like the form it renders.
  • The Radio has no label prop either. Its content is its label, which also keeps it inside the <label> that checks it.

Type-safe

Props, events and slots are typed, so your editor can autocomplete them and your type checker (tsc, vue-tsc, svelte-check, ngc or astro check) catches mistakes like a group without a legend, which every group needs.

Radio Group propTypeDefaultDescription
namestringRequired. Every radio submits under it, and the legend's, hint's and error's IDs start with it
legendstringRequired. The question, which names the group
hintstringHelp that applies to every option, which describes the group
errorstringWhat's wrong and how to fix it. While it's set, the group is invalid
size"xs" | "sm" | "md" | "lg" | "xl""md"How big the radios are
orientation"vertical" | "horizontal""vertical"Stacks the radios, or sets them side by side, for two short options
disabledbooleanfalseDisables every radio, and leaves the group out of its form
requiredbooleanfalseMakes every radio required, so its form can't be sent without an answer
valuestringThe checked radio's value, when you bind it. Without it, the group keeps its own
Radio propTypeDefaultDescription
valuestringRequired. What the group's value is, and its form sends, while this radio is checked
disabledbooleanfalseTakes this one option away, while the others stay
checkedbooleanfalseStarts the group on this radio, by handing it its value when it has none yet, on the server too. Give it to one radio per group
EventPayloadDescription
changeEventFires when people check a radio, from its <input>, and bubbles up to the group

A binding to value listens to it for you. The browser doesn't fire change on the radio that gets unchecked, so listen on the group, or bind its value, rather than listening to each radio.

SlotDescription
Radio Group defaultThe radios, one Radio for each option
Radio defaultThe label, which names the radio and checks it when pressed

We also export RadioGroupProps and RadioProps, which come in handy for wrappers and data-driven forms:

import type { RadioGroupProps, RadioProps } from "@/components/RadioGroup";

const question: RadioGroupProps & { options: (RadioProps & { label: string })[] } = {
  name: "delivery",
  legend: "How would you like it delivered?",
  options: [
    { value: "standard", label: "Standard" },
    { value: "express", label: "Express" },
  ],
};

Composable

The Radio Group renders the <fieldset>, so everything you pass it that isn't a prop lands there, like a class, an id or a listener. A Radio renders two elements, so it splits what you pass between them, as the Input does. Classes and styles go on its root, the <label>, so a class can set its variables. Everything else lands on the <input>, including ARIA attributes and event listeners, and a ref to the Radio reaches the <input> too, since that's what focus() needs.

Anything can sit between the radios, like the Input beside "Other" under Layout, as long as every Radio stays inside its group, which is where it gets its name, its size and its value from.

Forms

The radios are real form controls, so the Radio Group works with your forms however you build them. You can bind its value, as above, or leave it to the browser and read it from the form when it's sent, by its name. That also means the form works before your JavaScript loads.

import type { FormEvent } from "react";

export function ChoosePlan() {
  function choose(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    api.choosePlan(data.get("plan"));
  }

  return (
    <form onSubmit={choose}>
      <RadioGroup name="plan" legend="Which plan would you like?" required>
        <Radio value="personal">Personal</Radio>
        <Radio value="team">Team</Radio>
      </RadioGroup>
      <Button type="submit" color="primary">Continue</Button>
    </form>
  );
}

Form libraries work the same way. They register the radios by their shared name, through their refs or their attributes and listeners, which is why each Radio puts them on its <input>.

With other components

  • An Input beside a radio, like "Other", takes the same size, so the two line up.
  • A Button sends the form the group is in. Checking a radio never does, as under Don't act on change.
  • The Field, on our Roadmap, labels a single control, like an Input. A group of radios is labelled by its legend instead, which is why the Radio Group renders its own legend, hint and error message, and why they look the way the Field's will.
  • The Checkbox, the Checkbox Group and the Switch use the same size names, and show they're invalid with aria-invalid and an error message, as the Radio Group does. The Select, on our Roadmap, will too. You'll find when to use each under FAQ.

Controllable

The Radio Group doesn't keep any state you can't reach. The value is yours to bind, or to leave to the browser, and every other state is a prop that you control:

StateHow you control it
Valuevalue, bound to your state, or left to the browser, and started by a Radio's checked
Disableddisabled on the group, or on one Radio
Requiredrequired on the group
Invaliderror, set while there's something to fix (see Invalid)
FocusA ref to a Radio's <input>, and focus()

DX Checklist

RuleLevelScopeRequirementBasisCheck
radio-group/shared-vocabularyMustComponentIt uses the shared names: size, disabled, required, name and valueOpen ComponentsReview
radio-group/typedMustComponentProps, events and slots are typed, with unions rather than string, and name, legend and each radio's value are requiredOpen ComponentsType check
radio-group/control-attributesMustComponentThe group's attributes land on its <fieldset>, and a radio's attributes, listeners and refs on its <input>, while its classes and styles land on its rootOpen ComponentsUnit test
radio-group/form-controlMustComponentIts form sends the checked radio's value under the group's nameHTMLUnit test
radio-group/controllableShouldComponentIts value can be bound both ways or left to the browser, and every other state is a propOpen ComponentsUnit test

FAQ

Copyright © 2026