Inputs

Radio Group

A React radio group: the selected dot springs into place, and in the card variant the selection outline slides between options, including on arrow-key changes. Supports controlled/uncontrolled use, vertical or horizontal layout, and form submission.

A data-driven radio group: pass an options array, where each option can carry a label, a description and a disabled state, and the card variant can also show extra info such as a price in its top-right corner. In the dot variant the inner dot springs in when selected; in the card variant the selection outline is its own element that slides between cards with layoutId — the same for mouse clicks and arrow-key changes. Keyboard support is a hand-written roving tabindex that behaves like native radios, with no headless library involved.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add radio-group.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

500
30
Active edited usageDefault props
<RadioGroup/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add https://webberui.com/r/radio-group.en.json

Or, once registries are configured in components.json, install it as @webberui/radio-group.

Usage

import { RadioGroup } from "@/components/ui/radio-group";

<RadioGroup
  variant="card"
  aria-label="Plan"
  name="plan"
  defaultValue="pro"
  options={[
    { value: "starter", label: "Starter", description: "3 projects", meta: "US$0/mo" },
    { value: "pro", label: "Pro", description: "Unlimited projects and version history", meta: "US$9/mo" },
    { value: "team", label: "Team", description: "Member management and single sign-on", meta: "US$29/mo" },
  ]}
  onValueChange={(value) => console.log(value)}
/>

Props

PropTypeDefaultDescription
optionsRadioGroupOption[]—Option list (data-driven)
valuestring—Controlled selected value; when omitted the component manages it internally (uncontrolled)
defaultValuestring—Initial selected value in uncontrolled mode
onValueChange(value: string) => void—Callback when the selection changes; the argument is the newly selected value
orientation"vertical" | "horizontal""vertical"Layout direction; also sets aria-orientation
variant"dot" | "card""dot"Visual variant
stiffnessnumber500Spring stiffness (shared by the dot scale-up and the outline slide)
dampingnumber30Spring damping (shared by the dot scale-up and the outline slide)
namestring—Form field name; when provided, a hidden input is rendered so forms can submit the selected value
disabledbooleanfalseDisable the whole group
aria-labelstring—Accessible name for the group (use when there is no visible heading)
aria-labelledbystring—Id of a visible heading element that names the group
classNamestring—Forwarded to the outermost container

RadioGroupOption

FieldTypeDefaultDescription
valuestring—Option value (must be unique within the group)
labelReactNode—Option label
descriptionReactNode—Supporting description, linked via aria-describedby
disabledbooleanfalseDisable this option: not clickable, and arrow keys skip it
metaReactNode—Extra info in the top-right corner of the card variant (e.g. a price); not shown in the dot variant

How it works

  • The selection outline is rendered only inside the selected card, and layoutId lets motion tween its position and size between cards; the layoutId is prefixed with useId, so several groups on one page never interfere.
  • The inner dot of a deselected option does not spring — it collapses in 150ms so two dots never wobble at once.
  • Border and text colour transitions use --wb-radio-group-duration (default 150ms) and --wb-ease-out, which you can override globally or on a wrapping container.
  • When name is provided a single hidden input is rendered holding the selected value; with nothing selected it submits an empty string.

Accessibility

  • The root is role="radiogroup" with aria-orientation; each option is a button with role="radio" and aria-checked, with its label and description linked via aria-labelledby and aria-describedby
  • Roving tabindex: the group has a single Tab stop (the selected option, or the first enabled option when nothing is selected)
  • ↑/← and ↓/→ move focus and select at the same time, skipping disabled options and wrapping around; Space selects the focused option
  • Keyboard focus shows a focus-visible outline that is separate from the selection outline, so the two are never confused
  • When the user's system has "reduce motion" enabled, the inner dot no longer springs in and the outline no longer slides; selection switches directly with a 100ms fade