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.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<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.jsonOr, 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
| Prop | Type | Default | Description |
|---|---|---|---|
options | RadioGroupOption[] | — | Option list (data-driven) |
value | string | — | Controlled selected value; when omitted the component manages it internally (uncontrolled) |
defaultValue | string | — | 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 |
stiffness | number | 500 | Spring stiffness (shared by the dot scale-up and the outline slide) |
damping | number | 30 | Spring damping (shared by the dot scale-up and the outline slide) |
name | string | — | Form field name; when provided, a hidden input is rendered so forms can submit the selected value |
disabled | boolean | false | Disable the whole group |
aria-label | string | — | Accessible name for the group (use when there is no visible heading) |
aria-labelledby | string | — | Id of a visible heading element that names the group |
className | string | — | Forwarded to the outermost container |
RadioGroupOption
| Field | Type | Default | Description |
|---|---|---|---|
value | string | — | Option value (must be unique within the group) |
label | ReactNode | — | Option label |
description | ReactNode | — | Supporting description, linked via aria-describedby |
disabled | boolean | false | Disable this option: not clickable, and arrow keys skip it |
meta | ReactNode | — | 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
layoutIdlets motion tween its position and size between cards; thelayoutIdis prefixed withuseId, 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(default150ms) and--wb-ease-out, which you can override globally or on a wrapping container. - When
nameis 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"witharia-orientation; each option is abuttonwithrole="radio"andaria-checked, with its label and description linked viaaria-labelledbyandaria-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-visibleoutline 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
Drawn Checkbox
A React checkbox whose check mark draws itself with a small squash-and-settle, morphing smoothly between the indeterminate dash and the check, with staggered select-all in groups.
Form Field
A form field wrapper React component that ties together label, hint, error and prefix/suffix addons; errors expand and collapse by height without layout jumps, addons share the input's focus ring, and a render prop wraps any control.