Skip to content
cennge-ui
GitHubv0.3.1, changelog

Radio

A group of round buttons where choosing one clears the others.

Version bump
From cennge-ui 0.3.1
Publish under

npm publish --tag latest # cennge-ui@0.4.0

  1. Tab enters a group on its chosen radio; the arrow keys move and choose, round from the last to the first.
  2. A click on a label or its description chooses that radio.
  3. Version bump is vertical and answers ↑ and ↓; Publish under is horizontal and answers ← and →.
Live: the component itself, not a picture of it.

Field marks

Import
import { RadioGroup, Radio } from "cennge-ui";
Size
2.7 KB gzip for this import, its dependencies aside
Pattern
WAI-ARIA radio group (Radix RadioGroup)
Keyboard
Arrow up Arrow down Arrow left Arrow right Space
State
Controlled with value and onValueChange on RadioGroup, or uncontrolled with defaultValue
SSR
Rendered on the server in the Next.js check; hydrates without a warning
Tab stops
One per group: the chosen radio, or the first one before a choice
Orientation
Vertical by default; "horizontal" wraps onto more lines when narrow
Tests
Unit tests; end-to-end in Chromium, Firefox and WebKit, on desktop and phone viewports: form

When to use#

Use radios when there are a few options, only one can be chosen, and people decide better with all of them in view: a visibility, a version bump, a plan. Each option can carry a line of description that stays in view, where a Select shows it only while its list is open.

Similar#

  • Select keeps the options in a list until it is opened; better when there are many of them or space is short.
  • ToggleGroup is a row of buttons for switching a view; it has no name and sends nothing with a form.
  • Checkbox allows any number of choices at once.

Descriptions and a disabled option#

description sits under a radio's label, inside the same <label>, so a click on it chooses the radio too. A disabled radio stays visible and the arrow keys skip it.

Visibility
descriptions.tsx
import { Radio, RadioGroup } from "cennge-ui";

export default function VisibilityPicker() {
  return (
    <RadioGroup label="Visibility" defaultValue="public">
      <Radio value="public" description="Anyone on the internet can see this repository">
        Public
      </Radio>
      <Radio value="private" description="Only you and the people you invite">
        Private
      </Radio>
      <Radio value="internal" description="Needs an organization" isDisabled>
        Internal
      </Radio>
    </RadioGroup>
  );
}

Horizontal#

orientation="horizontal" lays the radios out in a row that wraps when it runs out of room, and the left and right arrows move between them.

Package manager
pnpm add cennge-ui
horizontal.tsx
import { useState } from "react";
import { Radio, RadioGroup } from "cennge-ui";

const commands: Record<string, string> = {
  pnpm: "pnpm add cennge-ui",
  npm: "npm install cennge-ui",
  yarn: "yarn add cennge-ui",
  bun: "bun add cennge-ui",
};

export default function PackageManager() {
  const [manager, setManager] = useState("pnpm");

  return (
    <div className="flex flex-col gap-4">
      <RadioGroup label="Package manager" orientation="horizontal" value={manager} onValueChange={setManager}>
        {Object.keys(commands).map((name) => (
          <Radio key={name} value={name}>
            {name}
          </Radio>
        ))}
      </RadioGroup>
      <code className="w-fit rounded-medium bg-surface-3 px-3 py-2 font-code text-[13px]">{commands[manager]}</code>
    </div>
  );
}

Required, in a form#

With a name, the chosen value goes into the form's data. Inside a Form, isRequired stops a submit with nothing chosen: the label and the circles turn red, the browser's message shows under the group, and focus moves to the first radio.

Changelog section*

Nothing submitted yet

in-a-form.tsx
import { useState } from "react";
import { Button, Form, Radio, RadioGroup } from "cennge-ui";

export default function ChangesetForm() {
  const [sent, setSent] = useState<string | null>(null);

  return (
    <Form
      onSubmit={(event) => {
        event.preventDefault();
        setSent(String(new FormData(event.currentTarget).get("section")));
      }}
      className="flex flex-col gap-4"
    >
      <RadioGroup name="section" label="Changelog section" orientation="horizontal" isRequired>
        <Radio value="added">Added</Radio>
        <Radio value="changed">Changed</Radio>
        <Radio value="fixed">Fixed</Radio>
      </RadioGroup>
      <Button type="submit" color="primary" className="self-start">
        Add changeset
      </Button>
      <p aria-live="polite" className="text-sm text-muted">
        {sent === null ? "Nothing submitted yet" : `Submitted section=${sent}`}
      </p>
    </Form>
  );
}

Accessibility#

The group is a radiogroup named by its label, with the description or error linked by aria-describedby and aria-invalid while it has an error. Each radio is named by its own <label>. Moving with the arrow keys chooses as it goes, as the WAI-ARIA pattern says; Space chooses the radio in focus, and Enter does nothing.

Props#

RadioGroup#

PropTypeDefault
label
React.ReactNode
–
description
React.ReactNode
–
errorMessage
React.ReactNode
–
value
string
–
defaultValue
string
–
onValueChange
(value: string) => void
–
orientation
"horizontal""vertical"
"vertical"
color
"default""primary""secondary""success""warning""danger""info"
"primary"
size
"sm""md""lg"
"md"
isInvalid
boolean
–
isRequired
boolean
–
isDisabled
boolean
–
name
string
–
className
string
–
children
React.ReactNode
–

Radio#

PropTypeDefault
valuerequired
string
–
description
React.ReactNode
–
isDisabled
boolean
–

Also takes every attribute of <button>, except value, color, which mean something else here.