PlRadioGroup
A set of options where exactly one is chosen. The set takes a single tab stop and the arrow keys move within it.
import { PlRadio, PlRadioGroup } from 'plass-ui';
<PlRadioGroup label="Plan" defaultValue="team">
<PlRadio value="starter" label="Starter" />
<PlRadio value="team" label="Team" />
</PlRadioGroup>;import 'package:plass_ui/plass_ui.dart';
PlRadioGroup<String>(
label: const Text('Plan'),
value: plan,
onChanged: (String next) => setState(() => plan = next),
options: const <PlRadioOption<String>>[
PlRadioOption<String>(value: 'starter', label: Text('Starter')),
PlRadioOption<String>(value: 'team', label: Text('Team')),
],
);Props
| Prop | Type | Default | Description |
|---|---|---|---|
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | The size of every dot and the type scale beside it. Set once on the group and inherited by all of them |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | The gradient a chosen dot fills with |
| orientationshared | 'horizontal' | 'vertical' | 'vertical' | Which way the options stack. Vertical by default — a row becomes unreadable the moment one label is long |
| label | ReactNode | — | The question the options answer. Rendered as the group's label |
| description | ReactNode | — | Helper text under the label |
| error | ReactNode | — | Error message below the options. Its presence also turns the group invalid |
| invalid | boolean | — | Forces the invalid state without a message. Defaults to whether error has content |
| value | unknown | — | The chosen option value. Use with onValueChange for a controlled group |
| defaultValue | unknown | — | The initially chosen value |
| onValueChange | (value: unknown, details) => void | — | Called with the new value |
| readOnly | boolean | false | The choice is shown but cannot be changed. Every option inherits it |
| disabled | boolean | false | Every option stops answering |
| name · required | string · boolean | — | For a native form submission. Passed straight to Base UI |
| classNames | { label?, control?, description?, error?: string } | — | Classes on the parts a className does not reach. control is the part a reader acts on |
| Prop | Type | Default | Description |
|---|---|---|---|
| options * | List<PlRadioOption<T>> | — | The options, as a list of descriptions rather than children — the group owns the roving focus and the arrow keys, so it has to know which is chosen and what comes next |
| value * | T? | — | Which option is chosen, or null for none |
| onChanged | ValueChanged<T>? | — | Called with the option that was chosen. Leaving it null disables the group |
| sizeshared | PlassSize | PlassSize.md | The size of every dot and the type scale beside it. Set once on the group and inherited by all of them |
| colorshared | PlassColor | PlassColor.primary | The gradient a chosen dot fills with |
| orientationshared | PlassOrientation | PlassOrientation.vertical | Which way the options stack. Vertical by default — a row becomes unreadable the moment one label is long |
| label | Widget? | — | The question the options answer. Rendered as the group's label |
| description | Widget? | — | Helper text under the label |
| error | Widget? | — | Error message below the options. Its presence also turns the group invalid |
| invalid | bool? | — | Forces the invalid state without a message. Defaults to whether error has content |
| readOnly | bool | false | The choice is shown but cannot be changed. Every option inherits it |
| disabled | bool | false | Every option stops answering |
Every other prop on Base UI's RadioGroup passes straight through. className and style land on the field wrapper; render is not offered.
classNames reaches the four parts inside that wrapper: label, control (the run of radios) description and error.
The group is generic in the option's type (PlRadioGroup<String>, PlRadioGroup<Plan>), so value and onChanged are typed rather than dynamic, and an option that does not belong to the set will not compile.
It is controlled, like every other control in the package.
PlRadio
| Prop | Type | Default | Description |
|---|---|---|---|
| value * | unknown | — | The group's value when this option is the chosen one |
| label | ReactNode | — | The text beside the dot. Wired to it by Base UI's Field, so pressing it chooses the option |
| description | ReactNode | — | Helper text under the label |
| disabled | boolean | false | This option cannot be chosen; the rest keep working |
| readOnly | boolean | — | Overrides the group's readOnly for this option alone |
PlRadio is not in the Flutter package yet.
PlRadioOption
PlRadioOption is not in the React package yet.
| Prop | Type | Default | Description |
|---|---|---|---|
| value * | T | — | The group's value when this option is the chosen one |
| label | Widget? | — | The text beside the dot. Wired to it by Base UI's Field, so pressing it chooses the option |
| description | Widget? | — | Helper text under the label |
| disabled | bool | false | This option cannot be chosen; the rest keep working |
size and color are read from the PlRadioGroup around the option, not set on it: a radio button says nothing on its own, so how it looks belongs to the set. Passing them per option would be four chances to get one of them wrong.
An option is a PlRadioOption, a description rather than a widget, and here the reason is sharper than it is on a breadcrumb: the group owns the roving focus and the arrow keys, so it has to know which option is chosen, which are available, and what comes after each one. None of that can be asked of a Widget.
It carries no size and no color either, and could not: a radio button says nothing on its own, so how it looks belongs to the set.
The dot grows out of the middle of its ring rather than switching on with the fill, and shrinks back out when another option in the set takes the value. It is the box that grows and not a transform: the ring centres a fixed-size child, so both ends of the change are laid out about the same point and nothing around the option moves. See motion.
What the shared axes (size color orientation) mean across the library is in prop conventions.
Examples
orientation
Vertical by default. A column of options is scannable at any length; a row silently becomes unreadable the moment one label is longer than expected.
color
Chosen, the dot fills with the family's gradient and the inner disc is the family's own on-solid ink. The dot is round, and one of only two round things in the library: roundness is exactly what tells a reader "one of these" rather than "any of these", and it is the one convention old enough that breaking it would cost more than it bought.
size
Set on the group and inherited by every option, so a set cannot end up with two dot sizes in it.
Every step's inner disc has the same parity as the ring's content box (12/6, 14/6, 16/8, 18/8, 22/10), so the margin round it is a whole number of pixels. A 7px disc inside an 18px ring with a 1px edge sits 4.5px from each side, and a circle antialiased at half coverage on all four sides reads as though it has drifted up and to the left. The line box the dot and its label share is a whole number for the same reason. The ratio wanders between 38% and 44% as a result.
readOnly · disabled · error
disabled on the group stops every option; on one PlRadio it stops only that one, and the option stays in the list, an option that vanishes when it cannot be chosen is an option the reader will look for.
error on the group also turns it invalid, which re-points the whole colour family at danger.
Controlled
Pass value with onValueChange. The value is whatever a PlRadio was given. Usually a string, but Base UI compares by identity, so anything works as long as it is stable between renders.
There is only the controlled form: value with onChanged. Options are compared with ==, so a value type with a sensible equality (a String, an enum, anything @immutable) works without being kept identical between builds.
Accessibility
- Base UI renders a
role="radiogroup"holding real radios, keepsaria-checkedin step, and owns the roving tab index. The set takes one tab stop and ↑ ↓ ← → move within it. That is the whole reason a radio group is a component rather than a<div>full of inputs. - The group's
label,descriptionanderrorare wired to it by Base UI's Field, and so is each option's own label, pressing a label chooses its option. - Each dot is centred on its label's first line, so it stays put when a label wraps.
- A chosen dot is a filled disc, not a colour change alone: the shape carries the state for a reader who cannot see the fill.
- With
name, Base UI renders the hidden input that makes the choice part of a native form submission.
- Each option is announced as one of a mutually exclusive set, checked or not.
- The set takes one focus stop: exactly one option is in the tab order and the rest are wrapped in an
ExcludeFocus, which is the roving tab index in one widget. ↑ ↓ ← → move the choice, wrapping at both ends and skipping an option that cannot be chosen. - Wrapping is what an arrow key does in a radio group and what it does not do in a list: the set is a ring of alternatives with no beginning.
- Pressing a label chooses its option: the whole row is the target.
- Each dot is centred on its label's first line, so it stays put when a label wraps.
- A chosen dot is a filled disc, not a colour change alone: the shape carries the state for a reader who cannot see the fill.
Differences from the React build
| React | Flutter | Why |
|---|---|---|
<PlRadio> children | options, as descriptions | The group owns the roving focus and the arrow keys, so it has to know which option is chosen and what comes after it. A Widget is opaque. |
defaultValue / onValueChange | value / onChanged | Flutter's own controls are controlled, and its name for the callback. |
a value of unknown, compared by identity | a generic T, compared with == | Dart has generics, so the type is checked rather than hoped for, and a value with sensible equality does not have to stay identical between builds. |
name, and a hidden input | — | There is no native form submission to be part of. |
className, style | — | There is no class list and no style attribute to pass through. |