react-native-paper icon indicating copy to clipboard operation
react-native-paper copied to clipboard

createMaterialBottomTabNavigator isn't working with expo-router and nested navigation

Open flyingL123 opened this issue 1 year ago • 29 comments

Current behaviour

I am attempting to nest a stack navigator inside one of the tabs of a Material Bottom Tab Navigator in a project that uses expo-router. Navigating between tabs works as expected, but, on the third tab I created a nested stack navigator. The tab contains a link to "Go to another page", which links to the next screen in the stack. However, when I click the link, no navigation takes place. On web, I see the URL does change as expected, but the screen does not change.

When I comment out my the material tab navigator, and instead use the built-in Tabs from expo-router, now the stack navigation inside the third tab works as expected.

Expected behaviour

I expect the stack navigation in tab 3 to work properly using createMaterialBottomTabNavigator.

How to reproduce?

I created this repo using the command npx create-expo-app@latest --template tabs

https://github.com/flyingL123/expo-nav-test

I made a few minor adjustments to the app that was created in order to add a 3rd tab with a nested stack navigator.

To reproduce the problem, you just need to clone that repo, install dependencies, and run the app with npx expo start. View the app, click on tab 2, click on tab 3. Tab navigation works as expected. While on tab 3, click the link that says "Go to another page". The stack navigator will take you to the page.

Now, open file app/(tabs)/_layout.tsx. Comment out lines 28-71 (the Tabs element). Then uncomment lines 73-96, in order to use the Material Tabs instead.

Reload the app, click on tab 2, click on tab 3. That all still works. However, click on the "Go to another page" link, and you will not navigate as expected. This is the bug. I think there may be an issue with the Material Bottom Tabs Navigator and expo-router having to do with stack navigators nested in the tabs.

Preview

Here is a screen recording of me doing the steps above to demonstrate the issue:

https://youtu.be/Hsbf8CT1RQg

What have you tried so far?

I have been searching for documentation, watching video clips, trying to find my own workarounds. I can't get it to work so I am wondering if there may be an issue with the library.

Your Environment

software version
ios latest through expo
android latest through expo
react-native 0.74.5
react-native-paper ^5.12.5
node 20.17.0
npm or yarn 10.8.2
expo sdk ~51.0.28

flyingL123 avatar Sep 14 '24 03:09 flyingL123

@flyingL123 This reads as though it is a react-navigation or expo-router issue regarding navigation.

What leads you to think it is a react-native-paper issue? If there's no clear indication that it is related to RNP, please close this issue.

gregfenton avatar Oct 05 '24 17:10 gregfenton

@gregfenton If you watch the video or follow the steps I outlined, you will see there is only a bug when the Material Bottom Tabs Navigator is used. Simply swapping out Material Bottom Tabs Navigator and using the default tabs instead, the bug goes away. So I assumed the issue has to be related to Material Bottom Tabs Navigator. I really don't know enough to offer more than that, but I think I pretty clearly isolated the issue down to Material Bottom Tabs Navigator.

flyingL123 avatar Oct 05 '24 17:10 flyingL123

Okay thanks for the clarification. I see it now. Not too surprising that this has issues. RNP is wrapping functionality from React-Navigation, and Expo-Router is wrapping React-Navigation. So trying to get RNP to work with Expo-Router is going to be a confusing mess.

My suspicion is that the "right fix" for this is to simply document "if you want bottom buttons with Expo-Router, use its bottom buttons and not RNP's" ?

gregfenton avatar Oct 05 '24 19:10 gregfenton

I guess if it’s really that difficult to make it work, then yea, but the RNP tabs are really nice. Especially the little animation when the highlighted tab changes. I must have spent 2 days trying to get it to work before giving up and just using the default tabs. That’s how much better I liked the RNP ones :)

flyingL123 avatar Oct 05 '24 20:10 flyingL123

I really wanted this to work, so I looked at what expo-router does and came up with this simple but working solution:

import { Tabs, withLayoutContext } from "expo-router";
import { createMaterialBottomTabNavigator } from "react-native-paper/react-navigation"

const MaterialTabs = withLayoutContext(createMaterialBottomTabNavigator().Navigator)
MaterialTabs.Screen = Tabs.Screen   

Then you can use MaterialTabs as you would use expo-router tabs:

    <MaterialTabs>
        <MaterialTabs.Screen name="dirname">

And can put your <Stack /> as you would in dirname/_layout.tsx

Shall I submit a doc PR?

TamasSzigeti avatar Nov 30 '24 17:11 TamasSzigeti

In order to use the expo-router provided Tabs, you can use BottomNavigation.Bar like so:

import { Tabs } from "expo-router";
import { CommonActions } from "@react-navigation/core";
import { PropsWithChildren } from "react";
import { BottomNavigation, BottomNavigationProps } from "react-native-paper";

export type MaterialBottomTabsProps = PropsWithChildren<
  Omit<
    BottomNavigationProps<any>,
    | "navigationState"
    | "safeAreaInsets"
    | "onTabPress"
    | "renderIcon"
    | "getLabelText"
    | "onIndexChange"
    | "renderScene"
  >
>;

export function MaterialBottomTabs({
  children,
  ...props
}: MaterialBottomTabsProps) {
  return (
    <Tabs
      screenOptions={{
        headerShown: false,
      }}
      tabBar={({ navigation, state, descriptors, insets }) => (
        <BottomNavigation.Bar
          {...props}
          navigationState={state}
          safeAreaInsets={insets}
          onTabPress={({ route, preventDefault }) => {
            const event = navigation.emit({
              type: "tabPress",
              target: route.key,
              canPreventDefault: true,
            });

            if (event.defaultPrevented) {
              preventDefault();
            } else {
              navigation.dispatch({
                ...CommonActions.navigate(route.name, route.params),
                target: state.key,
              });
            }
          }}
          renderIcon={({ route, focused, color }) => {
            const { options } = descriptors[route.key];
            if (options.tabBarIcon) {
              return options.tabBarIcon({ focused, color, size: 24 });
            }

            return null;
          }}
          getLabelText={({ route }) => {
            const { options } = descriptors[route.key];
            const label =
              options.tabBarLabel !== undefined
                ? options.tabBarLabel
                : options.title !== undefined
                  ? options.title
                  : "title" in route
                    ? route.title
                    : route.name;

            return String(label);
          }}
        />
      )}
    >
      {children}
    </Tabs>
  );
}

MaterialBottomTabs.Screen = Tabs.Screen;

You can then use Tabs as documented, and it takes props for the bar:

import { MaterialBottomTabs as Tabs } from "[...]";

// ...

  <Tabs
    activeIndicatorStyle={{ backgroundColor: routeColor }}
    barStyle={{
      alignContent: "center",
      backgroundColor,
      elevation: 2,
      zIndex: 2,
    }}
    compact
    shifting
    sceneAnimationEnabled={false}
    activeColor={activeColor}
    inactiveColor={inactiveColor}
  >
      <Tabs.Screen
        name="Home"
        options={{
          tabBarLabel: 'Home',
          tabBarIcon: ({ color, size }) => {
            return <Icon name="home" size={size} color={color} />;
          },
        }}
        href="[...]"
      />

      <Tabs.Screen
        name="Settings"
        options={{
          tabBarLabel: 'Settings',
          tabBarIcon: ({ color, size }) => {
            return <Icon name="cog" size={size} color={color} />;
          },
        }}
        href="[...]"
      />
  </Tabs>

SleeplessByte avatar Jan 07 '25 02:01 SleeplessByte

@SleeplessByte Might need a few adjustments to support AppBar, but apart from that it's working wonderfully. Thank you!

leifniem avatar Jan 07 '25 18:01 leifniem

As in use the integrated react router appbar?

I never use that hence the header shown false 🤝😁

SleeplessByte avatar Jan 07 '25 18:01 SleeplessByte

I tried it and it works to a degree. The only thing i couldn't manage to get working was respecting the TabBar labels in the header. So nothing too bad 😄

leifniem avatar Jan 07 '25 18:01 leifniem

Ah yeah sorry, I don't use that Header. Always mount it myself in the screen ;)

SleeplessByte avatar Jan 09 '25 18:01 SleeplessByte

How did you get away with not passing in these required props?

Image

leekaiwei avatar Jan 10 '25 23:01 leekaiwei

You need to add at least one child (a tab).

SleeplessByte avatar Jan 11 '25 02:01 SleeplessByte

Yeah I did. I even tried copying and pasting your example code and it was the same error.

Image

leekaiwei avatar Jan 13 '25 22:01 leekaiwei

It looks like you're not importing Tabs from the new file:

import { MaterialBottomTabs as Tabs } from "[...]";

This should replace whatever other Tabs import you previously had.

SleeplessByte avatar Jan 14 '25 02:01 SleeplessByte

I did do that, unless I am doing it incorrectly?

Image

leekaiwei avatar Jan 14 '25 23:01 leekaiwei

That looks right. Can you update the file you import to:

export type MaterialBottomTabsProps = PropsWithChildren<
  Omit<
    BottomNavigationProps<any>,
    | "navigationState"
    | "safeAreaInsets"
    | "onTabPress"
    | "renderIcon"
    | "getLabelText"
    | "onIndexChange"
    | "renderScene"
  >
>;

export function MaterialBottomTabs({
  children,
  ...props
}: MaterialBottomTabsProps) {
  // ...same as before

The props excluded are being set by the component, so the type, indeed, was not correct.

SleeplessByte avatar Jan 15 '25 03:01 SleeplessByte

Still appears to have the same error.

Image

leekaiwei avatar Jan 16 '25 06:01 leekaiwei

I cannot reproduce that, as children is supposed to be supplied by PropsWithChildren and should be { children?: React.ReactNode | undefined; }, which is definitely not { children: Element }.

Perhaps a different version or tsconfig mismatch.

However, you can safely Omit children from the inner type, e.g.

export type MaterialBottomTabsProps = PropsWithChildren<
  Omit<
    BottomNavigationProps<any>,
    | "navigationState"
    | "safeAreaInsets"
    | "onTabPress"
    | "renderIcon"
    | "getLabelText"
    | "onIndexChange"
    | "renderScene"
    | "children" // <-- this
  >
>;

That should not do anything because:

children does not exist on BottomNavigationProps

Alternatively you can not use PropsWithChildren and manually add it:

export type MaterialBottomTabsProps = Omit<
  BottomNavigationProps<any>,
  | "navigationState"
  | "safeAreaInsets"
  | "onTabPress"
  | "renderIcon"
  | "getLabelText"
  | "onIndexChange"
  | "renderScene"
> & { children?: React.ReactNode | undefined }

If that works, then the issue is with the react version where PropsWithChildren must be imported from or the typescript version or configuration that doesn't correctly interpret the tsx syntax (the nested <...> inside <Tabs> should have been picked up as children.

Finally, as you need to return Element, you could always restructure your tabs with the children props:

  <Tabs
    children={[
      <Tabs.Screen
        name="Home"
        options={{
          tabBarLabel: 'Home',
          tabBarIcon: ({ color, size }) => {
            return <Icon name="home" size={size} color={color} />;
          },
        }}
        href="[...]"
      />

      <Tabs.Screen
        name="Settings"
        options={{
          tabBarLabel: 'Settings',
          tabBarIcon: ({ color, size }) => {
            return <Icon name="cog" size={size} color={color} />;
          },
        }}
        href="[...]"
      />
  </Tabs>]} />

Or if it doesn't take an Array for you, wrap it in <Fragment>.

SleeplessByte avatar Jan 16 '25 12:01 SleeplessByte

Thank you very much for your help. I have tried all those solutions and they all work. It's enough to get me going, very much appreciate it. Just a heads up, I do get this warning if you have time, otherwise thank you very much for your time again!

Warning: A props object containing a "key" prop is being spread into JSX:
  let props = {key: someKey, route: ..., borderless: ..., centered: ..., rippleColor: ..., onPress: ..., onLongPress: ..., testID: ..., accessibilityLabel: ..., accessibilityRole: ..., accessibilityState: ..., style: ..., children: ...};
  <Touchable {...props} />
React keys must be passed directly to JSX without using spread:
  let props = {route: ..., borderless: ..., centered: ..., rippleColor: ..., onPress: ..., onLongPress: ..., testID: ..., accessibilityLabel: ..., accessibilityRole: ..., accessibilityState: ..., style: ..., children: ...};
  <Touchable key={someKey} {...props} />
    in BottomNavigation.Bar (created by SafeAreaInsetsContext)

leekaiwei avatar Jan 18 '25 10:01 leekaiwei

There's two PR open to address that. One of them has a patch package fix you can apply today: https://github.com/callstack/react-native-paper/pull/4494#discussion_r1893017286

SleeplessByte avatar Jan 18 '25 12:01 SleeplessByte

After huge battles to make this work with custom google material icons used as bottom icons, as well as changing the barStyle background color properly, etc., heres the final version of mine based off of the solution @SleeplessByte :

tabs layout:

import * as React from "react";
import { Image, type ImageSourcePropType } from "react-native";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { MaterialBottomTabs as Tabs } from "../../navigation/MaterialBottomTabs";

type TabBarIconProps = {
  source: ImageSourcePropType;
  color: string;
  focused: boolean;
};

export const TabBarIcon = ({ source, color, focused }: TabBarIconProps) => (
  <Image
    source={source}
    style={{
      width: 24,
      height: 24,
      tintColor: color,
      opacity: focused ? 1 : 0.6,
    }}
  />
);

function TabsLayout() {
  return (
    <Tabs
      activeColor="white"
      inactiveColor="white"
      theme={{
        colors: {
          secondaryContainer: "#2D5FA7",
        },
      }}
    >
      <Tabs.Screen
        name="index"
        options={{
          tabBarIcon: ({ color, focused }) => (
            <TabBarIcon
              source={require("../../assets/images/boat.png")}
              color={color}
              focused={focused}
            />
          ),
        }}
      />
      <Tabs.Screen
        name="supply"
        options={{
          title: "Supply",
          tabBarIcon: ({ color, focused }) => (
            <MaterialCommunityIcons
              name="ship-wheel"
              color={color}
              size={24}
              style={{
                color: color,
                opacity: focused ? 1 : 0.6,
              }}
              focused={focused}
            />
          ),
        }}
      />
      <Tabs.Screen
        name="services"
        options={{
          tabBarIcon: ({ color, focused }) => (
            <TabBarIcon
              source={require("../../assets/images/service.png")}
              color={color}
              focused={focused}
            />
          ),
        }}
      />
      <Tabs.Screen
        name="support"
        options={{
          tabBarIcon: ({ color, focused }) => (
            <TabBarIcon
              source={require("../../assets/images/support.png")}
              color={color}
              focused={focused}
            />
          ),
        }}
      />
    </Tabs>
  );
}

export default TabsLayout;

and MaterialBottomTabs file:

import { Tabs } from "expo-router";
import { CommonActions } from "@react-navigation/core";
import { PropsWithChildren } from "react";
import { BottomNavigation, BottomNavigationProps } from "react-native-paper";
import { useAppTheme } from "@/utils/theme";

export type MaterialBottomTabsProps = PropsWithChildren<
  Omit<
    BottomNavigationProps<any>,
    | "navigationState"
    | "safeAreaInsets"
    | "onTabPress"
    | "renderIcon"
    | "getLabelText"
    | "onIndexChange"
    | "renderScene"
  >
>;

export function MaterialBottomTabs({
  children,
  ...props
}: MaterialBottomTabsProps) {
  const theme = useAppTheme();

  return (
    <Tabs
      screenOptions={{
        headerShown: false,
      }}
      tabBar={({ navigation, state, descriptors, insets }) => (
        <BottomNavigation.Bar
          {...props}
          navigationState={state}
          safeAreaInsets={insets}
          style={{
            backgroundColor: theme.colors.background,
          }}
          onTabPress={({ route, preventDefault }) => {
            const event = navigation.emit({
              type: "tabPress",
              target: route.key,
              canPreventDefault: true,
            });

            if (event.defaultPrevented) {
              preventDefault();
            } else {
              navigation.dispatch({
                ...CommonActions.navigate(route.name, route.params),
                target: state.key,
              });
            }
          }}
          renderIcon={({ route, focused, color }) => {
            const { options } = descriptors[route.key];

            if (options.tabBarIcon) {
              return options.tabBarIcon({ focused, color, size: 24 });
            }

            return null;
          }}
          getLabelText={({ route }) => {
            const { options } = descriptors[route.key];
            const label =
              options.tabBarLabel !== undefined
                ? options.tabBarLabel
                : options.title !== undefined
                ? options.title
                : "title" in route
                ? route.title
                : route.name;

            return String(label);
          }}
        />
      )}
    >
      {children}
    </Tabs>
  );
}

MaterialBottomTabs.Screen = Tabs.Screen;

MatkoMilic avatar Mar 17 '25 18:03 MatkoMilic

I really wanted this to work, so I looked at what expo-router does and came up with this simple but working solution:

import { Tabs, withLayoutContext } from "expo-router";
import { createMaterialBottomTabNavigator } from "react-native-paper/react-navigation"

const MaterialTabs = withLayoutContext(createMaterialBottomTabNavigator().Navigator)
MaterialTabs.Screen = Tabs.Screen   

Then you can use MaterialTabs as you would use expo-router tabs:

    <MaterialTabs>
        <MaterialTabs.Screen name="dirname">

And can put your <Stack /> as you would in dirname/_layout.tsx

Shall I submit a doc PR?

This is the best answer. It's the simplest one.

rvmelo avatar Apr 04 '25 20:04 rvmelo

I was able to get this working correctly except for one thing: my app has a conditional tab which should only show at certain times. Expo Tabs documentation indicates passing href=null in the options: https://docs.expo.dev/router/advanced/tabs/#hiding-a-tab

This doesn't seem to be working with RNP, as if I remove the RNP tabBar, the tab is correctly hidden

cezard avatar Apr 13 '25 02:04 cezard

I was able to get the href=null option working by slightly tweaking the custom MaterialBottomTabs component @SleeplessByte shared:

import React from "react"; 
import { Tabs } from "expo-router";
import { CommonActions } from "@react-navigation/core";
import { PropsWithChildren } from "react";
import { BottomNavigation, BottomNavigationProps, useTheme } from "react-native-paper";

export type MaterialBottomTabsProps = PropsWithChildren<
  Omit<
    BottomNavigationProps<any>,
    | "navigationState"
    | "safeAreaInsets"
    | "onTabPress"
    | "renderIcon"
    | "getLabelText"
    | "onIndexChange"
    | "renderScene"
  >
>;

export function MaterialBottomTabs({
  children,
  ...props
}: MaterialBottomTabsProps) {
  const theme = useTheme();
 
  //Building list of routes that need to be excluded
  const validChildren = React.Children.toArray(children).filter(React.isValidElement) as React.ReactElement[];

  const excludeRoutes = validChildren
    .filter(child => child.props.options?.href === null)
    .map(child => child.props.name);

  const updateState = (state: any) => {
    const filteredRoutes = state.routes.filter((route: any) => !excludeRoutes.includes(route.name));

    // Recalculate the active index. If the active route was filtered, default to the first route.
    const currentActiveRoute = state.routes[state.index];
    let updatedIndex = filteredRoutes.findIndex((route: any) => route.key === currentActiveRoute?.key);
    if (updatedIndex === -1) {
      updatedIndex = 0;
    }

    return {
      ...state,
      routes: filteredRoutes,
      index: updatedIndex,
    };
  };

  return (
    <Tabs
      screenOptions={{
        headerShown: false,
      }}
      tabBar={({ navigation, state, descriptors, insets }) => {

         //Updating the navigationState with the updated state
        const modifiedState = updateState(state);

        return (
          <BottomNavigation.Bar
            {...props}
            navigationState={modifiedState}
            safeAreaInsets={insets}
            style={{
              backgroundColor: theme.colors.background,
            }}
            onTabPress={({ route, preventDefault }) => {
              const event = navigation.emit({
                type: "tabPress",
                target: route.key,
                canPreventDefault: true,
              });

              if (event.defaultPrevented) {
                preventDefault();
              } else {
                navigation.dispatch({
                  ...CommonActions.navigate(route.name, route.params),
                  target: state.key,
                });
              }
            }}
            renderIcon={({ route, focused, color }) => {
              const { options } = descriptors[route.key];

              if (options.tabBarIcon) {
                return options.tabBarIcon({ focused, color, size: 24 });
              }

              return null;
            }}
            getLabelText={({ route }) => {
              const { options } = descriptors[route.key];
              const label =
                options.tabBarLabel !== undefined
                  ? options.tabBarLabel
                  : options.title !== undefined
                  ? options.title
                  : "title" in route
                  ? route.title
                  : route.name;

              return String(label);
            }}
          />
        );
      }
    }
    >
      {children}
    </Tabs>
  );
}

MaterialBottomTabs.Screen = Tabs.Screen;

cezard avatar Apr 13 '25 23:04 cezard

In order to use the expo-router provided Tabs, you can use BottomNavigation.Bar like so:

import { Tabs } from "expo-router"; import { CommonActions } from "@react-navigation/core"; import { PropsWithChildren } from "react"; import { BottomNavigation, BottomNavigationProps } from "react-native-paper";

export type MaterialBottomTabsProps = PropsWithChildren< Omit< BottomNavigationProps, | "navigationState" | "safeAreaInsets" | "onTabPress" | "renderIcon" | "getLabelText" | "onIndexChange" | "renderScene"

;

export function MaterialBottomTabs({ children, ...props }: MaterialBottomTabsProps) { return ( <Tabs screenOptions={{ headerShown: false, }} tabBar={({ navigation, state, descriptors, insets }) => ( <BottomNavigation.Bar {...props} navigationState={state} safeAreaInsets={insets} onTabPress={({ route, preventDefault }) => { const event = navigation.emit({ type: "tabPress", target: route.key, canPreventDefault: true, });

        if (event.defaultPrevented) {
          preventDefault();
        } else {
          navigation.dispatch({
            ...CommonActions.navigate(route.name, route.params),
            target: state.key,
          });
        }
      }}
      renderIcon={({ route, focused, color }) => {
        const { options } = descriptors[route.key];
        if (options.tabBarIcon) {
          return options.tabBarIcon({ focused, color, size: 24 });
        }

        return null;
      }}
      getLabelText={({ route }) => {
        const { options } = descriptors[route.key];
        const label =
          options.tabBarLabel !== undefined
            ? options.tabBarLabel
            : options.title !== undefined
              ? options.title
              : "title" in route
                ? route.title
                : route.name;

        return String(label);
      }}
    />
  )}
>
  {children}
</Tabs>

); }

MaterialBottomTabs.Screen = Tabs.Screen; You can then use Tabs as documented, and it takes props for the bar:

import { MaterialBottomTabs as Tabs } from "[...]";

// ...

<Tabs activeIndicatorStyle={{ backgroundColor: routeColor }} barStyle={{ alignContent: "center", backgroundColor, elevation: 2, zIndex: 2, }} compact shifting sceneAnimationEnabled={false} activeColor={activeColor} inactiveColor={inactiveColor}

  <Tabs.Screen
    name="Home"
    options={{
      tabBarLabel: 'Home',
      tabBarIcon: ({ color, size }) => {
        return <Icon name="home" size={size} color={color} />;
      },
    }}
    href="[...]"
  />

  <Tabs.Screen
    name="Settings"
    options={{
      tabBarLabel: 'Settings',
      tabBarIcon: ({ color, size }) => {
        return <Icon name="cog" size={size} color={color} />;
      },
    }}
    href="[...]"
  />

Great solution, but how do i use badge here? I am having trouble implementing one.

Michota avatar Apr 18 '25 12:04 Michota

Great solution, but how do i use badge here? I am having trouble implementing one.

To implement badge: getBadge={({ route }) => descriptors[route.key].options.tabBarBadge}

Michota avatar Apr 18 '25 12:04 Michota

Any idea how can I make this work? I wanted to create a navigator with some options prefilled, but it didnt work due to WARN Layout children must be of type Screen, all other children are ignored. To use custom children, create a custom <Layout />... - the Bottom Tabs were not rendering...

import { ComponentProps } from "react";
import MaterialBottomTabs from "../MaterialBottomTabs";
import { MaterialIcon } from "@/src/types/icons";
import TabBarIcon from "../TabBarIcon";

type MaterialTabsScreenProps = ComponentProps<typeof MaterialBottomTabs.Screen>;

interface ScreenProps extends MaterialTabsScreenProps {
  icon: MaterialIcon | ((props: { focused: boolean; color: string; size: number }) => JSX.Element);
}

const Screen = function Screen({ options, icon, ...props }: ScreenProps) {
  return (
    <MaterialBottomTabs.Screen
      {...props}
      options={{
        ...options,
        tabBarIcon: (iconProps) =>
          typeof icon === "function" ? icon(iconProps) : <TabBarIcon {...iconProps} iconName={icon} />,
      }}
    />
  );
};



const Tabs = MaterialBottomTabs as typeof MaterialBottomTabs & {
  Screen: typeof Screen;
};

Tabs.Screen = Screen;

export default Tabs;

Michota avatar Apr 18 '25 13:04 Michota

Just use this native library.

import { withLayoutContext } from "expo-router";
import {
  createNativeBottomTabNavigator,
  NativeBottomTabNavigationOptions,
  NativeBottomTabNavigationEventMap,
} from "@bottom-tabs/react-navigation";
import { ParamListBase, TabNavigationState } from "@react-navigation/native";

const BottomTabNavigator = createNativeBottomTabNavigator().Navigator;

export const Tabs = withLayoutContext<
  NativeBottomTabNavigationOptions,
  typeof BottomTabNavigator,
  TabNavigationState<ParamListBase>,
  NativeBottomTabNavigationEventMap
>(BottomTabNavigator);
import * as React from "react";
import FastImageWrapper from "@/components/FastImageWrapper";
import { FastImageProps } from "@d11/react-native-fast-image";
import { useTheme } from "@react-navigation/native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Tabs } from "@/navigation/Tabs";

type TabBarIconProps = {
  source: FastImageProps["source"];
  color: string;
  focused: boolean;
};

export const TabBarIcon = ({ source, color, focused }: TabBarIconProps) => (
  <FastImageWrapper
    source={source}
    tintColor={color}
    style={{
      width: 24,
      height: 24,
      opacity: focused ? 1 : 0.6,
    }}
  />
);

function TabsLayout() {
  const theme = useTheme();

  return (
    <Tabs
      tabBarStyle={{
        backgroundColor: "#3871C2",
      }}
      activeIndicatorColor={"#2D5FA7"}
      rippleColor={"#3871C2"}
      tabBarInactiveTintColor="rgba(255, 255, 255, 0.6)"
      tabBarActiveTintColor="white"
      labeled
    >
      <Tabs.Screen
        name="index"
        options={{
          title: "Home",
          tabBarIcon: () => require("@/assets/images/boat.png"),
        }}
      />
      <Tabs.Screen
        name="supply"
        options={{
          title: "Supply",
          tabBarIcon: () => require("@/assets/images/anchor.png"),
        }}
      />
      <Tabs.Screen
        name="services"
        options={{
          title: "Services",
          tabBarIcon: () => require("@/assets/images/service.png"),
        }}
      />
      <Tabs.Screen
        name="orders"
        options={{
          title: "Orders",
          tabBarIcon: () => require("@/assets/images/assignment.png"),
        }}
      />
    </Tabs>
  );
}

export default TabsLayout;

MatkoMilic avatar Apr 18 '25 13:04 MatkoMilic

How do i make unlabeled tabs take only as much space as its needed? I want to achieve something like Gmail has:

Image

The styles of buttons are hide in the element that is being rendered by renderTouchable, but you cant access its styles, they are placed in styles inside react-native-paper\src\components\BottomNavigation\BottomNavigationBar.tsx

Michota avatar Apr 18 '25 15:04 Michota

How do i make unlabeled tabs take only as much space as its needed? I want to achieve something like Gmail has:

Image

The styles of buttons are hide in the element that is being rendered by renderTouchable, but you cant access its styles, they are placed in styles inside react-native-paper\src\components\BottomNavigation\BottomNavigationBar.tsx

I also wanted to reduce this spacing, but for the labeled variant. It looks like this can't be changed as of right now.

Although it was pretty straight forward using a patch-package, I think it the library should allow this customization.

joaolfern avatar Jul 15 '25 18:07 joaolfern