1

I'm trying to mimic the Apple Contacts navigation header with search with large title but only on the Home Screen. All other tabs should have their own settings, such as no different title or sometimes even hidden navigation bar. I got it to a solid foundation I think, but the search and large title settings aren't having any effect. The reproducible repo is here.

Generally my structure is an entry point that points to /apps/_layout.tsx:

import { Stack } from 'expo-router';

export default function Layout() {
  return (
    <Stack screenOptions={{ headerShown: false }}>
      <Stack.Screen name="(tabs)" />
    </Stack>
  );
}

The root layout points to the first stack which is a tab view that houses the main app, it is at /apps/(tabs)/_layout.tsx and as follows:

import { BlurView } from 'expo-blur';
import { Tabs } from 'expo-router';
import { SafeAreaProvider } from 'react-native-safe-area-context';

export default function TabsLayout() {
  return (
    <SafeAreaProvider>
      <Tabs
        screenOptions={{
          tabBarStyle: { position: 'absolute', elevation: 0 },
          tabBarBackground: () =>
            <BlurView tint="prominent" intensity={100} style={StyleSheet.absoluteFill} />
        }}>
        <Tabs.Screen name="index" />
        <Tabs.Screen name="tab2" />
      </Tabs>
    </SafeAreaProvider>
  );
}

The first tab is the index page at /apps/(tabs)/index.tsx and as follows:

import { BottomTabBarHeightContext, useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
import { useHeaderHeight } from '@react-navigation/elements';
import { BlurView } from 'expo-blur';
import { Stack } from 'expo-router';
import { ScrollView } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Text, StyleSheet } from 'react-native';

export default function Home() {
  const items = Array.from({ length: 60 }, (_, index) => index);
  const headerHeight = useHeaderHeight();
  const bottomTabBarHeight = useBottomTabBarHeight();

  return (
    <ScrollView style={{ flex: 1, backgroundColor: 'blue' }}>
      <Stack.Screen
        options={{
          headerShown: true,
          headerTransparent: true,
          headerBackground: () =>
            <BlurView tint="prominent" intensity={100} style={StyleSheet.absoluteFill} />
        }}
      />
      <SafeAreaView
          edges={['left', 'right']}
          style={{
            flex: 1,
            backgroundColor: 'green',
            paddingTop: headerHeight,
            paddingBottom: bottomTabBarHeight
          }}
      >
        {items.map((item, index) => (
          <Text key={index} style={{...}}>{`Item ${item}`}</Text>
        ))}
      </SafeAreaView>
    </ScrollView>
  );
}

This got me to a good, native foundation where the content extends under the header and bottom tabs and the content scrolls under it.

enter image description here

It even works in landscape orientation which is a good sign:

enter image description here

Now I'm trying to make the title large and add a search bar in the nav bar like the Apple Contact app:

enter image description here

I tried adding the following in different areas but it has no effect:

headerLargeTitle: true,
headerSearchBarOptions: {
  placeholder: 'Search'
}

The documentation is pretty light, and examples were outdated or not using the file-based Expo Router v3. It is has diverged from React Navigation so the docs there are not aligned with Expo Router's usage. Do I have my structure correct or what am I doing wrong? Any help or insight would be appreciated!

2 Answers 2

2

The Expo Router seems limited since it's new - at the very least, the documentation is incomplete regarding it. To properly achieve what you're trying to do, you need to flip the structure so the stacks are in the tab instead of stack with a tab in it:

<SafeAreaProvider>
<NavigationContainer>
    <Tab.Navigator
    screenOptions={{
        headerShown: false,
        ...(Platform.OS === 'ios'
        ? {
            tabBarStyle: { position: 'absolute', elevation: 0 },
            tabBarBackground: () => (
                <BlurView tint="prominent" intensity={100} style={StyleSheet.absoluteFill} />
            )
            }
        : undefined)
    }}
    >
    <Tab.Screen name="HomeTab" component={HomeStack} />
    <Tab.Screen name="SettingsTab" component={SettingsStack} />
    <Tab.Screen name="MoreTab" component={MoreStack} />
    </Tab.Navigator>
</NavigationContainer>
</SafeAreaProvider>
const Stack = createNativeStackNavigator();

export function Home({ navigation }: NativeStackScreenProps<ParamListBase>) {
  const data = Array.from({ length: 50 });

  useLayoutEffect(() => {
    navigation.setOptions({
      headerSearchBarOptions: {
        onChangeText: (text) => console.log(text)
      }
    });
  }, [navigation]);

  return (
    <View style={{ flex: 1, backgroundColor: 'yellow' }}>
      <Text>Home screen</Text>
      <Button title="Go to Details" onPress={() => navigation.navigate('Details')} />
      {data.map((_, index) => (
        <Text key={index} style={{ padding: 10, fontSize: 18, fontWeight: 'bold', color: 'blue' }}>
          Item {index + 1}
        </Text>
      ))}
    </View>
  );
}

export function HomeStack() {
  return (
    <Stack.Navigator
      screenOptions={{
        headerTransparent: Platform.OS === 'ios',
        headerBlurEffect: 'systemThickMaterial'
      }}
    >
      <Stack.Screen name="Home" component={withScrollStackView(Home)} options={{ headerLargeTitle: true }} />
      <Stack.Screen name="Details" component={withScrollStackView(Details)} />
    </Stack.Navigator>
  );
}
export default function Details() {
  return (
    <View style={{ flex: 1, backgroundColor: 'yellow' }}>
      <Text>Details screen</Text>
    </View>
  );
}

More details can be found directly on the React Navigation documentation, which is a subset of Expo Router: https://reactnavigation.org/docs/tab-based-navigation#a-stack-navigator-for-each-tab

1

@TruMan1's suggestion is correct. It worked for me. Here's an implementation example using Expo's file-based routing.

Folder structure

app
├── (tabs)
│   ├── _layout.tsx
│   ├── index
│   │   ├── _layout.tsx
│   │   └── index.tsx
│   └── settings
│       ├── _layout.tsx
│       └── index.tsx
├── +not-found.tsx
└── _layout.tsx

Files

app/_layout.tsx

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      <Stack.Screen name="+not-found" />
    </Stack>
  );
}

app/(tabs)/_layout.tsx

export default function TabLayout() {
  return (
    <Tabs
      screenOptions={{
        headerShown: false,
        tabBarStyle: { position: "absolute", elevation: 0 },
        tabBarBackground: () => (
          <BlurView
            intensity={80}
            tint={"prominent"}
            style={StyleSheet.absoluteFill}
          />
        ),
      }}
    >
      <Tabs.Screen name="index" options={{ title: "Home" }} />
      <Tabs.Screen name="settings" options={{ title: "Settings" }} />
    </Tabs>
  );
}

app/(tabs)/index/_layout.tsx

export default function HomeLayout() {
  return (
    <Stack>
      <Stack.Screen
        name="index"
        options={{
          headerShown: true,
          headerLargeTitle: true,
          headerTitle: "Home",
        }}
      />
    </Stack>
  );
}

app/(tabs)/index/index.tsx

import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs";

export default function HomeScreen({ children }: Props) {
  const bottomTabHeight = useBottomTabBarHeight();
  return (
    <ScrollView contentInsetAdjustmentBehavior="automatic">
      <View style={{ paddingBottom: bottomTabHeight }}>{children}</View>
    </ScrollView>
  );
}

app/(tabs)/settings/_layout.tsx

export default function SettingsLayout() {
  return (
    <Stack>
      <Stack.Screen
        name="index"
        options={{
          headerShown: true,
          headerLargeTitle: true,
          headerTitle: "Settings",
        }}
      />
    </Stack>
  );
}

app/(tabs)/settings/index.tsx

import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs";

export default function SettingsScreen({ children }: Props) {
  const bottomTabHeight = useBottomTabBarHeight();
  return (
    <ScrollView contentInsetAdjustmentBehavior="automatic">
      <View style={{ paddingBottom: bottomTabHeight }}>{children}</View>
    </ScrollView>
  );
}
1
  • Thanks it's working but title is starting collapsed (instead of expanded and big). I need to scroll up for the large title to expand. Do you have the same issue ?
    – bulby97
    Commented Jun 20 at 10:38

Not the answer you're looking for? Browse other questions tagged or ask your own question.