Radio Group

A set of mutually exclusive options.

Density

Installation

Terminal
pnpm dlx @dowel-ui/cli add radio-group

npm packages installed: radix-ui.

Accessibility

Arrow keys move between options and select as they go, and the group is a single tab stop — the standard radio model. Give the group an accessible name with aria-labelledby, and each item a Label tied by htmlFor/id.

Props

RadioGroup

Every prop of Radix UI’s RadioGroup.Root, plus className.

RadioGroupItem

Every prop of Radix UI’s RadioGroup.Item, plus className.

Quality

8/8 checks, measured from the source and its tests

  • Testedpasses
  • axe assertionpasses
  • Keyboard testeddoes not apply
  • Storybook examplespasses
  • Accessibility documentedpasses
  • Semantic tokens onlypasses
  • Motion from tokenspasses
  • className mergedpasses
  • Visible focusdoes not apply
  • No fixed widthspasses

Used in

Whole screens assembled from this component. Installing one brings this and everything else it needs with it.

Source

This is exactly what dowel add radio-group writes into your project, with imports rewritten to your own path alias.

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

// Motion from SmoothUI RadioGroup (MIT, © 2024 Eduardo Calvo). See THIRD_PARTY_NOTICES.md.
import { RadioGroup as RadioGroupPrimitive } from "radix-ui";
import type { ComponentPropsWithRef } from "react";

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

/**
 * A set of mutually exclusive options.
 *
 * Radios are for choosing one of a small, visible set. Once a radio is
 * selected it cannot be unselected by clicking it again, so include an explicit
 * "None" option if the choice must be reversible.
 */
export type RadioGroupProps = ComponentPropsWithRef<typeof RadioGroupPrimitive.Root>;

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

export type RadioGroupItemProps = ComponentPropsWithRef<typeof RadioGroupPrimitive.Item>;

export function RadioGroupItem({ className, ...props }: RadioGroupItemProps) {
  return (
    <RadioGroupPrimitive.Item
      data-slot="radio-group-item"
      className={cn(
        "peer aspect-square size-4 shrink-0 rounded-full border border-input bg-background shadow-xs",
        "transition-[background-color,border-color,box-shadow] duration-[var(--duration-fast)]",
        "data-[state=checked]:border-primary",
        "disabled:cursor-not-allowed disabled:opacity-55",
        "aria-invalid:border-destructive",
        focusRing,
        className,
      )}
      {...props}
    >
      <RadioGroupPrimitive.Indicator
        data-slot="radio-group-indicator"
        className="grid size-full place-items-center"
      >
        {/* The dot springs in from nothing: `starting:` gives its first frame
            a zero scale and the overshoot curve carries it slightly past full
            size. A transition rather than a keyframe, so without
            @starting-style, or under reduced motion, it is simply there. */}
        <span
          data-slot="radio-group-dot"
          className={cn(
            "size-2 rounded-full bg-primary",
            "transition-[scale] duration-[var(--duration-normal)] ease-[var(--ease-overshoot)] starting:scale-0",
          )}
        />
      </RadioGroupPrimitive.Indicator>
    </RadioGroupPrimitive.Item>
  );
}