---
title: Radio
description: A radio button input component for single-choice selection within a group, featuring customizable size and automatic state management.
---

```tsx
'use client';

import { Radio } from '@/ui/forms/radios/Radio';
import { useState } from 'react';

export function RadioDemo() {
  const [selectedValue, setSelectedValue] = useState<string>('');

  return (
    <div className="flex flex-col gap-8">
      <Radio
        name="option"
        value="option1"
        checked={selectedValue === 'option1'}
        onChange={(_, value) => setSelectedValue(value)}
      >
        Option 1
      </Radio>
      <Radio
        name="option"
        value="option2"
        checked={selectedValue === 'option2'}
        onChange={(_, value) => setSelectedValue(value)}
      >
        Option 2
      </Radio>
      <Radio
        name="option"
        value="option3"
        checked={selectedValue === 'option3'}
        onChange={(_, value) => setSelectedValue(value)}
      >
        Option 3
      </Radio>
    </div>
  );
}
```

## Installation

Using the CLI:

```bash
npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/radio.json
```

Or manually, copy the following code into `/ui/forms/radios/radio.tsx`:

```tsx
import clsx from 'clsx';
import { type InputHTMLAttributes, useId } from 'react';

export interface RadioProps
  extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'onChange'> {
  /**
   * The data-testid to apply to the button.
   * This is used for testing purposes.
   * @deprecated use ["data-testid"] instead.
   */
  dataTestId?: string;
  /**
   * The diameter of the radio button.
   * */
  size?: number;
  onChange?: (event: Event, value: string) => void;
}

export function Radio(props: RadioProps) {
  const internalId = useId();

  const {
    id = internalId,
    name = id,
    className,
    dataTestId,
    disabled,
    size = 24,
    tabIndex = 0,
    children,
    onChange,
    ...rest
  } = props;

  return (
    <label
      className={clsx(className, 'relative flex items-center gap-8', {
        'text-grey': disabled,
      })}
    >
      <span className="relative">
        <input
          data-testid={dataTestId}
          {...rest}
          id={id}
          name={name}
          type="radio"
          tabIndex={tabIndex}
          data-name="Radio"
          disabled={disabled}
          onChange={(e) => {
            onChange?.(e.nativeEvent, e.target.value);
          }}
        />

        <span style={{ height: size, width: size }} className={clsx({ 'bg-pearl': disabled })}>
          <svg viewBox="0 0 16 16" width={(size * 2) / 3} className="overflow-visible">
            <circle cx="8" cy="8" r="8px" style={{ fill: 'var(--color-saffron)' }} />
            <circle cx="8" cy="8" r="4px" style={{ fill: 'var(--color-black)' }} />
          </svg>
        </span>
      </span>

      {children}
    </label>
  );
}

Radio.displayName = 'Radio';
```

## Usage

```tsx showLineNumbers
import { Radio } from '@/docs/lib/ui/forms/radios/radio';
import { useState } from 'react';

export function Component() {
  const [selectedValue, setSelectedValue] = useState<string>('option1');

  return (
    <div className="flex flex-col gap-8">
      <Radio
        name="choice"
        value="option1"
        checked={selectedValue === 'option1'}
        onChange={(_, value) => setSelectedValue(value)}
      >
        Option 1
      </Radio>
      <Radio
        name="choice"
        value="option2"
        checked={selectedValue === 'option2'}
        onChange={(_, value) => setSelectedValue(value)}
      >
        Option 2
      </Radio>
      <Radio
        name="choice"
        value="option3"
        checked={selectedValue === 'option3'}
        onChange={(_, value) => setSelectedValue(value)}
      >
        Option 3
      </Radio>
    </div>
  );
}
```

## API Reference

### Radio

| Prop         | Type                                                     | Default        | Description                                                                                           |
| ------------ | -------------------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------- |
| `name`       | `string`                                                 | `id`           | The name attribute for the radio input. If not provided, defaults to the generated ID.                |
| `value`      | `string`                                                 | `undefined`    | The value attribute for the radio input.                                                              |
| `checked`    | `boolean`                                                | `false`        | Whether the radio is currently checked.                                                               |
| `onChange`   | `(event: Event, value: string) => void`    | `undefined`    | Change handler receiving browser event, selected value.                              |
| `disabled`   | `boolean`                                                | `false`        | Whether the radio is disabled. Disabled radios cannot be interacted with and appear in a muted color. |
| `size`       | `number`                                                 | `24`           | The diameter of the radio button in pixels.                                                           |
| `tabIndex`   | `number`                                                 | `0`            | The tab index for keyboard navigation.                                                                |
| `dataTestId` | `string`                                                 | `undefined`    | The data-testid attribute for testing purposes.                                                       |
| `className`  | `string`                                                 | `undefined`    | Additional CSS classes to apply to the label container.                                               |
| `children`   | `ReactNode`                                              | `undefined`    | The label content to display next to the radio button.                                                |
| `id`         | `string`                                                 | auto-generated | The ID attribute for the radio input. Automatically generated if not provided.                        |

The Radio component also accepts all standard HTML input attributes via `InputHTMLAttributes<HTMLInputElement>`.

## Notes

- **Controlled Component**: This is a fully controlled component. You must manage the `checked` state and `onChange` handler in the parent component.
- **Normalized onChange**: Uses the shared forms contract `onChange(event, value)` for consistency across form components.
- **Single Choice Selection**: Radio buttons are designed for selecting a single option from a group. Use the same `name` prop for all radios in the same group.
- **Custom Size**: The `size` prop controls the diameter of the radio button. The inner circle is automatically sized to 2/3 of the specified size.
- **Disabled State**: When disabled, the radio appears in a muted grey color and cannot be interacted with. The onChange callback is not triggered.
- **Label Container**: The entire component is wrapped in a label element, making the children clickable to toggle the radio.
- **Keyboard Support**: Supports standard keyboard navigation with customizable tab index.
- **Data Attributes**: Includes `data-name="Radio"` for easy targeting in tests and styling.
- **Client Component**: This component uses React hooks and must be used in a client component (requires "use client" directive in Next.js App Router).
- **Saffron Color**: The radio button indicator uses the saffron color from the design system (`var(--color-saffron)`).
