# RadioGroup

> RadioGroup and RadioGroupItem: a single choice from a list on the SelectionGroup Primitive, vertical or horizontal, with a haptic tick.

Component · forms. Registry item `radio-group`, shown in the vega Style. JSON: https://nativecn.dev/r/styles/vega/radio-group.json

## Installation

```sh
npx nativecn-cli@latest add radio-group
```

Also installs: [theme](https://nativecn.dev/docs/theme/theme), [pressable](https://nativecn.dev/docs/primitives/pressable), [selection-group](https://nativecn.dev/docs/primitives/selection-group), [text](https://nativecn.dev/docs/components/text).

## Usage

### RadioGroup demo

A vertical and a horizontal RadioGroup, with a disabled item.

```tsx
import { useState } from "react";
import { View } from "react-native";

import { RadioGroup, RadioGroupItem } from "@/components/radio-group";
import { createStyles } from "@/theme";

const useStyles = createStyles((t) => ({
  root: { gap: t.spacing[6] },
}));

export default function RadioGroupDemo() {
  const styles = useStyles();
  const [plan, setPlan] = useState("monthly");

  return (
    <View style={styles.root}>
      <RadioGroup value={plan} onValueChange={setPlan}>
        <RadioGroupItem value="monthly" label="Monthly" />
        <RadioGroupItem value="yearly" label="Yearly" />
        <RadioGroupItem value="lifetime" label="Lifetime (sold out)" disabled />
      </RadioGroup>
      <RadioGroup defaultValue="m" orientation="horizontal">
        <RadioGroupItem value="s" label="S" />
        <RadioGroupItem value="m" label="M" />
        <RadioGroupItem value="l" label="L" />
      </RadioGroup>
    </View>
  );
}
```

## Props

### RadioGroup

| Prop | Type and description |
|---|---|
| `value` | string: the controlled selected value |
| `defaultValue` | string: the starting value while uncontrolled |
| `onValueChange` | (value: string) => void: called with the newly selected value |
| `orientation` | "vertical" (default) \| "horizontal" |
| `disabled` | boolean: disables every item |
| `style` | layout only, merged last onto the root |

### RadioGroupItem

| Prop | Type and description |
|---|---|
| `value` | string (required): the value this item selects |
| `label` | string: visible text next to the dot, also the accessible name |
| `disabled` | boolean: this item only |
| `style` | layout only, merged last onto the root |

## Accessibility

- Announced as a radiogroup of radio items with aria-checked.
- An item's label is its accessible name.
- Each row is the tap target, at least 48.
- disabled, on the group or an item, is announced as disabled and blocks selection.

## Notes

Named exports RadioGroup and RadioGroupItem. A light haptic tick on selection when config.haptics is on. The dot uses the radio.dot Style Slot and the pressed look radio.pressed.

## Source

### {components}/radio-group.tsx

```tsx
import { View, type GestureResponderEvent, type StyleProp, type ViewStyle } from "react-native";

import { Pressable, type PressableProps } from "@/components/primitives/pressable";
import {
  SelectionGroup,
  useSelectionItem,
  type SelectionGroupSingleProps,
} from "@/components/primitives/selection-group";
import { Text } from "@/components/text";
import { createStyles } from "@/theme";

export type RadioGroupOrientation = "vertical" | "horizontal";

export type RadioGroupProps = Omit<SelectionGroupSingleProps, "type" | "itemRole" | "style"> & {
  /** Controlled selected value. */
  value?: string;
  /** Starting value while uncontrolled. */
  defaultValue?: string;
  /** Called with the newly selected value. */
  onValueChange?: (value: string) => void;
  /** How the items are laid out (default `vertical`). */
  orientation?: RadioGroupOrientation;
  /** Layout only, merged last onto the root. */
  style?: StyleProp<ViewStyle>;
};

export type RadioGroupItemProps = Omit<
  PressableProps,
  "children" | "style" | "size" | "pressedStyle" | "haptic" | "loading" | "role"
> & {
  /** The value this item selects in its RadioGroup. */
  value: string;
  /** Visible text next to the dot; it is also the accessible name. */
  label?: string;
  disabled?: boolean;
  /** Layout only, merged last onto the root. */
  style?: StyleProp<ViewStyle>;
};

const useStyles = createStyles((t) => {
  const dot = { width: t.scaleValue(20), height: t.scaleValue(20) };
  return {
    group: { gap: t.spacing[3] },
    vertical: { flexDirection: "column" },
    horizontal: { flexDirection: "row", flexWrap: "wrap", gap: t.spacing[4] },
    item: {
      flexDirection: "row",
      alignItems: "center",
      gap: t.spacing[2],
      alignSelf: "flex-start",
    },
    dot: {
      ...dot,
      alignItems: "center",
      justifyContent: "center",
      borderRadius: t.radius.full,
      borderWidth: t.borderWidth.default,
      borderColor: t.colors.input,
      backgroundColor: t.colors.background,
    },
    dotOn: { borderColor: t.colors.primary },
    // The inner dot is half the outer one.
    inner: {
      width: dot.width / 2,
      height: dot.height / 2,
      borderRadius: t.radius.full,
      backgroundColor: t.colors.primary,
    },
    pressed: { opacity: 0.7 },
    disabled: { opacity: t.opacity.disabled },
  };
});

/**
 * A single choice from a list, built on the SelectionGroup Primitive: announced as a
 * `radiogroup` of `radio` items with `aria-checked`. Works controlled (`value` +
 * `onValueChange`) or uncontrolled (`defaultValue`); `disabled` disables every item.
 */
export function RadioGroup({ orientation = "vertical", style, ...props }: RadioGroupProps) {
  const styles = useStyles();
  return (
    <SelectionGroup
      type="single"
      itemRole="radio"
      style={[styles.group, styles[orientation], style]}
      {...props}
    />
  );
}

/**
 * One option of a RadioGroup: a dot and an optional label. The whole row is the tap target
 * (at least 48) and gives a light haptic tick when `config.haptics` is on.
 */
export function RadioGroupItem({
  value,
  label,
  disabled: ownDisabled = false,
  onPress,
  style,
  ...props
}: RadioGroupItemProps) {
  const styles = useStyles();
  const { selected, disabled, itemProps } = useSelectionItem({ value, disabled: ownDisabled });

  const handlePress = (event: GestureResponderEvent) => {
    itemProps.onPress();
    onPress?.(event);
  };

  return (
    <Pressable
      {...itemProps}
      haptic="selection"
      size={label == null ? { width: styles.dot.width, height: styles.dot.height } : undefined}
      pressedStyle={styles.pressed}
      style={[styles.item, disabled ? styles.disabled : null, style]}
      {...props}
      onPress={handlePress}
    >
      <View style={[styles.dot, selected ? styles.dotOn : null]}>
        {selected ? <View style={styles.inner} /> : null}
      </View>
      {label != null ? <Text variant="label">{label}</Text> : null}
    </Pressable>
  );
}
```
