Skip to content

Commit d19ff06

Browse files
committed
chore: add experimental prop to enable baking tint color
1 parent 4cf55ad commit d19ff06

11 files changed

Lines changed: 99 additions & 25 deletions

File tree

.changeset/large-hats-teach.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
'react-native-bottom-tabs': patch
33
---
44

5-
Fix active and inactive tint color behavior on iOS 26 Liquid Glass
5+
Add `experimental_bakedTintColors` to opt into the iOS 26 Liquid Glass active and inactive tint color workaround.

apps/example/src/Examples/TintColors.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export default function TintColorsExample() {
5757
renderScene={renderScene}
5858
tabBarActiveTintColor="red"
5959
tabBarInactiveTintColor="orange"
60+
experimental_bakedTintColors={false}
6061
scrollEdgeAppearance="default"
6162
/>
6263
);

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ Color for the active tab.
175175
- Type: `ColorValue`
176176

177177
:::warning
178-
On iOS 26 (Liquid Glass), using this prop applies a workaround that bakes tab labels into images for correct tinting. This disables Dynamic Type (accessibility font sizes) and the Bold Text accessibility setting for tab labels.
178+
On iOS 26 (Liquid Glass), enable `experimental_bakedTintColors` to apply a workaround that bakes tab labels into images for correct tinting. This disables Dynamic Type (accessibility font sizes) and the Bold Text accessibility setting for tab labels.
179179
:::
180180

181181
#### `tabBarInactiveTintColor`
@@ -185,7 +185,15 @@ Color for inactive tabs.
185185
- Type: `ColorValue`
186186

187187
:::warning
188-
On iOS 26 (Liquid Glass), using this prop applies a workaround that bakes tab labels into images for correct tinting. This disables Dynamic Type (accessibility font sizes) and the Bold Text accessibility setting for tab labels.
188+
On iOS 26 (Liquid Glass), enable `experimental_bakedTintColors` to apply a workaround that bakes tab labels into images for correct tinting. This disables Dynamic Type (accessibility font sizes) and the Bold Text accessibility setting for tab labels.
189+
:::
190+
191+
#### `experimental_bakedTintColors` <Badge text="iOS" type="info" /> <Badge text="experimental" type="danger"/>
192+
193+
Enables the iOS 26 Liquid Glass workaround for active and inactive tint colors. When enabled with custom active or inactive tint colors, tab labels are baked into images so the icon and label receive the same tint during normal selection and Liquid Glass tab scrubbing.
194+
195+
:::warning
196+
This disables Dynamic Type (accessibility font sizes) and the Bold Text accessibility setting for tab labels, and icon sizing can vary with label width.
189197
:::
190198

191199
#### `tabBarStyle`

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,15 +125,23 @@ It's recommended to use `transparent` or `opaque` without lazy loading as the ta
125125
Color for the active tab.
126126

127127
:::warning
128-
On iOS 26 (Liquid Glass), using this prop applies a workaround that bakes tab labels into images for correct tinting. This disables Dynamic Type (accessibility font sizes) and the Bold Text accessibility setting for tab labels.
128+
On iOS 26 (Liquid Glass), enable `experimental_bakedTintColors` to apply a workaround that bakes tab labels into images for correct tinting. This disables Dynamic Type (accessibility font sizes) and the Bold Text accessibility setting for tab labels.
129129
:::
130130

131131
#### `tabBarInactiveTintColor`
132132

133133
Color for the inactive tabs.
134134

135135
:::warning
136-
On iOS 26 (Liquid Glass), using this prop applies a workaround that bakes tab labels into images for correct tinting. This disables Dynamic Type (accessibility font sizes) and the Bold Text accessibility setting for tab labels.
136+
On iOS 26 (Liquid Glass), enable `experimental_bakedTintColors` to apply a workaround that bakes tab labels into images for correct tinting. This disables Dynamic Type (accessibility font sizes) and the Bold Text accessibility setting for tab labels.
137+
:::
138+
139+
#### `experimental_bakedTintColors` <Badge text="iOS" type="info" /> <Badge text="experimental" type="danger"/>
140+
141+
Enables the iOS 26 Liquid Glass workaround for active and inactive tint colors. When enabled with custom active or inactive tint colors, tab labels are baked into images so the icon and label receive the same tint during normal selection and Liquid Glass tab scrubbing.
142+
143+
:::warning
144+
This disables Dynamic Type (accessibility font sizes) and the Bold Text accessibility setting for tab labels, and icon sizing can vary with label width.
137145
:::
138146

139147
#### `tabBarStyle`
@@ -249,7 +257,7 @@ Label text of the tab displayed in the navigation bar. When undefined, scene tit
249257
Color for the active tab.
250258

251259
:::warning
252-
On iOS 26 (Liquid Glass), using this prop applies a workaround that bakes tab labels into images for correct tinting. This disables Dynamic Type (accessibility font sizes) and the Bold Text accessibility setting for tab labels.
260+
On iOS 26 (Liquid Glass), enable `experimental_bakedTintColors` to apply a workaround that bakes tab labels into images for correct tinting. This disables Dynamic Type (accessibility font sizes) and the Bold Text accessibility setting for tab labels.
253261
:::
254262

255263
:::note

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-

packages/react-native-bottom-tabs/ios/TabViewImpl.swift

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ struct TabViewImpl: View {
7070
#else
7171
tabBar = tabController.tabBar
7272
updateTabBarAppearance(props: props, tabBar: tabController.tabBar)
73-
updateTabBarImages(props: props, tabBar: tabController.tabBar)
73+
updateExperimentalBakedTintColors(props: props, tabBar: tabController.tabBar)
7474
if !props.tabBarHidden {
7575
onTabBarMeasured(
7676
Int(tabController.tabBar.frame.size.height)
@@ -114,16 +114,16 @@ struct TabViewImpl: View {
114114
}
115115

116116
#if !os(macOS)
117-
private func updateTabBarImages(props: TabViewProps, tabBar: UITabBar?) {
118-
guard shouldApplyLiquidGlassTintWorkaround(),
117+
private func updateExperimentalBakedTintColors(props: TabViewProps, tabBar: UITabBar?) {
118+
guard shouldUseExperimentalBakedTintColors(props: props),
119119
let tabBar,
120120
let items = tabBar.items else { return }
121121

122-
configureTabBarItemImages(items: items, props: props)
122+
configureExperimentalBakedTintColors(items: items, props: props)
123123

124124
DispatchQueue.main.async { [weak tabBar] in
125125
guard let tabBar, let items = tabBar.items else { return }
126-
configureTabBarItemImages(items: items, props: props)
126+
configureExperimentalBakedTintColors(items: items, props: props)
127127
}
128128
}
129129

@@ -218,7 +218,7 @@ struct TabViewImpl: View {
218218
}
219219
}
220220

221-
private func configureTabBarItemImages(items: [UITabBarItem], props: TabViewProps) {
221+
private func configureExperimentalBakedTintColors(items: [UITabBarItem], props: TabViewProps) {
222222
for (tabBarIndex, item) in items.enumerated() {
223223
guard let tabData = props.filteredItems[safe: tabBarIndex],
224224
let itemIndex = props.items.firstIndex(where: { $0.key == tabData.key }) else { continue }
@@ -272,6 +272,24 @@ struct TabViewImpl: View {
272272
}
273273
}
274274

275+
private func resetExperimentalBakedTintColors(props: TabViewProps, tabBar: UITabBar?) {
276+
guard let tabBar,
277+
let items = tabBar.items else { return }
278+
279+
for (tabBarIndex, item) in items.enumerated() {
280+
guard let tabData = props.filteredItems[safe: tabBarIndex],
281+
let itemIndex = props.items.firstIndex(where: { $0.key == tabData.key }) else { continue }
282+
283+
let assetIcon = props.icons[itemIndex]
284+
let icon = assetIcon ?? makeSFSymbolImage(named: tabData.sfSymbol)
285+
286+
item.title = props.labeled ? tabData.title : nil
287+
item.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: 0)
288+
item.image = icon
289+
item.selectedImage = icon
290+
}
291+
}
292+
275293
private func makeSFSymbolImage(named sfSymbol: String?) -> UIImage? {
276294
guard let sfSymbol, !sfSymbol.isEmpty else { return nil }
277295

@@ -287,7 +305,11 @@ struct TabViewImpl: View {
287305
)
288306
}
289307

290-
private func shouldApplyLiquidGlassTintWorkaround() -> Bool {
308+
private func shouldUseExperimentalBakedTintColors(props: TabViewProps) -> Bool {
309+
guard props.experimentalBakedTintColors else {
310+
return false
311+
}
312+
291313
#if os(iOS)
292314
if #available(iOS 26.0, *) {
293315
return true
@@ -420,32 +442,40 @@ extension View {
420442
}
421443
.onChange(of: props.inactiveTintColor) { _ in
422444
updateTabBarAppearance(props: props, tabBar: tabBar)
423-
updateTabBarImages(props: props, tabBar: tabBar)
445+
updateExperimentalBakedTintColors(props: props, tabBar: tabBar)
424446
}
425447
.onChange(of: props.activeTintColor) { _ in
426448
updateTabBarAppearance(props: props, tabBar: tabBar)
427-
updateTabBarImages(props: props, tabBar: tabBar)
449+
updateExperimentalBakedTintColors(props: props, tabBar: tabBar)
428450
}
429451
.onChange(of: props.selectedActiveTintColor) { newValue in
430452
tabBar?.tintColor = newValue
431453
}
432454
.onChange(of: props.iconsRevision) { _ in
433-
updateTabBarImages(props: props, tabBar: tabBar)
455+
updateExperimentalBakedTintColors(props: props, tabBar: tabBar)
434456
}
435457
.onChange(of: props.labeled) { _ in
436-
updateTabBarImages(props: props, tabBar: tabBar)
458+
updateExperimentalBakedTintColors(props: props, tabBar: tabBar)
437459
}
438460
.onChange(of: props.fontSize) { _ in
439461
updateTabBarAppearance(props: props, tabBar: tabBar)
440-
updateTabBarImages(props: props, tabBar: tabBar)
462+
updateExperimentalBakedTintColors(props: props, tabBar: tabBar)
441463
}
442464
.onChange(of: props.fontFamily) { _ in
443465
updateTabBarAppearance(props: props, tabBar: tabBar)
444-
updateTabBarImages(props: props, tabBar: tabBar)
466+
updateExperimentalBakedTintColors(props: props, tabBar: tabBar)
445467
}
446468
.onChange(of: props.fontWeight) { _ in
447469
updateTabBarAppearance(props: props, tabBar: tabBar)
448-
updateTabBarImages(props: props, tabBar: tabBar)
470+
updateExperimentalBakedTintColors(props: props, tabBar: tabBar)
471+
}
472+
.onChange(of: props.experimentalBakedTintColors) { newValue in
473+
updateTabBarAppearance(props: props, tabBar: tabBar)
474+
if newValue {
475+
updateExperimentalBakedTintColors(props: props, tabBar: tabBar)
476+
} else {
477+
resetExperimentalBakedTintColors(props: props, tabBar: tabBar)
478+
}
449479
}
450480
.onChange(of: props.tabBarHidden) { newValue in
451481
tabBar?.isHidden = newValue

packages/react-native-bottom-tabs/ios/TabViewProps.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class TabViewProps: ObservableObject {
6464
@Published var barTintColor: PlatformColor?
6565
@Published var activeTintColor: PlatformColor?
6666
@Published var inactiveTintColor: PlatformColor?
67+
@Published var experimentalBakedTintColors: Bool = false
6768
@Published var translucent: Bool = true
6869
@Published var disablePageAnimations: Bool = false
6970
@Published var hapticFeedbackEnabled: Bool = false

packages/react-native-bottom-tabs/ios/TabViewProvider.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,12 @@ public final class TabInfo: NSObject {
136136
}
137137
}
138138

139+
@objc public var experimentalBakedTintColors: Bool = false {
140+
didSet {
141+
props.experimentalBakedTintColors = experimentalBakedTintColors
142+
}
143+
}
144+
139145
@objc public var fontFamily: NSString? {
140146
didSet {
141147
props.fontFamily = fontFamily as? String
@@ -262,12 +268,16 @@ public final class TabInfo: NSObject {
262268
DispatchQueue.main.async { [weak self] in
263269
guard let self else { return }
264270
let icon = image.resizeImageTo(size: iconSize)
265-
#if os(macOS)
266-
props.icons[index] = icon
271+
#if os(iOS)
272+
if props.experimentalBakedTintColors {
273+
props.icons[index] = icon?.withRenderingMode(.alwaysTemplate)
274+
props.iconsRevision += 1
275+
} else {
276+
props.icons[index] = icon
277+
}
267278
#else
268-
props.icons[index] = icon?.withRenderingMode(.alwaysTemplate)
279+
props.icons[index] = icon
269280
#endif
270-
props.iconsRevision += 1
271281
}
272282
})
273283
}

packages/react-native-bottom-tabs/src/TabView.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ interface Props<Route extends BaseRoute> {
7979
* Inactive tab color.
8080
*/
8181
tabBarInactiveTintColor?: ColorValue;
82+
/**
83+
* Enables the iOS 26 Liquid Glass tint color workaround that bakes tab labels
84+
* into images. This can affect icon sizing when labels have different widths.
85+
*
86+
* @platform ios
87+
*/
88+
experimental_bakedTintColors?: boolean;
8289
/**
8390
* State for the tab view.
8491
*
@@ -251,6 +258,7 @@ const TabView = <Route extends BaseRoute>({
251258
tabLabelStyle,
252259
renderBottomAccessoryView,
253260
layoutDirection = 'locale',
261+
experimental_bakedTintColors: experimentalBakedTintColors = false,
254262
...props
255263
}: Props<Route>) => {
256264
// @ts-ignore
@@ -413,6 +421,7 @@ const TabView = <Route extends BaseRoute>({
413421
layoutDirection={layoutDirection}
414422
activeTintColor={activeTintColor}
415423
inactiveTintColor={inactiveTintColor}
424+
experimentalBakedTintColors={experimentalBakedTintColors}
416425
barTintColor={tabBarStyle?.backgroundColor}
417426
rippleColor={rippleColor}
418427
labeled={labeled}

0 commit comments

Comments
 (0)