Monet
@etched/monet 0.0.1

Radio

Ported

Pick 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.

Kumo source
Retry policy
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

Retry policy

Applies to runs that fail on a bench error.

Employment
Rows per page
Label first
Would you bet on them?
Choose an answer.
Region (locked)
Reviewer (optional)
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.

Plan
Domain strength
Bench (circle first)
Report format
Choose a format.
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

Window
Window (disabled)
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.

Window (small)
View
Layout
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.

Source

Set by the sourcing tool.

Interview format
Window (read-only)
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.

Conviction

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

  • Tab enters the group at the checked radio. Arrow keys move to the next or previous radio, select it, and skip disabled radios.
  • Space selects the focused radio, for a group with nothing checked yet.
  • A radiogroup named by legend or RadioLegend, with radio items named by their label. An icon-only segment needs aria-label; the types require it.
  • The group's description and error describe it. An item's description describes that radio and is not part of its name.
  • error sets aria-invalid on every radio. required, readOnly and disabled set aria-required, aria-readonly and aria-disabled on the group.
  • Without legend, add a RadioLegend child or an aria-label. Do not put the group in a Field; it is a field of its own.

API reference

RadioGroup

PropTypeDefaultDescription
legendReactNode–Title of the group. Without it, add a legend child or an aria-label.
descriptionReactNode–Help text below the group. Hidden while an error shows.
errorFieldError–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.
controlFirstbooleantrue, or false for cardsDefault controlFirst of every item.
requiredboolean–false adds the optional marker after the legend. true makes a choice required in a form.
classNamestring–Class on the outer element, for layout.

RadioItem

PropTypeDefaultDescription
appearanceRadioItemAppearance–Overrides the group's default or card appearance. A segmented group ignores it.
controlFirstbooleanthe group's, true, or false for a cardCircle before the label. false puts the label first. A segmented group ignores it.
iconReactNode–For a segmented group: an icon element before the label, or alone in a square segment.
descriptionReactNode–Secondary text under the label. It describes the option; it is not part of its name. Not shown when segmented.
value *T–
classNamestring–Class on the outer element, for layout.
labelReactNode | undefined–The option's text, which names it.
aria-labelstring–

RadioLegend

PropTypeDefaultDescription
classNamestring–