Fixing the .NET 10 MAUI status bar overlap on Android 16 (edge-to-edge)
If you upgraded a .NET MAUI app to .NET 10 and then ran it on a device or emulator with Android 16 (API 36), you may have noticed something ugly: the status bar now sits on top of your Shell header.
If you upgraded a .NET MAUI app to .NET 10 and then ran it on a device or emulator with Android 16 (API 36), you may have noticed something ugly: the status bar now sits on top of your Shell header, and the bottom tab bar disappears behind the gesture navigation pill.
This is not a bug in your app. It is Android 16 finally enforcing edge-to-edge for everyone whose targetSdk is 36, and .NET MAUI 10 does not yet ship a default handler that pushes window insets into the visual tree.
Here is what is happening and how I worked around it without rewriting the whole layout.
What changed in Android 16
Starting with API 36, calling Window.SetDecorFitsSystemWindows(true) is a no-op, and Window.SetStatusBarColor is deprecated. The system draws your app full-bleed under the status bar and the gesture nav, and it is your job to pad your content using WindowInsets.
Google gives you two doors:
Embrace it. Call EnableEdgeToEdge() (from AndroidX Activity 1.10+) and consume insets yourself.
Opt out per-screen with android:windowOptOutEdgeToEdgeEnforcement="true" in your activity theme (Android 16 only, removed in 17).
MAUI Shell does neither out of the box.
Minimum repro
dotnet new maui -n EdgeRepro
cd EdgeRepro
dotnet build -t:Run -f net10.0-android
Run on a Pixel emulator running Android 16. The Shell title bar is clipped. Set the page background to red and you can see content drawing all the way under the status bar.
The fix
The cleanest fix I found is a small MauiAppBuilder lifecycle hook that listens for window insets on the root decor view and publishes the top/bottom values as a static property pages can bind to.
// Platforms/Android/InsetsTracker.cs
using Android.Views;
using AndroidX.Core.View;
public static class InsetsTracker
{
public static Thickness Safe { get; private set; } = new(0);
public static event Action? Changed;
public static void Attach(Android.App.Activity activity)
{
var root = activity.Window!.DecorView;
ViewCompat.SetOnApplyWindowInsetsListener(root, (v, insets) =>
{
var bars = insets.GetInsets(
WindowInsetsCompat.Type.SystemBars()
| WindowInsetsCompat.Type.DisplayCutout());
var density = activity.Resources!.DisplayMetrics!.Density;
Safe = new Thickness(
bars.Left / density,
bars.Top / density,
bars.Right / density,
bars.Bottom / density);
Changed?.Invoke();
return insets;
});
}
}
Now in your AppShell.xaml.cs (or any base page), subscribe and push the inset to a bindable property:
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
#if ANDROID
Apply();
InsetsTracker.Changed += Apply;
#endif
}
void Apply()
{
Padding = new Thickness(
0, InsetsTracker.Safe.Top,
0, InsetsTracker.Safe.Bottom);
}
}
That is enough to get the Shell chrome back under the status bar and above the gesture nav.
Per-page opt-out (legacy screens)
If you have one screen — a video player, a camera view — that genuinely needs the old behavior, do not opt the whole app out. Apply a per-activity theme override:
A walkthrough of why .NET MAUI apps show black bars on iOS 18 and 26 edge-to-edge devices, what changed in Apple's safe-area defaults, and the exact SafeAreaEdges fix I shipped to production.
A step-by-step walkthrough of the duplicate class androidx error that surfaces after upgrading MAUI from .NET 9 to .NET 10, with diagnosis commands, real fixes, and the version pins that actually held up in production.