Skip to contentVibraUI
Inputs & filters

Radio group

One choice from a short list of options, each a round mark on the card plane.

Vibra puts each mark on the card plane with its --input boundary in both modes, gives it the kit's solid focus outline, and fades the dot in over --duration-fast instead of letting it pop; the chosen mark fills in the accent through --primary. A disabled mark and a Label beside it dim through Base UI's data-disabled — the mark is a span, which is never :disabled. Name the group from a legend: a fieldset around it and aria-labelledby on the RadioGroup.

Install

npx shadcn@latest add @vibra/radio-group

Needs the @vibra registry in your components.json — set it up once.

Examples

Side by side

A few short options in one row that wraps on a phone; the arrow keys still move the choice.

Disabled option, with a reason

An expired card can't be chosen and says why under it; the arrow keys pass over it, and the reason keeps its contrast.

Required, with an error

No safe default, so nothing is chosen. Pressing on without an answer marks the group invalid once and moves the focus into it.

With an "other" field

"Something else" opens its own labelled field under it, as the next Tab stop, without taking the focus.

A filter with counts

A sidebar filter that applies at once: each option carries its count, the chosen row is a tint, and a status line says what shows.

Drives a summary

The choice changes a figure elsewhere, so the figure is a status line, read out when it changes.

Plan cards

Plans side by side from md up. The title names each radio, the price and line describe it, and a badge marks the current plan.

Tiles with icons

Square tiles for a choice easier seen than read: the icon draws it, the word names it, the radio sits in the corner.

A rating scale

Five figures in a row with the ends anchored in words; each radio's name carries its meaning, "4, good".

A matrix

One radio group per row, named by the row, each radio named by its column head. Arrows move in a row; Tab moves between rows.

Right to left

The demo in Arabic: each mark at the right of its label, the chosen dot centred from the middle.

Props

PropTypeDefaultDescription
value / defaultValueunknown—The chosen option's value, controlled or not; null chooses nothing.
onValueChange(value: unknown) => void—Called with the newly chosen value.
requiredbooleanfalseAn answer is needed before the form submits. Say "required" in the legend too.
RadioGroupItem.disabledbooleanfalseTakes one option out: skipped by the arrow keys and dimmed, with a Label beside it.

Dependencies

Source

components/ui/radio-group.tsx
"use client"

import { Radio as RadioPrimitive } from "@base-ui/react/radio"
import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group"
import { cn } from "@/lib/utils"

function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) {
  return (
    <RadioGroupPrimitive
      data-slot="radio-group"
      className={cn("grid w-full gap-2", className)}
      {...props}
    />
  )
}

function RadioGroupItem({ className, ...props }: RadioPrimitive.Root.Props) {
  return (
    // Base UI renders a span, which is never :disabled — the disabled look
    // answers data-disabled.
    <RadioPrimitive.Root
      data-slot="radio-group-item"
      className={cn(
        "group/radio-group-item peer relative flex aspect-square size-4 shrink-0 rounded-full border border-input bg-card transition-[background-color,border-color] duration-(--duration-fast) ease-(--ease-standard) focus-ring group-has-[:focus-visible]/field-label:not-data-checked:border-input after:absolute after:-inset-x-3 after:-inset-y-2 data-disabled:cursor-not-allowed data-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:aria-checked:border-primary data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground group-has-[:focus-visible]/field-label:data-checked:border-primary",
        className
      )}
      {...props}
    >
      <RadioPrimitive.Indicator
        data-slot="radio-group-indicator"
        className="flex size-4 items-center justify-center transition-opacity duration-(--duration-fast) ease-(--ease-standard) data-unchecked:opacity-0"
      >
        <span className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary-foreground" />
      </RadioPrimitive.Indicator>
    </RadioPrimitive.Root>
  )
}

export { RadioGroup, RadioGroupItem }