Skip to content

[Error: [RNScreens] Incorrect icon format. You must provide sfSymbolName, imageSource or templateSource.] #3460

@bilal1031

Description

@bilal1031

When using the experimental native bottom tabs API (imported from @react-navigation/bottom-tabs/unstable as documented here) with platform-specific icon configurations, the app crashes with an icon format error. The BottomTabsScreen component only accepts iOS-style icon props (sfSymbolName, imageSource, templateSource) and throws an error when provided with platform-specific icon objects that include ios, android, or shared properties.

Note: I understand this navigator is currently experimental and the API may change in future releases. I'm posting this issue along with a working patch to help others who encounter the same problem while this feature is being developed.

Error message:

[RNScreens] Incorrect icon format. You must provide sfSymbolName, imageSource or templateSource.

What I expected:
Based on the React Navigation documentation, the bottom tabs should support platform-specific icons using Platform.select:

tabBarIcon: Platform.select({
  ios: {
    type: "sfSymbol",
    name: "heart",
  },
  android: {
    type: "drawableResource",
    name: "heart_icon",
  },
});

Or unified platform objects:

tabBarIcon: {
  ios: { type: 'sfSymbol', name: 'house.fill' },
  android: { type: 'drawableResource', name: 'ic_home' },
  shared: { type: 'image', source: require('./home.png') }
}

What happens instead:
The app throws the error above because parseIconToNativeProps in BottomTabsScreen.tsx doesn't recognize the platform-specific icon structure and attempts to access sfSymbolName, imageSource, or templateSource directly on the wrapper object.

Steps to reproduce

  1. Clone the reproduction repository: https://github.com/bilal1031/rnscreens-example-app.git
  2. Install dependencies:
cd rnscreens-example-app
yarn install
  1. Start the app:
yarn start
  1. Run on iOS or Android simulator
  2. The error appears immediately when the tabs try to render

Environment

Package Version
react-native-screens ~4.16.0
react-native 0.81.5
expo ~54.0.27
@react-navigation/bottom-tabs ^7.8.11
@react-navigation/native ^7.1.24

Platform: iOS Simulator (iPhone 17 Pro Max), also reproduces on Android

Working patch

I have created a working patch that fixes this issue. The patch is available in the reproduction repository and can be applied using patch-package.

Patch contents:

Click to expand patch file
diff --git a/node_modules/react-native-screens/src/components/bottom-tabs/BottomTabsScreen.tsx b/node_modules/react-native-screens/src/components/bottom-tabs/BottomTabsScreen.tsx
index ce02d94..22f29f9 100644
--- a/node_modules/react-native-screens/src/components/bottom-tabs/BottomTabsScreen.tsx
+++ b/node_modules/react-native-screens/src/components/bottom-tabs/BottomTabsScreen.tsx
@@ -4,6 +4,7 @@ import React from 'react';
 import { Freeze } from 'react-freeze';
 import {
   Image,
+  Platform,
   StyleSheet,
   findNodeHandle,
   processColor,
@@ -17,6 +18,8 @@ import BottomTabsScreenNativeComponent, {
   type Appearance,
   type ItemAppearance,
   type ItemStateAppearance,
+  type IconProps,
+  type PlatformIcon,
 } from '../../fabric/bottom-tabs/BottomTabsScreenNativeComponent';
 import { featureFlags } from '../../flags';
 import type {
@@ -241,7 +244,7 @@ function shouldFreezeScreen(
   return !nativeViewVisible;
 }

-function parseIconToNativeProps(icon: Icon | undefined): {
+function parseIconToNativeProps(icon: Icon | IconProps | undefined): {
   iconType?: IconType;
   iconImageSource?: ImageSourcePropType;
   iconSfSymbolName?: string;
@@ -250,23 +253,55 @@ function parseIconToNativeProps(icon: Icon | undefined): {
     return {};
   }

-  if ('sfSymbolName' in icon) {
-    // iOS-specific: SFSymbol usage
+  // Check if it's a PlatformIcon (has platform-specific properties)
+  const isPlatformIcon =
+    typeof icon === 'object' &&
+    ('ios' in icon || 'android' in icon || 'shared' in icon);
+  let platformIcon: PlatformIcon | Icon | undefined = undefined;
+
+  if (isPlatformIcon) {
+    // Handle PlatformIcon with platform-specific logic
+    const platformIconTyped = icon as IconProps;
+
+    if (Platform.OS === 'ios') {
+      platformIcon = platformIconTyped.ios ?? platformIconTyped.shared;
+    } else if (Platform.OS === 'android') {
+      platformIcon = platformIconTyped.android ?? platformIconTyped.shared;
+    } else {
+      platformIcon = platformIconTyped.shared;
+    }
+  } else {
+    // Handle Icon type directly
+    platformIcon = icon as PlatformIcon;
+  }
+
+  if (!platformIcon) {
+    return {};
+  }
+
+  if ('sfSymbolName' in platformIcon) {
     return {
       iconType: 'sfSymbol',
-      iconSfSymbolName: icon.sfSymbolName,
+      iconSfSymbolName: platformIcon.sfSymbolName as string,
     };
-  } else if ('imageSource' in icon) {
+  } else if ('type' in platformIcon && platformIcon.type === 'sfSymbol') {
+    return {
+      iconType: 'sfSymbol',
+      iconSfSymbolName: platformIcon.name,
+    };
+  } else if ('imageSource' in platformIcon) {
     return {
       iconType: 'image',
-      iconImageSource: icon.imageSource,
+      iconImageSource: platformIcon.imageSource as ImageSourcePropType,
     };
-  } else if ('templateSource' in icon) {
-    // iOS-specifig: image as a template usage
+  } else if ('templateSource' in platformIcon) {
     return {
       iconType: 'template',
-      iconImageSource: icon.templateSource,
+      iconImageSource: platformIcon.templateSource as ImageSourcePropType,
     };
+  } else if ('type' in platformIcon && platformIcon.type === 'drawableResource') {
+    // Android drawableResource is handled separately through iconResource/iconResourceName
+    return {};
   } else {
     // iOS-specific: SFSymbol, image as a template usage
     throw new Error(
@@ -285,6 +320,7 @@ function parseIconsToNativeProps(
   selectedIconImageSource?: ImageSourcePropType;
   selectedIconSfSymbolName?: string;
 } {
+
   const { iconImageSource, iconSfSymbolName, iconType } =
     parseIconToNativeProps(icon);
   const {
diff --git a/node_modules/react-native-screens/src/fabric/bottom-tabs/BottomTabsScreenNativeComponent.ts b/node_modules/react-native-screens/src/fabric/bottom-tabs/BottomTabsScreenNativeComponent.ts
index 2bc5724..25fa672 100644
--- a/node_modules/react-native-screens/src/fabric/bottom-tabs/BottomTabsScreenNativeComponent.ts
+++ b/node_modules/react-native-screens/src/fabric/bottom-tabs/BottomTabsScreenNativeComponent.ts
@@ -20,6 +20,38 @@ import { UnsafeMixed } from './codegenUtils';
 // iOS-specific: SFSymbol, image as a template usage
 export type IconType = 'image' | 'template' | 'sfSymbol';

+export type SFSIconProps = {
+  type: 'sfSymbol';
+  name: string;
+};
+
+export type TemplateIconProps = {
+  type: 'templateSource';
+  templateSource: ImageSource;
+};
+
+export type ImageIconProps = {
+  type: 'imageSource';
+  imageSource: ImageSource;
+};
+
+export type DrawableIconProps = {
+  type: 'drawableResource';
+  name: string;
+};
+
+export type PlatformIcon =
+  | SFSIconProps
+  | TemplateIconProps
+  | ImageIconProps
+  | DrawableIconProps;
+
+export type IconProps = {
+  ios?: SFSIconProps | TemplateIconProps;
+  android?: DrawableIconProps;
+  shared?: ImageIconProps;
+};
+
 // eslint-disable-next-line @typescript-eslint/ban-types
 type GenericEmptyEvent = Readonly<{}>;

Screens version

~4.16.0

React Native version

0.81.5

Platforms

iOS

JavaScript runtime

None

Workflow

Expo managed workflow

Architecture

Fabric (New Architecture)

Build type

None

Device

iOS simulator

Device model

No response

Acknowledgements

Yes

Metadata

Metadata

Assignees

Labels

Missing infoThe user didn't precise the problem enoughPlatform: iOSThis issue is specific to iOSRepro providedA reproduction with a snack or repo is provided

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions