bx

Radio Group

A set of checkable buttons where no more than one can be checked at a time.

tsx
<RadioGroup name="density">
  <RadioGroupItem value="comfortable" id="density-comfortable" label="Comfortable">Comfortable</RadioGroupItem>
  <RadioGroupItem value="compact" id="density-compact" label="Compact" checked>Compact</RadioGroupItem>
  <RadioGroupItem value="default" id="density-default" label="Default">Default</RadioGroupItem>
</RadioGroup>

RadioGroup is an exclusive choice among 2+ options (payment method, tier, orientation). It is semantically different from a Select: use Radio when all the options are visible at once.

Each RadioGroupItem renders the round button and its label: pass the text as children (or as the label prop). The initial selection is set with checked on one item; the island then keeps a single item checked per group. With name, the group renders a hidden <input> that holds the value of the checked item, so the choice posts with the surrounding form.

Installation

Terminal
$ bext ui add radio-group

Usage

tsx
import { RadioGroup, RadioGroupItem } from "@bext-stack/ui/primitives"

<RadioGroup name="plan" ariaLabel="Plan">
  <RadioGroupItem value="free" label="Free">Free</RadioGroupItem>
  <RadioGroupItem value="pro" label="Pro" checked>Pro</RadioGroupItem>
</RadioGroup>

Anatomy

<RadioGroup name="…" ariaLabel="…">               // role="radiogroup"
  ├── <RadioGroupItem value="a">Label A</RadioGroupItem>
  ├── <RadioGroupItem value="b" checked>Label B</RadioGroupItem>
  └── <RadioGroupItem value="c">Label C</RadioGroupItem>

API Reference

RadioGroup

Prop Type Default Description
name string — Form field name. Renders a hidden input that the island keeps equal to the value of the checked item (nothing is posted while no item is checked). Also written to data-name.
ariaLabel string — Accessible name of the group (aria-label).
ariaLabelledby string — id of a visible element that names the group (aria-labelledby).
id string — id of the group.
children ReactNode — The RadioGroupItems.
className string — Extra classes on the group (grid gap-2 by default).

RadioGroupItem

Prop Type Default Description
value string — Written to data-value on the radio button. Also the accessible name when no label is given.
checked boolean false Initial state. Set it on one item only.
disabled boolean false Disables the item: it is skipped by the arrow keys and cannot be checked.
children ReactNode — Visible label, rendered in a <label> linked to the button.
label string — Label text when no children are passed. Also used as the aria-label of the button.
id string "radio-" + value id of the button, referenced by the label. Set it when the same value appears twice on a page.

Data Attributes

Attributes you can target in custom CSS to override the style for each state.

Attribute On Values Description
data-ui="radio-group" RadioGroup — Scope of the exclusive selection: checking one item unchecks the others in the same group.
data-ui="radio" item button data-value="…" Clicking checks the item.
aria-checked item button "true" | "false" Current state, set on render and updated by the island. The most reliable hook for styling.
data-state item button "checked" | "unchecked" Same state, set on render and updated by the island.
data-ui="radio-input" hidden input — Rendered when name is set. Its value follows the checked item and it emits a change event.

Keyboard

Key Action
Tab / Shift+Tab Moves focus into the group, onto the checked item (the first one when none is checked), then out of it.
Arrow Down / Arrow Right Moves to the next item and checks it. Wraps to the first.
Arrow Up / Arrow Left Moves to the previous item and checks it. Wraps to the last.
Space Checks the focused item.

Accessibility

RadioGroup sets role="radiogroup". Name it with ariaLabel, or with ariaLabelledby pointing at a visible heading or label. Each RadioGroupItem renders a <button role="radio"> with aria-checked and an aria-label (the label prop, otherwise the value), plus a <label for> for the visible text. When you pass the visible text as children, also pass label if the value is not a readable name.

The island keeps a roving tabindex: the group is a single tab stop, and the arrow keys move between items and check the item they land on. Disabled items are skipped.

Try it live
Open in play

Edit this component live in the bext playground. The PRISM + signals version is the bext-native idiom: fine-grained reactivity, no virtual DOM.

src/app/page.tsx 2 files · signals · runs in your browser
Pure PRISM + real bext signals: fine-grained, no re-render.