Skip to content

Background layer behind bottom navigation bar creating cutout effect with app backgroundΒ #87

@Subramanyarao11

Description

@Subramanyarao11

GitHub Issue: react-native-curved-bottom-bar

Title: Critical: Background layer behind bottom navigation bar creating cutout effect with app background

Description:

We're experiencing a critical UI issue with the react-native-curved-bottom-bar library where there's a background layer behind the bottom navigation bar that has the same background color as our app's main content. This layer is creating a "cutout" effect where the app's background is visible through the navigation bar, making navigation items unreadable and creating a poor user experience.

🚨 Issue Details

Problem: There's a background layer that exists behind the bottom navigation bar and circular button that has the same background color as our app's main content. This layer is creating a visual "cutout" effect where the app's background (dark/black textured background) is visible through the white navigation bar, making the navigation items unreadable.

Visual Effect:

  • A background layer exists behind the bottom navigation bar and circular button
  • This layer has the same background color as the app's main content (dark/black textured background)
  • The white navigation bar appears to have "cutouts" where this underlying background layer shows through
  • This effect is most noticeable when on the "More" screen
  • The circular button is not the cause - it's the background layer behind everything

πŸ› οΈ Environment & Versions

  • Library Version: react-native-curved-bottom-bar@3.5.1
  • React Native: 0.73.2
  • Platform: iOS (primary issue observed)
  • Device: iPhone (tested on multiple devices)
  • Safe Area: Using react-native-safe-area-context

πŸ“± Screenshots

Current State: Shows a white bottom navigation bar with four tabs (Home, Approvals, Documents, More), but there's a dark, textured background layer visible behind it that creates a "cutout" effect. The "More" tab is currently selected (purple underline). A circular button with purple/blue gradient is positioned on the right side, but the issue is not with the button overlap - it's with the background layer behind everything.

IOS

IOS Screenshot

Android Screenshot

Another sample screen

πŸ’» Complete Implementation Code

1. BottomTabStack.tsx (Main Component)

import { CurvedBottomBar } from "react-native-curved-bottom-bar";
import React, { useState, useCallback, useRef, useEffect } from "react";
import {
  TouchableOpacity,
  StyleSheet,
  View,
  Platform,
  AppState,
} from "react-native";
import { useSelector, useDispatch } from "react-redux";
import { EVENT_NAME, trackEvent } from "../utils/analytics";
import {
  isChatWindowVisibleSelector,
  isDarkModeSelector,
} from "../containers/redux/selectors";
import { createStructuredSelector } from "reselect";
import { OCARoutes } from "./BottomTabsRoutes";
import { isEmpty } from "lodash";
import { CustomText, IconButtonWrapper } from "../components";
import { RfH, RfW } from "../utils/helpers";
import { Colors, CommonStyles } from "../theme";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { checkUserDataSelector } from "../containers/LoginHome/redux/selectors";
import { doLogout } from "../containers/LoginHome/redux/actions";
import CustomModal from "../components/CustomModal";
import AppPrimaryButton from "../components/AppPrimaryButton";
import AppPrimaryOutLineButton from "../components/AppPrimaryOutLineButton";
import { localize } from "../locale/utils";
import { getColorWithOpacity } from "../utils/helper";
import LottieView from "lottie-react-native";
import { LOTTIE_JSON_FILES } from "../utils/constants";
import { navigate } from "../appContainer/rootNavigation";
import NavigationRouteNames from "./ScreenNames";
import { setIsChatWindowVisible } from "../containers/redux/actions";
import { useRoute } from "@react-navigation/core";
import { isTablet } from "react-native-device-info";

const stateSelector = createStructuredSelector({
  isDarkMode: isDarkModeSelector,
  isChatWindowVisible: isChatWindowVisibleSelector,
  checkUserData: checkUserDataSelector,
});

const BottomTabStack = () => {
  const { isDarkMode, isChatWindowVisible, checkUserData } =
    useSelector(stateSelector);
  const dispatch = useDispatch();
  const insets = useSafeAreaInsets();
  const previousTabRef = useRef<string | null>(null);
  const [isShowModal, setIsShowModal] = useState(false);

  const lottieViewRef = useRef<LottieView>(null);

  const route = useRoute();
  const currentRouteName = route?.name;

  let routes = OCARoutes;
  const regularRoutes = routes.filter((route) => route.name !== "ChatGPT");

  useEffect(() => {
    const handleAppStateChange = (nextAppState: string) => {
      if (nextAppState === "active" && lottieViewRef.current) {
        lottieViewRef.current.reset();
        lottieViewRef.current.play();
      }
    };

    const subscription = AppState.addEventListener(
      "change",
      handleAppStateChange
    );

    return () => {
      subscription?.remove();
    };
  }, []);

  const handleTabPress = useCallback(
    (routeName, navigate) => {
      if (
        routeName === "REWARDS_PROFILE" &&
        checkUserData?.username === "guest.user@cenomi.com"
      ) {
        setIsShowModal(true);
      } else {
        previousTabRef.current = routeName;
        navigate(routeName);
        trackEvent(`${EVENT_NAME.PRESSED_BOTTOM_TAB}${routeName}`);
      }
    },
    [checkUserData]
  );

  const shouldHideTabBar =
    isChatWindowVisible || currentRouteName === NavigationRouteNames.CHAT_GPT;

  if (isEmpty(routes)) return null;

  const tabBackgroundColor = isDarkMode
    ? getColorWithOpacity(Colors.white, 0.2)
    : Colors.white;

  return (
    <>
      <CurvedBottomBar.Navigator
        type="DOWN"
        height={70}
        circleWidth={60}
        circlePosition="RIGHT"
        shadowStyle={
          isDarkMode
            ? {}
            : {
                shadowColor: "#DDDDDD",
                shadowOffset: {
                  width: 0,
                  height: -5,
                },
                shadowOpacity: 1,
                shadowRadius: 5,
              }
        }
        bgColor={tabBackgroundColor}
        initialRouteName={regularRoutes[0]?.name}
        borderTopLeftRight
        screenOptions={{ headerShown: false }}
        style={{
          position: "absolute",
          bottom: 0,
          width: "100%",
          zIndex: 99999,
          overflow: "visible",
          paddingBottom: insets.bottom || RfH(Platform.OS === "ios" ? 30 : 0),
          opacity: shouldHideTabBar ? 0 : 1,
          pointerEvents: shouldHideTabBar ? "none" : "auto",
        }}
        tabBar={({ routeName, selectedTab, navigate }) => {
          if (routeName === "ChatGPT") return <View />;
          const currentRoute = routes.find((r) => r.name === routeName);
          if (!currentRoute) return <View />;

          const isFocused = selectedTab === routeName;
          if (isFocused) {
            previousTabRef.current = routeName;
          }
          const iconName = isFocused
            ? currentRoute.icon.active
            : currentRoute.icon.inactive;
          const { IconComponent } = currentRoute;

          return (
            <TouchableOpacity
              accessibilityRole="button"
              onPress={() => handleTabPress(routeName, navigate)}
              style={[
                styles.tabStyle,
                {
                  borderBottomColor: isFocused
                    ? isDarkMode
                      ? Colors.white
                      : Colors.dark_violet
                    : Colors.transparent,
                  borderBottomWidth: RfH(3),
                },
              ]}
            >
              {IconComponent ? (
                <IconComponent isFocused={isFocused} />
              ) : (
                <IconButtonWrapper
                  iconImage={iconName}
                  iconWidth={RfW(24)}
                  iconHeight={RfH(24)}
                  styling={{ paddingRight: RfW(5) }}
                />
              )}

              <CustomText
                color={isDarkMode ? Colors.white : Colors.black}
                styling={{
                  ...CommonStyles.regularFontStyle,
                  marginTop: RfH(7),
                }}
                fontSize={Platform.OS === "ios" ? 10 : 12}
              >
                {currentRoute?.label}
              </CustomText>
            </TouchableOpacity>
          );
        }}
        renderCircle={({}) => (
          <View
            style={[styles.circleContainer, { right: isTablet() ? -10 : -2 }]}
          >
            <TouchableOpacity
              onPress={() => {
                dispatch(setIsChatWindowVisible.trigger(true));
                navigate(NavigationRouteNames.CHAT_GPT, {
                  previousScreen:
                    previousTabRef.current || regularRoutes[0]?.name,
                });
              }}
              style={styles.circleButton}
            >
              <LottieView
                ref={lottieViewRef}
                source={LOTTIE_JSON_FILES.aiBotJson}
                autoPlay={true}
                loop={true}
                speed={1}
                style={styles.lottieAnimation}
              />
            </TouchableOpacity>
          </View>
        )}
      >
        {routes.map((route) => (
          <CurvedBottomBar.Screen
            key={route.name}
            name={route.name}
            component={route.component}
            position="LEFT"
          />
        ))}
      </CurvedBottomBar.Navigator>

      {isShowModal && (
        <CustomModal
          modalVisible={isShowModal}
          onRequestClose={() => setIsShowModal(false)}
        >
          <>
            <View style={{ marginTop: RfH(22), width: "90%" }}>
              <AppPrimaryButton
                buttonText={localize("common.login")}
                onPress={() => {
                  dispatch(doLogout.trigger());
                  setIsShowModal(false);
                }}
              />
            </View>
            <View
              style={{
                marginTop: RfH(22),
                marginBottom: RfH(12),
                width: "90%",
              }}
            >
              <AppPrimaryOutLineButton
                height={RfH(48)}
                buttonText={localize("common.cancel")}
                onPress={() => setIsShowModal(false)}
              />
            </View>
          </>
        </CustomModal>
      )}
    </>
  );
};

const styles = StyleSheet.create({
  tabStyle: {
    justifyContent: "center",
    alignItems: "center",
    flex: 1,
  },
  circleContainer: {
    position: "absolute",
    alignItems: "center",
    justifyContent: "center",
    bottom: Platform.OS === "ios" ? 35 : 30,
    left: undefined,
  },
  circleButton: {
    height: RfH(70),
    width: RfW(70),
    borderRadius: 35,
    backgroundColor: Colors.transparent,
    justifyContent: "center",
    alignItems: "center",
  },
  lottieAnimation: {
    width: "100%",
    height: "100%",
  },
});

export default BottomTabStack;

2. BottomTabsRoutes.ts (Navigation Configuration)

import { Approvals, Home, MoreOptions } from "../containers";

import ApprovalIconSVG from "../components/SVG/approvalIconSVG";
import BenefitsIconSVG from "../components/SVG/benefitsIconSVG";
import HomeIconSVG from "../components/SVG/homeIconSVG";
import HrRequestIconSVG from "../components/SVG/hrRequestIconSVG";
import MoreOptionsIconSVG from "../components/SVG/moreOptionsIconSVG";
import ProfileIconSVG from "../components/SVG/profileIconSVG";
import RewardsHome from "../containers/RewardsHome";
import RewardsProfile from "../containers/RewardsHome/RewardsProfile";
import { localize } from "../locale/utils";
import { Images } from "../theme";
import { rewardsBottomTabsName } from "../utils/constants";
import NavigationRouteNames from "./ScreenNames";
import Documents from "../containers/Documents";
import DocumentIconSVG from "../components/SVG/documentIconSVG";
import ChatGPT from "../containers/ChatGpt";
import ApprovalNewIconSVG from "../components/SVG/approvalIconNewSVG";

const OCARoutes = [
  {
    name: NavigationRouteNames.HOME,
    component: Home,
    icon: {
      active: Images.homeActiveIcon,
      inactive: Images.homeInActiveIcon,
    },
    label: localize("bottombar.home"),
    IconComponent: HomeIconSVG,
  },
  {
    name: NavigationRouteNames.APPROVALS,
    component: Approvals,
    icon: {
      active: Images.approvalActiveIcon,
      inactive: Images.approvalInactiveIcon,
    },
    label: localize("home.approvals"),
    IconComponent: ApprovalNewIconSVG,
  },
  {
    name: NavigationRouteNames.DOCUMENTS,
    component: Documents,
    icon: {
      active: "",
      inactive: "",
    },
    label: localize("home.documents"),
    IconComponent: DocumentIconSVG,
  },
  {
    name: NavigationRouteNames.MORE_OPTIONS,
    component: MoreOptions,
    icon: {
      active: Images.moreActiveIcon,
      inactive: Images.moreInActiveIcon,
    },
    label: localize("home.more"),
    IconComponent: MoreOptionsIconSVG,
  },
  {
    name: NavigationRouteNames.CHAT_GPT,
    component: ChatGPT,
    icon: {
      active: Images.chat_gpt,
      inactive: Images.chat_gpt,
    },
    label: localize("home.more"),
    IconComponent: null,
  },
];

export { OCARoutes };

3. AppStackRoutes.tsx (Main Navigation Integration)

// Relevant section showing how BottomTabStack is integrated
<Stack.Screen
  name={NavigationRouteNames.HOME_TAB}
  component={BottomTabStack}
  options={{ headerShown: false, gestureEnabled: false }}
/>

🚫 What We've Tried (Failed Attempts)

  1. Removed container background: Eliminated backgroundColor from the main container style
  2. Adjusted positioning: Modified bottom, right, and left values in circleContainer
  3. Z-index management: Set high z-index values (99999)
  4. Overflow handling: Tried different overflow values
  5. Safe area adjustments: Used useSafeAreaInsets() for proper bottom padding
  6. Platform-specific styling: Applied different styles for iOS vs Android

βœ… Expected vs Actual Behavior

Expected:

  • Bottom navigation bar should have a solid white background
  • No underlying background layer should be visible through the navigation bar
  • The navigation bar should appear as a clean, floating element above the app content
  • No "cutout" effects or transparency issues

Actual:

  • There's a background layer behind the navigation bar that has the same color as the app's main background
  • This layer creates a "cutout" effect where the app's background shows through the white navigation bar
  • The effect is most noticeable when on the "More" screen
  • The navigation bar doesn't appear as a clean, floating element

πŸ” Technical Analysis

The issue appears to be in how the library handles:

  1. Background layering: There's an unintended background layer behind the navigation bar
  2. Background color inheritance: This layer is inheriting or being set to the same color as the app's main background
  3. Layer composition: The library is creating multiple background layers instead of a single, clean navigation bar
  4. Background rendering: The navigation bar background is not being rendered as a solid, opaque layer

❓ Questions for Maintainers

  1. Is this a known issue with the current version (3.5.1)?
  2. Why is there a background layer behind the navigation bar that inherits the app's background color?
  3. Are there configuration options to control or disable this background layer?
  4. Is this related to the borderTopLeftRight or bgColor props?
  5. How can we ensure the navigation bar has only its intended background without additional layers?
  6. Are there alternative configurations that avoid creating this background layer?

πŸ§ͺ Reproduction Steps

  1. Implement the CurvedBottomBar.Navigator with the provided configuration
  2. Set up navigation with multiple tabs including a "More" screen
  3. Navigate to the "More" screen where the issue is most visible
  4. Observe the dark background layer visible behind the white navigation bar
  5. Notice the "cutout" effect where the app's background shows through the navigation bar

πŸ”§ Potential Solutions to Investigate

  1. Library-level fix: Remove or make configurable the background layer behind the navigation bar
  2. Configuration options: Add props to control background layer behavior
  3. Background handling: Ensure only the intended navigation bar background is rendered
  4. Layer management: Proper layering without unintended background layers

πŸ“‹ Additional Context

Theme Colors Used:

// From your theme file
Colors.white = "#FFFFFF"
Colors.transparent = "transparent"
Colors.dark_violet = "#6B46C1" // or similar purple color

Helper Functions:

// RfH and RfW are responsive height/width helpers
RfH(70) // Responsive height for 70px
RfW(70) // Responsive width for 70px

Safe Area Handling:

// Using react-native-safe-area-context
const insets = useSafeAreaInsets();
paddingBottom: insets.bottom || RfH(Platform.OS === "ios" ? 30 : 0)

Note: This is a critical UI issue that significantly impacts the user experience. The navigation bar appears to have an unintended background layer that creates visual inconsistencies and makes the interface look unpolished. We're willing to provide additional code samples, screenshots, or test cases if needed to help resolve this issue.

Priority: High - This affects the core navigation experience and visual quality of our app.

Repository: react-native-curved-bottom-bar

Labels: bug, ui, critical, ios, navigation, background-layer

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions