# Radio (/docs/radio)





## API Reference [#api-reference]

<BaseUIReference href="https://base-ui.com/react/components/radio" />

## Basic [#basic]

`RadioGroupItemControl` renders the radio indicator automatically. Use `indicator` or `classNames.indicator` when the control needs custom visuals.

<Preview cssProperties="radioPlaygroundCssProperties">
  <RadioExample />

  <Preview.Code>
    {`
          import {
            RadioGroup,
            RadioGroupItem,
            RadioGroupItemControl,
            RadioGroupItemLabel,
            RadioGroupLabel,
            RadioGroupList,
          } from "moduix";
          import { useId } from "react";

          export function RadioDemo() {
            const labelId = useId();

            return (
              <RadioGroup aria-labelledby={labelId} defaultValue="team">
                <RadioGroupLabel id={labelId}>Account Type</RadioGroupLabel>
                <RadioGroupList>
                  {options.map((option) => (
                    <RadioGroupItem key={option.value}>
                      <RadioGroupItemControl value={option.value} />
                      <RadioGroupItemLabel>{option.label}</RadioGroupItemLabel>
                    </RadioGroupItem>
                  ))}
                </RadioGroupList>
              </RadioGroup>
            );
          }
        `}
  </Preview.Code>

  <Preview.Data>
    {`
          const options = [
            { value: "personal", label: "Personal account" },
            { value: "team", label: "Team account" },
            { value: "enterprise", label: "Enterprise account" },
          ];
        `}
  </Preview.Data>

  <Preview.CSSProperties>
    {(context) => <RadioCssPropertiesPanel {...context} />}
  </Preview.CSSProperties>

  <Preview.CSSPlayground>
    {(context) => <RadioCssPlaygroundPanel {...context} />}
  </Preview.CSSPlayground>
</Preview>

## Anatomy [#anatomy]

`Radio` is always used inside `RadioGroup`. The recommended group composition keeps the clickable
row, control, and label explicit while the indicator stays internal.

```text
RadioGroup
├─ RadioGroupLabel
└─ RadioGroupList
   └─ RadioGroupItem
      ├─ RadioGroupItemControl
      │  └─ indicator (internal)
      └─ RadioGroupItemLabel
```

```tsx
<RadioGroup defaultValue="team" aria-labelledby={labelId}>
  <RadioGroupLabel id={labelId}>Account Type</RadioGroupLabel>
  <RadioGroupList>
    <RadioGroupItem>
      <RadioGroupItemControl value="team" />
      <RadioGroupItemLabel>Team account</RadioGroupItemLabel>
    </RadioGroupItem>
  </RadioGroupList>
</RadioGroup>
```

| Part                    | Role                                                                                                            |
| ----------------------- | --------------------------------------------------------------------------------------------------------------- |
| `RadioGroup`            | Root state machine. Controls selected value, disabled/read-only state, form ownership, and validation metadata. |
| `RadioGroupLabel`       | Visible group label. Pair it with `aria-labelledby` on `RadioGroup`.                                            |
| `RadioGroupList`        | Layout wrapper for the available options.                                                                       |
| `RadioGroupItem`        | Enclosing label row for one option.                                                                             |
| `RadioGroupItemControl` | The radio control. It renders the indicator automatically and accepts `size`, `indicator`, and `classNames`.    |
| `RadioGroupItemLabel`   | Text label for the option.                                                                                      |
| `RadioField`            | Compact enclosing-label helper for direct `Radio` + `RadioLabel` composition.                                   |
| `Radio`                 | Standalone radio control for custom layouts, field composition, and sibling-label/native-button patterns.       |
| `RadioLabel`            | Text label used with `RadioField` or form field labels.                                                         |

`Radio` does not expose portal-like service layers. Its indicator is also rendered internally by
default, so the public composition stays focused on the group, option rows, control, and labels.
Use `indicator` for custom graphics and `classNames.indicator` or `classNames.indicatorIcon` when
the internal indicator needs targeted styling.

## Composition [#composition]

Use `RadioGroup` for behavior props such as `value`, `defaultValue`, `onValueChange`, `disabled`,
`readOnly`, `required`, `name`, and `form`. Use `RadioGroupItemControl` for the common grouped
option pattern; it is a `Radio` with a clearer name for group rows.

For compact form rows, use `RadioField`, `Radio`, and `RadioLabel`. For sibling labels with
`htmlFor`, render `Radio` as a native button with `nativeButton` and `render={<button />}`.

Visible parts accept `className`. The radio indicator is internal, so customize it through the
`indicator` prop or the `classNames` object:

```tsx
<RadioGroupItemControl
  value="team"
  indicator={<CustomRadioIcon />}
  classNames={{
    indicator: styles.indicator,
    indicatorIcon: styles.indicatorIcon,
  }}
/>
```

## Examples [#examples]

### Sizes [#sizes]

Use `size` to align the radio control with different form densities.

<Preview>
  <RadioSizesExample />

  <Preview.Code>
    {`
          import {
            RadioGroup,
            RadioGroupItem,
            RadioGroupItemControl,
            RadioGroupItemLabel,
            RadioGroupLabel,
            RadioGroupList,
          } from "moduix";
          import { useId } from "react";

          export function RadioSizesDemo() {
            const labelId = useId();

            return (
              <RadioGroup aria-labelledby={labelId} defaultValue="md">
                <RadioGroupLabel id={labelId}>Control Size</RadioGroupLabel>
                <RadioGroupList>
                  {sizes.map((item) => (
                    <RadioGroupItem key={item.value}>
                      <RadioGroupItemControl value={item.value} size={item.value} />
                      <RadioGroupItemLabel>{item.label}</RadioGroupItemLabel>
                    </RadioGroupItem>
                  ))}
                </RadioGroupList>
              </RadioGroup>
            );
          }
        `}
  </Preview.Code>

  <Preview.Data>
    {`
          const sizes = [
            { value: "xs", label: "Extra-small" },
            { value: "sm", label: "Small" },
            { value: "md", label: "Medium" },
            { value: "lg", label: "Large" },
            { value: "xl", label: "Extra-large" },
          ] as const;
        `}
  </Preview.Data>
</Preview>

### Controlled [#controlled]

Control `value` from React state when the selected option needs to coordinate with other UI.

<Preview>
  <ControlledRadioExample />

  <Preview.Code>
    {`
          import {
            RadioGroup,
            RadioGroupItem,
            RadioGroupItemControl,
            RadioGroupItemLabel,
            RadioGroupLabel,
            RadioGroupList,
          } from "moduix";
          import { useId, useState } from "react";

          export function ControlledRadioDemo() {
            const labelId = useId();
            const [value, setValue] = useState("personal");

            return (
              <RadioGroup aria-labelledby={labelId} value={value} onValueChange={setValue}>
                <RadioGroupLabel id={labelId}>Workspace Visibility</RadioGroupLabel>
                <RadioGroupList>
                  <RadioGroupItem>
                    <RadioGroupItemControl value="personal" />
                    <RadioGroupItemLabel>Only me</RadioGroupItemLabel>
                  </RadioGroupItem>
                  <RadioGroupItem>
                    <RadioGroupItemControl value="team" />
                    <RadioGroupItemLabel>Team</RadioGroupItemLabel>
                  </RadioGroupItem>
                </RadioGroupList>
              </RadioGroup>
            );
          }
        `}
  </Preview.Code>
</Preview>

### Disabled [#disabled]

Use `disabled` on the group to prevent interaction with every radio in the group.

<Preview>
  <DisabledRadioExample />

  <Preview.Code>
    {`
          import {
            RadioGroup,
            RadioGroupItem,
            RadioGroupItemControl,
            RadioGroupItemLabel,
            RadioGroupLabel,
            RadioGroupList,
          } from "moduix";
          import { useId } from "react";

          export function DisabledRadioDemo() {
            const labelId = useId();

            return (
              <RadioGroup aria-labelledby={labelId} defaultValue="enterprise" disabled>
                <RadioGroupLabel id={labelId}>Plan</RadioGroupLabel>
                <RadioGroupList>
                  {options.map((option) => (
                    <RadioGroupItem key={option.value}>
                      <RadioGroupItemControl value={option.value} />
                      <RadioGroupItemLabel>{option.label}</RadioGroupItemLabel>
                    </RadioGroupItem>
                  ))}
                </RadioGroupList>
              </RadioGroup>
            );
          }
        `}
  </Preview.Code>

  <Preview.Data>
    {`
          const options = [
            { value: "personal", label: "Personal account" },
            { value: "team", label: "Team account" },
            { value: "enterprise", label: "Enterprise account" },
          ];
        `}
  </Preview.Data>
</Preview>

### Custom Indicator [#custom-indicator]

Pass a custom `indicator` node to `Radio` or `RadioGroupItemControl` to use any icon from your application or icon library.

<Preview>
  <CustomIndicatorRadioExample />

  <Preview.Code>
    {`
          import {
            RadioGroup,
            RadioGroupItem,
            RadioGroupItemControl,
            RadioGroupItemLabel,
            RadioGroupLabel,
            RadioGroupList,
          } from "moduix";
          import type { ComponentProps } from "react";
          import { useId } from "react";

          function CustomRadioIcon(props: ComponentProps<"svg">) {
            return (
              <svg
                width="12"
                height="12"
                viewBox="0 0 12 12"
                fill="none"
                aria-hidden="true"
                focusable="false"
                {...props}
              >
                <path d="M6 1.5L10.5 6L6 10.5L1.5 6L6 1.5Z" fill="currentColor" />
              </svg>
            );
          }

          export function CustomIndicatorRadioDemo() {
            const labelId = useId();

            return (
              <RadioGroup aria-labelledby={labelId} defaultValue="team">
                <RadioGroupLabel id={labelId}>Account Type</RadioGroupLabel>
                <RadioGroupList>
                  {options.map((option) => (
                    <RadioGroupItem key={option.value}>
                      <RadioGroupItemControl value={option.value} indicator={<CustomRadioIcon />} />
                      <RadioGroupItemLabel>{option.label}</RadioGroupItemLabel>
                    </RadioGroupItem>
                  ))}
                </RadioGroupList>
              </RadioGroup>
            );
          }
        `}
  </Preview.Code>

  <Preview.Data>
    {`
          const options = [
            { value: "personal", label: "Personal account" },
            { value: "team", label: "Team account" },
            { value: "enterprise", label: "Enterprise account" },
          ];
        `}
  </Preview.Data>
</Preview>

### Class Names [#class-names]

Pass `className` to visible slots and use `classNames` for internal indicator styling.

<Preview>
  <RadioClassNameExample />

  <Preview.Code>
    {`
          import {
            RadioGroup,
            RadioGroupItem,
            RadioGroupItemControl,
            RadioGroupItemLabel,
            RadioGroupLabel,
            RadioGroupList,
          } from "moduix";
          import { useId } from "react";
          import styles from "./styled-radio-demo.module.css";

          export function StyledRadioDemo() {
            const labelId = useId();

            return (
              <RadioGroup aria-labelledby={labelId} defaultValue="team" className={styles.group}>
                <RadioGroupLabel id={labelId} className={styles.label}>
                  Styled Account Type
                </RadioGroupLabel>
                <RadioGroupList className={styles.list}>
                  {options.map((option) => (
                    <RadioGroupItem key={option.value} className={styles.item}>
                      <RadioGroupItemControl
                        value={option.value}
                        className={styles.control}
                        classNames={{ indicator: styles.indicator }}
                      />
                      <RadioGroupItemLabel className={styles.label}>
                        {option.label}
                      </RadioGroupItemLabel>
                    </RadioGroupItem>
                  ))}
                </RadioGroupList>
              </RadioGroup>
            );
          }
        `}
  </Preview.Code>

  <Preview.CSS>
    {`
          .group {
            gap: var(--spacing-3);
          }

          .label {
            color: var(--color-primary);
          }

          .list {
            gap: var(--spacing-3);
          }

          .item {
            gap: var(--spacing-3);
          }

          .control {
            border-color: var(--color-primary);
          }

          .control[data-checked] {
            background-color: var(--color-primary);
          }

          .indicator {
            color: var(--color-primary-foreground);
          }
        `}
  </Preview.CSS>

  <Preview.Data>
    {`
          const options = [
            { value: "personal", label: "Personal account" },
            { value: "team", label: "Team account" },
            { value: "enterprise", label: "Enterprise account" },
          ];
        `}
  </Preview.Data>
</Preview>

### Field Composition [#field-composition]

Use `RadioField`, `Radio`, and `RadioLabel` directly when you want a compact enclosing-label composition.

<Preview>
  <RadioFieldCompositionExample />

  <Preview.Code>
    {`
          import {
            Radio,
            RadioField,
            RadioGroup,
            RadioGroupLabel,
            RadioLabel,
          } from "moduix";
          import { useId } from "react";

          export function RadioFieldDemo() {
            const labelId = useId();

            return (
              <RadioGroup aria-labelledby={labelId} defaultValue="personal">
                <RadioGroupLabel id={labelId}>Members</RadioGroupLabel>
                <div>
                  {options.map((option) => (
                    <RadioField key={option.value}>
                      <Radio value={option.value} />
                      <RadioLabel>{option.label}</RadioLabel>
                    </RadioField>
                  ))}
                </div>
              </RadioGroup>
            );
          }
        `}
  </Preview.Code>

  <Preview.Data>
    {`
          const options = [
            { value: "personal", label: "Personal account" },
            { value: "team", label: "Team account" },
            { value: "enterprise", label: "Enterprise account" },
          ];
        `}
  </Preview.Data>
</Preview>

### Sibling Label [#sibling-label]

Use `nativeButton` with `render={<button />}` for the sibling `label` pattern with `htmlFor` and `id`.

<Preview>
  <RadioSiblingLabelNativeButtonExample />

  <Preview.Code>
    {`
          import { Radio, RadioGroup } from "moduix";
          import { useId } from "react";

          export function SiblingLabelRadioDemo() {
            const id = useId();
            const labelId = useId();

            return (
              <div>
                <div id={labelId}>Delivery method</div>
                <RadioGroup defaultValue="email" aria-labelledby={labelId}>
                  <Radio nativeButton render={<button />} id={id} value="email" />
                </RadioGroup>
                <label htmlFor={id}>Email</label>
              </div>
            );
          }
        `}
  </Preview.Code>
</Preview>

### Form Integration [#form-integration]

Use `Field` and `Fieldset` when the group needs form naming, validation state, or shared labeling.

<Preview>
  <RadioFormIntegrationExample />

  <Preview.Code>
    {`
          import {
            Field,
            FieldItem,
            FieldLabel,
            Fieldset,
            FieldsetLegend,
            Radio,
            RadioGroup,
            RadioLabel,
          } from "moduix";

          export function RadioFormDemo() {
            return (
              <Field name="storageType">
                <Fieldset render={<RadioGroup defaultValue="ssd" />}>
                  <FieldsetLegend>Storage type</FieldsetLegend>
                  <FieldItem>
                    <FieldLabel>
                      <Radio value="ssd" />
                      <RadioLabel>SSD</RadioLabel>
                    </FieldLabel>
                  </FieldItem>
                  <FieldItem>
                    <FieldLabel>
                      <Radio value="hdd" />
                      <RadioLabel>HDD</RadioLabel>
                    </FieldLabel>
                  </FieldItem>
                </Fieldset>
              </Field>
            );
          }
        `}
  </Preview.Code>
</Preview>
