# Radio

> A set of checkable buttons—known as radio buttons—where no more than one of the buttons can be checked at a time.

**BNA UI** — a React Native / Expo component library.
These components render through `react-native`, not the DOM: there are no HTML
elements, no Tailwind classes and no Radix primitives. Source is copied into your
project and imported through `@/components/ui/*`, `@/components/charts/*`,
`@/hooks/*` and `@/theme/*`. Colours come from the `useColor` hook rather than
hardcoded hex; sizing tokens (`HEIGHT`, `FONT_SIZE`, `BORDER_RADIUS`, `CORNERS`)
come from `@/theme/globals`.

- Docs: https://ui.ahmedbna.com/docs/components/radio
- Markdown: https://ui.ahmedbna.com/docs/components/radio.md
- Structured JSON (props, usage, source, examples): https://ui.ahmedbna.com/r/ai/radio.json
- Install payload (source plus every file it imports): https://ui.ahmedbna.com/r/radio.json
- Install: `npx bna-ui add radio`
- npm dependencies: `expo-haptics`, `react-native`
- Registry dependencies: `mode-provider`, `useColorScheme`, `colors`, `useColor`, `useHaptics`, `globals`, `text`
- Preview recording: https://demo.ahmedbna.com/0233-radio-demo.MP4

---

**Example:** A basic radio group with multiple options

```tsx
// components/demo/radio/radio-demo.tsx
import { RadioGroup } from '@/components/ui/radio';
import React, { useState } from 'react';

export function RadioDemo() {
  const [value, setValue] = useState('option1');

  return (
    <RadioGroup
      options={[
        { label: 'Default', value: 'option1' },
        { label: 'Comfortable', value: 'option2' },
        { label: 'Compact', value: 'option3' },
      ]}
      value={value}
      onValueChange={setValue}
    />
  );
}
```

## Installation

### CLI

```bash
npx bna-ui add radio
```

### Manual

**1.** Copy and paste the following code into your project.

```tsx
// components/ui/radio.tsx
import { Text } from '@/components/ui/text';
import { useColor } from '@/hooks/useColor';
import { useHaptics } from '@/hooks/useHaptics';
import { BORDER_RADIUS, CORNERS, FONT_SIZE } from '@/theme/globals';
import React from 'react';
import { TextStyle, TouchableOpacity, View, ViewStyle } from 'react-native';

export interface RadioOption {
  label: string;
  value: string;
  disabled?: boolean;
}

interface RadioGroupProps {
  options: RadioOption[];
  value?: string;
  onValueChange?: (value: string) => void;
  disabled?: boolean;
  orientation?: 'vertical' | 'horizontal';
  style?: ViewStyle;
  optionStyle?: ViewStyle;
  labelStyle?: TextStyle;
  haptic?: boolean;
}

interface RadioButtonProps {
  option: RadioOption;
  selected: boolean;
  onPress: () => void;
  disabled?: boolean;
  style?: ViewStyle;
  labelStyle?: TextStyle;
  haptic?: boolean;
}

export function RadioButton({
  option,
  selected,
  onPress,
  disabled = false,
  style,
  labelStyle,
  haptic = true,
}: RadioButtonProps) {
  const primaryColor = useColor('primary');
  const borderColor = useColor('border');
  const textColor = useColor('text');
  const mutedColor = useColor('textMuted');
  const feedback = useHaptics(haptic);

  const isDisabled = disabled || option.disabled;

  // Re-tapping the option that is already selected is a no-op, so it should not
  // feel like one. RadioGroup deliberately does not fire its own — this is the
  // single source of feedback for the interaction.
  const handlePress = () => {
    if (!selected) feedback('selection');
    onPress();
  };

  const radioButtonStyle: ViewStyle = {
    width: BORDER_RADIUS,
    height: BORDER_RADIUS,
    borderRadius: CORNERS,
    borderWidth: 1.5,
    borderColor: selected ? primaryColor : borderColor,
    backgroundColor: 'transparent',
    alignItems: 'center',
    justifyContent: 'center',
    marginRight: 12,
  };

  const innerCircleStyle: ViewStyle = {
    width: 16,
    height: 16,
    borderRadius: CORNERS,
    backgroundColor: selected ? primaryColor : 'transparent',
  };

  const containerStyle: ViewStyle = {
    flexDirection: 'row',
    alignItems: 'center',
    paddingVertical: 4,
    paddingHorizontal: 4,
    opacity: isDisabled ? 0.5 : 1,
  };

  const textStyle: TextStyle = {
    color: isDisabled ? mutedColor : textColor,
    fontSize: FONT_SIZE,
    fontWeight: '400',
    lineHeight: 24,
  };

  return (
    <TouchableOpacity
      style={[containerStyle, style]}
      onPress={handlePress}
      disabled={isDisabled}
      activeOpacity={0.7}
      hitSlop={{ top: 9, bottom: 9, left: 9, right: 9 }}
      accessibilityRole='radio'
      accessibilityState={{ checked: selected, disabled: isDisabled }}
      accessibilityLabel={option.label}
    >
      <View style={radioButtonStyle}>
        <View style={innerCircleStyle} />
      </View>
      <Text style={[textStyle, labelStyle]}>{option.label}</Text>
    </TouchableOpacity>
  );
}

export function RadioGroup({
  options,
  value,
  onValueChange,
  disabled = false,
  orientation = 'vertical',
  style,
  optionStyle,
  labelStyle,
  haptic = true,
}: RadioGroupProps) {
  const containerStyle: ViewStyle = {
    flexDirection: orientation === 'horizontal' ? 'row' : 'column',
    gap: orientation === 'horizontal' ? 16 : 4,
  };

  const handlePress = (optionValue: string) => {
    if (onValueChange && !disabled) {
      onValueChange(optionValue);
    }
  };

  return (
    <View style={[containerStyle, style]} accessibilityRole='radiogroup'>
      {options.map((option) => (
        <RadioButton
          key={option.value}
          option={option}
          selected={value === option.value}
          onPress={() => handlePress(option.value)}
          disabled={disabled}
          style={optionStyle}
          labelStyle={labelStyle}
          haptic={haptic}
        />
      ))}
    </View>
  );
}
```

**2.** Update the import paths to match your project setup.

## Usage

```tsx
import { RadioGroup, RadioButton } from '@/components/ui/radio';
```

```tsx
const [value, setValue] = useState('option1');

<RadioGroup
  options={[
    { label: 'Option 1', value: 'option1' },
    { label: 'Option 2', value: 'option2' },
    { label: 'Option 3', value: 'option3' },
  ]}
  value={value}
  onValueChange={setValue}
/>;
```

## Examples

#### Default

**Example:** A basic radio group with multiple options

```tsx
// components/demo/radio/radio-demo.tsx
import { RadioGroup } from '@/components/ui/radio';
import React, { useState } from 'react';

export function RadioDemo() {
  const [value, setValue] = useState('option1');

  return (
    <RadioGroup
      options={[
        { label: 'Default', value: 'option1' },
        { label: 'Comfortable', value: 'option2' },
        { label: 'Compact', value: 'option3' },
      ]}
      value={value}
      onValueChange={setValue}
    />
  );
}
```

#### Horizontal Layout

**Example:** Radio buttons arranged horizontally

```tsx
// components/demo/radio/radio-horizontal.tsx
import { RadioGroup } from '@/components/ui/radio';
import React, { useState } from 'react';

export function RadioHorizontal() {
  const [value, setValue] = useState('small');

  return (
    <RadioGroup
      orientation='horizontal'
      options={[
        { label: 'Small', value: 'small' },
        { label: 'Medium', value: 'medium' },
        { label: 'Large', value: 'large' },
      ]}
      value={value}
      onValueChange={setValue}
    />
  );
}
```

#### Disabled Options

**Example:** Radio group with some disabled options

```tsx
// components/demo/radio/radio-disabled.tsx
import { RadioGroup } from '@/components/ui/radio';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';

export function RadioDisabled() {
  const [value1, setValue1] = useState('option1');
  const [value2, setValue2] = useState('option2');

  return (
    <View style={{ gap: 24 }}>
      {/* Some disabled options */}
      <View>
        <Text style={{ marginBottom: 12, fontWeight: '500' }}>
          With disabled options
        </Text>
        <RadioGroup
          options={[
            { label: 'Available', value: 'option1' },
            { label: 'Disabled', value: 'option2', disabled: true },
            { label: 'Available', value: 'option3' },
            { label: 'Disabled', value: 'option4', disabled: true },
          ]}
          value={value1}
          onValueChange={setValue1}
        />
      </View>

      {/* Entire group disabled */}
      <View>
        <Text style={{ marginBottom: 12, fontWeight: '500' }}>
          Entire group disabled
        </Text>
        <RadioGroup
          disabled
          options={[
            { label: 'Option 1', value: 'option1' },
            { label: 'Option 2', value: 'option2' },
            { label: 'Option 3', value: 'option3' },
          ]}
          value={value2}
          onValueChange={setValue2}
        />
      </View>
    </View>
  );
}
```

#### Custom Styling

**Example:** Radio buttons with custom colors and styling

```tsx
// components/demo/radio/radio-styled.tsx
import { RadioGroup } from '@/components/ui/radio';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import { useColor } from '@/hooks/useColor';
import React, { useState } from 'react';

export function RadioStyled() {
  const green = useColor('green');
  const card = useColor('card');

  const [value1, setValue1] = useState('red');
  const [value2, setValue2] = useState('plan1');

  return (
    <View style={{ gap: 24 }}>
      {/* Custom colors */}
      <View>
        <Text style={{ marginBottom: 12, fontWeight: '500' }}>
          Card-like options
        </Text>
        <RadioGroup
          options={[
            { label: 'Red Theme', value: 'red' },
            { label: 'Blue Theme', value: 'blue' },
            { label: 'Green Theme', value: 'green' },
          ]}
          value={value1}
          onValueChange={setValue1}
          optionStyle={{
            paddingVertical: 12,
            paddingHorizontal: 12,
            backgroundColor: card,
            borderRadius: 8,
            marginBottom: 4,
          }}
          labelStyle={{
            fontSize: 16,
            fontWeight: '500',
          }}
        />
      </View>

      {/* Card-like styling */}
      <View>
        <Text style={{ marginBottom: 12, fontWeight: '500' }}>
          Custom styling
        </Text>
        <RadioGroup
          options={[
            { label: 'Basic Plan - $9/month', value: 'plan1' },
            { label: 'Pro Plan - $19/month', value: 'plan2' },
            { label: 'Enterprise - $49/month', value: 'plan3' },
          ]}
          value={value2}
          onValueChange={setValue2}
          optionStyle={{
            paddingVertical: 16,
            paddingHorizontal: 16,
            backgroundColor: green,
            borderRadius: 12,
            marginBottom: 8,
            shadowColor: '#000',
            shadowOffset: { width: 0, height: 1 },
            shadowOpacity: 0.05,
            shadowRadius: 2,
            elevation: 1,
          }}
          labelStyle={{
            fontSize: 15,
            fontWeight: '500',
            color: '#1f2937',
          }}
        />
      </View>
    </View>
  );
}
```

#### Form Integration

**Example:** Radio group integrated with form validation

```tsx
// components/demo/radio/radio-form.tsx
import { Button } from '@/components/ui/button';
import { RadioGroup } from '@/components/ui/radio';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';
import { Alert } from 'react-native';

export function RadioForm() {
  const [experience, setExperience] = useState('');
  const [notification, setNotification] = useState('email');
  const [theme, setTheme] = useState('system');

  const handleSubmit = () => {
    if (!experience) {
      Alert.alert('Error', 'Please select your experience level');
      return;
    }

    Alert.alert(
      'Form Submitted',
      `Experience: ${experience}\nNotifications: ${notification}\nTheme: ${theme}`
    );
  };

  return (
    <View style={{ paddingVertical: 16, gap: 24 }}>
      <Text style={{ fontSize: 18, fontWeight: '600' }}>User Preferences</Text>

      <View>
        <Text style={{ marginBottom: 12, fontWeight: '500', fontSize: 16 }}>
          Experience Level *
        </Text>
        <RadioGroup
          options={[
            { label: 'Beginner', value: 'beginner' },
            { label: 'Intermediate', value: 'intermediate' },
            { label: 'Advanced', value: 'advanced' },
            { label: 'Expert', value: 'expert' },
          ]}
          value={experience}
          onValueChange={setExperience}
        />
      </View>

      <View>
        <Text style={{ marginBottom: 12, fontWeight: '500', fontSize: 16 }}>
          Notification Preference
        </Text>
        <RadioGroup
          options={[
            { label: 'Email notifications', value: 'email' },
            { label: 'Push notifications', value: 'push' },
            { label: 'SMS notifications', value: 'sms' },
            { label: 'No notifications', value: 'none' },
          ]}
          value={notification}
          onValueChange={setNotification}
        />
      </View>

      <View>
        <Text style={{ marginBottom: 12, fontWeight: '500', fontSize: 16 }}>
          Theme Preference
        </Text>
        <RadioGroup
          orientation='horizontal'
          options={[
            { label: 'Light', value: 'light' },
            { label: 'Dark', value: 'dark' },
            { label: 'System', value: 'system' },
          ]}
          value={theme}
          onValueChange={setTheme}
        />
      </View>

      <Button onPress={handleSubmit} style={{ marginTop: 8 }}>
        Save Preferences
      </Button>
    </View>
  );
}
```

#### Large Size

**Example:** Radio buttons with larger size and spacing

```tsx
// components/demo/radio/radio-large.tsx
import { RadioGroup } from '@/components/ui/radio';
import React, { useState } from 'react';

export function RadioLarge() {
  const [value, setValue] = useState('option1');

  return (
    <RadioGroup
      options={[
        { label: 'Large Option One', value: 'option1' },
        { label: 'Large Option Two', value: 'option2' },
        { label: 'Large Option Three', value: 'option3' },
      ]}
      value={value}
      onValueChange={setValue}
      style={{ gap: 12 }}
      optionStyle={{
        paddingVertical: 12,
      }}
      labelStyle={{
        fontSize: 18,
        fontWeight: '500',
        lineHeight: 28,
      }}
    />
  );
}
```

#### Single Radio Button

**Example:** Individual radio button component usage

```tsx
// components/demo/radio/radio-single.tsx
import { RadioButton } from '@/components/ui/radio';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';

export function RadioSingle() {
  const [selectedValue, setSelectedValue] = useState('option2');

  const options = [
    { label: 'First Option', value: 'option1' },
    { label: 'Second Option', value: 'option2' },
    { label: 'Third Option', value: 'option3' },
    { label: 'Disabled Option', value: 'option4', disabled: true },
  ];

  return (
    <View style={{ gap: 16 }}>
      <Text style={{ fontWeight: '500', fontSize: 16 }}>
        Individual Radio Buttons
      </Text>

      <View style={{ gap: 8 }}>
        {options.map((option) => (
          <RadioButton
            key={option.value}
            option={option}
            selected={selectedValue === option.value}
            onPress={() => setSelectedValue(option.value)}
          />
        ))}
      </View>

      <Text variant='caption'>Selected: {selectedValue}</Text>
    </View>
  );
}
```

## API Reference

### RadioGroup

The main container component that manages a group of radio buttons. Uses `value`/`onValueChange` (matching `ToggleGroup`'s group-selection convention) rather than `checkbox`'s `checked`/`onCheckedChange` or `toggle`'s `pressed`/`onPressedChange` — each naming convention matches its own control's interaction model and is intentional, not an inconsistency.

| Prop            | Type                         | Default      | Description                                                                                                   |
| --------------- | ---------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------- |
| `haptic`        | `boolean`                    | `true`       | Whether to trigger haptic feedback when an option is selected. Forwarded to every `RadioButton` in the group. |
| `options`       | `RadioOption[]`              | -            | Array of radio button options.                                                                                |
| `value`         | `string`                     | -            | The currently selected value.                                                                                 |
| `onValueChange` | `(value: string) => void`    | -            | Callback fired when the selection changes.                                                                    |
| `disabled`      | `boolean`                    | `false`      | Whether the entire group is disabled.                                                                         |
| `orientation`   | `'vertical' \| 'horizontal'` | `'vertical'` | Layout orientation of the radio buttons.                                                                      |
| `style`         | `ViewStyle`                  | -            | Additional styles for the group container.                                                                    |
| `optionStyle`   | `ViewStyle`                  | -            | Additional styles for each radio button.                                                                      |
| `labelStyle`    | `TextStyle`                  | -            | Additional styles for radio button labels.                                                                    |

### RadioButton

Individual radio button component for custom layouts.

| Prop         | Type          | Default | Description                                                                                                  |
| ------------ | ------------- | ------- | ------------------------------------------------------------------------------------------------------------ |
| `haptic`     | `boolean`     | `true`  | Whether to trigger haptic feedback when the option is selected. Re-selecting the active option stays silent. |
| `option`     | `RadioOption` | -       | The radio option data.                                                                                       |
| `selected`   | `boolean`     | -       | Whether this radio button is selected.                                                                       |
| `onPress`    | `() => void`  | -       | Callback fired when the button is pressed.                                                                   |
| `disabled`   | `boolean`     | `false` | Whether this radio button is disabled.                                                                       |
| `style`      | `ViewStyle`   | -       | Additional styles for the button container.                                                                  |
| `labelStyle` | `TextStyle`   | -       | Additional styles for the button label.                                                                      |

### RadioOption

The shape of each radio option object.

| Prop       | Type      | Description                               |
| ---------- | --------- | ----------------------------------------- |
| `label`    | `string`  | The display text for the radio button.    |
| `value`    | `string`  | The value associated with this option.    |
| `disabled` | `boolean` | Whether this specific option is disabled. |

## Accessibility

The Radio component is built with accessibility in mind:

- Uses TouchableOpacity for proper touch feedback
- Supports disabled states with appropriate visual feedback
- Proper opacity changes for disabled options
- Screen reader friendly with semantic structure
- Supports keyboard navigation patterns
- Clear visual indicators for selected state
