# Mode Toggle

> An animated button component for switching between light and dark themes.

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

---

**Example:** Animated theme toggle button

```tsx
// components/demo/mode-toggle/mode-toggle-demo.tsx
import { ModeToggle } from '@/components/ui/mode-toggle';
import React from 'react';

export function ModeToggleDemo() {
  return <ModeToggle />;
}
```

## Installation

### CLI

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

### Manual

**1.** Install the following dependencies:

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

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

```tsx
// components/ui/mode-toggle.tsx
import { Button, ButtonSize, ButtonVariant } from '@/components/ui/button';
import { Icon } from '@/components/ui/icon';
import { useModeToggle } from '@/hooks/useModeToggle';
import { Moon, Sun } from 'lucide-react-native';
import { useEffect, useState } from 'react';
import Animated, {
  runOnJS,
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from 'react-native-reanimated';

type Props = {
  variant?: ButtonVariant;
  size?: ButtonSize;
  haptic?: boolean;
};

export const ModeToggle = ({
  variant = 'outline',
  size = 'icon',
  haptic = true,
}: Props) => {
  const { toggleMode, isDark } = useModeToggle();
  const rotation = useSharedValue(0);
  const scale = useSharedValue(1);
  const [showIcon, setShowIcon] = useState<'sun' | 'moon'>(
    isDark ? 'moon' : 'sun'
  );

  useEffect(() => {
    // Animate icon change
    scale.value = withTiming(0, { duration: 150 }, () => {
      runOnJS(setShowIcon)(isDark ? 'moon' : 'sun');
      scale.value = withTiming(1, { duration: 150 });
    });

    // Only rotate when switching to sun (sun rays spinning effect)
    if (!isDark) {
      rotation.value = withTiming(rotation.value + 180, { duration: 300 });
    }
  }, [isDark]);

  const animatedStyle = useAnimatedStyle(() => {
    return {
      transform: [
        { rotate: showIcon === 'sun' ? `${rotation.value}deg` : '0deg' },
        { scale: scale.value },
      ],
    };
  });

  return (
    <Button
      variant={variant}
      size={size}
      onPress={toggleMode}
      haptic={haptic}
      accessibilityRole='button'
      accessibilityLabel={`Switch to ${isDark ? 'light' : 'dark'} theme`}
    >
      <Animated.View style={animatedStyle}>
        <Icon name={showIcon === 'moon' ? Moon : Sun} size={24} />
      </Animated.View>
    </Button>
  );
};
```

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

## Usage

```tsx
import { ModeToggle } from '@/components/ui/mode-toggle';
```

```tsx
<ModeToggle />
```

## Examples

#### Default

**Example:** Animated theme toggle button

```tsx
// components/demo/mode-toggle/mode-toggle-demo.tsx
import { ModeToggle } from '@/components/ui/mode-toggle';
import React from 'react';

export function ModeToggleDemo() {
  return <ModeToggle />;
}
```

## API Reference

### ModeToggle

An animated button that toggles between light and dark themes. The component uses the `Button` component internally, so it inherits button styling and behavior.

| Prop      | Type            | Default     | Description                                                                        |
| --------- | --------------- | ----------- | ---------------------------------------------------------------------------------- |
| `haptic`  | `boolean`       | `true`      | Whether to trigger haptic feedback on press. Forwarded to the underlying `Button`. |
| `variant` | `ButtonVariant` | `'outline'` | The visual variant passed through to the underlying `Button`.                      |
| `size`    | `ButtonSize`    | `'icon'`    | The size passed through to the underlying `Button`.                                |

## Animation Details

### Icon Transition

- **Scale Animation**: Icons scale down to 0, change, then scale back to 1
- **Duration**: 150ms for each scale phase (300ms total)
- **Icons**: Sun for light mode, Moon for dark mode

### Sun Rotation

- **Rotation**: 180° rotation when switching to light mode
- **Duration**: 300ms
- **Effect**: Creates a spinning sun rays effect
- **Timing**: Only rotates when switching to sun icon

## Theme Integration

The component is a thin shell over the
[`useModeToggle`](/docs/hooks/useModeToggle) hook — it takes `isDark` to pick
the icon and `toggleMode` for the press handler, and owns nothing else:

```tsx
const { toggleMode, isDark } = useModeToggle();
```

That hook reads the mode from [`ModeProvider`](/docs/providers/mode-provider), so a provider
has to be mounted above the toggle or it throws. Wrapping your app in
[`ThemeProvider`](/docs/providers/theme-provider) is enough — it mounts one — and
every scaffold from `npx bna-ui init` already does.

## Performance

The component is optimized for smooth animations:

- Uses `react-native-reanimated` for 60fps animations
- Animations run on the UI thread
- Minimal re-renders with `useSharedValue`
- Efficient icon switching with `runOnJS`

## Accessibility

The ModeToggle maintains accessibility:

- Uses semantic button component
- Screen readers announce theme changes
- Maintains proper focus behavior
- Works with keyboard navigation
- Respects system accessibility settings

## Integration Example

```tsx
// app/_layout.tsx
import { ModeToggle } from '@/components/ui/mode-toggle';
import { ThemeProvider } from '@/providers/theme-provider';

export default function RootLayout() {
  return (
    <ThemeProvider>
      <View style={styles.header}>
        <Text>My App</Text>
        <ModeToggle />
      </View>
      {/* Rest of your app */}
    </ThemeProvider>
  );
}
```

Add `storage={SecureStore}` to `ThemeProvider` to have the choice survive a
restart — see [`ModeProvider`](/docs/providers/mode-provider).

## Dependencies

The component requires these utilities to function:

- `useModeToggle`: Hook for theme switching logic
- `Button`: Base button component
- `Icon`: Themed icon wrapper
- `ModeProvider`: Holds the mode, mounted for you by `ThemeProvider`
