Vela UI Docsvenizia.ai

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.

npx shadcn@latest add @velaui/radio-group
npx shadcn@latest add @velaui/radio-group

Examples

  • Basic
  • Disabled
  • Size
  • Description
  • Invalid
  • Choice card
  • Choice card price
  • Choice card group

View code

Radio Group · Basic

Installation

pnpm dlx velaui add @velaui/radio-group
pnpm dlx velaui add @velaui/radio-group

Code

radio-group-basic.tsx
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';

export default function Example() {
  return (
    <RadioGroup defaultValue="comfortable" className="w-fit">
      <div className="flex items-center gap-3">
        <RadioGroupItem value="default" id="radio1"/>
        <Label htmlFor="radio1">Default</Label>
      </div>
      <div className="flex items-center gap-3">
        <RadioGroupItem value="comfortable" id="radio2"/>
        <Label htmlFor="radio2">Comfortable</Label>
      </div>
      <div className="flex items-center gap-3">
        <RadioGroupItem value="compact" id="radio3"/>
        <Label htmlFor="radio3">Comptact</Label>
      </div>
    </RadioGroup>
  )
}

View code

Radio Group · Disabled

Installation

pnpm dlx velaui add @velaui/radio-group
pnpm dlx velaui add @velaui/radio-group

Code

radio-group-disabled.tsx
import {
  RadioGroup,
  RadioGroupItem,
} from "@/components/ui/radio-group";
import { Label } from "@/components/ui/label";

export default function Example() {
  return (
    <RadioGroup defaultValue="hard" className="flex items-center gap-x-3 w-fit">
      <div className="flex items-center gap-x-2">
        <RadioGroupItem id="easy" value="easy" />
        <Label htmlFor="easy">Easy</Label>
      </div>
      <div className="flex items-center gap-x-2">
        <RadioGroupItem id="medium" value="medium" />
        <Label htmlFor="medium">Medium</Label>
      </div>
      <div className="flex items-center gap-x-2">
        <RadioGroupItem disabled id="hard" value="hard" />
        <Label htmlFor="hard">Hard</Label>
      </div>
    </RadioGroup>
  );
}

View code

Radio Group · Size

Installation

pnpm dlx velaui add @velaui/radio-group
pnpm dlx velaui add @velaui/radio-group

Code

radio-group-size.tsx
import { Label } from "@/components/ui/label";
import {
  RadioGroup,
  RadioGroupItem,
} from "@/components/ui/radio-group";

export default function Example() {
  return (
    <RadioGroup defaultValue="md" className="flex items-center gap-x-3 w-fit">
      <div className="flex items-center gap-x-2">
        <RadioGroupItem size={"sm"} id="sm" value="sm" />
        <Label htmlFor="sm" className="text-sm">
          small
        </Label>
      </div>
      <div className="flex items-center gap-x-2">
        <RadioGroupItem size={"md"} id="md" value="md" />
        <Label htmlFor="md" className="text-sm">
          medium
        </Label>
      </div>
      <div className="flex items-center gap-x-2">
        <RadioGroupItem size={"lg"} id="lg" value="lg" />
        <Label htmlFor="lg" className="text-sm">
          large
        </Label>
      </div>
    </RadioGroup>
  );
}

View code

Radio Group · Description

Installation

pnpm dlx velaui add @velaui/radio-group
pnpm dlx velaui add @velaui/radio-group

Code

radio-group-description.tsx
import { Field, FieldContent, FieldDescription, FieldLabel } from '@/components/ui/field'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'

export default function Example() {
  return (
    <RadioGroup defaultValue="comfortable" className="w-fit">
      <Field orientation="horizontal">
        <RadioGroupItem value="default" id="desc-r1" />
        <FieldContent>
          <FieldLabel htmlFor="desc-r1">Default</FieldLabel>
          <FieldDescription>
            Standard spacing for most use cases.
          </FieldDescription>
        </FieldContent>
      </Field>
      <Field orientation="horizontal">
        <RadioGroupItem value="comfortable" id="desc-r2" />
        <FieldContent>
          <FieldLabel htmlFor="desc-r2">Comfortable</FieldLabel>
          <FieldDescription>More space between elements.</FieldDescription>
        </FieldContent>
      </Field>
      <Field orientation="horizontal">
        <RadioGroupItem value="compact" id="desc-r3" />
        <FieldContent>
          <FieldLabel htmlFor="desc-r3">Compact</FieldLabel>
          <FieldDescription>
            Minimal spacing for dense layouts.
          </FieldDescription>
        </FieldContent>
      </Field>
    </RadioGroup>
  )
}

View code

Radio Group · Invalid

Installation

pnpm dlx velaui add @velaui/radio-group
pnpm dlx velaui add @velaui/radio-group

Code

radio-group-invalid.tsx
import {
  Field,
  FieldDescription,
  FieldLabel,
  FieldLegend,
  FieldSet,
} from "@/components/ui/field";
import {
  RadioGroup,
  RadioGroupItem,
} from "@/components/ui/radio-group";

export default function Example() {
  return (
    <FieldSet className="w-full max-w-xs">
      <FieldLegend variant="label">Notification Preferences</FieldLegend>
      <FieldDescription>
        Choose how you want to receive notifications.
      </FieldDescription>
      <RadioGroup defaultValue="email">
        <Field orientation="horizontal" data-invalid>
          <RadioGroupItem value="email" id="invalid-email" aria-invalid />
          <FieldLabel htmlFor="invalid-email" className="font-normal">
            Email only
          </FieldLabel>
        </Field>
        <Field orientation="horizontal" data-invalid>
          <RadioGroupItem value="sms" id="invalid-sms" aria-invalid />
          <FieldLabel htmlFor="invalid-sms" className="font-normal">
            SMS only
          </FieldLabel>
        </Field>
        <Field orientation="horizontal" data-invalid>
          <RadioGroupItem value="both" id="invalid-both" aria-invalid />
          <FieldLabel htmlFor="invalid-both" className="font-normal">
            Both Email & SMS
          </FieldLabel>
        </Field>
      </RadioGroup>
    </FieldSet>
  );
}

View code

Radio Group · Choice card

Installation

pnpm dlx velaui add @velaui/radio-group
pnpm dlx velaui add @velaui/radio-group

Code

radio-group-choice-card.tsx
import {
  RadioGroup,
  RadioGroupItem,
} from "@/components/ui/radio-group";
import {
  Field,
  FieldContent,
  FieldDescription,
  FieldLabel,
  FieldTitle,
} from "@/components/ui/field";

export default function Example() {
  return (
    <RadioGroup defaultValue="plus" className="max-w-sm">
      <FieldLabel
        htmlFor="plus-plan"
        className="cursor-pointer"
      >
        <Field orientation={"horizontal"}>
          <FieldContent>
            <FieldTitle>Plus</FieldTitle>
            <FieldDescription>
              For individuals and small teams.
            </FieldDescription>
          </FieldContent>
          <RadioGroupItem value="plus" id="plus-plan" />
        </Field>
      </FieldLabel>
      <FieldLabel
        htmlFor="pro-plan"
        className="cursor-pointer"
      >
        <Field orientation={"horizontal"}>
          <FieldContent>
            <FieldTitle>Pro</FieldTitle>
            <FieldDescription>For growing businesses.</FieldDescription>
          </FieldContent>
          <RadioGroupItem value="pro" id="pro-plan" />
        </Field>
      </FieldLabel>
      <FieldLabel
        htmlFor="enterprise-plan"
        className="cursor-pointer"
      >
        <Field orientation={"horizontal"}>
          <FieldContent>
            <FieldTitle>Enterprise</FieldTitle>
            <FieldDescription>For large terms and enterprise.</FieldDescription>
          </FieldContent>
          <RadioGroupItem value="enterprise" id="enterprise-plan" />
        </Field>
      </FieldLabel>
    </RadioGroup>
  );
}

View code

Radio Group · Choice card price

Installation

pnpm dlx velaui add @velaui/radio-group
pnpm dlx velaui add @velaui/radio-group

Code

radio-group-choice-card-price.tsx
import {
  RadioGroup,
  RadioGroupItem,
} from "@/components/ui/radio-group";
import { Label } from "@/components/ui/label";

export default function Example() {
  const packages = [
    {
      label: "Basic",
      value: "basic",
      description: "Get 1 project with 10 team members.",
      price: "Free",
    },
    {
      label: "Premium",
      value: "premium",
      description: "Get 10 project with 100 team members.",
      price: "$10.00",
    },
  ];

  return (
    <RadioGroup>
      {packages.map((pkg) => (
        <div key={pkg.value} className="flex items-center p-4 gap-x-2 relative rounded-md border border-input has-data-checked:border-foreground/50">
          <RadioGroupItem id={`package-${pkg.value}`} value={pkg.value} />

          <div className="flex flex-col gap-x-2">
            <Label
              htmlFor={`package-${pkg.value}`}
              className="justify-between after:absolute after:inset-0 cursor-pointer"
            >
              {pkg.label}
              <span className="text-muted-foreground text-xs">{pkg.price}</span>
            </Label>
            <p className="text-muted-foreground text-xs">{pkg.description}</p>
          </div>
        </div>
      ))}
    </RadioGroup>
  );
}

View code

Radio Group · Choice card group

Installation

pnpm dlx velaui add @velaui/radio-group
pnpm dlx velaui add @velaui/radio-group

Code

radio-group-choice-card-group.tsx
import { Label } from "@/components/ui/label";
import {
  RadioGroup,
  RadioGroupItem,
} from "@/components/ui/radio-group";

export default function Example() {
  const items = [
    { value: 1, label: "Pro", price: "$29/mo" },
    { value: 2, label: "Team", price: "$59/mo" },
    { value: 3, label: "Enterprise", price: "Custom" },
  ];

  return (
    <RadioGroup
      className="w-full max-w-50 gap-0 -space-y-px rounded-md shadow-xs"
      defaultValue="2"
    >
      {items.map((item) => (
        <div
          key={item.value}
          className="border-input has-data-checked:border-foreground/50 has-data-checked:bg-accent relative flex flex-col gap-4 border p-4 outline-none first:rounded-t-md last:rounded-b-md has-data-checked:z-10"
        >
          <div className="flex items-center justify-between">
            <div className="flex items-center gap-2">
              <RadioGroupItem
                id={item.value.toString()}
                value={item.value.toString()}
                aria-label={`plan-radio-${item.value}`}
              />
              <Label
                className="inline-flex items-center after:absolute after:inset-0 after:cursor-pointer"
                htmlFor={item.value.toString()}
              >
                {item.label}
              </Label>
            </div>
            <div
              id={`${item.value}-price`}
              className="text-muted-foreground text-xs"
            >
              {item.price}
            </div>
          </div>
        </div>
      ))}
    </RadioGroup>
  );
}

Usage

import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';

export default function Example() {
  return (
    <RadioGroup defaultValue="comfortable" className="w-fit">
      <div className="flex items-center gap-3">
        <RadioGroupItem value="default" id="radio1"/>
        <Label htmlFor="radio1">Default</Label>
      </div>
      <div className="flex items-center gap-3">
        <RadioGroupItem value="comfortable" id="radio2"/>
        <Label htmlFor="radio2">Comfortable</Label>
      </div>
      <div className="flex items-center gap-3">
        <RadioGroupItem value="compact" id="radio3"/>
        <Label htmlFor="radio3">Comptact</Label>
      </div>
    </RadioGroup>
  )
}

Props

RadioGroup

Takes every prop of RadioGroupProps.

RadioGroupItem

PropTypeDefault
size"sm" | "md" | "lg""md"

Takes these and every prop of RadioGroupItemProps.

RadioGroupField

PropTypeDefault
size"sm" | "md" | "lg""md"

Takes these and every prop of RadioGroupFieldProps.

Dependencies

npm
class-variance-authority@0.7.1radix-ui@1.6.0
Installs with it
nothing else
npm
@base-ui/react@1.6.0class-variance-authority@0.7.1
Installs with it
nothing else

Source

components/ui/radio-group.tsx
/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import { RadioGroup as RadioGroupPrimitive } from "radix-ui";
import { cva, type VariantProps } from "class-variance-authority";

import { cn } from "@/lib/utils";

export type RadioGroupProps = React.ComponentProps<typeof RadioGroupPrimitive.Root>;

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

const radioGroupItemVariants = cva(
  // Focus and alert-focus are drawn as a 2px halo OUTSIDE the control (the box
  // measures the same in every state), so they are a ring, not a border width.
  // Checked draws no stroke — the border stays reserved but transparent.
  "group/radio-group-item peer relative flex aspect-square shrink-0 rounded-full border-[1.5px] border-border-default bg-transparent outline-none transition-colors after:absolute after:-inset-x-3 after:-inset-y-2 not-data-checked:hover:border-border-hover not-data-checked:hover:bg-bg-surface-1 focus-visible:ring-2 not-data-checked:focus-visible:ring-ring-neutral-outline data-checked:focus-visible:ring-border-default not-data-checked:focus-visible:bg-bg-main disabled:pointer-events-none disabled:cursor-not-allowed not-data-checked:disabled:border-border-disabled not-data-checked:disabled:bg-input-disabled not-data-checked:aria-invalid:border-negative-middle aria-invalid:focus-visible:ring-negative data-checked:border-transparent data-checked:bg-bg-foreground data-checked:hover:bg-primary-hover data-checked:disabled:bg-bg-disabled data-checked:aria-invalid:bg-negative-middle",
  {
    variants: {
      size: {
        sm: "size-4",
        md: "size-[18px]",
        lg: "size-[22px]",
      },
    },
    defaultVariants: {
      size: "md",
    },
  },
);

const radioGroupIndicatorVariants = cva(
  "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-bg-main group-disabled/radio-group-item:bg-bg-surface-2",
  {
    variants: {
      size: {
        sm: "size-1.5",
        md: "size-1.75",
        lg: "size-2",
      },
    },
    defaultVariants: {
      size: "md",
    },
  },
);

export type RadioGroupItemProps = React.ComponentProps<
  typeof RadioGroupPrimitive.Item
> &
  VariantProps<typeof radioGroupItemVariants>;

function RadioGroupItem({
  className,
  size = "md",
  ...props
}: RadioGroupItemProps) {
  return (
    <RadioGroupPrimitive.Item
      data-slot="radio-group-item"
      data-size={size}
      className={cn(radioGroupItemVariants({ size }), className)}
      {...props}
    >
      <RadioGroupPrimitive.Indicator
        data-slot="radio-group-indicator"
        className="flex items-center justify-center"
      >
        <span className={cn(radioGroupIndicatorVariants({ size }))} />
      </RadioGroupPrimitive.Indicator>
    </RadioGroupPrimitive.Item>
  );
}

const radioGroupFieldVariants = cva(
  "flex w-fit items-center gap-2 text-sm leading-[1.4] font-normal text-text-muted has-disabled:cursor-not-allowed has-disabled:text-text-disabled",
  {
    variants: {
      size: {
        sm: "h-5",
        md: "h-5",
        lg: "h-5.5",
      },
    },
    defaultVariants: {
      size: "md",
    },
  },
);

export type RadioGroupFieldProps = React.ComponentProps<"label"> &
  VariantProps<typeof radioGroupFieldVariants>;

function RadioGroupField({
  className,
  size = "md",
  ...props
}: RadioGroupFieldProps) {
  return (
    <label
      data-slot="radio-group-field"
      data-size={size}
      className={cn(radioGroupFieldVariants({ size }), className)}
      {...props}
    />
  );
}

export {
  RadioGroup,
  RadioGroupItem,
  RadioGroupField,
  radioGroupItemVariants,
  radioGroupFieldVariants,
};
components/ui/radio-group.tsx
/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import { Radio as RadioPrimitive } from "@base-ui/react/radio";
import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group";
import { cva, type VariantProps } from "class-variance-authority";

import { cn } from "@/lib/utils";

export type RadioGroupProps = React.ComponentProps<typeof RadioGroupPrimitive>;

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

const radioGroupItemVariants = cva(
  // Focus and alert-focus are drawn as a 2px halo OUTSIDE the control (the box
  // measures the same in every state), so they are a ring, not a border width.
  // Checked draws no stroke — the border stays reserved but transparent.
  // Base UI renders the item as a span, so disabled styling keys off
  // data-disabled instead of :disabled.
  "group/radio-group-item peer relative flex aspect-square shrink-0 rounded-full border-[1.5px] border-border-default bg-transparent outline-none transition-colors after:absolute after:-inset-x-3 after:-inset-y-2 not-data-checked:hover:border-border-hover not-data-checked:hover:bg-bg-surface-1 focus-visible:ring-2 not-data-checked:focus-visible:ring-ring-neutral-outline data-checked:focus-visible:ring-border-default not-data-checked:focus-visible:bg-bg-main data-disabled:pointer-events-none data-disabled:cursor-not-allowed not-data-checked:data-disabled:border-border-disabled not-data-checked:data-disabled:bg-input-disabled not-data-checked:aria-invalid:border-negative-middle aria-invalid:focus-visible:ring-negative data-checked:border-transparent data-checked:bg-bg-foreground data-checked:hover:bg-primary-hover data-checked:data-disabled:bg-bg-disabled data-checked:aria-invalid:bg-negative-middle",
  {
    variants: {
      size: {
        sm: "size-4",
        md: "size-[18px]",
        lg: "size-[22px]",
      },
    },
    defaultVariants: {
      size: "md",
    },
  },
);

const radioGroupIndicatorVariants = cva(
  "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-bg-main group-data-disabled/radio-group-item:bg-bg-surface-2",
  {
    variants: {
      size: {
        sm: "size-1.5",
        md: "size-1.75",
        lg: "size-2",
      },
    },
    defaultVariants: {
      size: "md",
    },
  },
);

export type RadioGroupItemProps = RadioPrimitive.Root.Props &
  VariantProps<typeof radioGroupItemVariants>;

function RadioGroupItem({
  className,
  size = "md",
  ...props
}: RadioGroupItemProps) {
  return (
    <RadioPrimitive.Root
      data-slot="radio-group-item"
      data-size={size}
      className={cn(radioGroupItemVariants({ size }), className)}
      {...props}
    >
      <RadioPrimitive.Indicator
        data-slot="radio-group-indicator"
        className="flex items-center justify-center"
      >
        <span className={cn(radioGroupIndicatorVariants({ size }))} />
      </RadioPrimitive.Indicator>
    </RadioPrimitive.Root>
  );
}

const radioGroupFieldVariants = cva(
  "flex w-fit items-center gap-2 text-sm leading-[1.4] font-normal text-text-muted has-disabled:cursor-not-allowed has-disabled:text-text-disabled",
  {
    variants: {
      size: {
        sm: "h-5",
        md: "h-5",
        lg: "h-5.5",
      },
    },
    defaultVariants: {
      size: "md",
    },
  },
);

export type RadioGroupFieldProps = React.ComponentProps<"label"> &
  VariantProps<typeof radioGroupFieldVariants>;

function RadioGroupField({
  className,
  size = "md",
  ...props
}: RadioGroupFieldProps) {
  return (
    <label
      data-slot="radio-group-field"
      data-size={size}
      className={cn(radioGroupFieldVariants({ size }), className)}
      {...props}
    />
  );
}

export {
  RadioGroup,
  RadioGroupItem,
  RadioGroupField,
  radioGroupItemVariants,
  radioGroupFieldVariants,
};