Skip to content

Commit 412e9ca

Browse files
authored
feat(iOS): experimental baked tint colors (#522)
* fix(iOS): icons not following inactiveTintColor * fix(iOS): labels not following inactiveTintColor on ios 26.4.1 * fix(iOS): hover through tabs show correct active color * chore: simplify solution * fix(iOS): fix glitch when switching tabs first time * fix(iOS): fix tint colors on SFSymbols * chore: add changeset * chore: cleanup * chore: only enable bake pass if providing props * chore: add experimental prop to enable baking tint color * chore: fix variable value after merge * chore: cleanup
1 parent 2a8a9a0 commit 412e9ca

11 files changed

Lines changed: 370 additions & 48 deletions

File tree

.changeset/tidy-eels-wish.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'react-native-bottom-tabs': minor
3+
---
4+
5+
Add `experimental_bakedTintColors` prop to opt into the iOS 26 Liquid Glass active and inactive tint color workaround.

apps/example/src/Examples/TintColors.tsx

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Article } from '../Screens/Article';
44
import { Albums } from '../Screens/Albums';
55
import { Contacts } from '../Screens/Contacts';
66
import { Chat } from '../Screens/Chat';
7+
import { Button, Platform, StyleSheet, View } from 'react-native';
78

89
const renderScene = SceneMap({
910
article: Article,
@@ -12,8 +13,11 @@ const renderScene = SceneMap({
1213
chat: Chat,
1314
});
1415

16+
const isAndroid = Platform.OS === 'android';
17+
1518
export default function TintColorsExample() {
1619
const [index, setIndex] = useState(0);
20+
const [bakedTintColors, setBakedTintColors] = useState(false);
1721
const [routes] = useState([
1822
{
1923
key: 'article',
@@ -31,9 +35,11 @@ export default function TintColorsExample() {
3135
},
3236
{
3337
key: 'contacts',
34-
focusedIcon: require('../../assets/icons/person_dark.png'),
38+
focusedIcon: isAndroid
39+
? require('../../assets/icons/person_dark.png')
40+
: { sfSymbol: 'person.fill' },
3541
title: 'Contacts',
36-
activeTintColor: 'yellow',
42+
activeTintColor: 'blue',
3743
},
3844
{
3945
key: 'chat',
@@ -45,14 +51,32 @@ export default function TintColorsExample() {
4551
]);
4652

4753
return (
48-
<TabView
49-
sidebarAdaptable
50-
navigationState={{ index, routes }}
51-
onIndexChange={setIndex}
52-
renderScene={renderScene}
53-
tabBarActiveTintColor="red"
54-
tabBarInactiveTintColor="orange"
55-
scrollEdgeAppearance="default"
56-
/>
54+
<View style={styles.container}>
55+
<View style={styles.controls}>
56+
<Button
57+
title={`${bakedTintColors ? 'Disable' : 'Enable'} Experimental Baked Tint Colors`}
58+
onPress={() => setBakedTintColors((value) => !value)}
59+
/>
60+
</View>
61+
<TabView
62+
sidebarAdaptable
63+
navigationState={{ index, routes }}
64+
onIndexChange={setIndex}
65+
renderScene={renderScene}
66+
tabBarActiveTintColor="red"
67+
tabBarInactiveTintColor="orange"
68+
experimental_bakedTintColors={bakedTintColors}
69+
scrollEdgeAppearance="default"
70+
/>
71+
</View>
5772
);
5873
}
74+
75+
const styles = StyleSheet.create({
76+
container: {
77+
flex: 1,
78+
},
79+
controls: {
80+
padding: 12,
81+
},
82+
});

docs/docs/docs/guides/standalone-usage.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,20 @@ Color for inactive tabs.
184184
On iOS >= 26 (Liquid Glass), this prop is ignored.
185185
:::
186186

187+
#### `experimental_bakedTintColors` <Badge text="iOS" type="info" /> <Badge text="experimental" type="danger"/>
188+
189+
Enable an experimental mode which bakes (rasterizes) the label into the icon image, allowing for better control of the tint colors on iOS 26+.
190+
191+
- Type: `boolean`
192+
193+
:::warning
194+
This feature is experimental, and might be removed in the future.
195+
196+
It has many drawbacks, such as SFSymbol icon sizes being different on label width, badges being positioned far away from the icon depending on the label width, and possibly breaking accessibility since the label will be baked inside the icon image.
197+
198+
Use with caution, only if you prioritize tint color consistency between platforms.
199+
:::
200+
187201
#### `tabBarStyle`
188202

189203
Object containing styles for the tab bar.

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,20 @@ Color for the inactive tabs.
132132
On iOS >= 26 (Liquid Glass), this prop is ignored.
133133
:::
134134

135+
#### `experimental_bakedTintColors` <Badge text="iOS" type="info" /> <Badge text="experimental" type="danger"/>
136+
137+
Enable an experimental mode which bakes (rasterizes) the label into the icon image, allowing for better control of the tint colors on iOS 26+.
138+
139+
- Type: `boolean`
140+
141+
:::warning
142+
This feature is experimental, and might be removed in the future.
143+
144+
It has many drawbacks, such as SFSymbol icon sizes being different on label width, badges being positioned far away from the icon depending on the label width, and possibly breaking accessibility since the label will be baked inside the icon image.
145+
146+
Use with caution, only if you prioritize tint color consistency between platforms.
147+
:::
148+
135149
#### `tabBarStyle`
136150

137151
Object containing styles for the tab bar.

packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewManager.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,9 @@ class RCTTabViewManager(context: ReactApplicationContext) :
151151
view.setInactiveTintColor(value)
152152
}
153153

154+
override fun setExperimentalBakedTintColors(view: ReactBottomNavigationView?, value: Boolean) {
155+
}
156+
154157
override fun setActiveIndicatorColor(view: ReactBottomNavigationView?, value: Int?) {
155158
if (view != null && value != null) {
156159
val color = ColorStateList.valueOf(value)

packages/react-native-bottom-tabs/ios/RCTTabViewComponentView.mm

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
156156
_tabViewProvider.inactiveTintColor = RCTUIColorFromSharedColor(newViewProps.inactiveTintColor);
157157
}
158158

159+
if (oldViewProps.experimentalBakedTintColors != newViewProps.experimentalBakedTintColors) {
160+
_tabViewProvider.experimentalBakedTintColors = newViewProps.experimentalBakedTintColors;
161+
}
162+
159163
if (oldViewProps.hapticFeedbackEnabled != newViewProps.hapticFeedbackEnabled) {
160164
_tabViewProvider.hapticFeedbackEnabled = newViewProps.hapticFeedbackEnabled;
161165
}
@@ -262,4 +266,3 @@ - (void)onLayoutWithSize:(CGSize)size reactTag:(NSNumber *)reactTag {
262266

263267
#endif // RCT_NEW_ARCH_ENABLED
264268

265-

0 commit comments

Comments
 (0)