Developer Experience (DX)
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:
| Name | Kind | What it means |
|---|---|---|
size | Prop | xs, sm, md, lg or xl, with the same heights everywhere |
disabled | Prop | The component can't be used, here the whole group or one radio |
required | Prop | Its form can't be sent until it has a value |
name | Prop | The name its form sends its value under |
value | Prop | The 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
variantorcolor. A radio has one checked look, from the primary colour, and its one other coloured state, invalid, comes fromaria-invalid, so its colour and its state can't disagree. - There's no
optionsprop. The radios are children, so each one can have its own label, its owndisabledand its own attributes, and the code reads like the form it renders. - The Radio has no
labelprop 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 prop | Type | Default | Description |
|---|---|---|---|
name | string | Required. Every radio submits under it, and the legend's, hint's and error's IDs start with it | |
legend | string | Required. The question, which names the group | |
hint | string | Help that applies to every option, which describes the group | |
error | string | What'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 |
disabled | boolean | false | Disables every radio, and leaves the group out of its form |
required | boolean | false | Makes every radio required, so its form can't be sent without an answer |
value | string | The checked radio's value, when you bind it. Without it, the group keeps its own |
| Radio prop | Type | Default | Description |
|---|---|---|---|
value | string | Required. What the group's value is, and its form sends, while this radio is checked | |
disabled | boolean | false | Takes this one option away, while the others stay |
checked | boolean | false | Starts 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 |
| Event | Payload | Description |
|---|---|---|
change | Event | Fires 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.
| Slot | Description |
|---|---|
Radio Group default | The radios, one Radio for each option |
Radio default | The 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-invalidand 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:
| State | How you control it |
|---|---|
| Value | value, bound to your state, or left to the browser, and started by a Radio's checked |
| Disabled | disabled on the group, or on one Radio |
| Required | required on the group |
| Invalid | error, set while there's something to fix (see Invalid) |
| Focus | A ref to a Radio's <input>, and focus() |
DX Checklist
| Rule | Level | Scope | Requirement | Basis | Check |
|---|---|---|---|---|---|
radio-group/shared-vocabulary | Must | Component | It uses the shared names: size, disabled, required, name and value | Open Components | Review |
radio-group/typed | Must | Component | Props, events and slots are typed, with unions rather than string, and name, legend and each radio's value are required | Open Components | Type check |
radio-group/control-attributes | Must | Component | The 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 root | Open Components | Unit test |
radio-group/form-control | Must | Component | Its form sends the checked radio's value under the group's name | HTML | Unit test |
radio-group/controllable | Should | Component | Its value can be bound both ways or left to the browser, and every other state is a prop | Open Components | Unit test |
FAQ
<fieldset> takes its name from the <legend> inside it, so the legend can't live anywhere else. The same goes for its hint and error message, which describe the whole group rather than one radio. That's also why the Field, on our Roadmap, won't wrap a Radio Group the way it'll wrap an Input.<fieldset>, so even before the page hydrates, people can check them with a click or the arrow keys, and their form sends the answer. Only the parts that need JavaScript have to wait for it, which are your bound value and your validation.