Skip to main content
Rippler uses React Navigation v6 with a nested navigator structure:
This nested structure provides tab-based navigation on the main screen with the ability to push full-screen modals (like WorkoutScreen) on top.

Root Stack Navigator

The top-level navigator manages the core app flow:
client/navigation/RootStackNavigator.tsx
import React from "react";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import MainTabNavigator from "@/navigation/MainTabNavigator";
import WorkoutScreen from "@/screens/WorkoutScreen";
import { useScreenOptions } from "@/hooks/useScreenOptions";

export type RootStackParamList = {
  Main: undefined;
  Workout: { week: number; day: string };
};

const Stack = createNativeStackNavigator<RootStackParamList>();

export default function RootStackNavigator() {
  const screenOptions = useScreenOptions();

  return (
    <Stack.Navigator screenOptions={screenOptions}>
      <Stack.Screen
        name="Main"
        component={MainTabNavigator}
        options={{ headerShown: false }}
      />
      <Stack.Screen
        name="Workout"
        component={WorkoutScreen}
        options={{
          headerTitle: "Log Workout",
        }}
      />
    </Stack.Navigator>
  );
}
Main
undefined
The main tab navigator (no params required)
Workout
object
Full-screen workout logging modal

Main Tab Navigator

The bottom tab bar provides access to the four main sections:
client/navigation/MainTabNavigator.tsx
import React from "react";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { Feather } from "@expo/vector-icons";
import { BlurView } from "expo-blur";
import { Platform, StyleSheet } from "react-native";
import ProgramStackNavigator from "@/navigation/ProgramStackNavigator";
import HistoryStackNavigator from "@/navigation/HistoryStackNavigator";
import ExercisesStackNavigator from "@/navigation/ExercisesStackNavigator";
import GoalsStackNavigator from "@/navigation/GoalsStackNavigator";
import { useTheme } from "@/hooks/useTheme";

export type MainTabParamList = {
  ProgramTab: undefined;
  GoalsTab: undefined;
  HistoryTab: undefined;
  ExercisesTab: undefined;
};

const Tab = createBottomTabNavigator<MainTabParamList>();

export default function MainTabNavigator() {
  const { theme, isDark } = useTheme();

  return (
    <Tab.Navigator
      initialRouteName="ProgramTab"
      screenOptions={{
        tabBarActiveTintColor: theme.tabIconSelected,
        tabBarInactiveTintColor: theme.tabIconDefault,
        tabBarStyle: {
          position: "absolute",
          backgroundColor: Platform.select({
            ios: "transparent",
            android: theme.backgroundRoot,
          }),
          borderTopWidth: 0,
          elevation: 0,
        },
        tabBarBackground: () =>
          Platform.OS === "ios" ? (
            <BlurView
              intensity={100}
              tint={isDark ? "dark" : "light"}
              style={StyleSheet.absoluteFill}
            />
          ) : null,
        headerShown: false,
      }}
    >
      <Tab.Screen
        name="ProgramTab"
        component={ProgramStackNavigator}
        options={{
          title: "Program",
          tabBarIcon: ({ color, size }) => (
            <Feather name="calendar" size={size} color={color} />
          ),
        }}
      />
      <Tab.Screen
        name="GoalsTab"
        component={GoalsStackNavigator}
        options={{
          title: "Goals",
          tabBarIcon: ({ color, size }) => (
            <Feather name="target" size={size} color={color} />
          ),
        }}
      />
      <Tab.Screen
        name="HistoryTab"
        component={HistoryStackNavigator}
        options={{
          title: "History",
          tabBarIcon: ({ color, size }) => (
            <Feather name="bar-chart-2" size={size} color={color} />
          ),
        }}
      />
      <Tab.Screen
        name="ExercisesTab"
        component={ExercisesStackNavigator}
        options={{
          title: "Exercises",
          tabBarIcon: ({ color, size }) => (
            <Feather name="list" size={size} color={color} />
          ),
        }}
      />
    </Tab.Navigator>
  );
}

Tab Screens

ProgramTab shows the workout program calendar:
client/navigation/ProgramStackNavigator.tsx
import React from "react";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import ProgramScreen from "@/screens/ProgramScreen";
import { HeaderTitle } from "@/components/HeaderTitle";
import { useScreenOptions } from "@/hooks/useScreenOptions";

export type ProgramStackParamList = {
  Program: undefined;
};

const Stack = createNativeStackNavigator<ProgramStackParamList>();

export default function ProgramStackNavigator() {
  const screenOptions = useScreenOptions();

  return (
    <Stack.Navigator screenOptions={screenOptions}>
      <Stack.Screen
        name="Program"
        component={ProgramScreen}
        options={{
          headerTitle: () => <HeaderTitle title="Rippler" />,
        }}
      />
    </Stack.Navigator>
  );
}
Uses custom HeaderTitle component for branding

Screen Options Hook

Centralized screen configuration using a custom hook:
client/hooks/useScreenOptions.ts
import { Platform } from "react-native";
import { NativeStackNavigationOptions } from "@react-navigation/native-stack";
import { isLiquidGlassAvailable } from "expo-glass-effect";
import { useTheme } from "@/hooks/useTheme";

interface UseScreenOptionsParams {
  transparent?: boolean;
}

export function useScreenOptions({
  transparent = true,
}: UseScreenOptionsParams = {}): NativeStackNavigationOptions {
  const { theme, isDark } = useTheme();

  return {
    headerTitleAlign: "center",
    headerTransparent: transparent,
    headerBlurEffect: isDark ? "dark" : "light",
    headerTintColor: theme.text,
    headerStyle: {
      backgroundColor: Platform.select({
        ios: undefined,
        android: theme.backgroundRoot,
        web: theme.backgroundRoot,
      }),
    },
    gestureEnabled: true,
    gestureDirection: "horizontal",
    fullScreenGestureEnabled: isLiquidGlassAvailable() ? false : true,
    contentStyle: {
      backgroundColor: theme.backgroundRoot,
    },
  };
}
headerTitleAlign
'center'
Centers header title on all platforms
headerTransparent
boolean
Makes header transparent on iOS (shows blur effect)
headerBlurEffect
'dark' | 'light'
Native blur effect based on color scheme
gestureEnabled
true
Enables swipe-back gesture navigation
contentStyle
object
Sets screen background color to match theme

Type-Safe Navigation

Use TypeScript for compile-time route safety:
Type-Safe Navigation
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { RootStackParamList } from '@/navigation/RootStackNavigator';

type WorkoutNavigationProp = NativeStackNavigationProp<RootStackParamList, 'Workout'>;

function ProgramScreen() {
  const navigation = useNavigation<WorkoutNavigationProp>();
  
  const handleStartWorkout = (week: number, day: string) => {
    // TypeScript ensures we pass correct params
    navigation.navigate('Workout', { week, day });
  };
}

Accessing Route Params

Route Params
import { RouteProp } from '@react-navigation/native';
import { RootStackParamList } from '@/navigation/RootStackNavigator';

type WorkoutRouteProp = RouteProp<RootStackParamList, 'Workout'>;

function WorkoutScreen() {
  const route = useRoute<WorkoutRouteProp>();
  const { week, day } = route.params;
  
  return (
    <View>
      <Text>Week {week} - {day}</Text>
    </View>
  );
}

Going Back

Navigation Back
function WorkoutScreen() {
  const navigation = useNavigation();
  
  const handleComplete = async () => {
    await saveLoggedWorkout(workout);
    navigation.goBack();
  };
}

Tab Navigation

Switching Tabs
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
import { MainTabParamList } from '@/navigation/MainTabNavigator';

type TabNavigationProp = BottomTabNavigationProp<MainTabParamList>;

function ProgramScreen() {
  const navigation = useNavigation<TabNavigationProp>();
  
  const viewHistory = () => {
    navigation.navigate('HistoryTab');
  };
}

Platform-Specific Navigation

iOS Features

Blur Headers

Translucent headers with native blur effect

Swipe Gestures

Full-screen swipe-back gesture support

Blur Tab Bar

Translucent tab bar using BlurView

Safe Areas

Automatic handling of notches and home indicator
iOS Blur Implementation
tabBarBackground: () =>
  Platform.OS === "ios" ? (
    <BlurView
      intensity={100}
      tint={isDark ? "dark" : "light"}
      style={StyleSheet.absoluteFill}
    />
  ) : null,

Android Features

  • Solid backgrounds for better performance
  • Back button hardware support (automatic)
  • Status bar color coordination
  • No blur effects (not supported natively)
Android Tab Bar
tabBarStyle: {
  position: "absolute",
  backgroundColor: Platform.select({
    ios: "transparent",
    android: theme.backgroundRoot, // Solid color
  }),
  borderTopWidth: 0,
  elevation: 0,
},

Deep Linking

Deep linking is not yet implemented but can be added via React Navigation’s linking configuration.
Deep Linking Setup (Future)
const linking = {
  prefixes: ['rippler://', 'https://rippler.app'],
  config: {
    screens: {
      Main: {
        screens: {
          ProgramTab: 'program',
          HistoryTab: 'history',
          GoalsTab: 'goals',
          ExercisesTab: 'exercises',
        },
      },
      Workout: 'workout/:week/:day',
    },
  },
};

<NavigationContainer linking={linking}>
  <RootStackNavigator />
</NavigationContainer>
Navigation state is automatically persisted by React Navigation when the app backgrounds.
To implement custom persistence:
State Persistence
import AsyncStorage from '@react-native-async-storage/async-storage';

const NAVIGATION_STATE_KEY = '@rippler/navigation_state';

function App() {
  const [initialState, setInitialState] = useState();

  useEffect(() => {
    const restoreState = async () => {
      const savedState = await AsyncStorage.getItem(NAVIGATION_STATE_KEY);
      if (savedState) {
        setInitialState(JSON.parse(savedState));
      }
    };
    restoreState();
  }, []);

  const saveState = (state) => {
    AsyncStorage.setItem(NAVIGATION_STATE_KEY, JSON.stringify(state));
  };

  return (
    <NavigationContainer
      initialState={initialState}
      onStateChange={saveState}
    >
      <RootStackNavigator />
    </NavigationContainer>
  );
}

Best Practices

Always define param lists for type safety. This prevents runtime errors from incorrect navigation params.
Use the useScreenOptions hook to maintain consistent navigation styling across all screens.
Keep navigator hierarchy shallow (max 3 levels). Deep nesting causes performance issues.
Don’t navigate during render. Use useEffect or event handlers.
Use navigation.reset() when user logs out to clear the navigation stack.

Troubleshooting

Use useFocusEffect instead of useEffect to run code when screen gains focus:
import { useFocusEffect } from '@react-navigation/native';

useFocusEffect(
  React.useCallback(() => {
    // Runs when screen gains focus
    fetchData();
  }, [])
);
Add bottom padding equal to tab bar height:
<ScrollView contentContainerStyle={{ paddingBottom: 80 }}>
  {/* Content */}
</ScrollView>
Ensure GestureHandlerRootView wraps the entire app in App.tsx:1

Theming

Learn how navigation adapts to theme changes

Screens

Explore individual screen implementations

Build docs developers (and LLMs) love