Skip to content

Commit 758746c

Browse files
authored
feat: render colored icons with iconRenderingMode original (#542)
* feat(iOS): render colored icons with iconRenderingMode alwaysOriginal * chore: fix alwaysOriginal tab label color when hovering * chore: support iconRenderingMode on Android * chore: rename alwaysOriginal option to original * chore: cleanup
1 parent 592ab6a commit 758746c

24 files changed

Lines changed: 266 additions & 30 deletions

.changeset/calm-icons-preserve.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'react-native-bottom-tabs': minor
3+
'@bottom-tabs/react-navigation': minor
4+
---
5+
6+
Add an icon rendering mode option for preserving original colors of tab icons.

apps/example/assets/avatar-3.png

169 KB
Loading

apps/example/assets/avatar-4.png

482 KB
Loading

apps/example/ios/Podfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2884,6 +2884,6 @@ SPEC CHECKSUMS:
28842884
SwiftUIIntrospect: fee9aa07293ee280373a591e1824e8ddc869ba5d
28852885
Yoga: a3ed390a19db0459bd6839823a6ac6d9c6db198d
28862886

2887-
PODFILE CHECKSUM: 94c446c3bbb7f59f580c73dea5e50cf22b6ff990
2887+
PODFILE CHECKSUM: e153d64da2cc12b425726c29ab2a7d70a1671df5
28882888

28892889
COCOAPODS: 1.16.2

apps/example/src/App.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ import BottomAccessoryView from './Examples/BottomAccessoryView';
4343
import TabBarHidden from './Examples/TabBarHidden';
4444
import CustomTabBar from './Examples/CustomTabBar';
4545
import NativeBottomTabsTabBarHidden from './Examples/NativeBottomTabsTabBarHidden';
46+
import OriginalIconColors from './Examples/OriginalIconColors';
47+
import NativeBottomTabsOriginalIcons from './Examples/NativeBottomTabsOriginalIcons';
4648
import { useLogger } from '@react-navigation/devtools';
4749
import LazyTabs from './Examples/LazyTabs';
4850
import { LogBox } from 'react-native';
@@ -118,6 +120,10 @@ const examples = [
118120
component: CustomTabBar,
119121
name: 'Custom tabBar',
120122
},
123+
{
124+
component: OriginalIconColors,
125+
name: 'Original icon colors',
126+
},
121127
{
122128
component: FourTabsRippleColor,
123129
name: 'Four Tabs with ripple Color',
@@ -186,6 +192,10 @@ const examples = [
186192
component: NativeBottomTabsTabBarHidden,
187193
name: 'Native Bottom Tabs with tabBarHidden',
188194
},
195+
{
196+
component: NativeBottomTabsOriginalIcons,
197+
name: 'Native Bottom Tabs with original icons',
198+
},
189199
{ component: NativeBottomTabs, name: 'Native Bottom Tabs' },
190200
{ component: JSBottomTabs, name: 'JS Bottom Tabs' },
191201
{
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation';
2+
import { Article } from '../Screens/Article';
3+
import { Albums } from '../Screens/Albums';
4+
import { Contacts } from '../Screens/Contacts';
5+
import { Chat } from '../Screens/Chat';
6+
7+
const Tab = createNativeBottomTabNavigator();
8+
9+
export default function NativeBottomTabsOriginalIcons() {
10+
return (
11+
<Tab.Navigator initialRouteName="Tinted" labeled>
12+
<Tab.Screen
13+
name="Tinted"
14+
component={Article}
15+
options={{
16+
tabBarIcon: () => require('../../assets/avatar-4.png'),
17+
}}
18+
/>
19+
<Tab.Screen
20+
name="Avatar"
21+
component={Contacts}
22+
options={{
23+
tabBarIcon: () => require('../../assets/avatar-1.png'),
24+
tabBarIconRenderingMode: 'original',
25+
}}
26+
/>
27+
<Tab.Screen
28+
name="Album"
29+
component={Albums}
30+
options={{
31+
tabBarIcon: ({ focused }) =>
32+
focused
33+
? require('../../assets/avatar-4.png')
34+
: require('../../assets/avatar-3.png'),
35+
tabBarIconRenderingMode: 'original',
36+
}}
37+
/>
38+
<Tab.Screen
39+
name="Chat"
40+
component={Chat}
41+
options={{
42+
tabBarIcon: () => require('../../assets/avatar-4.png'),
43+
tabBarIconRenderingMode: 'original',
44+
}}
45+
/>
46+
</Tab.Navigator>
47+
);
48+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import TabView, { SceneMap } from 'react-native-bottom-tabs';
2+
import { useState } from 'react';
3+
import { Article } from '../Screens/Article';
4+
import { Albums } from '../Screens/Albums';
5+
import { Contacts } from '../Screens/Contacts';
6+
import { Chat } from '../Screens/Chat';
7+
8+
const renderScene = SceneMap({
9+
tinted: Article,
10+
avatar: Contacts,
11+
album: Albums,
12+
chat: Chat,
13+
});
14+
15+
export default function OriginalIconColors() {
16+
const [index, setIndex] = useState(0);
17+
const [routes] = useState([
18+
{
19+
key: 'tinted',
20+
title: 'Tinted',
21+
focusedIcon: require('../../assets/avatar-4.png'),
22+
},
23+
{
24+
key: 'avatar',
25+
title: 'Avatar',
26+
focusedIcon: require('../../assets/avatar-1.png'),
27+
iconRenderingMode: 'original',
28+
},
29+
{
30+
key: 'album',
31+
title: 'Album',
32+
unfocusedIcon: require('../../assets/avatar-3.png'),
33+
focusedIcon: require('../../assets/avatar-4.png'),
34+
iconRenderingMode: 'original',
35+
},
36+
{
37+
key: 'chat',
38+
title: 'Chat',
39+
focusedIcon: require('../../assets/avatar-4.png'),
40+
iconRenderingMode: 'original',
41+
},
42+
]);
43+
44+
return (
45+
<TabView
46+
navigationState={{ index, routes }}
47+
onIndexChange={setIndex}
48+
renderScene={renderScene}
49+
/>
50+
);
51+
}

apps/example/src/Examples/TintColors.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ export default function TintColorsExample() {
2929
{
3030
key: 'albums',
3131
title: 'Albums',
32-
focusedIcon: require('../../assets/icons/grid_dark.png'),
33-
badge: '5',
34-
activeTintColor: 'green',
32+
unfocusedIcon: require('../../assets/avatar-3.png'),
33+
focusedIcon: require('../../assets/avatar-4.png'),
34+
activeTintColor: 'purple',
35+
iconRenderingMode: 'original',
3536
},
3637
{
3738
key: 'contacts',

docs/docs/docs/guides/standalone-usage.mdx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ Each route in the `routes` array can have the following properties:
236236
- `title`: Display title for the tab
237237
- `focusedIcon`: Icon to show when tab is active
238238
- `unfocusedIcon`: Icon to show when tab is inactive (optional)
239+
- `iconRenderingMode`: Rendering mode for icons. Use `'original'` to preserve multicolor icons instead of applying the native tab tint.
239240
- `badge`: Badge text to display on the tab
240241
- `activeTintColor`: Custom active tint color for this specific tab
241242
- `lazy`: Whether to lazy load this tab's content
@@ -280,6 +281,15 @@ Function to get the icon for a tab.
280281

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

284+
#### `getIconRenderingMode`
285+
286+
Function to get the rendering mode for an image icon.
287+
288+
- Default: Uses `route.iconRenderingMode`
289+
- Options: `'automatic' | 'original'`
290+
291+
Use `original` to preserve the original colors of multicolor image icons, such as logos or branded assets, instead of applying the native tab tint.
292+
283293
#### `getHidden`
284294

285295
Function to determine if a tab should be hidden.

docs/docs/docs/guides/usage-with-react-navigation.mdx

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ Controls how the tab bar behaves when content is scrolled.
185185
- Default: `undefined` (uses system default)
186186

187187
Options:
188+
188189
- `automatic`: Platform determines the behavior
189190
- `onScrollDown`: Tab bar minimizes when scrolling down
190191
- `onScrollUp`: Tab bar minimizes when scrolling up
@@ -225,15 +226,14 @@ function MyTabBar({ state, descriptors, navigation }) {
225226

226227
function MyTabs() {
227228
return (
228-
<Tab.Navigator
229-
tabBar={(props) => <MyTabBar {...props} />}
230-
>
229+
<Tab.Navigator tabBar={(props) => <MyTabBar {...props} />}>
231230
<Tab.Screen name="Home" component={HomeScreen} />
232231
<Tab.Screen name="Profile" component={ProfileScreen} />
233232
</Tab.Navigator>
234233
);
235234
}
236235
```
236+
237237
#### `renderBottomAccessoryView` <Badge text="iOS" type="info" /> <Badge text="experimental" type="danger"/>
238238

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

289+
#### `tabBarIconRenderingMode`
290+
291+
Rendering mode for image icons.
292+
293+
- Type: `'automatic' | 'original'`
294+
- Default: `'automatic'`
295+
296+
Use `original` to preserve the original colors of multicolor image icons, such as logos or branded assets, instead of applying the native tab tint.
297+
298+
```tsx
299+
<Tab.Screen
300+
name="Brand"
301+
component={Brand}
302+
options={{
303+
tabBarIcon: () => require('brand-logo.png'),
304+
tabBarIconRenderingMode: 'original',
305+
}}
306+
/>
307+
```
308+
289309
:::warning
290310

291-
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).
311+
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).
292312

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

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

314334
Then change your require calls like so:
335+
315336
```
316337
tabBarIcon: () => require('person.svgx')
317338
```
318339

319340
:::
320341

321-
322342
#### `tabBarBadge`
323343

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

330350
#### `tabBarBadgeBackgroundColor`
331351

332-
- Type: `string`
352+
- Type: `string`
333353

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

337357
#### `tabBarBadgeTextColor`
338358

339-
- Type: `string`
359+
- Type: `string`
340360

341361
Set the text color for the badge on android.
342362
Uses the system color by default.
@@ -356,6 +376,7 @@ Due to native limitations on iOS, this option doesn't hide the tab item **when h
356376
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.
357377

358378
#### `freezeOnBlur`
379+
359380
Boolean indicating whether to prevent inactive screens from re-rendering. Defaults to false.
360381

361382
It's working separately from `enableFreeze()` in `react-native-screens`. So settings won't be shared between them.
@@ -371,7 +392,6 @@ Whether to prevent default tab switching behavior when this tab is pressed. This
371392
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.
372393
:::
373394

374-
375395
#### `tabBarButtonTestID`
376396

377397
Test ID for the tab item. This can be used to find the tab item in the native view hierarchy.
@@ -413,7 +433,6 @@ React.useEffect(() => {
413433
const unsubscribe = navigation.addListener('tabPress', (e) => {
414434
// Note: For iOS 26+, use the `preventsDefault` option instead of `e.preventDefault()`
415435
// to avoid animation delays
416-
417436
// Do something manually
418437
// ...
419438
});

0 commit comments

Comments
 (0)