# useModeToggle

> A hook that provides complete control over theme mode switching with support for light, dark, and system modes.

**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/hooks/useModeToggle
- Markdown: https://ui.ahmedbna.com/docs/hooks/useModeToggle.md
- Structured JSON (props, usage, source, examples): https://ui.ahmedbna.com/r/ai/useModeToggle.json
- Install payload (source plus every file it imports): https://ui.ahmedbna.com/r/useModeToggle.json
- Install: `npx bna-ui add useModeToggle`
- Registry dependencies: `mode-provider`

---

## Installation

### CLI

```bash
npx bna-ui add useModeToggle
```

### Manual

**1.** This hook reads the theme mode from ModeProvider. Install it first:

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

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

```tsx
// hooks/useModeToggle.tsx
import { Mode, useModeContext } from '@/providers/mode-provider';

interface UseModeToggleReturn {
  isDark: boolean;
  mode: Mode;
  setMode: (mode: Mode) => void;
  currentMode: 'light' | 'dark';
  toggleMode: () => void;
}

/**
 * Reads and writes the app-wide theme mode held by `ModeProvider`.
 *
 * The mode deliberately lives in context rather than in this hook: it used to
 * be local `useState` paired with a global `Appearance.setColorScheme` call, so
 * remounting the toggle reset the cycle to `'system'` while the app stayed
 * dark, and two toggles on screen disagreed. Sharing the state also makes the
 * toggle work on web, where `Appearance` is read-only.
 */
export function useModeToggle(): UseModeToggleReturn {
  const context = useModeContext();

  if (!context) {
    throw new Error(
      'useModeToggle requires a <ModeProvider>. Wrap your app in the ' +
        '<ThemeProvider> in providers/theme-provider, which mounts one, or ' +
        'mount <ModeProvider> from providers/mode-provider yourself.'
    );
  }

  const { mode, setMode, scheme } = context;

  const toggleMode = () => {
    switch (mode) {
      case 'light':
        setMode('dark');
        break;
      case 'dark':
        setMode('system');
        break;
      case 'system':
        setMode('light');
        break;
    }
  };

  return {
    isDark: scheme === 'dark',
    mode,
    setMode,
    currentMode: scheme,
    toggleMode,
  };
}
```

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

## Requirements

The mode lives in [`ModeProvider`](/docs/providers/mode-provider), not in this hook, so one
has to be mounted above every component that calls `useModeToggle`. The
[`ThemeProvider`](/docs/providers/theme-provider) you already wrap your app in
mounts it for you — if you use that, there is nothing to do:

```tsx
import { ThemeProvider } from '@/providers/theme-provider';

export default function RootLayout() {
  return (
    <ThemeProvider>
      <Stack />
    </ThemeProvider>
  );
}
```

Calling the hook without a provider throws rather than silently doing nothing.

Sharing the state this way is what makes the toggle behave: every toggle in the
app agrees on the current mode, remounting a screen no longer resets the cycle,
and the theme changes on web as well as native.

## Usage

```tsx
import { useModeToggle } from '@/hooks/useModeToggle';
```

```tsx
export function ThemeToggle() {
  const { isDark, mode, setMode, toggleMode } = useModeToggle();

  return (
    <View>
      <Text>Current mode: {mode}</Text>
      <Text>Is dark: {isDark ? 'Yes' : 'No'}</Text>
      <TouchableOpacity onPress={toggleMode}>
        <Text>Toggle Theme</Text>
      </TouchableOpacity>
    </View>
  );
}
```

## API Reference

### useModeToggle

A hook that provides complete control over theme mode switching with support for light, dark, and system modes.

#### Returns

| Property      | Type                            | Description                                                    |
| ------------- | ------------------------------- | -------------------------------------------------------------- |
| `isDark`      | `boolean`                       | Whether the current effective theme is dark                    |
| `mode`        | `'light' \| 'dark' \| 'system'` | The currently selected mode setting                            |
| `setMode`     | `(mode: Mode) => void`          | Function to set a specific mode                                |
| `currentMode` | `'light' \| 'dark'`             | The resolved color scheme, with `'system'` already applied     |
| `toggleMode`  | `() => void`                    | Function to cycle through modes: light → dark → system → light |

#### Type Definitions

```tsx
type Mode = 'light' | 'dark' | 'system';

interface UseModeToggleReturn {
  isDark: boolean;
  mode: Mode;
  setMode: (mode: Mode) => void;
  currentMode: 'light' | 'dark';
  toggleMode: () => void;
}
```

## Mode Behavior

Setting a mode writes it to `ModeProvider`, which every `useColorScheme` call in
the app reads — that is what repaints your components. On native the choice is
additionally mirrored into React Native's global `Appearance`, so the status
bar, the Android navigation bar and native sheets follow along.

### Light Mode

- Forces the app to use light theme regardless of system preference
- `isDark` returns `false`
- `currentMode` returns `'light'`

### Dark Mode

- Forces the app to use dark theme regardless of system preference
- `isDark` returns `true`
- `currentMode` returns `'dark'`

### System Mode

- Follows the system's color scheme preference
- `isDark` reflects the actual system preference
- `currentMode` returns the system's current preference

## Toggle Cycle

The `toggleMode` function cycles through modes in this order:

```
light → dark → system → light → ...
```

This provides an intuitive way for users to quickly switch between all available options.

## Use Cases

This hook is perfect for:

- Creating theme toggle buttons in settings screens
- Building comprehensive theme selection interfaces
- Implementing persistent theme preferences
- Providing users with granular control over app appearance
- Creating theme-aware components that need to know the current mode
- Building accessibility-compliant theme switching

## Best Practices

### Settings Screen Implementation

Create a comprehensive settings screen with theme options:

```tsx
export function ThemeSettings() {
  const { mode, setMode, isDark, currentMode } = useModeToggle();

  const options = [
    { key: 'light', label: 'Light', icon: '☀️' },
    { key: 'dark', label: 'Dark', icon: '🌙' },
    { key: 'system', label: 'System', icon: '⚙️' },
  ];

  return (
    <View style={{ padding: 20 }}>
      <Text style={{ fontSize: 18, marginBottom: 16 }}>Theme</Text>
      {options.map((option) => (
        <TouchableOpacity
          key={option.key}
          style={{
            flexDirection: 'row',
            alignItems: 'center',
            paddingVertical: 12,
            backgroundColor: mode === option.key ? '#007AFF20' : 'transparent',
            borderRadius: 8,
            paddingHorizontal: 12,
          }}
          onPress={() => setMode(option.key as Mode)}
        >
          <Text style={{ marginRight: 12 }}>{option.icon}</Text>
          <Text style={{ flex: 1 }}>{option.label}</Text>
          {mode === option.key && <Text>✓</Text>}
        </TouchableOpacity>
      ))}
      <Text style={{ marginTop: 16, opacity: 0.6 }}>
        Current: {currentMode} (Effective: {isDark ? 'dark' : 'light'})
      </Text>
    </View>
  );
}
```

### Persistent Theme Storage

Persistence is a prop on the provider, not something to wrap this hook in. Pass
any key/value store — `expo-secure-store` matches the shape as-is, and is what
every `npx bna-ui init` scaffold uses:

```tsx
import * as SecureStore from 'expo-secure-store';
import { ThemeProvider } from '@/providers/theme-provider';

export default function RootLayout() {
  return (
    <ThemeProvider storage={SecureStore}>
      <Stack />
    </ThemeProvider>
  );
}
```

The mode is restored on mount and written on every change. A missing or
unreadable value falls back to `defaultMode`, so a failed read can never block
startup — including on web, where SecureStore is unavailable and persistence
simply no-ops. See [`ModeProvider`](/docs/providers/mode-provider) for the full contract.

### Animated Theme Toggle

Create smooth transitions between theme modes:

```tsx
import { useEffect, useRef } from 'react';
import { Animated } from 'react-native';

export function AnimatedThemeToggle() {
  const { isDark, toggleMode } = useModeToggle();
  const animatedValue = useRef(new Animated.Value(isDark ? 1 : 0)).current;

  useEffect(() => {
    Animated.timing(animatedValue, {
      toValue: isDark ? 1 : 0,
      duration: 300,
      useNativeDriver: false,
    }).start();
  }, [isDark]);

  const backgroundColor = animatedValue.interpolate({
    inputRange: [0, 1],
    outputRange: ['#ffffff', '#000000'],
  });

  return (
    <Animated.View style={{ backgroundColor, flex: 1 }}>
      <TouchableOpacity onPress={toggleMode}>
        <Text>Toggle Theme</Text>
      </TouchableOpacity>
    </Animated.View>
  );
}
```

### Header Integration

Integrate theme toggle into your app header:

```tsx
export function AppHeader() {
  const { mode, toggleMode } = useModeToggle();

  const getToggleIcon = () => {
    switch (mode) {
      case 'light':
        return '☀️';
      case 'dark':
        return '🌙';
      case 'system':
        return '⚙️';
    }
  };

  return (
    <View
      style={{
        flexDirection: 'row',
        justifyContent: 'space-between',
        alignItems: 'center',
        padding: 16,
      }}
    >
      <Text style={{ fontSize: 18, fontWeight: 'bold' }}>My App</Text>
      <TouchableOpacity onPress={toggleMode}>
        <Text style={{ fontSize: 20 }}>{getToggleIcon()}</Text>
      </TouchableOpacity>
    </View>
  );
}
```

## Performance Considerations

### Memoization

The hook uses internal state management and is already optimized, but you can memoize dependent calculations:

```tsx
const themeStyles = useMemo(
  () => ({
    container: {
      backgroundColor: isDark ? '#000' : '#fff',
      color: isDark ? '#fff' : '#000',
    },
  }),
  [isDark]
);
```

### Avoiding Unnecessary Re-renders

Only destructure the values you actually need:

```tsx
// Good: Only get what you need
const { isDark, toggleMode } = useModeToggle();

// Less optimal: Getting all values when you only need some
const modeToggle = useModeToggle();
```

## Advanced Usage

### Custom Mode Validation

Add validation for supported modes:

```tsx
export function useValidatedModeToggle() {
  const modeToggle = useModeToggle();

  const setModeWithValidation = (mode: string) => {
    if (['light', 'dark', 'system'].includes(mode)) {
      modeToggle.setMode(mode as Mode);
    } else {
      console.warn(`Invalid mode: ${mode}`);
    }
  };

  return {
    ...modeToggle,
    setMode: setModeWithValidation,
  };
}
```

### Reading the Mode Without the Toggle

You do not need a context of your own — the mode already lives in one. For
components that only read the theme, use
[`useColorScheme`](/docs/hooks/useColorScheme) or
[`useColor`](/docs/hooks/useColor); both resolve through the same provider. To
read or set the mode without the light → dark → system cycle, reach for
[`useModeContext`](/docs/providers/mode-provider):

```tsx
import { useModeContext } from '@/providers/mode-provider';

export function ThemeLabel() {
  const { mode, scheme } = useModeContext() ?? {};

  return (
    <Text>
      {mode} (rendering as {scheme})
    </Text>
  );
}
```

## Dependencies

- `@/providers/mode-provider` - Holds the mode and resolves it to a scheme
- `react` - Required for context

## Accessibility

The hook supports accessibility by:

- Providing programmatic access to theme state for screen readers
- Enabling proper contrast ratios through theme mode control
- Supporting system-level accessibility preferences in system mode
- Allowing users to override system preferences when needed

## Platform Support

All three modes work on every platform, because the mode is held in React
context rather than pushed through a platform API.

### iOS

- System mode respects iOS system preferences
- The choice is mirrored into `Appearance.setColorScheme`, so the status bar and
  native sheets follow

### Android

- System mode respects Android system preferences
- The choice is mirrored into `Appearance.setColorScheme`, so the status bar and
  the navigation bar follow

### Web

- System mode detection works through `useColorScheme`
- react-native-web's `Appearance` is read-only — it has `getColorScheme` and
  `addChangeListener` but no setter — so the provider is the only thing driving
  the theme here. That is by design, and why the toggle works on web

## Related Hooks

- [`useColorScheme`](/docs/hooks/useColorScheme) - Base hook for theme detection
- [`useColor`](/docs/hooks/useColor) - For color resolution based on theme
- [`ModeProvider`](/docs/providers/mode-provider) - Holds the mode this hook reads and writes
