# Radio

> Native radio input with label wrapper and RadioGroup context for single-choice selection. Pairs with Fieldset for semantic grouping.

- Category: form
- Status: stable (since 0.3.0)
- A11y pattern: https://www.w3.org/WAI/ARIA/apg/patterns/radio/
- Tokens: --cta-background, --font-sans, --fs-md, --foreground-primary
- Playground: https://design.freecodecamp.org/playground#radio
- npm dependencies: `react@>=18 <20`
- Registry dependencies: [theme](https://design.freecodecamp.org/registry/theme.md)
- Files:
  - `Radio.tsx` → `src/ui/radio/Radio.tsx` (raw: https://design.freecodecamp.org/registry/radio/Radio.tsx)
  - `radio.css` → `src/ui/radio/radio.css` (raw: https://design.freecodecamp.org/registry/radio/radio.css)

## Install (copy source)

1. Ensure the theme is installed once per project - tokens.css + base.css imported globally, fonts available. See https://design.freecodecamp.org/registry/theme.md and https://design.freecodecamp.org/registry/starter.md.
2. Copy the files below into `src/ui/radio/` (adjust to your project layout) and import the CSS once from your global stylesheet, e.g. `@import './ui/radio/radio.css';`.
3. Colors, spacing and type come from tokens - tailor the component by editing the copied source; recolour by editing tokens.css, not the component CSS.

## Usage

Radio is the native single-choice control. It renders a real
`<input type="radio">` wrapped in a `<label>` when `label` is provided.
RadioGroup is a thin container that wires a shared `name`, the current
`value`, and the `onChange` handler to every child Radio through
context - no prop-drilling.

## Accessibility

Follows the [APG Radio Group pattern](https://www.w3.org/WAI/ARIA/apg/patterns/radio/).
RadioGroup carries `role="radiogroup"`; pair with `aria-label` or
`aria-labelledby` (for example via a preceding heading or a
wrapping Fieldset+Legend). Keyboard behaviour comes from the native
radio pair - Tab to enter, arrow keys to move within the group.

## Example

```tsx
import { Radio, RadioGroup } from './ui/radio/Radio';

<RadioGroup name="theme" defaultValue="dark" label="Theme">
  <Radio value="dark" label="Dark - default" />
  <Radio value="light" label="Light" />
  <Radio value="system" label="System" />
</RadioGroup>
```

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `label` | `ReactNode` | no | - |  |
| `labelClassName` | `string` | no | `` |  |

## Source: Radio.tsx

```tsx
import React, {
  createContext,
  forwardRef,
  useCallback,
  useContext,
  useState
} from 'react';

interface RadioGroupContextValue {
  name?: string;
  value?: string;
  onChange?: React.ChangeEventHandler<HTMLInputElement>;
}

const RadioGroupContext = createContext<RadioGroupContextValue | null>(null);

export interface RadioProps extends Omit<
  React.InputHTMLAttributes<HTMLInputElement>,
  'type'
> {
  label?: React.ReactNode;
  labelClassName?: string;
}

export const Radio = forwardRef<HTMLInputElement, RadioProps>(
  (
    {
      label,
      labelClassName = '',
      className = '',
      id,
      name,
      value,
      checked,
      onChange,
      ...rest
    },
    ref
  ) => {
    const group = useContext(RadioGroupContext);
    const resolvedName = name ?? group?.name;
    const resolvedChecked =
      checked !== undefined
        ? checked
        : group && value !== undefined
          ? group.value === value
          : undefined;
    const resolvedOnChange = onChange ?? group?.onChange;
    const input = (
      <input
        ref={ref}
        type='radio'
        id={id}
        className={className}
        name={resolvedName}
        value={value}
        checked={resolvedChecked}
        onChange={resolvedOnChange}
        {...rest}
      />
    );
    if (label === undefined) return input;
    const classes = ['radio', labelClassName].filter(Boolean).join(' ');
    return (
      <label className={classes} htmlFor={id}>
        {input}
        <span>{label}</span>
      </label>
    );
  }
);
Radio.displayName = 'Radio';

export interface RadioGroupProps extends Omit<
  React.HTMLAttributes<HTMLDivElement>,
  'onChange' | 'defaultValue'
> {
  name: string;
  /** Controlled selected value. When set the group ignores `defaultValue`. */
  value?: string;
  /** Uncontrolled initial value. Used when `value` is omitted. */
  defaultValue?: string;
  onChange?: React.ChangeEventHandler<HTMLInputElement>;
}

export const RadioGroup = forwardRef<HTMLDivElement, RadioGroupProps>(
  (
    { name, value, defaultValue, onChange, className = '', children, ...rest },
    ref
  ) => {
    const isControlled = value !== undefined;
    const [internalValue, setInternalValue] = useState<string | undefined>(
      defaultValue
    );
    const resolvedValue = isControlled ? value : internalValue;
    const handleChange = useCallback<
      React.ChangeEventHandler<HTMLInputElement>
    >(
      event => {
        if (!isControlled) {
          setInternalValue(event.target.value);
        }
        onChange?.(event);
      },
      [isControlled, onChange]
    );
    const classes = ['radio-group', className].filter(Boolean).join(' ');
    return (
      <RadioGroupContext.Provider
        value={{ name, value: resolvedValue, onChange: handleChange }}
      >
        <div ref={ref} role='radiogroup' className={classes} {...rest}>
          {children}
        </div>
      </RadioGroupContext.Provider>
    );
  }
);
RadioGroup.displayName = 'RadioGroup';
```

## Source: radio.css

```css
.radio {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  font-family: var(--font-sans);
  font-size: var(--fs-md);
  color: var(--foreground-primary);
  cursor: pointer;
}
.radio input[type='radio'] {
  width: 18px;
  height: 18px;
  margin-top: 3px;
  accent-color: var(--cta-background);
  cursor: inherit;
}
.radio:has(input:disabled) {
  opacity: 0.5;
  cursor: not-allowed;
}
.radio-group {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
```

## HTML / vanilla variant

```html
<div class="radio-group" role="radiogroup">
  <label class="radio"><input type="radio" name="theme" checked /> Dark</label>
  <label class="radio"><input type="radio" name="theme" /> Light</label>
</div>
```

Interactive behaviours for plain HTML come from the vanilla runtime (data-uikit-* attributes): https://design.freecodecamp.org/registry/vanilla.md - or download https://design.freecodecamp.org/cdn/uikit.global.js once and self-host it (do not hotlink).

## For coding agents

This library is distributed as copyable source, not an npm package. Start at https://design.freecodecamp.org/registry/starter.md, discover components via https://design.freecodecamp.org/llms.txt, and copy files into the consuming project. Keep token names intact; recolour by editing the copied tokens.css.
