From 0792ae13faf3aa98c68e167467f596d19c2a810c Mon Sep 17 00:00:00 2001 From: Thiago Brezinski Date: Tue, 19 May 2026 13:48:52 +0100 Subject: [PATCH 1/9] fix(iOS): icons not following inactiveTintColor --- .../ios/TabItem.swift | 18 ++++- .../ios/TabView/LegacyTabView.swift | 4 +- .../ios/TabView/NewTabView.swift | 4 +- .../ios/TabViewImpl.swift | 65 ++++++++++++++++++- .../ios/TabViewProps.swift | 1 + .../ios/TabViewProvider.swift | 8 ++- 6 files changed, 93 insertions(+), 7 deletions(-) diff --git a/packages/react-native-bottom-tabs/ios/TabItem.swift b/packages/react-native-bottom-tabs/ios/TabItem.swift index 3866c59..fe9888c 100644 --- a/packages/react-native-bottom-tabs/ios/TabItem.swift +++ b/packages/react-native-bottom-tabs/ios/TabItem.swift @@ -5,20 +5,36 @@ struct TabItem: View { var icon: PlatformImage? var sfSymbol: String? var labeled: Bool? + var tintColor: PlatformColor? + private var tint: Color? { + tintColor.map(Color.init) + } + + #if !os(macOS) + private var tintedIcon: PlatformImage? { + guard let icon else { return nil } + guard let tintColor else { return icon } + return icon.withTintColor(tintColor, renderingMode: .alwaysOriginal) + } + #endif + + @ViewBuilder var body: some View { if let icon { #if os(macOS) Image(nsImage: icon) #else - Image(uiImage: icon) + Image(uiImage: tintedIcon ?? icon) #endif } else if let sfSymbol, !sfSymbol.isEmpty { Image(systemName: sfSymbol) .noneSymbolVariant() + .foregroundColor(tint) } if labeled != false { Text(title ?? "") + .foregroundColor(tint) } } } diff --git a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift index 5bfc58f..5a4f687 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift @@ -40,6 +40,7 @@ struct LegacyTabView: AnyTabView { if !tabData.hidden || isFocused { let icon = props.icons[index] + let tintColor = isFocused ? tabData.activeTintColor ?? props.activeTintColor : props.inactiveTintColor let child = props.children[safe: index]?.view ?? PlatformView() let context = TabAppearContext( index: index, @@ -56,7 +57,8 @@ struct LegacyTabView: AnyTabView { title: tabData.title, icon: icon, sfSymbol: tabData.sfSymbol, - labeled: props.labeled + labeled: props.labeled, + tintColor: tintColor ) .accessibilityIdentifier(tabData.testID ?? "") } diff --git a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift index 9e8dd80..2b4a300 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift @@ -29,6 +29,7 @@ struct NewTabView: AnyTabView { if !tabData.hidden || isFocused { let icon = props.icons[index] + let tintColor = isFocused ? tabData.activeTintColor ?? props.activeTintColor : props.inactiveTintColor let context = TabAppearContext( index: index, @@ -48,7 +49,8 @@ struct NewTabView: AnyTabView { title: tabData.title, icon: icon, sfSymbol: tabData.sfSymbol, - labeled: props.labeled + labeled: props.labeled, + tintColor: tintColor ) } #if !os(tvOS) diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift index 29eb3bb..d2f05c7 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift @@ -69,6 +69,7 @@ struct TabViewImpl: View { tabBar = tabController #else tabBar = tabController.tabBar + updateTabBarAppearance(props: props, tabBar: tabController.tabBar) if !props.tabBarHidden { onTabBarMeasured( Int(tabController.tabBar.frame.size.height) @@ -82,6 +83,9 @@ struct TabViewImpl: View { .tintColor(props.selectedActiveTintColor) .getSidebarAdaptable(enabled: props.sidebarAdaptable ?? false) .onChange(of: props.selectedPage ?? "") { newValue in + #if !os(macOS) + updateTabBarAppearance(props: props, tabBar: tabBar) + #endif #if !os(macOS) if props.disablePageAnimations { UIView.setAnimationsEnabled(false) @@ -120,36 +124,41 @@ struct TabViewImpl: View { if props.scrollEdgeAppearance == "transparent" { configureTransparentAppearance(tabBar: tabBar, props: props) return + } else { + configureStandardAppearance(tabBar: tabBar, props: props) } - - configureStandardAppearance(tabBar: tabBar, props: props) } #endif #if !os(macOS) private func configureTransparentAppearance(tabBar: UITabBar, props: TabViewProps) { tabBar.barTintColor = props.barTintColor + tabBar.tintColor = props.selectedActiveTintColor #if !os(visionOS) tabBar.isTranslucent = props.translucent #endif tabBar.unselectedItemTintColor = props.inactiveTintColor guard let items = tabBar.items else { return } + configureTabBarItemImages(items: items, props: props) let attributes = TabBarFontSize.createNormalStateAttributes( fontSize: props.fontSize, fontFamily: props.fontFamily, fontWeight: props.fontWeight, - inactiveColor: nil + inactiveColor: props.inactiveTintColor ) items.forEach { item in item.setTitleTextAttributes(attributes, for: .normal) + item.setTitleTextAttributes(selectedAttributes(props: props), for: .selected) } } private func configureStandardAppearance(tabBar: UITabBar, props: TabViewProps) { let appearance = UITabBarAppearance() + tabBar.tintColor = props.selectedActiveTintColor + tabBar.unselectedItemTintColor = props.inactiveTintColor // Configure background switch props.scrollEdgeAppearance { @@ -180,8 +189,12 @@ struct TabViewImpl: View { if let inactiveTintColor = props.inactiveTintColor { itemAppearance.normal.iconColor = inactiveTintColor } + if let activeTintColor = props.selectedActiveTintColor { + itemAppearance.selected.iconColor = activeTintColor + } itemAppearance.normal.titleTextAttributes = attributes + itemAppearance.selected.titleTextAttributes = selectedAttributes(props: props) // Apply item appearance to all layouts appearance.stackedLayoutAppearance = itemAppearance @@ -193,6 +206,49 @@ struct TabViewImpl: View { if #available(iOS 15.0, *) { tabBar.scrollEdgeAppearance = appearance.copy() } + if let items = tabBar.items { + configureTabBarItemImages(items: items, props: props) + configureTabBarItemTitles(items: items, props: props) + } + } + + private func configureTabBarItemTitles(items: [UITabBarItem], props: TabViewProps) { + let normalAttributes = TabBarFontSize.createNormalStateAttributes( + fontSize: props.fontSize, + fontFamily: props.fontFamily, + fontWeight: props.fontWeight, + inactiveColor: props.inactiveTintColor + ) + let selectedAttributes = selectedAttributes(props: props) + + items.forEach { item in + item.setTitleTextAttributes(normalAttributes, for: .normal) + item.setTitleTextAttributes(selectedAttributes, for: .selected) + } + } + + private func configureTabBarItemImages(items: [UITabBarItem], props: TabViewProps) { + for (tabBarIndex, item) in items.enumerated() { + guard let tabData = props.filteredItems[safe: tabBarIndex], + let itemIndex = props.items.firstIndex(where: { $0.key == tabData.key }), + let icon = props.icons[itemIndex] else { continue } + + item.image = props.inactiveTintColor.map { + icon.withTintColor($0, renderingMode: .alwaysOriginal) + } ?? icon + item.selectedImage = props.selectedActiveTintColor.map { + icon.withTintColor($0, renderingMode: .alwaysOriginal) + } ?? icon + } + } + + private func selectedAttributes(props: TabViewProps) -> [NSAttributedString.Key: Any] { + TabBarFontSize.createFontAttributes( + size: props.fontSize.map(CGFloat.init) ?? TabBarFontSize.defaultSize, + family: props.fontFamily, + weight: props.fontWeight, + color: props.selectedActiveTintColor + ) } #endif @@ -250,6 +306,9 @@ extension View { .onChange(of: props.selectedActiveTintColor) { _ in updateTabBarAppearance(props: props, tabBar: tabBar) } + .onChange(of: props.iconsRevision) { _ in + updateTabBarAppearance(props: props, tabBar: tabBar) + } .onChange(of: props.fontSize) { _ in updateTabBarAppearance(props: props, tabBar: tabBar) } diff --git a/packages/react-native-bottom-tabs/ios/TabViewProps.swift b/packages/react-native-bottom-tabs/ios/TabViewProps.swift index e9dc5d9..4f9a79a 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewProps.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewProps.swift @@ -56,6 +56,7 @@ class TabViewProps: ObservableObject { @Published var items: [TabInfo] = [] @Published var selectedPage: String? @Published var icons: [Int: PlatformImage] = [:] + @Published var iconsRevision: Int = 0 @Published var sidebarAdaptable: Bool? @Published var labeled: Bool = false @Published var minimizeBehavior: MinimizeBehavior? diff --git a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift index deac524..f4b0146 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift @@ -261,7 +261,13 @@ public final class TabInfo: NSObject { guard let image else { return } DispatchQueue.main.async { [weak self] in guard let self else { return } - props.icons[index] = image.resizeImageTo(size: iconSize) + let icon = image.resizeImageTo(size: iconSize) + #if os(macOS) + props.icons[index] = icon + #else + props.icons[index] = icon?.withRenderingMode(.alwaysTemplate) + #endif + props.iconsRevision += 1 } }) } From df858e581828c369533d8a8b28b575a48d10a9fe Mon Sep 17 00:00:00 2001 From: Thiago Brezinski Date: Tue, 19 May 2026 20:26:44 +0100 Subject: [PATCH 2/9] fix(iOS): labels not following inactiveTintColor on ios 26.4.1 --- .../ios/TabViewImpl.swift | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift index d2f05c7..6b88426 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift @@ -153,6 +153,7 @@ struct TabViewImpl: View { item.setTitleTextAttributes(attributes, for: .normal) item.setTitleTextAttributes(selectedAttributes(props: props), for: .selected) } + configureTabBarItemImagesAfterLayout(tabBar: tabBar, props: props) } private func configureStandardAppearance(tabBar: UITabBar, props: TabViewProps) { @@ -209,6 +210,7 @@ struct TabViewImpl: View { if let items = tabBar.items { configureTabBarItemImages(items: items, props: props) configureTabBarItemTitles(items: items, props: props) + configureTabBarItemImagesAfterLayout(tabBar: tabBar, props: props) } } @@ -233,6 +235,25 @@ struct TabViewImpl: View { let itemIndex = props.items.firstIndex(where: { $0.key == tabData.key }), let icon = props.icons[itemIndex] else { continue } + if shouldRenderTabBarLabelsIntoImages(), props.labeled { + item.title = "" + item.accessibilityLabel = tabData.title + item.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: 100) + item.image = makeTabBarItemImage( + icon: icon, + title: tabData.title, + color: props.inactiveTintColor, + props: props + ) + item.selectedImage = makeTabBarItemImage( + icon: icon, + title: tabData.title, + color: props.selectedActiveTintColor, + props: props + ) + continue + } + item.image = props.inactiveTintColor.map { icon.withTintColor($0, renderingMode: .alwaysOriginal) } ?? icon @@ -242,6 +263,15 @@ struct TabViewImpl: View { } } + private func configureTabBarItemImagesAfterLayout(tabBar: UITabBar, props: TabViewProps) { + guard shouldRenderTabBarLabelsIntoImages() else { return } + + DispatchQueue.main.async { [weak tabBar] in + guard let tabBar, let items = tabBar.items else { return } + configureTabBarItemImages(items: items, props: props) + } + } + private func selectedAttributes(props: TabViewProps) -> [NSAttributedString.Key: Any] { TabBarFontSize.createFontAttributes( size: props.fontSize.map(CGFloat.init) ?? TabBarFontSize.defaultSize, @@ -250,6 +280,62 @@ struct TabViewImpl: View { color: props.selectedActiveTintColor ) } + + private func shouldRenderTabBarLabelsIntoImages() -> Bool { + let version = ProcessInfo.processInfo.operatingSystemVersion + return version.majorVersion > 26 || (version.majorVersion == 26 && version.minorVersion >= 4) + } + + private func makeTabBarItemImage( + icon: UIImage, + title: String, + color: UIColor?, + props: TabViewProps + ) -> UIImage { + let color = color ?? .label + let iconSize = CGSize(width: 27, height: 27) + let font = TabBarFontSize.createFontAttributes( + size: props.fontSize.map(CGFloat.init) ?? TabBarFontSize.defaultSize, + family: props.fontFamily, + weight: props.fontWeight + )[.font] as? UIFont ?? UIFont.boldSystemFont(ofSize: TabBarFontSize.defaultSize) + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .center + let attributes: [NSAttributedString.Key: Any] = [ + .font: font, + .foregroundColor: color, + .paragraphStyle: paragraphStyle, + ] + let titleSize = (title as NSString).size(withAttributes: attributes) + let imageSize = CGSize( + width: max(iconSize.width, ceil(titleSize.width)) + 8, + height: iconSize.height + 3 + ceil(titleSize.height) + ) + let format = UIGraphicsImageRendererFormat() + format.scale = UIScreen.main.scale + + let image = UIGraphicsImageRenderer(size: imageSize, format: format).image { _ in + let tintedIcon = icon.withTintColor(color, renderingMode: .alwaysOriginal) + tintedIcon.draw(in: CGRect( + x: (imageSize.width - iconSize.width) / 2, + y: 0, + width: iconSize.width, + height: iconSize.height + )) + + (title as NSString).draw( + in: CGRect( + x: 0, + y: iconSize.height + 3, + width: imageSize.width, + height: ceil(titleSize.height) + ), + withAttributes: attributes + ) + } + + return image.withRenderingMode(.alwaysOriginal) + } #endif extension View { From 5177c2fc213c87bcc7fc807cefc7ec3f47720e17 Mon Sep 17 00:00:00 2001 From: Thiago Brezinski Date: Wed, 20 May 2026 19:21:40 +0100 Subject: [PATCH 3/9] fix(iOS): hover through tabs show correct active color --- .../ios/TabItem.swift | 9 +++--- .../ios/TabView/LegacyTabView.swift | 3 +- .../ios/TabView/NewTabView.swift | 3 +- .../ios/TabViewImpl.swift | 29 ++++++++++++++----- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/packages/react-native-bottom-tabs/ios/TabItem.swift b/packages/react-native-bottom-tabs/ios/TabItem.swift index fe9888c..b97df53 100644 --- a/packages/react-native-bottom-tabs/ios/TabItem.swift +++ b/packages/react-native-bottom-tabs/ios/TabItem.swift @@ -5,21 +5,20 @@ struct TabItem: View { var icon: PlatformImage? var sfSymbol: String? var labeled: Bool? - var tintColor: PlatformColor? + var inactiveTintColor: PlatformColor? private var tint: Color? { - tintColor.map(Color.init) + inactiveTintColor.map(Color.init) } #if !os(macOS) private var tintedIcon: PlatformImage? { guard let icon else { return nil } - guard let tintColor else { return icon } - return icon.withTintColor(tintColor, renderingMode: .alwaysOriginal) + guard let inactiveTintColor else { return icon } + return icon.withTintColor(inactiveTintColor, renderingMode: .alwaysOriginal) } #endif - @ViewBuilder var body: some View { if let icon { #if os(macOS) diff --git a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift index 5a4f687..58867fc 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift @@ -40,7 +40,6 @@ struct LegacyTabView: AnyTabView { if !tabData.hidden || isFocused { let icon = props.icons[index] - let tintColor = isFocused ? tabData.activeTintColor ?? props.activeTintColor : props.inactiveTintColor let child = props.children[safe: index]?.view ?? PlatformView() let context = TabAppearContext( index: index, @@ -58,7 +57,7 @@ struct LegacyTabView: AnyTabView { icon: icon, sfSymbol: tabData.sfSymbol, labeled: props.labeled, - tintColor: tintColor + inactiveTintColor: props.inactiveTintColor ) .accessibilityIdentifier(tabData.testID ?? "") } diff --git a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift index 2b4a300..7afb3ee 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift @@ -29,7 +29,6 @@ struct NewTabView: AnyTabView { if !tabData.hidden || isFocused { let icon = props.icons[index] - let tintColor = isFocused ? tabData.activeTintColor ?? props.activeTintColor : props.inactiveTintColor let context = TabAppearContext( index: index, @@ -50,7 +49,7 @@ struct NewTabView: AnyTabView { icon: icon, sfSymbol: tabData.sfSymbol, labeled: props.labeled, - tintColor: tintColor + inactiveTintColor: props.inactiveTintColor ) } #if !os(tvOS) diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift index 6b88426..787093e 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift @@ -84,7 +84,7 @@ struct TabViewImpl: View { .getSidebarAdaptable(enabled: props.sidebarAdaptable ?? false) .onChange(of: props.selectedPage ?? "") { newValue in #if !os(macOS) - updateTabBarAppearance(props: props, tabBar: tabBar) + tabBar?.tintColor = props.selectedActiveTintColor #endif #if !os(macOS) if props.disablePageAnimations { @@ -140,7 +140,6 @@ struct TabViewImpl: View { tabBar.unselectedItemTintColor = props.inactiveTintColor guard let items = tabBar.items else { return } - configureTabBarItemImages(items: items, props: props) let attributes = TabBarFontSize.createNormalStateAttributes( fontSize: props.fontSize, @@ -153,6 +152,7 @@ struct TabViewImpl: View { item.setTitleTextAttributes(attributes, for: .normal) item.setTitleTextAttributes(selectedAttributes(props: props), for: .selected) } + configureTabBarItemImages(items: items, props: props) configureTabBarItemImagesAfterLayout(tabBar: tabBar, props: props) } @@ -208,8 +208,8 @@ struct TabViewImpl: View { tabBar.scrollEdgeAppearance = appearance.copy() } if let items = tabBar.items { - configureTabBarItemImages(items: items, props: props) configureTabBarItemTitles(items: items, props: props) + configureTabBarItemImages(items: items, props: props) configureTabBarItemImagesAfterLayout(tabBar: tabBar, props: props) } } @@ -235,6 +235,8 @@ struct TabViewImpl: View { let itemIndex = props.items.firstIndex(where: { $0.key == tabData.key }), let icon = props.icons[itemIndex] else { continue } + let tabActiveColor = tabData.activeTintColor ?? props.activeTintColor + if shouldRenderTabBarLabelsIntoImages(), props.labeled { item.title = "" item.accessibilityLabel = tabData.title @@ -248,7 +250,7 @@ struct TabViewImpl: View { item.selectedImage = makeTabBarItemImage( icon: icon, title: tabData.title, - color: props.selectedActiveTintColor, + color: tabActiveColor, props: props ) continue @@ -257,9 +259,19 @@ struct TabViewImpl: View { item.image = props.inactiveTintColor.map { icon.withTintColor($0, renderingMode: .alwaysOriginal) } ?? icon - item.selectedImage = props.selectedActiveTintColor.map { + item.selectedImage = tabActiveColor.map { icon.withTintColor($0, renderingMode: .alwaysOriginal) } ?? icon + + item.setTitleTextAttributes( + TabBarFontSize.createFontAttributes( + size: props.fontSize.map(CGFloat.init) ?? TabBarFontSize.defaultSize, + family: props.fontFamily, + weight: props.fontWeight, + color: tabActiveColor + ), + for: .selected + ) } } @@ -283,7 +295,7 @@ struct TabViewImpl: View { private func shouldRenderTabBarLabelsIntoImages() -> Bool { let version = ProcessInfo.processInfo.operatingSystemVersion - return version.majorVersion > 26 || (version.majorVersion == 26 && version.minorVersion >= 4) + return version.majorVersion >= 26 } private func makeTabBarItemImage( @@ -389,9 +401,12 @@ extension View { .onChange(of: props.inactiveTintColor) { _ in updateTabBarAppearance(props: props, tabBar: tabBar) } - .onChange(of: props.selectedActiveTintColor) { _ in + .onChange(of: props.activeTintColor) { _ in updateTabBarAppearance(props: props, tabBar: tabBar) } + .onChange(of: props.selectedActiveTintColor) { newValue in + tabBar?.tintColor = newValue + } .onChange(of: props.iconsRevision) { _ in updateTabBarAppearance(props: props, tabBar: tabBar) } From 16ec5b4b4777b230b996bee6a0744dec42819a6c Mon Sep 17 00:00:00 2001 From: Thiago Brezinski Date: Wed, 20 May 2026 20:32:54 +0100 Subject: [PATCH 4/9] chore: simplify solution --- .../ios/TabItem.swift | 17 +---------- .../ios/TabView/LegacyTabView.swift | 3 +- .../ios/TabView/NewTabView.swift | 3 +- .../ios/TabViewImpl.swift | 29 ++++--------------- 4 files changed, 8 insertions(+), 44 deletions(-) diff --git a/packages/react-native-bottom-tabs/ios/TabItem.swift b/packages/react-native-bottom-tabs/ios/TabItem.swift index b97df53..3866c59 100644 --- a/packages/react-native-bottom-tabs/ios/TabItem.swift +++ b/packages/react-native-bottom-tabs/ios/TabItem.swift @@ -5,35 +5,20 @@ struct TabItem: View { var icon: PlatformImage? var sfSymbol: String? var labeled: Bool? - var inactiveTintColor: PlatformColor? - - private var tint: Color? { - inactiveTintColor.map(Color.init) - } - - #if !os(macOS) - private var tintedIcon: PlatformImage? { - guard let icon else { return nil } - guard let inactiveTintColor else { return icon } - return icon.withTintColor(inactiveTintColor, renderingMode: .alwaysOriginal) - } - #endif var body: some View { if let icon { #if os(macOS) Image(nsImage: icon) #else - Image(uiImage: tintedIcon ?? icon) + Image(uiImage: icon) #endif } else if let sfSymbol, !sfSymbol.isEmpty { Image(systemName: sfSymbol) .noneSymbolVariant() - .foregroundColor(tint) } if labeled != false { Text(title ?? "") - .foregroundColor(tint) } } } diff --git a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift index 58867fc..5bfc58f 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift @@ -56,8 +56,7 @@ struct LegacyTabView: AnyTabView { title: tabData.title, icon: icon, sfSymbol: tabData.sfSymbol, - labeled: props.labeled, - inactiveTintColor: props.inactiveTintColor + labeled: props.labeled ) .accessibilityIdentifier(tabData.testID ?? "") } diff --git a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift index 7afb3ee..9e8dd80 100644 --- a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift +++ b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift @@ -48,8 +48,7 @@ struct NewTabView: AnyTabView { title: tabData.title, icon: icon, sfSymbol: tabData.sfSymbol, - labeled: props.labeled, - inactiveTintColor: props.inactiveTintColor + labeled: props.labeled ) } #if !os(tvOS) diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift index 787093e..5635a5b 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift @@ -83,9 +83,6 @@ struct TabViewImpl: View { .tintColor(props.selectedActiveTintColor) .getSidebarAdaptable(enabled: props.sidebarAdaptable ?? false) .onChange(of: props.selectedPage ?? "") { newValue in - #if !os(macOS) - tabBar?.tintColor = props.selectedActiveTintColor - #endif #if !os(macOS) if props.disablePageAnimations { UIView.setAnimationsEnabled(false) @@ -124,9 +121,9 @@ struct TabViewImpl: View { if props.scrollEdgeAppearance == "transparent" { configureTransparentAppearance(tabBar: tabBar, props: props) return - } else { - configureStandardAppearance(tabBar: tabBar, props: props) } + + configureStandardAppearance(tabBar: tabBar, props: props) } #endif @@ -141,15 +138,15 @@ struct TabViewImpl: View { guard let items = tabBar.items else { return } - let attributes = TabBarFontSize.createNormalStateAttributes( + let fontAttributes = TabBarFontSize.createNormalStateAttributes( fontSize: props.fontSize, fontFamily: props.fontFamily, fontWeight: props.fontWeight, - inactiveColor: props.inactiveTintColor + inactiveColor: nil ) items.forEach { item in - item.setTitleTextAttributes(attributes, for: .normal) + item.setTitleTextAttributes(fontAttributes, for: .normal) item.setTitleTextAttributes(selectedAttributes(props: props), for: .selected) } configureTabBarItemImages(items: items, props: props) @@ -208,27 +205,11 @@ struct TabViewImpl: View { tabBar.scrollEdgeAppearance = appearance.copy() } if let items = tabBar.items { - configureTabBarItemTitles(items: items, props: props) configureTabBarItemImages(items: items, props: props) configureTabBarItemImagesAfterLayout(tabBar: tabBar, props: props) } } - private func configureTabBarItemTitles(items: [UITabBarItem], props: TabViewProps) { - let normalAttributes = TabBarFontSize.createNormalStateAttributes( - fontSize: props.fontSize, - fontFamily: props.fontFamily, - fontWeight: props.fontWeight, - inactiveColor: props.inactiveTintColor - ) - let selectedAttributes = selectedAttributes(props: props) - - items.forEach { item in - item.setTitleTextAttributes(normalAttributes, for: .normal) - item.setTitleTextAttributes(selectedAttributes, for: .selected) - } - } - private func configureTabBarItemImages(items: [UITabBarItem], props: TabViewProps) { for (tabBarIndex, item) in items.enumerated() { guard let tabData = props.filteredItems[safe: tabBarIndex], From ccf1b8954d831355d905ff9cadcc47cd6cc55da8 Mon Sep 17 00:00:00 2001 From: Thiago Brezinski Date: Mon, 25 May 2026 11:22:27 +0100 Subject: [PATCH 5/9] fix(iOS): fix glitch when switching tabs first time --- .../ios/TabViewImpl.swift | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift index 5635a5b..16248d9 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift @@ -70,6 +70,7 @@ struct TabViewImpl: View { #else tabBar = tabController.tabBar updateTabBarAppearance(props: props, tabBar: tabController.tabBar) + updateTabBarImages(props: props, tabBar: tabController.tabBar) if !props.tabBarHidden { onTabBarMeasured( Int(tabController.tabBar.frame.size.height) @@ -113,6 +114,12 @@ struct TabViewImpl: View { } #if !os(macOS) + private func updateTabBarImages(props: TabViewProps, tabBar: UITabBar?) { + guard let tabBar, let items = tabBar.items else { return } + configureTabBarItemImages(items: items, props: props) + configureTabBarItemImagesAfterLayout(tabBar: tabBar, props: props) + } + private func updateTabBarAppearance(props: TabViewProps, tabBar: UITabBar?) { guard let tabBar else { return } @@ -149,8 +156,6 @@ struct TabViewImpl: View { item.setTitleTextAttributes(fontAttributes, for: .normal) item.setTitleTextAttributes(selectedAttributes(props: props), for: .selected) } - configureTabBarItemImages(items: items, props: props) - configureTabBarItemImagesAfterLayout(tabBar: tabBar, props: props) } private func configureStandardAppearance(tabBar: UITabBar, props: TabViewProps) { @@ -204,10 +209,6 @@ struct TabViewImpl: View { if #available(iOS 15.0, *) { tabBar.scrollEdgeAppearance = appearance.copy() } - if let items = tabBar.items { - configureTabBarItemImages(items: items, props: props) - configureTabBarItemImagesAfterLayout(tabBar: tabBar, props: props) - } } private func configureTabBarItemImages(items: [UITabBarItem], props: TabViewProps) { @@ -381,24 +382,29 @@ extension View { } .onChange(of: props.inactiveTintColor) { _ in updateTabBarAppearance(props: props, tabBar: tabBar) + updateTabBarImages(props: props, tabBar: tabBar) } .onChange(of: props.activeTintColor) { _ in updateTabBarAppearance(props: props, tabBar: tabBar) + updateTabBarImages(props: props, tabBar: tabBar) } .onChange(of: props.selectedActiveTintColor) { newValue in tabBar?.tintColor = newValue } .onChange(of: props.iconsRevision) { _ in - updateTabBarAppearance(props: props, tabBar: tabBar) + updateTabBarImages(props: props, tabBar: tabBar) } .onChange(of: props.fontSize) { _ in updateTabBarAppearance(props: props, tabBar: tabBar) + updateTabBarImages(props: props, tabBar: tabBar) } .onChange(of: props.fontFamily) { _ in updateTabBarAppearance(props: props, tabBar: tabBar) + updateTabBarImages(props: props, tabBar: tabBar) } .onChange(of: props.fontWeight) { _ in updateTabBarAppearance(props: props, tabBar: tabBar) + updateTabBarImages(props: props, tabBar: tabBar) } .onChange(of: props.tabBarHidden) { newValue in tabBar?.isHidden = newValue From 3fc4f4c66a1e5b69d816510783189f0814f641b7 Mon Sep 17 00:00:00 2001 From: Thiago Brezinski Date: Mon, 25 May 2026 15:14:02 +0100 Subject: [PATCH 6/9] fix(iOS): fix tint colors on SFSymbols --- apps/example/src/Examples/TintColors.tsx | 2 +- .../ios/TabViewImpl.swift | 63 ++++++++++++++----- 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/apps/example/src/Examples/TintColors.tsx b/apps/example/src/Examples/TintColors.tsx index c5f4433..5c16e39 100644 --- a/apps/example/src/Examples/TintColors.tsx +++ b/apps/example/src/Examples/TintColors.tsx @@ -33,7 +33,7 @@ export default function TintColorsExample() { key: 'contacts', focusedIcon: require('../../assets/icons/person_dark.png'), title: 'Contacts', - activeTintColor: 'yellow', + activeTintColor: 'blue', }, { key: 'chat', diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift index 16248d9..e04cea1 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift @@ -214,12 +214,16 @@ struct TabViewImpl: View { private func configureTabBarItemImages(items: [UITabBarItem], props: TabViewProps) { for (tabBarIndex, item) in items.enumerated() { guard let tabData = props.filteredItems[safe: tabBarIndex], - let itemIndex = props.items.firstIndex(where: { $0.key == tabData.key }), - let icon = props.icons[itemIndex] else { continue } + let itemIndex = props.items.firstIndex(where: { $0.key == tabData.key }) else { continue } let tabActiveColor = tabData.activeTintColor ?? props.activeTintColor + let assetIcon = props.icons[itemIndex] + let icon = assetIcon ?? makeSFSymbolImage(named: tabData.sfSymbol) - if shouldRenderTabBarLabelsIntoImages(), props.labeled { + if shouldRenderTabBarLabelsIntoImages(), + props.labeled, + tabData.role != .search, + let icon { item.title = "" item.accessibilityLabel = tabData.title item.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: 100) @@ -238,12 +242,14 @@ struct TabViewImpl: View { continue } - item.image = props.inactiveTintColor.map { - icon.withTintColor($0, renderingMode: .alwaysOriginal) - } ?? icon - item.selectedImage = tabActiveColor.map { - icon.withTintColor($0, renderingMode: .alwaysOriginal) - } ?? icon + if let icon { + item.image = props.inactiveTintColor.map { + icon.withTintColor($0, renderingMode: .alwaysOriginal) + } ?? icon + item.selectedImage = tabActiveColor.map { + icon.withTintColor($0, renderingMode: .alwaysOriginal) + } ?? icon + } item.setTitleTextAttributes( TabBarFontSize.createFontAttributes( @@ -257,6 +263,12 @@ struct TabViewImpl: View { } } + private func makeSFSymbolImage(named sfSymbol: String?) -> UIImage? { + guard let sfSymbol, !sfSymbol.isEmpty else { return nil } + + return UIImage(systemName: sfSymbol) + } + private func configureTabBarItemImagesAfterLayout(tabBar: UITabBar, props: TabViewProps) { guard shouldRenderTabBarLabelsIntoImages() else { return } @@ -310,12 +322,17 @@ struct TabViewImpl: View { let image = UIGraphicsImageRenderer(size: imageSize, format: format).image { _ in let tintedIcon = icon.withTintColor(color, renderingMode: .alwaysOriginal) - tintedIcon.draw(in: CGRect( - x: (imageSize.width - iconSize.width) / 2, - y: 0, - width: iconSize.width, - height: iconSize.height - )) + let iconFrame = aspectFitRect( + size: tintedIcon.size, + in: CGRect( + x: (imageSize.width - iconSize.width) / 2, + y: 0, + width: iconSize.width, + height: iconSize.height + ) + ) + + tintedIcon.draw(in: iconFrame) (title as NSString).draw( in: CGRect( @@ -330,6 +347,22 @@ struct TabViewImpl: View { return image.withRenderingMode(.alwaysOriginal) } + + private func aspectFitRect(size: CGSize, in rect: CGRect) -> CGRect { + guard size.width > 0, size.height > 0 else { + return rect + } + + let scale = min(rect.width / size.width, rect.height / size.height) + let fittedSize = CGSize(width: size.width * scale, height: size.height * scale) + + return CGRect( + x: rect.minX + (rect.width - fittedSize.width) / 2, + y: rect.minY + (rect.height - fittedSize.height) / 2, + width: fittedSize.width, + height: fittedSize.height + ) + } #endif extension View { From 101c9d3dcacd10dee4e3941dcd901ac925aa6b6e Mon Sep 17 00:00:00 2001 From: Thiago Brezinski Date: Mon, 25 May 2026 15:30:37 +0100 Subject: [PATCH 7/9] chore: add changeset --- .changeset/large-hats-teach.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/large-hats-teach.md diff --git a/.changeset/large-hats-teach.md b/.changeset/large-hats-teach.md new file mode 100644 index 0000000..d7474e7 --- /dev/null +++ b/.changeset/large-hats-teach.md @@ -0,0 +1,5 @@ +--- +'react-native-bottom-tabs': patch +--- + +Fix active and inactive tint color behavior on iOS 26 Liquid Glass From 690d08eadb92a34e431a09f549ff8fc7663d2a88 Mon Sep 17 00:00:00 2001 From: Thiago Brezinski Date: Tue, 26 May 2026 10:10:30 +0100 Subject: [PATCH 8/9] chore: cleanup --- apps/example/src/Examples/TintColors.tsx | 7 ++- .../ios/TabViewImpl.swift | 46 +++++++++++-------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/apps/example/src/Examples/TintColors.tsx b/apps/example/src/Examples/TintColors.tsx index 5c16e39..2cf72e4 100644 --- a/apps/example/src/Examples/TintColors.tsx +++ b/apps/example/src/Examples/TintColors.tsx @@ -4,6 +4,7 @@ import { Article } from '../Screens/Article'; import { Albums } from '../Screens/Albums'; import { Contacts } from '../Screens/Contacts'; import { Chat } from '../Screens/Chat'; +import { Platform } from 'react-native'; const renderScene = SceneMap({ article: Article, @@ -12,6 +13,8 @@ const renderScene = SceneMap({ chat: Chat, }); +const isAndroid = Platform.OS === 'android'; + export default function TintColorsExample() { const [index, setIndex] = useState(0); const [routes] = useState([ @@ -31,7 +34,9 @@ export default function TintColorsExample() { }, { key: 'contacts', - focusedIcon: require('../../assets/icons/person_dark.png'), + focusedIcon: isAndroid + ? require('../../assets/icons/person_dark.png') + : { sfSymbol: 'person.fill' }, title: 'Contacts', activeTintColor: 'blue', }, diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift index e04cea1..9030990 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift @@ -115,9 +115,16 @@ struct TabViewImpl: View { #if !os(macOS) private func updateTabBarImages(props: TabViewProps, tabBar: UITabBar?) { - guard let tabBar, let items = tabBar.items else { return } + guard shouldApplyLiquidGlassTintWorkaround(), + let tabBar, + let items = tabBar.items else { return } + configureTabBarItemImages(items: items, props: props) - configureTabBarItemImagesAfterLayout(tabBar: tabBar, props: props) + + DispatchQueue.main.async { [weak tabBar] in + guard let tabBar, let items = tabBar.items else { return } + configureTabBarItemImages(items: items, props: props) + } } private func updateTabBarAppearance(props: TabViewProps, tabBar: UITabBar?) { @@ -219,13 +226,12 @@ struct TabViewImpl: View { let tabActiveColor = tabData.activeTintColor ?? props.activeTintColor let assetIcon = props.icons[itemIndex] let icon = assetIcon ?? makeSFSymbolImage(named: tabData.sfSymbol) + let shouldRenderLabelIntoImage = props.labeled && tabData.role != .search && icon != nil - if shouldRenderTabBarLabelsIntoImages(), - props.labeled, - tabData.role != .search, - let icon { + item.accessibilityLabel = tabData.title + + if shouldRenderLabelIntoImage, let icon { item.title = "" - item.accessibilityLabel = tabData.title item.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: 100) item.image = makeTabBarItemImage( icon: icon, @@ -242,6 +248,9 @@ struct TabViewImpl: View { continue } + item.title = props.labeled ? tabData.title : nil + item.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: 0) + if let icon { item.image = props.inactiveTintColor.map { icon.withTintColor($0, renderingMode: .alwaysOriginal) @@ -269,15 +278,6 @@ struct TabViewImpl: View { return UIImage(systemName: sfSymbol) } - private func configureTabBarItemImagesAfterLayout(tabBar: UITabBar, props: TabViewProps) { - guard shouldRenderTabBarLabelsIntoImages() else { return } - - DispatchQueue.main.async { [weak tabBar] in - guard let tabBar, let items = tabBar.items else { return } - configureTabBarItemImages(items: items, props: props) - } - } - private func selectedAttributes(props: TabViewProps) -> [NSAttributedString.Key: Any] { TabBarFontSize.createFontAttributes( size: props.fontSize.map(CGFloat.init) ?? TabBarFontSize.defaultSize, @@ -287,9 +287,14 @@ struct TabViewImpl: View { ) } - private func shouldRenderTabBarLabelsIntoImages() -> Bool { - let version = ProcessInfo.processInfo.operatingSystemVersion - return version.majorVersion >= 26 + private func shouldApplyLiquidGlassTintWorkaround() -> Bool { + #if os(iOS) + if #available(iOS 26.0, *) { + return true + } + #endif + + return false } private func makeTabBarItemImage( @@ -427,6 +432,9 @@ extension View { .onChange(of: props.iconsRevision) { _ in updateTabBarImages(props: props, tabBar: tabBar) } + .onChange(of: props.labeled) { _ in + updateTabBarImages(props: props, tabBar: tabBar) + } .onChange(of: props.fontSize) { _ in updateTabBarAppearance(props: props, tabBar: tabBar) updateTabBarImages(props: props, tabBar: tabBar) From 4cf55adbda7cefe8612d0801a5ec2a7bf1e59d96 Mon Sep 17 00:00:00 2001 From: Thiago Brezinski Date: Tue, 26 May 2026 11:57:17 +0100 Subject: [PATCH 9/9] chore: only enable bake pass if providing props --- docs/docs/docs/guides/standalone-usage.md | 8 ++++++++ .../docs/docs/guides/usage-with-react-navigation.mdx | 12 ++++++++++++ .../react-native-bottom-tabs/ios/TabViewImpl.swift | 2 +- .../react-native-bottom-tabs/ios/TabViewProps.swift | 6 ++++++ 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/docs/docs/guides/standalone-usage.md b/docs/docs/docs/guides/standalone-usage.md index 959a84f..3d877a4 100644 --- a/docs/docs/docs/guides/standalone-usage.md +++ b/docs/docs/docs/guides/standalone-usage.md @@ -174,12 +174,20 @@ Color for the active tab. - Type: `ColorValue` +:::warning +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. +::: + #### `tabBarInactiveTintColor` Color for inactive tabs. - Type: `ColorValue` +:::warning +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. +::: + #### `tabBarStyle` Object containing styles for the tab bar. diff --git a/docs/docs/docs/guides/usage-with-react-navigation.mdx b/docs/docs/docs/guides/usage-with-react-navigation.mdx index 8272217..683be2a 100644 --- a/docs/docs/docs/guides/usage-with-react-navigation.mdx +++ b/docs/docs/docs/guides/usage-with-react-navigation.mdx @@ -124,10 +124,18 @@ It's recommended to use `transparent` or `opaque` without lazy loading as the ta Color for the active tab. +:::warning +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. +::: + #### `tabBarInactiveTintColor` Color for the inactive tabs. +:::warning +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. +::: + #### `tabBarStyle` Object containing styles for the tab bar. @@ -240,6 +248,10 @@ Label text of the tab displayed in the navigation bar. When undefined, scene tit Color for the active tab. +:::warning +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. +::: + :::note The `tabBarInactiveTintColor` is not supported on route level due to native limitations. Use `inactiveTintColor` in the `Tab.Navigator` instead. ::: diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift index 9030990..2a5abdc 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift @@ -226,7 +226,7 @@ struct TabViewImpl: View { let tabActiveColor = tabData.activeTintColor ?? props.activeTintColor let assetIcon = props.icons[itemIndex] let icon = assetIcon ?? makeSFSymbolImage(named: tabData.sfSymbol) - let shouldRenderLabelIntoImage = props.labeled && tabData.role != .search && icon != nil + let shouldRenderLabelIntoImage = props.hasCustomTintColors && props.labeled && tabData.role != .search && icon != nil item.accessibilityLabel = tabData.title diff --git a/packages/react-native-bottom-tabs/ios/TabViewProps.swift b/packages/react-native-bottom-tabs/ios/TabViewProps.swift index 4f9a79a..612408d 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewProps.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewProps.swift @@ -83,6 +83,12 @@ class TabViewProps: ObservableObject { return activeTintColor } + var hasCustomTintColors: Bool { + activeTintColor != nil + || inactiveTintColor != nil + || items.contains(where: { $0.activeTintColor != nil }) + } + var filteredItems: [TabInfo] { items.filter { !$0.hidden || $0.key == selectedPage