# Toggle

> A two-state button that can be either on or off.

**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/toggle
- Markdown: https://ui.ahmedbna.com/docs/components/toggle.md
- Structured JSON (props, usage, source, examples): https://ui.ahmedbna.com/r/ai/toggle.json
- Install payload (source plus every file it imports): https://ui.ahmedbna.com/r/toggle.json
- Install: `npx bna-ui add toggle`
- npm dependencies: `expo-haptics`, `lucide-react-native`, `react-native-svg`
- Registry dependencies: `mode-provider`, `useColorScheme`, `colors`, `useColor`, `useHaptics`, `globals`, `text`, `view`, `icon`
- Preview recording: https://demo.ahmedbna.com/0312-toggle-demo.MP4

---

**Example:** A basic toggle button with icon

```tsx
// components/demo/toggle/toggle-demo.tsx
import { Toggle } from '@/components/ui/toggle';
import { Bold } from 'lucide-react-native';
import React, { useState } from 'react';

export function ToggleDemo() {
  const [pressed, setPressed] = useState(false);

  return (
    <Toggle pressed={pressed} onPressedChange={setPressed}>
      <Bold size={16} />
    </Toggle>
  );
}
```

## Installation

### CLI

```bash
npx bna-ui add toggle
```

### Manual

**1.** Install the following dependencies:

```bash
npx expo install lucide-react-native
```

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

```tsx
// components/ui/toggle.tsx
import { Icon } from '@/components/ui/icon';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import { useColor } from '@/hooks/useColor';
import { useHaptics } from '@/hooks/useHaptics';
import { CORNERS, FONT_SIZE, HEIGHT } from '@/theme/globals';
import { LucideProps } from 'lucide-react-native';
import React, { useCallback } from 'react';
import { TextStyle, TouchableOpacity, ViewStyle } from 'react-native';

type ToggleVariant = 'default' | 'outline';
type ToggleSize = 'default' | 'icon';

interface ToggleProps {
  children: React.ReactNode;
  pressed?: boolean;
  onPressedChange?: (pressed: boolean) => void;
  variant?: ToggleVariant;
  size?: ToggleSize;
  disabled?: boolean;
  style?: ViewStyle;
  textStyle?: TextStyle;
  haptic?: boolean;
}

export function Toggle({
  children,
  pressed = false,
  onPressedChange,
  variant = 'default',
  size = 'icon',
  disabled = false,
  style,
  textStyle,
  haptic = true,
}: ToggleProps) {
  const primaryColor = useColor('primary');
  const primaryForegroundColor = useColor('primaryForeground');
  const secondaryColor = useColor('secondary');
  const secondaryForegroundColor = useColor('secondaryForeground');
  const borderColor = useColor('border');
  const feedback = useHaptics(haptic);

  // The single source of haptic feedback for toggles: ToggleGroup and
  // ToggleGroupItemButton forward `haptic` down rather than firing their own,
  // so a grouped item still buzzes exactly once.
  const handlePress = () => {
    if (!disabled) {
      feedback(pressed ? 'toggle-off' : 'toggle-on');
      onPressedChange?.(!pressed);
    }
  };

  const getToggleStyle = (): ViewStyle => {
    const baseStyle: ViewStyle = {
      borderRadius: CORNERS,
      alignItems: 'center',
      justifyContent: 'center',
      flexDirection: 'row',
    };

    // Size variants - following button component pattern
    switch (size) {
      case 'icon':
        Object.assign(baseStyle, {
          width: HEIGHT,
          height: HEIGHT,
        });
        break;
      default:
        Object.assign(baseStyle, { height: HEIGHT, paddingHorizontal: 32 });
    }

    // State and variant styles - following button component pattern
    if (pressed) {
      switch (variant) {
        case 'outline':
          return {
            ...baseStyle,
            backgroundColor: primaryColor,
            borderWidth: 1,
            borderColor: primaryColor,
          };
        default:
          return {
            ...baseStyle,
            backgroundColor: primaryColor,
          };
      }
    } else {
      switch (variant) {
        case 'outline':
          return {
            ...baseStyle,
            backgroundColor: 'transparent',
            borderWidth: 1,
            borderColor: borderColor,
          };
        default:
          return {
            ...baseStyle,
            backgroundColor: secondaryColor,
          };
      }
    }
  };

  const getToggleTextStyle = (): TextStyle => {
    const baseTextStyle: TextStyle = {
      fontSize: FONT_SIZE,
      fontWeight: '500',
    };

    if (pressed) {
      switch (variant) {
        case 'outline':
          return { ...baseTextStyle, color: primaryForegroundColor };
        default:
          return { ...baseTextStyle, color: primaryForegroundColor };
      }
    } else {
      switch (variant) {
        case 'outline':
          return { ...baseTextStyle, color: primaryColor };
        default:
          return { ...baseTextStyle, color: secondaryForegroundColor };
      }
    }
  };

  const toggleStyle = getToggleStyle();
  const finalTextStyle = getToggleTextStyle();

  return (
    <TouchableOpacity
      style={[toggleStyle, disabled && { opacity: 0.5 }, style]}
      onPress={handlePress}
      disabled={disabled}
      activeOpacity={0.8}
      accessibilityRole='togglebutton'
      accessibilityState={{ selected: pressed, disabled }}
    >
      {typeof children === 'string' ? (
        <Text style={[finalTextStyle, textStyle]}>{children}</Text>
      ) : (
        children
      )}
    </TouchableOpacity>
  );
}

type ToggleGroupType = 'single' | 'multiple';
type ToggleGroupVariant = 'default' | 'outline';
type ToggleGroupSize = 'default' | 'icon';

interface ToggleGroupItem {
  value: string;
  label: string;
  icon?: React.ComponentType<LucideProps>;
  disabled?: boolean;
}

interface ToggleGroupProps {
  type?: ToggleGroupType;
  value?: string | string[];
  onValueChange?: (value: string | string[]) => void;
  items: ToggleGroupItem[];
  variant?: ToggleGroupVariant;
  size?: ToggleGroupSize;
  disabled?: boolean;
  style?: ViewStyle;
  orientation?: 'horizontal' | 'vertical';
  haptic?: boolean;
}

// Split out and memoized so a selection change only re-renders the affected
// item, not every item in the group — pointless without a stable onPress
// identity, which is why ToggleGroup wraps handleItemPress in useCallback.
const ToggleGroupItemButton = React.memo(function ToggleGroupItemButton({
  item,
  pressed,
  variant,
  size,
  disabled,
  style,
  onPress,
  haptic,
}: {
  item: ToggleGroupItem;
  pressed: boolean;
  variant: ToggleGroupVariant;
  size: ToggleGroupSize;
  disabled: boolean;
  style: ViewStyle;
  onPress: (value: string) => void;
  haptic: boolean;
}) {
  const primaryColor = useColor('primary');
  const primaryForegroundColor = useColor('primaryForeground');
  const secondaryForegroundColor = useColor('secondaryForeground');

  const handlePress = useCallback(() => {
    onPress(item.value);
  }, [onPress, item.value]);

  const color = pressed
    ? primaryForegroundColor
    : variant === 'outline'
      ? primaryColor
      : secondaryForegroundColor;

  return (
    <Toggle
      pressed={pressed}
      onPressedChange={handlePress}
      variant={variant}
      size={size}
      disabled={disabled}
      style={style}
      haptic={haptic}
    >
      {item.icon && item.label ? (
        <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
          <Icon name={item.icon} size={16} strokeWidth={2.5} color={color} />
          <Text style={{ color }}>{item.label}</Text>
        </View>
      ) : item.icon ? (
        <Icon name={item.icon} size={16} strokeWidth={2.5} color={color} />
      ) : (
        <Text style={{ color }}>{item.label}</Text>
      )}
    </Toggle>
  );
});

export function ToggleGroup({
  type = 'single',
  value,
  onValueChange,
  items,
  variant = 'default',
  size = 'default',
  disabled = false,
  style,
  orientation = 'horizontal',
  haptic = true,
}: ToggleGroupProps) {
  const borderColor = useColor('border');

  const handleItemPress = useCallback(
    (itemValue: string) => {
      if (disabled) return;

      if (type === 'single') {
        // Single selection
        const newValue = value === itemValue ? undefined : itemValue;
        onValueChange?.(newValue || '');
      } else {
        // Multiple selection
        const currentValues = Array.isArray(value) ? value : [];
        const newValues = currentValues.includes(itemValue)
          ? currentValues.filter((v) => v !== itemValue)
          : [...currentValues, itemValue];
        onValueChange?.(newValues);
      }
    },
    [disabled, type, value, onValueChange]
  );

  const isItemPressed = (itemValue: string): boolean => {
    if (type === 'single') {
      return value === itemValue;
    } else {
      return Array.isArray(value) && value.includes(itemValue);
    }
  };

  const containerStyle: ViewStyle = {
    flexDirection: orientation === 'horizontal' ? 'row' : 'column',
    borderWidth: 1,
    borderColor: borderColor,
    borderRadius: CORNERS,
    overflow: 'hidden',
    backgroundColor: 'transparent',
  };

  const getItemStyle = (index: number): ViewStyle => {
    const isLast = index === items.length - 1;

    const itemStyle: ViewStyle = {
      flex: orientation === 'horizontal' ? 1 : 0,
      borderRadius: 0,
      borderWidth: 0,
      borderRightWidth: orientation === 'horizontal' && !isLast ? 1 : 0,
      borderBottomWidth: orientation === 'vertical' && !isLast ? 1 : 0,
      borderColor: borderColor,
    };

    return itemStyle;
  };

  return (
    <View
      style={[containerStyle, style]}
      accessibilityRole={type === 'single' ? 'radiogroup' : undefined}
    >
      {items.map((item, index) => (
        <ToggleGroupItemButton
          key={item.value}
          item={item}
          pressed={isItemPressed(item.value)}
          variant={variant}
          size={size}
          disabled={disabled || !!item.disabled}
          style={getItemStyle(index)}
          onPress={handleItemPress}
          haptic={haptic}
        />
      ))}
    </View>
  );
}

// Convenience components for common use cases
export function ToggleGroupSingle({
  value,
  onValueChange,
  ...props
}: Omit<ToggleGroupProps, 'type' | 'value' | 'onValueChange'> & {
  value?: string;
  onValueChange?: (value: string) => void;
}) {
  const handleValueChange = (newValue: string | string[]) => {
    // For single selection, we know it will always be a string
    onValueChange?.(newValue as string);
  };

  return (
    <ToggleGroup
      type='single'
      value={value}
      onValueChange={handleValueChange}
      {...props}
    />
  );
}

export function ToggleGroupMultiple({
  value,
  onValueChange,
  ...props
}: Omit<ToggleGroupProps, 'type' | 'value' | 'onValueChange'> & {
  value?: string[];
  onValueChange?: (value: string[]) => void;
}) {
  const handleValueChange = (newValue: string | string[]) => {
    // For multiple selection, we know it will always be a string array
    onValueChange?.(newValue as string[]);
  };

  return (
    <ToggleGroup
      type='multiple'
      value={value}
      onValueChange={handleValueChange}
      {...props}
    />
  );
}
```

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

## Usage

```tsx
import {
  Toggle,
  ToggleGroup,
  ToggleGroupSingle,
  ToggleGroupMultiple,
} from '@/components/ui/toggle';
```

```tsx
<Toggle pressed={pressed} onPressedChange={setPressed}>
  <Bold size={16} />
</Toggle>
```

## Examples

#### Default

**Example:** A basic toggle button with icon

```tsx
// components/demo/toggle/toggle-demo.tsx
import { Toggle } from '@/components/ui/toggle';
import { Bold } from 'lucide-react-native';
import React, { useState } from 'react';

export function ToggleDemo() {
  const [pressed, setPressed] = useState(false);

  return (
    <Toggle pressed={pressed} onPressedChange={setPressed}>
      <Bold size={16} />
    </Toggle>
  );
}
```

#### Variants

**Example:** Toggle buttons in different variants

```tsx
// components/demo/toggle/toggle-variants.tsx
import { Toggle } from '@/components/ui/toggle';
import { View } from '@/components/ui/view';
import { Bold, Italic } from 'lucide-react-native';
import React, { useState } from 'react';

export function ToggleVariants() {
  const [pressed1, setPressed1] = useState(false);
  const [pressed2, setPressed2] = useState(true);
  const [pressed3, setPressed3] = useState(false);
  const [pressed4, setPressed4] = useState(true);

  return (
    <View style={{ flexDirection: 'row', gap: 12, alignItems: 'center' }}>
      <Toggle
        pressed={pressed1}
        onPressedChange={setPressed1}
        variant='default'
      >
        <Bold size={16} />
      </Toggle>
      <Toggle
        pressed={pressed2}
        onPressedChange={setPressed2}
        variant='default'
      >
        <Italic size={16} />
      </Toggle>
      <Toggle
        pressed={pressed3}
        onPressedChange={setPressed3}
        variant='outline'
      >
        <Bold size={16} />
      </Toggle>
      <Toggle
        pressed={pressed4}
        onPressedChange={setPressed4}
        variant='outline'
      >
        <Italic size={16} />
      </Toggle>
    </View>
  );
}
```

#### Sizes

**Example:** Toggle buttons in different sizes

```tsx
// components/demo/toggle/toggle-sizes.tsx
import { Toggle } from '@/components/ui/toggle';
import { View } from '@/components/ui/view';
import { Bold } from 'lucide-react-native';
import React, { useState } from 'react';

export function ToggleSizes() {
  const [pressed1, setPressed1] = useState(false);
  const [pressed2, setPressed2] = useState(true);

  return (
    <View style={{ flexDirection: 'row', gap: 12, alignItems: 'center' }}>
      <Toggle pressed={pressed1} onPressedChange={setPressed1} size='icon'>
        <Bold size={16} />
      </Toggle>
      <Toggle pressed={pressed2} onPressedChange={setPressed2} size='default'>
        Bold
      </Toggle>
    </View>
  );
}
```

#### With Text

**Example:** Toggle buttons with text labels

```tsx
// components/demo/toggle/toggle-text.tsx
import { Toggle } from '@/components/ui/toggle';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';

export function ToggleText() {
  const [pressed1, setPressed1] = useState(false);
  const [pressed2, setPressed2] = useState(true);
  const [pressed3, setPressed3] = useState(false);

  return (
    <View style={{ flexDirection: 'row', gap: 12, alignItems: 'center' }}>
      <Toggle pressed={pressed1} onPressedChange={setPressed1} size='default'>
        Bold
      </Toggle>
      <Toggle
        pressed={pressed2}
        onPressedChange={setPressed2}
        size='default'
        variant='outline'
      >
        Italic
      </Toggle>
      <Toggle pressed={pressed3} onPressedChange={setPressed3} size='default'>
        Underline
      </Toggle>
    </View>
  );
}
```

#### Disabled

**Example:** Disabled toggle buttons

```tsx
// components/demo/toggle/toggle-disabled.tsx
import { Toggle } from '@/components/ui/toggle';
import { View } from '@/components/ui/view';
import { Bold, Italic } from 'lucide-react-native';
import React from 'react';

export function ToggleDisabled() {
  return (
    <View style={{ flexDirection: 'row', gap: 12, alignItems: 'center' }}>
      <Toggle pressed={false} disabled>
        <Bold size={16} />
      </Toggle>
      <Toggle pressed={true} disabled>
        <Italic size={16} />
      </Toggle>
      <Toggle pressed={false} disabled variant='outline'>
        <Bold size={16} />
      </Toggle>
      <Toggle pressed={true} disabled variant='outline'>
        <Italic size={16} />
      </Toggle>
    </View>
  );
}
```

#### Toggle Group Single

**Example:** Single selection toggle group

```tsx
// components/demo/toggle/toggle-group-single.tsx
import { ToggleGroupSingle } from '@/components/ui/toggle';
import { AlignCenter, AlignLeft, AlignRight } from 'lucide-react-native';
import React, { useState } from 'react';

export function ToggleGroupSingleDemo() {
  const [value, setValue] = useState('left');

  const items = [
    { value: 'left', label: 'Left', icon: AlignLeft },
    { value: 'center', label: 'Center', icon: AlignCenter },
    { value: 'right', label: 'Right', icon: AlignRight },
  ];

  return (
    <ToggleGroupSingle
      value={value}
      onValueChange={setValue}
      items={items}
      size='icon'
    />
  );
}
```

#### Toggle Group Multiple

**Example:** Multiple selection toggle group

```tsx
// components/demo/toggle/toggle-group-multiple.tsx
import { ToggleGroupMultiple } from '@/components/ui/toggle';
import { Bold, Italic, Underline } from 'lucide-react-native';
import React, { useState } from 'react';

export function ToggleGroupMultipleDemo() {
  const [value, setValue] = useState(['bold']);

  const items = [
    { value: 'bold', label: 'Bold', icon: Bold },
    { value: 'italic', label: 'Italic', icon: Italic },
    { value: 'underline', label: 'Underline', icon: Underline },
  ];

  return (
    <ToggleGroupMultiple
      value={value}
      onValueChange={setValue}
      items={items}
      size='icon'
    />
  );
}
```

#### Toggle Group Vertical

**Example:** Vertical toggle group layout

```tsx
// components/demo/toggle/toggle-group-vertical.tsx
import { ToggleGroupSingle } from '@/components/ui/toggle';
import { AlignCenter, AlignLeft, AlignRight } from 'lucide-react-native';
import React, { useState } from 'react';

export function ToggleGroupVertical() {
  const [value, setValue] = useState('left');

  const items = [
    { value: 'left', label: 'Left Align', icon: AlignLeft },
    { value: 'center', label: 'Center Align', icon: AlignCenter },
    { value: 'right', label: 'Right Align', icon: AlignRight },
  ];

  return (
    <ToggleGroupSingle
      value={value}
      onValueChange={setValue}
      items={items}
      orientation='vertical'
      size='default'
    />
  );
}
```

#### Toggle Group Outline

**Example:** Toggle group with outline variant

```tsx
// components/demo/toggle/toggle-group-outline.tsx
import { ToggleGroupSingle } from '@/components/ui/toggle';
import { Bold, Italic, Underline } from 'lucide-react-native';
import React, { useState } from 'react';

export function ToggleGroupOutline() {
  const [value, setValue] = useState('bold');

  const items = [
    { value: 'bold', label: 'Bold', icon: Bold },
    { value: 'italic', label: 'Italic', icon: Italic },
    { value: 'underline', label: 'Underline', icon: Underline },
  ];

  return (
    <ToggleGroupSingle
      value={value}
      onValueChange={setValue}
      items={items}
      variant='outline'
      size='default'
    />
  );
}
```

## API Reference

### Toggle

A two-state button that can be either on (pressed) or off. Uses `pressed`/`onPressedChange` (ARIA `aria-pressed` semantics) rather than `checkbox`'s `checked`/`onCheckedChange` or `radio`'s `value`/`onValueChange` — each naming convention matches its own control's interaction model and is intentional, not an inconsistency. `ToggleGroup` below uses `value`/`onValueChange` instead, matching `RadioGroup`'s group-selection convention.

| Prop              | Type                         | Default     | Description                                                    |
| ----------------- | ---------------------------- | ----------- | -------------------------------------------------------------- |
| `haptic`          | `boolean`                    | `true`      | Whether to trigger haptic feedback when the toggle is pressed. |
| `children`        | `ReactNode`                  | -           | The content to display inside the toggle.                      |
| `pressed`         | `boolean`                    | `false`     | Whether the toggle is pressed (on).                            |
| `onPressedChange` | `(pressed: boolean) => void` | -           | Callback fired when the pressed state changes.                 |
| `variant`         | `'default' \| 'outline'`     | `'default'` | The visual variant of the toggle.                              |
| `size`            | `'default' \| 'icon'`        | `'icon'`    | The size of the toggle.                                        |
| `disabled`        | `boolean`                    | `false`     | Whether the toggle is disabled.                                |
| `style`           | `ViewStyle`                  | -           | Additional styles to apply to the toggle container.            |
| `textStyle`       | `TextStyle`                  | -           | Additional styles to apply to the toggle text.                 |

### ToggleGroup

A set of two-state buttons that can be toggled on or off.

| Prop            | Type                                  | Default        | Description                                                                                                         |
| --------------- | ------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------- |
| `haptic`        | `boolean`                             | `true`         | Whether to trigger haptic feedback when an item is pressed. Forwarded to each item so a press is felt exactly once. |
| `type`          | `'single' \| 'multiple'`              | `'single'`     | Whether to allow single or multiple selection.                                                                      |
| `value`         | `string \| string[]`                  | -              | The controlled value(s) of the toggle group.                                                                        |
| `onValueChange` | `(value: string \| string[]) => void` | -              | Callback fired when the value changes.                                                                              |
| `items`         | `ToggleGroupItem[]`                   | -              | Array of toggle items to render.                                                                                    |
| `variant`       | `'default' \| 'outline'`              | `'default'`    | The visual variant of the toggles.                                                                                  |
| `size`          | `'default' \| 'icon'`                 | `'default'`    | The size of the toggles.                                                                                            |
| `disabled`      | `boolean`                             | `false`        | Whether the entire group is disabled.                                                                               |
| `style`         | `ViewStyle`                           | -              | Additional styles to apply to the group container.                                                                  |
| `orientation`   | `'horizontal' \| 'vertical'`          | `'horizontal'` | The orientation of the toggle group.                                                                                |

### ToggleGroupItem

Configuration for individual items in a toggle group.

| Prop       | Type                               | Default | Description                             |
| ---------- | ---------------------------------- | ------- | --------------------------------------- |
| `value`    | `string`                           | -       | The unique value for this toggle item.  |
| `label`    | `string`                           | -       | The text label for this toggle item.    |
| `icon`     | `React.ComponentType<LucideProps>` | -       | Optional icon component to display.     |
| `disabled` | `boolean`                          | `false` | Whether this specific item is disabled. |

### ToggleGroupSingle

Convenience component for single-selection toggle groups.

| Prop            | Type                          | Description                                            |
| --------------- | ----------------------------- | ------------------------------------------------------ |
| `value`         | `string`                      | The controlled value of the selected toggle.           |
| `onValueChange` | `(value: string) => void`     | Callback fired when the selected value changes.        |
| `...props`      | `Omit<ToggleGroupProps, ...>` | All other ToggleGroup props except type and callbacks. |

### ToggleGroupMultiple

Convenience component for multiple-selection toggle groups.

| Prop            | Type                          | Description                                            |
| --------------- | ----------------------------- | ------------------------------------------------------ |
| `value`         | `string[]`                    | The controlled array of selected toggle values.        |
| `onValueChange` | `(value: string[]) => void`   | Callback fired when the selected values change.        |
| `...props`      | `Omit<ToggleGroupProps, ...>` | All other ToggleGroup props except type and callbacks. |

## Accessibility

The Toggle components are built with accessibility in mind:

- Uses TouchableOpacity for proper touch handling
- Supports disabled state with visual feedback
- Proper color contrast for different states
- Screen reader compatible with semantic structure
- Keyboard navigation support through native React Native components
