Radio Group

A set of checkable buttons, known as radio buttons, where no more than one of the buttons can be checked at a time.

"use client"

import * as React from "react"
import { cn } from "cn"
import { RadioGroup as RadioGroupPrimitive } from "@animate-ui/react/primitives/radix-ui"
import { Label } from "@/components/ui/label"

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

function RadioGroupItem({
  className,
  ...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
  return (
    <RadioGroupPrimitive.Item
      data-slot="radio-group-item"
      className={cn(
        "border-input dark:bg-input/30 data-checked:text-primary-foreground aria-invalid:border-destructive focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 dark:aria-invalid:border-destructive/50 flex size-4 rounded-full focus-visible:ring-3 aria-invalid:ring-3 group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:not-data-checked:border-input group-has-[:focus-visible]/field-label:data-checked:border-primary group/radio-group-item peer relative aspect-square shrink-0 border outline-none after:absolute after:-inset-x-3 after:-inset-y-2 before:absolute before:-inset-px before:rounded-[inherit] before:bg-primary before:opacity-0 before:scale-70 before:transition-[scale,opacity] before:duration-300 motion-reduce:before:scale-100 data-checked:before:opacity-100 data-checked:before:scale-100 disabled:cursor-not-allowed disabled:opacity-50",
        className
      )}
      {...props}
    >
      <RadioGroupPrimitive.Indicator
        data-slot="radio-group-indicator"
        className="flex size-4 items-center justify-center"
      >
        <span className="bg-primary-foreground absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full" />
      </RadioGroupPrimitive.Indicator>
    </RadioGroupPrimitive.Item>
  )
}

export { RadioGroup, RadioGroupItem }

export default function RadioGroupDemo() {
  return (
    <RadioGroup defaultValue="comfortable" className="w-fit">
      <div className="flex items-center gap-3">
        <RadioGroupItem value="default" id="r1" />
        <Label htmlFor="r1">Default</Label>
      </div>
      <div className="flex items-center gap-3">
        <RadioGroupItem value="comfortable" id="r2" />
        <Label htmlFor="r2">Comfortable</Label>
      </div>
      <div className="flex items-center gap-3">
        <RadioGroupItem value="compact" id="r3" />
        <Label htmlFor="r3">Compact</Label>
      </div>
    </RadioGroup>
  )
}

Installation

Install the packages

npm install @animate-ui/react motion
pnpm add @animate-ui/react motion
yarn add @animate-ui/react motion
bun add @animate-ui/react motion

Add the shadcn/ui component

npx shadcn@latest add radio-group
pnpm dlx shadcn@latest add radio-group
yarn dlx shadcn@latest add radio-group
bun x shadcn@latest add radio-group

Animate it

The CLI finds your shadcn/ui components through components.json, swaps their primitive imports for Animate UI's and removes the CSS animation classes Motion replaces.

npx @animate-ui/cli@latest animate radio-group
pnpm dlx @animate-ui/cli@latest animate radio-group
yarn dlx @animate-ui/cli@latest animate radio-group
bun x @animate-ui/cli@latest animate radio-group

Usage

Import the primitive from Animate UI instead of Radix UI. Everything else stays the same.

import { RadioGroup } from '@animate-ui/react/primitives/radix-ui';

export function Density() {
  return (
    <RadioGroup.Root defaultValue="comfortable" aria-label="Density">
      {['default', 'comfortable', 'compact'].map((value) => (
        <label key={value} className="flex items-center gap-2">
          <RadioGroup.Item value={value}>
            <RadioGroup.Indicator>
              <span className="size-2 rounded-full bg-current" />
            </RadioGroup.Indicator>
          </RadioGroup.Item>
          {value}
        </label>
      ))}
    </RadioGroup.Root>
  );
}

Coming from the @radix-ui/react-radio-group package? Use the subpath: import * as RadioGroupPrimitive from '@animate-ui/react/primitives/radix-ui/radio-group'.

Animation

The motion of Animate UI v1's radio group:

  • Indicator: the dot pops in when its item is selected. Its scale and its opacity spring from 0 to 1 (stiffness 200, damping 16: it overshoots a little before settling), and back to 0 the same way when another item is selected, then the indicator unmounts, as in Radix. The indicator element itself never moves: its content (the dot) animates, so shadcn/ui's centered dot stays centered.
  • Item scales to 0.97 while pressed (no hover scale), on Motion's default spring for scale (stiffness 550, damping 30).
  • In the demo, an item's fill is a layer of its own (before: in its classes) that grows from 70% while it fades in, in 300ms, and shrinks back as it fades out, like the Checkbox's: the dot pops in over it. The layer covers the border (before:-inset-px) and the border keeps its color, so no outline rings the growing fill. It's CSS, so copy the classes for the same effect; under reduced motion the layer only fades.
  • An item already selected when the group appears (the page loading, a dialog opening) shows its dot at rest, unlike v1's: only a change of selection animates a dot. On a server-rendered page, the dot is there before any script runs.
  • Selecting with the arrow keys animates like a click, as v1's did.
  • Selecting again mid-animation turns back from where the dot is.
  • Under reduced motion, the dot only fades and nothing scales.
  • With forceMount, an unselected item's indicator stays in the DOM with its dot hidden.

API Reference

Every part takes exactly the props of Radix UI's Radio Group. Animate UI adds the animation props below. With asChild, a part renders your element with Radix's behaviour, without the animation.

Root

Radix UI props only: nothing animates on the group itself.

Item

Radix UI props, plus:

PropTypeDefault
transition?
Transition{ type: 'spring', stiffness: 550, damping: 30, restSpeed: 10 }

Indicator

Radix UI props, plus:

PropTypeDefault
transition?
Transition{ type: 'spring', stiffness: 200, damping: 16 }
exitTransition?
Transitionthe same spring as transition

AnimateUIProvider's tempo scales these transitions with everything else.

Built by Skyleen. The source code is available on GitHub.

Last updated: 10/18/2026

On this page