Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/calm-icons-preserve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'react-native-bottom-tabs': minor
'@bottom-tabs/react-navigation': minor
---

Add an icon rendering mode option for preserving original colors of tab icons.
Binary file added apps/example/assets/avatar-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/example/assets/avatar-4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion apps/example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2884,6 +2884,6 @@ SPEC CHECKSUMS:
SwiftUIIntrospect: fee9aa07293ee280373a591e1824e8ddc869ba5d
Yoga: a3ed390a19db0459bd6839823a6ac6d9c6db198d

PODFILE CHECKSUM: 94c446c3bbb7f59f580c73dea5e50cf22b6ff990
PODFILE CHECKSUM: e153d64da2cc12b425726c29ab2a7d70a1671df5

COCOAPODS: 1.16.2
10 changes: 10 additions & 0 deletions apps/example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import BottomAccessoryView from './Examples/BottomAccessoryView';
import TabBarHidden from './Examples/TabBarHidden';
import CustomTabBar from './Examples/CustomTabBar';
import NativeBottomTabsTabBarHidden from './Examples/NativeBottomTabsTabBarHidden';
import OriginalIconColors from './Examples/OriginalIconColors';
import NativeBottomTabsOriginalIcons from './Examples/NativeBottomTabsOriginalIcons';
import { useLogger } from '@react-navigation/devtools';
import LazyTabs from './Examples/LazyTabs';
import { LogBox } from 'react-native';
Expand Down Expand Up @@ -118,6 +120,10 @@ const examples = [
component: CustomTabBar,
name: 'Custom tabBar',
},
{
component: OriginalIconColors,
name: 'Original icon colors',
},
{
component: FourTabsRippleColor,
name: 'Four Tabs with ripple Color',
Expand Down Expand Up @@ -186,6 +192,10 @@ const examples = [
component: NativeBottomTabsTabBarHidden,
name: 'Native Bottom Tabs with tabBarHidden',
},
{
component: NativeBottomTabsOriginalIcons,
name: 'Native Bottom Tabs with original icons',
},
{ component: NativeBottomTabs, name: 'Native Bottom Tabs' },
{ component: JSBottomTabs, name: 'JS Bottom Tabs' },
{
Expand Down
48 changes: 48 additions & 0 deletions apps/example/src/Examples/NativeBottomTabsOriginalIcons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation';
import { Article } from '../Screens/Article';
import { Albums } from '../Screens/Albums';
import { Contacts } from '../Screens/Contacts';
import { Chat } from '../Screens/Chat';

const Tab = createNativeBottomTabNavigator();

export default function NativeBottomTabsOriginalIcons() {
return (
<Tab.Navigator initialRouteName="Tinted" labeled>
<Tab.Screen
name="Tinted"
component={Article}
options={{
tabBarIcon: () => require('../../assets/avatar-4.png'),
}}
/>
<Tab.Screen
name="Avatar"
component={Contacts}
options={{
tabBarIcon: () => require('../../assets/avatar-1.png'),
tabBarIconRenderingMode: 'original',
}}
/>
<Tab.Screen
name="Album"
component={Albums}
options={{
tabBarIcon: ({ focused }) =>
focused
? require('../../assets/avatar-4.png')
: require('../../assets/avatar-3.png'),
tabBarIconRenderingMode: 'original',
}}
/>
<Tab.Screen
name="Chat"
component={Chat}
options={{
tabBarIcon: () => require('../../assets/avatar-4.png'),
tabBarIconRenderingMode: 'original',
}}
/>
</Tab.Navigator>
);
}
51 changes: 51 additions & 0 deletions apps/example/src/Examples/OriginalIconColors.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import TabView, { SceneMap } from 'react-native-bottom-tabs';
import { useState } from 'react';
import { Article } from '../Screens/Article';
import { Albums } from '../Screens/Albums';
import { Contacts } from '../Screens/Contacts';
import { Chat } from '../Screens/Chat';

const renderScene = SceneMap({
tinted: Article,
avatar: Contacts,
album: Albums,
chat: Chat,
});

export default function OriginalIconColors() {
const [index, setIndex] = useState(0);
const [routes] = useState([
{
key: 'tinted',
title: 'Tinted',
focusedIcon: require('../../assets/avatar-4.png'),
},
{
key: 'avatar',
title: 'Avatar',
focusedIcon: require('../../assets/avatar-1.png'),
iconRenderingMode: 'original',
},
{
key: 'album',
title: 'Album',
unfocusedIcon: require('../../assets/avatar-3.png'),
focusedIcon: require('../../assets/avatar-4.png'),
iconRenderingMode: 'original',
},
{
key: 'chat',
title: 'Chat',
focusedIcon: require('../../assets/avatar-4.png'),
iconRenderingMode: 'original',
},
]);

return (
<TabView
navigationState={{ index, routes }}
onIndexChange={setIndex}
renderScene={renderScene}
/>
);
}
7 changes: 4 additions & 3 deletions apps/example/src/Examples/TintColors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ export default function TintColorsExample() {
{
key: 'albums',
title: 'Albums',
focusedIcon: require('../../assets/icons/grid_dark.png'),
badge: '5',
activeTintColor: 'green',
unfocusedIcon: require('../../assets/avatar-3.png'),
focusedIcon: require('../../assets/avatar-4.png'),
activeTintColor: 'purple',
iconRenderingMode: 'original',
},
{
key: 'contacts',
Expand Down
10 changes: 10 additions & 0 deletions docs/docs/docs/guides/standalone-usage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ Each route in the `routes` array can have the following properties:
- `title`: Display title for the tab
- `focusedIcon`: Icon to show when tab is active
- `unfocusedIcon`: Icon to show when tab is inactive (optional)
- `iconRenderingMode`: Rendering mode for icons. Use `'original'` to preserve multicolor icons instead of applying the native tab tint.
- `badge`: Badge text to display on the tab
- `activeTintColor`: Custom active tint color for this specific tab
- `lazy`: Whether to lazy load this tab's content
Expand Down Expand Up @@ -280,6 +281,15 @@ Function to get the icon for a tab.

- Default: Uses `route.focusedIcon` and `route.unfocusedIcon`

#### `getIconRenderingMode`

Function to get the rendering mode for an image icon.

- Default: Uses `route.iconRenderingMode`
- Options: `'automatic' | 'original'`

Use `original` to preserve the original colors of multicolor image icons, such as logos or branded assets, instead of applying the native tab tint.

#### `getHidden`

Function to determine if a tab should be hidden.
Expand Down
37 changes: 28 additions & 9 deletions docs/docs/docs/guides/usage-with-react-navigation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ Controls how the tab bar behaves when content is scrolled.
- Default: `undefined` (uses system default)

Options:

- `automatic`: Platform determines the behavior
- `onScrollDown`: Tab bar minimizes when scrolling down
- `onScrollUp`: Tab bar minimizes when scrolling up
Expand Down Expand Up @@ -225,15 +226,14 @@ function MyTabBar({ state, descriptors, navigation }) {

function MyTabs() {
return (
<Tab.Navigator
tabBar={(props) => <MyTabBar {...props} />}
>
<Tab.Navigator tabBar={(props) => <MyTabBar {...props} />}>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
);
}
```

#### `renderBottomAccessoryView` <Badge text="iOS" type="info" /> <Badge text="experimental" type="danger"/>

Function that returns a React element to render as [bottom accessory](https://developer.apple.com/documentation/uikit/uitabbarcontroller/bottomaccessory).
Expand Down Expand Up @@ -286,9 +286,29 @@ Function that given `{ focused: boolean }` returns `ImageSource` or `AppleIcon`
SF Symbols are only supported on Apple platforms.
:::

#### `tabBarIconRenderingMode`

Rendering mode for image icons.

- Type: `'automatic' | 'original'`
- Default: `'automatic'`

Use `original` to preserve the original colors of multicolor image icons, such as logos or branded assets, instead of applying the native tab tint.

```tsx
<Tab.Screen
name="Brand"
component={Brand}
options={{
tabBarIcon: () => require('brand-logo.png'),
tabBarIconRenderingMode: 'original',
}}
/>
```

:::warning

Metro transformers that modify the import resolutions of images to something that is *not* React Native's `ImageSource` will break this. A notable community example of such is [react-native-svg-transformer](https://github.com/kristerkari/react-native-svg-transformer).
Metro transformers that modify the import resolutions of images to something that is _not_ React Native's `ImageSource` will break this. A notable community example of such is [react-native-svg-transformer](https://github.com/kristerkari/react-native-svg-transformer).

For best results, avoid the use of such transformers altogether.

Expand All @@ -312,13 +332,13 @@ config.resolver.sourceExts = [...config.resolver.sourceExts, "svg"];
```

Then change your require calls like so:

```
tabBarIcon: () => require('person.svgx')
```

:::


#### `tabBarBadge`

Badge to show on the tab icon.
Expand All @@ -329,14 +349,14 @@ To display a badge without text (just a dot), you need to pass a string with a s

#### `tabBarBadgeBackgroundColor`

- Type: `string`
- Type: `string`

Set the background color for the badge on android.
Uses the system color by default.

#### `tabBarBadgeTextColor`

- Type: `string`
- Type: `string`

Set the text color for the badge on android.
Uses the system color by default.
Expand All @@ -356,6 +376,7 @@ Due to native limitations on iOS, this option doesn't hide the tab item **when h
Whether this screens should render the first time it's accessed. Defaults to true. Set it to false if you want to render the screen on initial render.

#### `freezeOnBlur`

Boolean indicating whether to prevent inactive screens from re-rendering. Defaults to false.

It's working separately from `enableFreeze()` in `react-native-screens`. So settings won't be shared between them.
Expand All @@ -371,7 +392,6 @@ Whether to prevent default tab switching behavior when this tab is pressed. This
Due to iOS 26's new tab switching animations, controlling tab switching from JavaScript can cause significant delays. The `preventsDefault` option allows you to define this behavior statically to avoid animation delays.
:::


#### `tabBarButtonTestID`

Test ID for the tab item. This can be used to find the tab item in the native view hierarchy.
Expand Down Expand Up @@ -413,7 +433,6 @@ React.useEffect(() => {
const unsubscribe = navigation.addListener('tabPress', (e) => {
// Note: For iOS 26+, use the `preventsDefault` option instead of `e.preventDefault()`
// to avoid animation delays

// Do something manually
// ...
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.Context
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.graphics.PorterDuff
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.os.Build
Expand All @@ -19,6 +20,7 @@ import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.MenuItemCompat
import androidx.core.view.forEachIndexed
import coil3.ImageLoader
import coil3.asDrawable
Expand Down Expand Up @@ -246,9 +248,11 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) {
}

menuItem.isVisible = !item.hidden
updateIconTintMode(menuItem, item)
if (iconSources.containsKey(index)) {
getDrawable(iconSources[index]!!) {
menuItem.icon = it
updateIconTintMode(menuItem, item)
}
}

Expand Down Expand Up @@ -297,6 +301,13 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) {
return bottomNavigation.menu.findItem(index) ?: bottomNavigation.menu.add(0, index, 0, title)
}

private fun updateIconTintMode(menuItem: MenuItem, item: TabInfo) {
MenuItemCompat.setIconTintMode(
menuItem,
if (item.iconRenderingMode == "original") PorterDuff.Mode.DST else null
)
}

fun setIcons(icons: ReadableArray?) {
if (icons == null || icons.size() == 0) {
return
Expand All @@ -316,6 +327,9 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) {
bottomNavigation.menu.findItem(idx)?.let { menuItem ->
getDrawable(imageSource) {
menuItem.icon = it
items.getOrNull(idx)?.let { item ->
updateIconTintMode(menuItem, item)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ data class TabInfo(
val badgeBackgroundColor: Int?,
val badgeTextColor: Int?,
val activeTintColor: Int?,
val iconRenderingMode: String?,
val hidden: Boolean,
val testID: String?
)
Expand Down Expand Up @@ -104,6 +105,7 @@ class RCTTabViewManager(context: ReactApplicationContext) :
badgeBackgroundColor = if (item.hasKey("badgeBackgroundColor")) item.getInt("badgeBackgroundColor") else null,
badgeTextColor = if (item.hasKey("badgeTextColor")) item.getInt("badgeTextColor") else null,
activeTintColor = if (item.hasKey("activeTintColor")) item.getInt("activeTintColor") else null,
iconRenderingMode = if (item.hasKey("iconRenderingMode")) item.getString("iconRenderingMode") else null,
hidden = if (item.hasKey("hidden")) item.getBoolean("hidden") else false,
testID = item.getString("testID")
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
lhs.sfSymbol == rhs.sfSymbol &&
lhs.badge == rhs.badge &&
lhs.activeTintColor == rhs.activeTintColor &&
lhs.iconRenderingMode == rhs.iconRenderingMode &&
lhs.hidden == rhs.hidden &&
lhs.testID == rhs.testID &&
lhs.role == rhs.role &&
Expand Down Expand Up @@ -197,6 +198,7 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
badge:RCTNSStringFromStringNilIfEmpty(item.badge)
sfSymbol:RCTNSStringFromStringNilIfEmpty(item.sfSymbol)
activeTintColor:RCTUIColorFromSharedColor(item.activeTintColor)
iconRenderingMode:RCTNSStringFromStringNilIfEmpty(item.iconRenderingMode)
hidden:item.hidden
testID:RCTNSStringFromStringNilIfEmpty(item.testID)
role:RCTNSStringFromStringNilIfEmpty(item.role)
Expand Down Expand Up @@ -265,4 +267,3 @@ - (void)onLayoutWithSize:(CGSize)size reactTag:(NSNumber *)reactTag {
}

#endif // RCT_NEW_ARCH_ENABLED

Loading
Loading