Radio
PortedPick one value from a few options. Arrow keys move between them. readOnly shows the value without a way to change it. It is a field of its own; do not put it in a Field. Native attributes and ref go to the group.
import { RadioGroup, RadioItem } from "@etched/monet/radio";
export function Preview() {
return (
<RadioGroup legend="Retry policy" defaultValue="once">
<RadioItem value="never" label="Never" />
<RadioItem value="once" label="Once" />
<RadioItem value="always" label="Until it passes" />
</RadioGroup>
);
}Installation
import { RadioGroup, RadioItem, RadioLegend } from "@etched/monet/radio";Examples
Groups
Applies to runs that fail on a bench error.
import { useState } from "react";
import { RadioGroup, RadioItem } from "@etched/monet/radio";
export function Groups() {
const [size, setSize] = useState(25);
return (
<div className="grid">
<RadioGroup legend="Retry policy" description="Applies to runs that fail on a bench error." defaultValue="once">
<RadioItem value="never" label="Never" />
<RadioItem value="once" label="Once" />
<RadioItem value="always" label="Until it passes" />
<RadioItem value="custom" label="Custom (admins only)" disabled />
</RadioGroup>
<RadioGroup legend="Employment" orientation="horizontal" name="employment">
<RadioItem value="full-time" label="Full-time" />
<RadioItem value="internship" label="Internship" />
</RadioGroup>
<RadioGroup<number> legend="Rows per page" orientation="horizontal" value={size} onValueChange={setSize}>
{[10, 25, 50].map((n) => (
<RadioItem<number> key={n} value={n} label={String(n)} />
))}
</RadioGroup>
<RadioGroup legend="Label first" controlFirst={false} defaultValue="a">
<RadioItem value="a" label="Option A" />
<RadioItem value="b" label="Option B" />
</RadioGroup>
<RadioGroup legend="Would you bet on them?" required error="Choose an answer.">
<RadioItem value="yes" label="Yes" />
<RadioItem value="no" label="No" />
<RadioItem value="complicated" label="It’s complicated" />
</RadioGroup>
<RadioGroup legend="Region (locked)" disabled defaultValue="us">
<RadioItem value="us" label="United States" />
<RadioItem value="tw" label="Taiwan" />
</RadioGroup>
<RadioGroup legend="Reviewer" required={false}>
<RadioItem value="me" label="Me" />
<RadioItem value="team" label="Anyone on the team" />
</RadioGroup>
</div>
);
}Cards
The cards of a card group join into one card, with the circle at the end.
import { RadioGroup, RadioItem } from "@etched/monet/radio";
const percentiles = [
{ value: "top-1", label: "Top 1%" },
{ value: "top-5", label: "Top 5%" },
{ value: "top-10", label: "Top 10%" },
{ value: "top-25", label: "Top 25%" },
{ value: "below", label: "Below top 25%" },
{ value: "unsure", label: "Not sure" },
];
export function Cards() {
return (
<div className="grid">
<RadioGroup legend="Plan" appearance="card" defaultValue="standard">
<RadioItem value="standard" label="Standard" description="Runs on shared benches, results in a day." />
<RadioItem value="priority" label="Priority" description="Runs first on the next free bench." />
<RadioItem value="dedicated" label="Dedicated" description="Needs a reserved bench." disabled />
</RadioGroup>
<RadioGroup legend="Domain strength" appearance="card" orientation="horizontal" defaultValue="top-5">
{percentiles.map((p) => (
<RadioItem key={p.value} value={p.value} label={p.label} />
))}
</RadioGroup>
<RadioGroup legend="Bench (circle first)" appearance="card" controlFirst defaultValue="shared">
<RadioItem value="shared" label="Shared" description="Any free bench in the lab." />
<RadioItem value="reserved" label="Reserved" description="The bench booked for this run." />
</RadioGroup>
<RadioGroup legend="Report format" appearance="card" required error="Choose a format.">
<RadioItem value="summary" label="Summary" description="One page per run." />
<RadioItem value="full" label="Full" description="Every log line and chart." />
</RadioGroup>
</div>
);
}Segmented
import { RadioGroup, RadioItem } from "@etched/monet/radio";
export function Segmented() {
return (
<div className="grid">
<RadioGroup legend="Window" appearance="segmented" defaultValue="12h">
<RadioItem value="1h" label="1h" />
<RadioItem value="12h" label="12h" />
<RadioItem value="24h" label="24h" />
<RadioItem value="7d" label="7d" disabled />
</RadioGroup>
<RadioGroup legend="Window (disabled)" appearance="segmented" defaultValue="1h" disabled>
<RadioItem value="1h" label="1h" />
<RadioItem value="12h" label="12h" />
</RadioGroup>
</div>
);
}Small and icon segments
size="sm" is 28 px high, like small buttons. An icon sits before the label, or alone in a square segment; then aria-label names it.
import { KanbanIcon, ListBulletsIcon, RowsIcon, SquaresFourIcon, TableIcon } from "@phosphor-icons/react";
import { RadioGroup, RadioItem } from "@etched/monet/radio";
export function SmallAndIconSegments() {
return (
<div className="grid">
<RadioGroup legend="Window (small)" appearance="segmented" size="sm" defaultValue="24h">
<RadioItem value="1h" label="1h" />
<RadioItem value="24h" label="24h" />
<RadioItem value="7d" label="7d" />
</RadioGroup>
<RadioGroup legend="View" appearance="segmented" size="sm" defaultValue="table">
<RadioItem value="table" icon={<TableIcon />} aria-label="Table" />
<RadioItem value="board" icon={<KanbanIcon />} aria-label="Board" />
<RadioItem value="list" icon={<ListBulletsIcon />} aria-label="List" />
</RadioGroup>
<RadioGroup legend="Layout" appearance="segmented" defaultValue="rows">
<RadioItem value="rows" icon={<RowsIcon />} label="Rows" />
<RadioItem value="cards" icon={<SquaresFourIcon />} label="Cards" />
</RadioGroup>
</div>
);
}Read-only
readOnly on the group shows the value without a way to change it: no hover, the default cursor, and the read-only fill in empty circles and in a segmented row.
Set by the sourcing tool.
import { RadioGroup, RadioItem } from "@etched/monet/radio";
export function ReadOnly() {
return (
<div className="grid">
<RadioGroup legend="Source" description="Set by the sourcing tool." readOnly defaultValue="referral">
<RadioItem value="referral" label="Referral" />
<RadioItem value="inbound" label="Inbound" />
</RadioGroup>
<RadioGroup legend="Interview format" appearance="card" readOnly defaultValue="onsite">
<RadioItem value="onsite" label="Onsite" description="Set by the recruiter." />
<RadioItem value="remote" label="Remote" description="Video call." />
</RadioGroup>
<RadioGroup legend="Window (read-only)" appearance="segmented" readOnly defaultValue="24h">
<RadioItem value="1h" label="1h" />
<RadioItem value="24h" label="24h" />
<RadioItem value="7d" label="7d" />
</RadioGroup>
</div>
);
}A description that follows the value
The meaning of the chosen score shows under the group, and screen readers read it with the group. It works on touch, where a tooltip would not.
Additional backchannel or tribal knowledge about this person
import { useState } from "react";
import { RadioGroup, RadioItem } from "@etched/monet/radio";
const meanings = new Map([
["1", "Profile looks good, unsure"],
["2", "Profile looks extra good, unsure"],
["3", "Additional backchannel or tribal knowledge about this person"],
["4", "Certified superstar has been deeply referenced"],
]);
export function ADescriptionThatFollowsTheValue() {
const [score, setScore] = useState("3");
return (
<RadioGroup legend="Conviction" appearance="segmented" value={score} onValueChange={(value) => setScore(String(value))} description={meanings.get(score)}>
{[...meanings.keys()].map((value) => (
<RadioItem key={value} value={value} label={value} />
))}
</RadioGroup>
);
}Accessibility
Tabenters the group at the checked radio. Arrow keys move to the next or previous radio, select it, and skip disabled radios.Spaceselects the focused radio, for a group with nothing checked yet.- A
radiogroupnamed bylegendorRadioLegend, withradioitems named by theirlabel. An icon-only segment needsaria-label; the types require it. - The group's
descriptionanderrordescribe it. An item'sdescriptiondescribes that radio and is not part of its name. errorsetsaria-invalidon every radio.required,readOnlyanddisabledsetaria-required,aria-readonlyandaria-disabledon the group.- Without
legend, add aRadioLegendchild or anaria-label. Do not put the group in aField; it is a field of its own.
API reference
RadioGroup
| Prop | Type | Default | Description |
|---|---|---|---|
| legend | ReactNode | – | Title of the group. Without it, add a legend child or an aria-label. |
| description | ReactNode | – | Help text below the group. Hidden while an error shows. |
| error | FieldError | – | Error below the group. It marks every control in it invalid. |
| orientation | "vertical" | "horizontal" | "vertical" | |
| children * | ReactNode | – | RadioItem children, and optionally a RadioLegend. |
| appearance | "default" | "card" | "segmented" | "default" | Card items join into one card. |
| size | "sm" | "base" | "base" | Height of a segmented group: sm is 28 px, for toolbars and table headers; base is 36 px. Other appearances ignore it. |
| controlFirst | boolean | true, or false for cards | Default controlFirst of every item. |
| required | boolean | – | false adds the optional marker after the legend. true makes a choice required in a form. |
| className | string | – | Class on the outer element, for layout. |
RadioItem
| Prop | Type | Default | Description |
|---|---|---|---|
| appearance | RadioItemAppearance | – | Overrides the group's default or card appearance. A segmented group ignores it. |
| controlFirst | boolean | the group's, true, or false for a card | Circle before the label. false puts the label first. A segmented group ignores it. |
| icon | ReactNode | – | For a segmented group: an icon element before the label, or alone in a square segment. |
| description | ReactNode | – | Secondary text under the label. It describes the option; it is not part of its name. Not shown when segmented. |
| value * | T | – | |
| className | string | – | Class on the outer element, for layout. |
| label | ReactNode | undefined | – | The option's text, which names it. |
| aria-label | string | – |
RadioLegend
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | – |