diff --git a/src/Controls/samples/Controls.Sample.Sandbox/App.xaml.cs b/src/Controls/samples/Controls.Sample.Sandbox/App.xaml.cs
index 9512dea98e39..73a2b4065ffe 100644
--- a/src/Controls/samples/Controls.Sample.Sandbox/App.xaml.cs
+++ b/src/Controls/samples/Controls.Sample.Sandbox/App.xaml.cs
@@ -1,24 +1,14 @@
-namespace Maui.Controls.Sample;
+namespace Maui.Controls.Sample;
public partial class App : Application
{
- public App()
- {
- InitializeComponent();
- }
-
- protected override Window CreateWindow(IActivationState? activationState)
- {
- // To test shell scenarios, change this to true
- bool useShell = false;
+public App()
+{
+InitializeComponent();
+}
- if (!useShell)
- {
- return new Window(new NavigationPage(new MainPage()));
- }
- else
- {
- return new Window(new SandboxShell());
- }
- }
+protected override Window CreateWindow(IActivationState? activationState)
+{
+return new Window(new SandboxShell());
+}
}
diff --git a/src/Controls/samples/Controls.Sample.Sandbox/MainPage2.xaml b/src/Controls/samples/Controls.Sample.Sandbox/MainPage2.xaml
new file mode 100644
index 000000000000..1d8367a1d780
--- /dev/null
+++ b/src/Controls/samples/Controls.Sample.Sandbox/MainPage2.xaml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/src/Controls/samples/Controls.Sample.Sandbox/MainPage2.xaml.cs b/src/Controls/samples/Controls.Sample.Sandbox/MainPage2.xaml.cs
new file mode 100644
index 000000000000..5300db6a8bff
--- /dev/null
+++ b/src/Controls/samples/Controls.Sample.Sandbox/MainPage2.xaml.cs
@@ -0,0 +1,9 @@
+namespace Maui.Controls.Sample;
+
+public partial class MainPage2 : ContentPage
+{
+ public MainPage2()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/src/Controls/samples/Controls.Sample.Sandbox/SandboxShell.xaml b/src/Controls/samples/Controls.Sample.Sandbox/SandboxShell.xaml
index 358b0b04ef63..3d4d59d20de1 100644
--- a/src/Controls/samples/Controls.Sample.Sandbox/SandboxShell.xaml
+++ b/src/Controls/samples/Controls.Sample.Sandbox/SandboxShell.xaml
@@ -1,22 +1,12 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SafeShellTabBarAppearanceTracker.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SafeShellTabBarAppearanceTracker.cs
index 168d119d8e2c..ec3c3b80cc37 100644
--- a/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SafeShellTabBarAppearanceTracker.cs
+++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SafeShellTabBarAppearanceTracker.cs
@@ -1,6 +1,7 @@
#nullable disable
using System;
using System.ComponentModel;
+using Microsoft.Maui.Platform;
using ObjCRuntime;
using UIKit;
@@ -13,6 +14,11 @@ public class SafeShellTabBarAppearanceTracker : IShellTabBarAppearanceTracker
UIColor _defaultTint;
UIColor _defaultUnselectedTint;
UITabBarAppearance _tabBarAppearance;
+
+ // iOS 26+ stores pending colors to re-apply during layout
+ UIColor _pendingUnselectedTintColor;
+ UIColor _pendingSelectedTintColor;
+
public virtual void ResetAppearance(UITabBarController controller)
{
if (_defaultTint == null)
@@ -22,6 +28,8 @@ public virtual void ResetAppearance(UITabBarController controller)
tabBar.BarTintColor = _defaultBarTint;
tabBar.TintColor = _defaultTint;
tabBar.UnselectedItemTintColor = _defaultUnselectedTint;
+ _pendingUnselectedTintColor = null;
+ _pendingSelectedTintColor = null;
}
public virtual void SetAppearance(UITabBarController controller, ShellAppearance appearance)
@@ -50,6 +58,19 @@ public virtual void SetAppearance(UITabBarController controller, ShellAppearance
public virtual void UpdateLayout(UITabBarController controller)
{
+ // iOS 26+: Re-apply colors on every layout pass.
+ // The liquid glass tab bar resets subview properties during layout.
+ if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
+ {
+ var tabBar = controller.TabBar;
+ if (_pendingSelectedTintColor is not null)
+ tabBar.TintColor = _pendingSelectedTintColor;
+ if (_pendingUnselectedTintColor is not null)
+ {
+ tabBar.UnselectedItemTintColor = _pendingUnselectedTintColor;
+ tabBar.ApplyPreColoredImagesForIOS26(_pendingUnselectedTintColor, _pendingSelectedTintColor);
+ }
+ }
}
#region IDisposable Support
@@ -76,6 +97,55 @@ void UpdateiOS15TabBarAppearance(UITabBarController controller, ShellAppearance
var unselectedColor = appearanceElement.EffectiveTabBarUnselectedColor;
var titleColor = appearanceElement.EffectiveTabBarTitleColor;
+ // iOS 26+: The UITabBarAppearance Normal state (TitleTextAttributes, IconColor) is
+ // ignored by the liquid glass tab bar. Skip the full appearance pipeline and use
+ // direct UITabBar properties plus subview coloring instead.
+ // See: https://github.com/dotnet/maui/issues/32125, https://github.com/dotnet/maui/issues/34605
+ if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
+ {
+ var tabBar = controller.TabBar;
+
+ // Background color via appearance (this still works on iOS 26)
+ if (_tabBarAppearance == null)
+ {
+ _tabBarAppearance = new UITabBarAppearance();
+ _tabBarAppearance.ConfigureWithDefaultBackground();
+ }
+ if (backgroundColor is not null)
+ _tabBarAppearance.BackgroundColor = backgroundColor.ToPlatform();
+
+ tabBar.StandardAppearance = _tabBarAppearance;
+ tabBar.ScrollEdgeAppearance = _tabBarAppearance;
+
+ // Selected color via TintColor (works on iOS 26)
+ var selectedColor = foregroundColor ?? titleColor;
+ if (selectedColor is not null)
+ {
+ _pendingSelectedTintColor = selectedColor.ToPlatform();
+ tabBar.TintColor = _pendingSelectedTintColor;
+ }
+ else
+ {
+ _pendingSelectedTintColor = null;
+ tabBar.TintColor = _defaultTint;
+ }
+
+ // Unselected color: set property + pre-colored images for visual rendering
+ if (unselectedColor is not null)
+ {
+ _pendingUnselectedTintColor = unselectedColor.ToPlatform();
+ tabBar.UnselectedItemTintColor = _pendingUnselectedTintColor;
+ tabBar.ApplyPreColoredImagesForIOS26(_pendingUnselectedTintColor, _pendingSelectedTintColor);
+ }
+ else
+ {
+ _pendingUnselectedTintColor = null;
+ tabBar.UnselectedItemTintColor = _defaultUnselectedTint;
+ }
+
+ return;
+ }
+
controller.TabBar
.UpdateiOS15TabBarAppearance(
ref _tabBarAppearance,
diff --git a/src/Controls/src/Core/Compatibility/Handlers/TabbedPage/iOS/TabbedRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/TabbedPage/iOS/TabbedRenderer.cs
index 78a0b58b383a..53d7f850be8e 100644
--- a/src/Controls/src/Core/Compatibility/Handlers/TabbedPage/iOS/TabbedRenderer.cs
+++ b/src/Controls/src/Core/Compatibility/Handlers/TabbedPage/iOS/TabbedRenderer.cs
@@ -8,6 +8,7 @@
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Controls.Platform;
using Microsoft.Maui.Graphics;
+using Microsoft.Maui.Platform;
using UIKit;
using static Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.Page;
using PageUIStatusBarAnimation = Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.UIStatusBarAnimation;
@@ -29,6 +30,10 @@ public class TabbedRenderer : UITabBarController, IPlatformViewHandler
UITabBarAppearance _tabBarAppearance;
WeakReference _element;
+ // iOS 26+: cached unselected tint color for re-application during layout
+ UIColor _pendingUnselectedTintColor;
+ UIColor _pendingSelectedTintColor;
+
Brush _currentBarBackground;
IMauiContext MauiContext => _mauiContext;
@@ -136,6 +141,15 @@ public override void ViewDidLayoutSubviews()
// in narrow viewports (< 667 points) before Element is set. Guard against this.
if (Element is IView view)
view.Arrange(View.Bounds.ToRectangle());
+
+ // iOS 26+: Re-apply unselected item colors on every layout pass.
+ // The liquid glass tab bar resets subview tint colors during layout.
+ if ((OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
+ && _pendingUnselectedTintColor is not null && TabBar is not null)
+ {
+ TabBar.UnselectedItemTintColor = _pendingUnselectedTintColor;
+ TabBar.ApplyPreColoredImagesForIOS26(_pendingUnselectedTintColor, _pendingSelectedTintColor);
+ }
}
protected override void Dispose(bool disposing)
@@ -190,7 +204,7 @@ void OnPagePropertyChanged(object sender, PropertyChangedEventArgs e)
UpdateTabBarItem(page);
}
}
-
+
public override void TraitCollectionDidChange(UITraitCollection previousTraitCollection)
{
if (previousTraitCollection.VerticalSizeClass == TraitCollection.VerticalSizeClass)
@@ -610,15 +624,32 @@ void UpdateiOS15TabBarAppearance()
{
if (Tabbed is not TabbedPage tabbed)
return;
+
+ var unselectedTabColor = tabbed.IsSet(TabbedPage.UnselectedTabColorProperty) ? tabbed.UnselectedTabColor : null;
+ var barTextColor = tabbed.IsSet(TabbedPage.BarTextColorProperty) ? tabbed.BarTextColor : null;
+
TabBar.UpdateiOS15TabBarAppearance(
ref _tabBarAppearance,
_defaultBarColor,
_defaultBarTextColor,
tabbed.IsSet(TabbedPage.SelectedTabColorProperty) ? tabbed.SelectedTabColor : null,
- tabbed.IsSet(TabbedPage.UnselectedTabColorProperty) ? tabbed.UnselectedTabColor : null,
+ unselectedTabColor,
tabbed.IsSet(TabbedPage.BarBackgroundColorProperty) ? tabbed.BarBackgroundColor : null,
- tabbed.IsSet(TabbedPage.BarTextColorProperty) ? tabbed.BarTextColor : null,
- tabbed.IsSet(TabbedPage.BarTextColorProperty) ? tabbed.BarTextColor : null);
+ barTextColor,
+ barTextColor);
+
+ // Cache the effective colors for iOS 26 layout re-application
+ if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
+ {
+ _pendingUnselectedTintColor = unselectedTabColor?.ToPlatform()
+ ?? barTextColor?.ToPlatform()
+ ?? _defaultBarTextColor;
+
+ var selectedTabColor2 = tabbed.IsSet(TabbedPage.SelectedTabColorProperty) ? tabbed.SelectedTabColor : null;
+ _pendingSelectedTintColor = selectedTabColor2?.ToPlatform()
+ ?? barTextColor?.ToPlatform()
+ ?? _defaultBarTextColor;
+ }
}
#region IPlatformViewHandler
diff --git a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTabBarTests.cs b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTabBarTests.cs
index 81fe5109fc59..94979d06046e 100644
--- a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTabBarTests.cs
+++ b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTabBarTests.cs
@@ -23,8 +23,7 @@ public partial class ShellTests
public async Task ForegroundColorSetsIconAndTitleColorSetsTitle()
{
#if IOS || MACCATALYST
- // Skip on iOS 26+ due to UITabBar internal API changes
- // See: https://github.com/dotnet/maui/issues/33004
+ // Pixel-based tab bar color verification doesn't work on iOS 26+ due to UITabBar rendering changes
if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
return;
#endif
@@ -54,8 +53,7 @@ await RunShellTabBarTests(shell =>
public async Task ShellTabBarTitleColorInitializesCorrectly(string colorHex)
{
#if IOS || MACCATALYST
- // Skip on iOS 26+ due to UITabBar internal API changes
- // See: https://github.com/dotnet/maui/issues/33004
+ // Pixel-based tab bar color verification doesn't work on iOS 26+ due to UITabBar rendering changes
if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
return;
#endif
@@ -78,8 +76,7 @@ await RunShellTabBarTests(shell => Shell.SetTabBarTitleColor(shell, expectedColo
public async Task ShellTabBarForegroundInitializesCorrectly(string colorHex)
{
#if IOS || MACCATALYST
- // Skip on iOS 26+ due to UITabBar internal API changes
- // See: https://github.com/dotnet/maui/issues/33004
+ // Pixel-based tab bar color verification doesn't work on iOS 26+ due to UITabBar rendering changes
if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
return;
#endif
@@ -100,8 +97,7 @@ await RunShellTabBarTests(shell => Shell.SetTabBarForegroundColor(shell, expecte
public async Task ShellTabBarUnselectedColorInitializesCorrectly(string colorHex)
{
#if IOS || MACCATALYST
- // Skip on iOS 26+ due to UITabBar internal API changes
- // See: https://github.com/dotnet/maui/issues/33004
+ // Pixel-based tab bar color verification doesn't work on iOS 26+ due to UITabBar rendering changes
if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
return;
#endif
@@ -116,6 +112,39 @@ await RunShellTabBarTests(shell => Shell.SetTabBarUnselectedColor(shell, expecte
});
}
+ // Regression test for https://github.com/dotnet/maui/issues/32125
+ // On iOS 26+, Shell.TabBarUnselectedColor was not applied to unselected tabs
+ [Fact(DisplayName = "Shell TabBar UnselectedColor and TitleColor work together")]
+ public async Task ShellTabBarUnselectedAndTitleColorWorkTogether()
+ {
+ var titleColor = Color.FromArgb("#FFFF0000");
+ var unselectedColor = Color.FromArgb("#FF808080");
+ await RunShellTabBarTests(shell =>
+ {
+ Shell.SetTabBarTitleColor(shell, titleColor);
+ Shell.SetTabBarUnselectedColor(shell, unselectedColor);
+ },
+ async (shell) =>
+ {
+#if IOS || MACCATALYST
+ if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
+ {
+ // On iOS 26+, verify the UnselectedItemTintColor property directly
+ // because pixel-based verification doesn't work with the new tab bar rendering
+ await ValidateTabBarUnselectedTintColorProperty(shell.CurrentSection, unselectedColor);
+ }
+ else
+#endif
+ {
+ // Selected tab should use title color
+ await ValidateTabBarTextColor(shell.CurrentSection, titleColor, true);
+ // Unselected tab should use unselected color
+ await ValidateTabBarTextColor(shell.Items[0].Items[1], unselectedColor, true);
+ await ValidateTabBarIconColor(shell.Items[0].Items[1], unselectedColor, true);
+ }
+ });
+ }
+
async Task RunShellTabBarTests(Action setup, Func runTest)
{
SetupBuilder();
diff --git a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTabBarTests.iOS.cs b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTabBarTests.iOS.cs
index 109d30a17d73..680fe5a63685 100644
--- a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTabBarTests.iOS.cs
+++ b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTabBarTests.iOS.cs
@@ -2,6 +2,7 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Maui.Controls;
+using Microsoft.Maui.Controls.Platform.Compatibility;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform;
using UIKit;
@@ -15,12 +16,35 @@ public partial class ShellTests
UITabBar GetTabBar(ShellSection item)
{
var shellItem = item.Parent as ShellItem;
- var shell = shellItem.Parent as Shell;
+ var shell = shellItem?.Parent as Shell;
- var pagerParent = (shell.CurrentPage.Handler as IPlatformViewHandler)
- .PlatformView.FindParent(x => x.NextResponder is UITabBarController);
+ if (shell?.Handler is IShellContext shellContext)
+ {
+ if (shellContext.CurrentShellItemRenderer is UITabBarController tabBarController)
+ return tabBarController.TabBar;
+ }
+
+ // Fallback: walk the view hierarchy from the current page
+ var platformView = (shell?.CurrentPage?.Handler as IPlatformViewHandler)?.PlatformView;
+ if (platformView is null)
+ return null;
+
+ var pagerParent = platformView.FindParent(x => x.NextResponder is UITabBarController);
+
+ if (pagerParent is null)
+ {
+ // iOS 26+: walk the responder chain to find the UITabBarController directly
+ UIResponder responder = platformView;
+ while (responder != null)
+ {
+ if (responder is UITabBarController tbc)
+ return tbc.TabBar;
+ responder = responder.NextResponder;
+ }
+ return null;
+ }
- // In macOS 15 Sequoia, the UITabBar is nested within the second subview (index 1) of the pagerParent.
+ // In macOS 15 Sequoia, the UITabBar is nested within the second subview (index 1) of the pagerParent.
if (OperatingSystem.IsMacCatalystVersionAtLeast(15, 0) || OperatingSystem.IsMacOSVersionAtLeast(15, 0))
{
var subview = pagerParent.Subviews.ElementAtOrDefault(1);
@@ -67,5 +91,16 @@ await AssertionExtensions.AssertTabItemTextDoesNotContainColor(GetTabBar(item),
item.Title, textColor, MauiContext);
}
}
+
+ Task ValidateTabBarUnselectedTintColorProperty(ShellSection item, Color expectedColor)
+ {
+ var tabBar = GetTabBar(item);
+ Assert.NotNull(tabBar);
+ Assert.NotNull(tabBar.UnselectedItemTintColor);
+ Assert.True(
+ ColorComparison.ARGBEquivalent(tabBar.UnselectedItemTintColor, expectedColor.ToPlatform(), 0.1),
+ $"Expected UnselectedItemTintColor to be {expectedColor} but got {tabBar.UnselectedItemTintColor}");
+ return Task.CompletedTask;
+ }
}
}
\ No newline at end of file
diff --git a/src/Controls/tests/DeviceTests/Elements/TabbedPage/TabbedPageTests.cs b/src/Controls/tests/DeviceTests/Elements/TabbedPage/TabbedPageTests.cs
index f8ddc4ac8cb5..065494182538 100644
--- a/src/Controls/tests/DeviceTests/Elements/TabbedPage/TabbedPageTests.cs
+++ b/src/Controls/tests/DeviceTests/Elements/TabbedPage/TabbedPageTests.cs
@@ -81,8 +81,8 @@ await CreateHandlerAndAddToWindow(new Window(tabbedPage), (ha
public async Task BarTextColor()
{
#if IOS || MACCATALYST
- // Skip on iOS 26+ due to UITabBar internal API changes
- // See: https://github.com/dotnet/maui/issues/33004
+ // Pixel-based text color verification doesn't work on iOS 26+ due to UITabBar rendering changes.
+ // Property-based tests (UnselectedItemTintColorSetFromBarTextColor) verify the fix instead.
if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
return;
#endif
@@ -96,20 +96,11 @@ public async Task BarTextColor()
tabbedPage.BarTextColor = Colors.Red;
await CreateHandlerAndAddToWindow(tabbedPage, async handler =>
{
- // Pre iOS15 you couldn't set the text color of the unselected tab
- // so only android/windows currently set the color of both
-
-#if IOS
- bool unselectedMatchesSelected = false;
-#else
- bool unselectedMatchesSelected = true;
-#endif
-
await ValidateTabBarTextColor(tabbedPage, tabbedPage.Children[0].Title, Colors.Red, true);
- await ValidateTabBarTextColor(tabbedPage, tabbedPage.Children[1].Title, Colors.Red, unselectedMatchesSelected);
+ await ValidateTabBarTextColor(tabbedPage, tabbedPage.Children[1].Title, Colors.Red, true);
tabbedPage.BarTextColor = Colors.Blue;
await ValidateTabBarTextColor(tabbedPage, tabbedPage.Children[0].Title, Colors.Blue, true);
- await ValidateTabBarTextColor(tabbedPage, tabbedPage.Children[1].Title, Colors.Blue, unselectedMatchesSelected);
+ await ValidateTabBarTextColor(tabbedPage, tabbedPage.Children[1].Title, Colors.Blue, true);
});
}
@@ -121,8 +112,8 @@ await CreateHandlerAndAddToWindow(tabbedPage, async handler =
public async Task SelectedAndUnselectedTabColor()
{
#if IOS || MACCATALYST
- // Skip on iOS 26+ due to UITabBar internal API changes
- // See: https://github.com/dotnet/maui/issues/33004
+ // Pixel-based text color verification doesn't work on iOS 26+ due to UITabBar rendering changes.
+ // Property-based tests (UnselectedItemTintColorSetFromUnselectedTabColor) verify the fix instead.
if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
return;
#endif
@@ -135,15 +126,8 @@ public async Task SelectedAndUnselectedTabColor()
await CreateHandlerAndAddToWindow(tabbedPage, async handler =>
{
- // Pre iOS15 you couldn't set the text color of the unselected tab
- // so only android/windows currently set the color of both
-#if IOS
- bool unselectedMatchesTabColor = false;
-#else
- bool unselectedMatchesTabColor = true;
-#endif
await ValidateTabBarTextColor(tabbedPage, tabbedPage.Children[0].Title, Colors.Red, true);
- await ValidateTabBarTextColor(tabbedPage, tabbedPage.Children[1].Title, Colors.Purple, unselectedMatchesTabColor);
+ await ValidateTabBarTextColor(tabbedPage, tabbedPage.Children[1].Title, Colors.Purple, true);
await ValidateTabBarIconColor(tabbedPage, tabbedPage.Children[0].Title, Colors.Red, true);
await ValidateTabBarIconColor(tabbedPage, tabbedPage.Children[1].Title, Colors.Purple, true);
@@ -151,12 +135,53 @@ await CreateHandlerAndAddToWindow(tabbedPage, async handler =
await OnNavigatedToAsync(tabbedPage.CurrentPage);
await ValidateTabBarTextColor(tabbedPage, tabbedPage.Children[0].Title, Colors.Purple, true);
- await ValidateTabBarTextColor(tabbedPage, tabbedPage.Children[1].Title, Colors.Red, unselectedMatchesTabColor);
+ await ValidateTabBarTextColor(tabbedPage, tabbedPage.Children[1].Title, Colors.Red, true);
await ValidateTabBarIconColor(tabbedPage, tabbedPage.Children[0].Title, Colors.Purple, true);
await ValidateTabBarIconColor(tabbedPage, tabbedPage.Children[1].Title, Colors.Red, true);
});
}
+ // Regression test for https://github.com/dotnet/maui/issues/34605
+ // On iOS 26.2+, BarTextColor was only applied to the selected tab, not unselected tabs
+ [Fact(DisplayName = "BarTextColor applies to unselected tabs without UnselectedTabColor set"
+#if MACCATALYST
+ , Skip = "Fails on Mac Catalyst, fixme"
+#endif
+ )]
+ public async Task BarTextColorAppliesToUnselectedTabsWithoutExplicitUnselectedColor()
+ {
+#if IOS || MACCATALYST
+ // Pixel-based text color verification doesn't work on iOS 26+ due to UITabBar rendering changes.
+ // Property-based tests (UnselectedItemTintColorSetFromBarTextColor) verify the fix instead.
+ if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
+ return;
+#endif
+ SetupBuilder();
+ var tabbedPage = CreateBasicTabbedPage(true, pages: new[]
+ {
+ new ContentPage() { Title = "Tab A", IconImageSource = "white.png" },
+ new ContentPage() { Title = "Tab B", IconImageSource = "white.png" },
+ new ContentPage() { Title = "Tab C", IconImageSource = "white.png" }
+ });
+
+ // Set ONLY BarTextColor (no SelectedTabColor or UnselectedTabColor)
+ tabbedPage.BarTextColor = Colors.Orange;
+
+ await CreateHandlerAndAddToWindow(tabbedPage, async handler =>
+ {
+ // All tabs (selected and unselected) should use the BarTextColor
+ await ValidateTabBarTextColor(tabbedPage, "Tab A", Colors.Orange, true);
+ await ValidateTabBarTextColor(tabbedPage, "Tab B", Colors.Orange, true);
+ await ValidateTabBarTextColor(tabbedPage, "Tab C", Colors.Orange, true);
+
+ // Switch selected tab and verify colors still correct
+ tabbedPage.CurrentPage = tabbedPage.Children[2];
+ await OnNavigatedToAsync(tabbedPage.CurrentPage);
+ await ValidateTabBarTextColor(tabbedPage, "Tab A", Colors.Orange, true);
+ await ValidateTabBarTextColor(tabbedPage, "Tab C", Colors.Orange, true);
+ });
+ }
+
#if !IOS && !MACCATALYST
// iOS currently can't handle recreating a handler if it's disconnecting
// This is left over behavior from Forms and will be fixed by a different PR
@@ -378,7 +403,7 @@ await CreateHandlerAndAddToWindow(new Window(tabbedPage), asy
}
#endif
-#if IOS
+#if IOS
[Theory(Skip = "Test doesn't work on iOS yet; probably because of https://github.com/dotnet/maui/issues/10591")]
#elif WINDOWS
[Theory(Skip = "Test doesn't work on Windows")]
diff --git a/src/Controls/tests/DeviceTests/Elements/TabbedPage/TabbedPageTests.iOS.cs b/src/Controls/tests/DeviceTests/Elements/TabbedPage/TabbedPageTests.iOS.cs
index a2e8a519c710..68b93eafa171 100644
--- a/src/Controls/tests/DeviceTests/Elements/TabbedPage/TabbedPageTests.iOS.cs
+++ b/src/Controls/tests/DeviceTests/Elements/TabbedPage/TabbedPageTests.iOS.cs
@@ -7,6 +7,8 @@
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform;
using UIKit;
+using Xunit;
+using TabbedViewHandler = Microsoft.Maui.Controls.Handlers.Compatibility.TabbedRenderer;
namespace Microsoft.Maui.DeviceTests
{
@@ -15,10 +17,27 @@ public partial class TabbedPageTests
{
UITabBar GetTabBar(TabbedPage tabbedPage)
{
- var pagerParent = (tabbedPage.CurrentPage.Handler as IPlatformViewHandler)
- .PlatformView.FindParent(x => x.NextResponder is UITabBarController);
+ // TabbedRenderer IS a UITabBarController — get TabBar from ViewController
+ var platformHandler = tabbedPage.Handler as IPlatformViewHandler;
+ if (platformHandler?.ViewController is UITabBarController tbc)
+ return tbc.TabBar;
- return pagerParent.Subviews.FirstOrDefault(v => v.GetType() == typeof(UITabBar)) as UITabBar;
+ // Fallback: walk the responder chain
+ var handler = tabbedPage.CurrentPage.Handler as IPlatformViewHandler;
+ var platformView = handler?.PlatformView;
+
+ if (platformView is null)
+ throw new Exception("Unable to get platform view from handler");
+
+ UIResponder responder = platformView;
+ while (responder != null)
+ {
+ if (responder is UITabBarController controller)
+ return controller.TabBar;
+ responder = responder.NextResponder;
+ }
+
+ throw new Exception("Unable to find UITabBarController in view hierarchy");
}
async Task ValidateTabBarIconColor(
@@ -60,5 +79,128 @@ await AssertionExtensions.AssertTabItemTextDoesNotContainColor(
tabText, iconColor, MauiContext);
}
}
+
+ // Regression test for https://github.com/dotnet/maui/issues/34605
+ // Verifies UnselectedItemTintColor is correctly set on iOS 26+ when BarTextColor is set
+ [Fact(DisplayName = "iOS 26: UnselectedItemTintColor set when BarTextColor specified")]
+ public async Task UnselectedItemTintColorSetFromBarTextColor()
+ {
+ if (!OperatingSystem.IsIOSVersionAtLeast(26) && !OperatingSystem.IsMacCatalystVersionAtLeast(26))
+ return;
+
+ SetupBuilder();
+ var tabbedPage = CreateBasicTabbedPage(true, pages: new[]
+ {
+ new ContentPage() { Title = "Tab 1" },
+ new ContentPage() { Title = "Tab 2" }
+ });
+
+ tabbedPage.BarTextColor = Colors.Red;
+
+ await CreateHandlerAndAddToWindow(tabbedPage, handler =>
+ {
+ var tabBar = GetTabBar(tabbedPage);
+
+ // On iOS 26+, our fix sets UnselectedItemTintColor as a workaround
+ Assert.NotNull(tabBar.UnselectedItemTintColor);
+ Assert.True(
+ ColorComparison.ARGBEquivalent(tabBar.UnselectedItemTintColor, Colors.Red.ToPlatform(), 0.1),
+ $"Expected UnselectedItemTintColor to be Red but got {tabBar.UnselectedItemTintColor}");
+
+ // Change the color and verify it updates
+ tabbedPage.BarTextColor = Colors.Blue;
+
+ Assert.NotNull(tabBar.UnselectedItemTintColor);
+ Assert.True(
+ ColorComparison.ARGBEquivalent(tabBar.UnselectedItemTintColor, Colors.Blue.ToPlatform(), 0.1),
+ $"Expected UnselectedItemTintColor to be Blue but got {tabBar.UnselectedItemTintColor}");
+
+ return Task.CompletedTask;
+ });
+ }
+
+ // Regression test for https://github.com/dotnet/maui/issues/32125
+ // Verifies UnselectedItemTintColor is set from UnselectedTabColor on iOS 26+
+ [Fact(DisplayName = "iOS 26: UnselectedItemTintColor set from UnselectedTabColor")]
+ public async Task UnselectedItemTintColorSetFromUnselectedTabColor()
+ {
+ if (!OperatingSystem.IsIOSVersionAtLeast(26) && !OperatingSystem.IsMacCatalystVersionAtLeast(26))
+ return;
+
+ SetupBuilder();
+ var tabbedPage = CreateBasicTabbedPage(true, pages: new[]
+ {
+ new ContentPage() { Title = "Tab 1" },
+ new ContentPage() { Title = "Tab 2" }
+ });
+
+ tabbedPage.SelectedTabColor = Colors.Green;
+ tabbedPage.UnselectedTabColor = Colors.Purple;
+
+ await CreateHandlerAndAddToWindow(tabbedPage, handler =>
+ {
+ var tabBar = GetTabBar(tabbedPage);
+
+ // UnselectedTabColor should take priority for UnselectedItemTintColor
+ Assert.NotNull(tabBar.UnselectedItemTintColor);
+ Assert.True(
+ ColorComparison.ARGBEquivalent(tabBar.UnselectedItemTintColor, Colors.Purple.ToPlatform(), 0.1),
+ $"Expected UnselectedItemTintColor to be Purple but got {tabBar.UnselectedItemTintColor}");
+
+ return Task.CompletedTask;
+ });
+ }
+
+ // Verifies changing BarTextColor updates UnselectedItemTintColor on iOS 26+
+ [Fact(DisplayName = "iOS 26: Changing BarTextColor updates UnselectedItemTintColor")]
+ public async Task ChangingBarTextColorUpdatesUnselectedItemTintColor()
+ {
+ if (!OperatingSystem.IsIOSVersionAtLeast(26) && !OperatingSystem.IsMacCatalystVersionAtLeast(26))
+ return;
+
+ SetupBuilder();
+ var tabbedPage = CreateBasicTabbedPage(true, pages: new[]
+ {
+ new ContentPage() { Title = "Tab 1" },
+ new ContentPage() { Title = "Tab 2" }
+ });
+
+ tabbedPage.BarTextColor = Colors.Red;
+
+ await CreateHandlerAndAddToWindow(tabbedPage, handler =>
+ {
+ var tabBar = GetTabBar(tabbedPage);
+
+ // Should be Red after BarTextColor is applied
+ Assert.NotNull(tabBar.UnselectedItemTintColor);
+ Assert.True(
+ ColorComparison.ARGBEquivalent(tabBar.UnselectedItemTintColor, Colors.Red.ToPlatform(), 0.1),
+ $"Expected Red but got {tabBar.UnselectedItemTintColor}");
+
+ // Change to Green
+ tabbedPage.BarTextColor = Colors.Green;
+
+ Assert.NotNull(tabBar.UnselectedItemTintColor);
+ Assert.True(
+ ColorComparison.ARGBEquivalent(tabBar.UnselectedItemTintColor, Colors.Green.ToPlatform(), 0.1),
+ $"Expected Green but got {tabBar.UnselectedItemTintColor}");
+
+ // Clear the color — should no longer be Red or Green
+ tabbedPage.BarTextColor = null;
+
+ // After clearing, the tint should not be Red or Green anymore
+ if (tabBar.UnselectedItemTintColor is not null)
+ {
+ Assert.False(
+ ColorComparison.ARGBEquivalent(tabBar.UnselectedItemTintColor, Colors.Red.ToPlatform(), 0.1),
+ "UnselectedItemTintColor should not still be Red after clearing");
+ Assert.False(
+ ColorComparison.ARGBEquivalent(tabBar.UnselectedItemTintColor, Colors.Green.ToPlatform(), 0.1),
+ "UnselectedItemTintColor should not still be Green after clearing");
+ }
+
+ return Task.CompletedTask;
+ });
+ }
}
}
diff --git a/src/Core/src/Platform/iOS/TabbedViewExtensions.cs b/src/Core/src/Platform/iOS/TabbedViewExtensions.cs
index 6c5ae4cf06b6..6b62594deef1 100644
--- a/src/Core/src/Platform/iOS/TabbedViewExtensions.cs
+++ b/src/Core/src/Platform/iOS/TabbedViewExtensions.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
using System.Text;
using CoreGraphics;
using Foundation;
@@ -90,37 +91,136 @@ internal static void UpdateiOS15TabBarAppearance(
}
// Set UnselectedTabColor
- if (unselectedTabColor is not null)
+ // On iOS 26+, UITabBarAppearance.Normal state (TitleTextAttributes, IconColor) is ignored
+ // by the liquid glass tab bar for unselected items. Skip setting Normal state and use
+ // UnselectedItemTintColor directly instead.
+ // See: https://github.com/dotnet/maui/issues/32125, https://github.com/dotnet/maui/issues/34605
+ bool isiOS26OrNewer = OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26);
+
+ if (!isiOS26OrNewer)
{
- var foregroundColor = unselectedTabColor.ToPlatform();
- var titleColor = effectiveUnselectedBarTextColor ?? foregroundColor;
- _tabBarAppearance.StackedLayoutAppearance.Normal.TitleTextAttributes = new UIStringAttributes { ForegroundColor = titleColor, ParagraphStyle = NSParagraphStyle.Default };
- _tabBarAppearance.StackedLayoutAppearance.Normal.IconColor = foregroundColor;
+ if (unselectedTabColor is not null)
+ {
+ var foregroundColor = unselectedTabColor.ToPlatform();
+ var titleColor = effectiveUnselectedBarTextColor ?? foregroundColor;
+ _tabBarAppearance.StackedLayoutAppearance.Normal.TitleTextAttributes = new UIStringAttributes { ForegroundColor = titleColor, ParagraphStyle = NSParagraphStyle.Default };
+ _tabBarAppearance.StackedLayoutAppearance.Normal.IconColor = foregroundColor;
- _tabBarAppearance.InlineLayoutAppearance.Normal.TitleTextAttributes = new UIStringAttributes { ForegroundColor = titleColor, ParagraphStyle = NSParagraphStyle.Default };
- _tabBarAppearance.InlineLayoutAppearance.Normal.IconColor = foregroundColor;
+ _tabBarAppearance.InlineLayoutAppearance.Normal.TitleTextAttributes = new UIStringAttributes { ForegroundColor = titleColor, ParagraphStyle = NSParagraphStyle.Default };
+ _tabBarAppearance.InlineLayoutAppearance.Normal.IconColor = foregroundColor;
- _tabBarAppearance.CompactInlineLayoutAppearance.Normal.TitleTextAttributes = new UIStringAttributes { ForegroundColor = titleColor, ParagraphStyle = NSParagraphStyle.Default };
- _tabBarAppearance.CompactInlineLayoutAppearance.Normal.IconColor = foregroundColor;
- }
- else
- {
- var foreground = UITabBar.Appearance.TintColor;
- var titleColor = effectiveUnselectedBarTextColor ?? foreground;
- _tabBarAppearance.StackedLayoutAppearance.Normal.TitleTextAttributes = new UIStringAttributes { ForegroundColor = titleColor, ParagraphStyle = NSParagraphStyle.Default };
- _tabBarAppearance.StackedLayoutAppearance.Normal.IconColor = foreground;
+ _tabBarAppearance.CompactInlineLayoutAppearance.Normal.TitleTextAttributes = new UIStringAttributes { ForegroundColor = titleColor, ParagraphStyle = NSParagraphStyle.Default };
+ _tabBarAppearance.CompactInlineLayoutAppearance.Normal.IconColor = foregroundColor;
+ }
+ else
+ {
+ var foreground = UITabBar.Appearance.TintColor;
+ var titleColor = effectiveUnselectedBarTextColor ?? foreground;
+ _tabBarAppearance.StackedLayoutAppearance.Normal.TitleTextAttributes = new UIStringAttributes { ForegroundColor = titleColor, ParagraphStyle = NSParagraphStyle.Default };
+ _tabBarAppearance.StackedLayoutAppearance.Normal.IconColor = foreground;
- _tabBarAppearance.InlineLayoutAppearance.Normal.TitleTextAttributes = new UIStringAttributes { ForegroundColor = titleColor, ParagraphStyle = NSParagraphStyle.Default };
- _tabBarAppearance.InlineLayoutAppearance.Normal.IconColor = foreground;
+ _tabBarAppearance.InlineLayoutAppearance.Normal.TitleTextAttributes = new UIStringAttributes { ForegroundColor = titleColor, ParagraphStyle = NSParagraphStyle.Default };
+ _tabBarAppearance.InlineLayoutAppearance.Normal.IconColor = foreground;
- _tabBarAppearance.CompactInlineLayoutAppearance.Normal.TitleTextAttributes = new UIStringAttributes { ForegroundColor = titleColor, ParagraphStyle = NSParagraphStyle.Default };
- _tabBarAppearance.CompactInlineLayoutAppearance.Normal.IconColor = foreground;
+ _tabBarAppearance.CompactInlineLayoutAppearance.Normal.TitleTextAttributes = new UIStringAttributes { ForegroundColor = titleColor, ParagraphStyle = NSParagraphStyle.Default };
+ _tabBarAppearance.CompactInlineLayoutAppearance.Normal.IconColor = foreground;
+ }
}
// Set the TabBarAppearance
+ // On iOS 26+, setting StandardAppearance may cause UIKit to ignore
+ // UnselectedItemTintColor. Only set appearance for background color and
+ // selected state; use direct properties for unselected state.
tabBar.StandardAppearance = tabBar.ScrollEdgeAppearance = _tabBarAppearance;
+
+ if (isiOS26OrNewer)
+ {
+ UIColor? effectiveUnselectedTint = null;
+
+ if (unselectedTabColor is not null)
+ effectiveUnselectedTint = unselectedTabColor.ToPlatform();
+ else if (effectiveUnselectedBarTextColor is not null)
+ effectiveUnselectedTint = effectiveUnselectedBarTextColor;
+
+ tabBar.UnselectedItemTintColor = effectiveUnselectedTint;
+
+ // Also ensure TintColor is set for selected items
+ var effectiveSelectedTint = selectedTabColor?.ToPlatform() ?? effectiveSelectedBarTextColor;
+ if (effectiveSelectedTint is not null)
+ tabBar.TintColor = effectiveSelectedTint;
+
+ // iOS 26 liquid glass strips tint colors during compositing.
+ // Use pre-colored images with AlwaysOriginal to bypass the tint pipeline.
+ tabBar.ApplyPreColoredImagesForIOS26(effectiveUnselectedTint, effectiveSelectedTint);
+ }
}
+ ///
+ /// On iOS 26+, the liquid glass tab bar's compositing pipeline strips TintColor from
+ /// unselected tab icons and labels. This method bypasses the tint system by creating
+ /// pre-colored copies of tab icons using AlwaysOriginal rendering mode, which bakes the
+ /// color into the image pixel data. Also sets per-item title text attributes.
+ /// Must be called on every layout pass because UIKit may reset these during layout.
+ /// See: https://github.com/dotnet/maui/issues/32125, https://github.com/dotnet/maui/issues/34605
+ ///
+ [System.Runtime.Versioning.SupportedOSPlatform("ios26.0")]
+ [System.Runtime.Versioning.SupportedOSPlatform("maccatalyst26.0")]
+ internal static void ApplyPreColoredImagesForIOS26(this UITabBar tabBar, UIColor? unselectedColor, UIColor? selectedColor)
+ {
+ if (tabBar.Items is null)
+ return;
+
+ foreach (var item in tabBar.Items)
+ {
+ if (item.Image is UIImage img && unselectedColor is not null)
+ {
+ // Retrieve or store the original template image so we always tint
+ // from a clean alpha mask, avoiding quality degradation from
+ // repeated AlwaysOriginal→Template round-trips.
+ if (!s_originalTemplateImages.TryGetValue(item, out var template))
+ {
+ template = img.RenderingMode == UIImageRenderingMode.AlwaysOriginal
+ ? img.ImageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate)
+ : img;
+ s_originalTemplateImages.AddOrUpdate(item, template);
+ }
+
+ // Only re-tint if UIKit has reset the image (rendering mode won't
+ // be AlwaysOriginal) or if this is the first application. This avoids
+ // creating new UIImage instances on every layout pass.
+ if (img.RenderingMode != UIImageRenderingMode.AlwaysOriginal)
+ {
+ item.Image = template.ApplyTintColor(unselectedColor)
+ ?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal);
+
+ if (selectedColor is not null)
+ {
+ item.SelectedImage = template.ApplyTintColor(selectedColor)
+ ?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal);
+ }
+ }
+ }
+
+ // Note: SetTitleTextAttributes for Normal state is intentionally not called on iOS 26+.
+ // Apple's Liquid Glass design system ignores the Normal state appearance to ensure visual
+ // consistency with the glass material. Icon coloring via AlwaysOriginal rendering (above)
+ // is the supported workaround. Text color customization for unselected tabs is not available
+ // on iOS 26+. See: https://github.com/dotnet/maui/issues/32125
+
+ if (selectedColor is not null)
+ {
+ item.SetTitleTextAttributes(
+ new UIStringAttributes { ForegroundColor = selectedColor, ParagraphStyle = NSParagraphStyle.Default },
+ UIControlState.Selected);
+ }
+ }
+ }
+
+ // Stores original template images per tab bar item so we always tint from
+ // a clean alpha mask. Uses ConditionalWeakTable so entries are collected
+ // when the UITabBarItem is garbage-collected.
+ static readonly ConditionalWeakTable s_originalTemplateImages = new();
+
internal static UIImage? AutoResizeTabBarImage(UITraitCollection traitCollection, UIImage image)
{
if (image == null || image.Size.Width <= 0 || image.Size.Height <= 0)
@@ -152,8 +252,15 @@ internal static void UpdateiOS15TabBarAppearance(
newSize.Width = isRegularTabBar ? regularSquareSize : compactSquareSize;
newSize.Height = newSize.Width;
}
-
- return image.ResizeImageSource(newSize.Width, newSize.Height, new CGSize(image.Size.Width, image.Size.Height));
+
+ var resizedImage = image.ResizeImageSource(newSize.Width, newSize.Height, new CGSize(image.Size.Width, image.Size.Height));
+
+ if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
+ {
+ return resizedImage?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate);
+ }
+
+ return resizedImage;
}
}
}