Android 15 Killed My Status Bar: The .NET MAUI Fix That Actually Works
A production Android 15 update made our status bar icons vanish overnight. Here's the real root cause and the four-part fix that works from Android 13 through 15+, Shell or not.
It Started With a Support Ticket
"The clock disappeared from the top of the app." That was the whole bug report. No stack trace, no crash log β just a black bar where the clock, battery, and Wi-Fi icons used to be.
The device: a phone that had just received the Android 15 update. Everything else in the app was fine. Just the status bar had gone dark β literally. White icons on a black background, except the background wasn't rendering black anymore either. It was rendering nothing we could see.
If you're maintaining a .NET MAUI app and haven't hit this yet, you will. Here's what's actually going on, and the fix that holds up across Android 13, 14, 15, and whatever comes next.
The Root Cause: Android 15 Stopped Listening
Android 15 (API 35) made edge-to-edge rendering mandatory. Every app now draws its content behind the system bars by default β status bar, navigation bar, all of it. That's the new normal, and it's not going away.
The problem is what this did to the old way of styling the status bar. For years, this was enough:
<style name="AppTheme">
<item name="android:statusBarColor">#000000</item>
<item name="android:windowLightStatusBar">false</item>
</style>
On Android 14 and earlier, statusBarColor and windowLightStatusBar did exactly what they said. Starting with Android 15, the system ignores both attributes completely. Not a bug on Google's end β a deliberate deprecation in favor of WindowInsetsControllerCompat, the API that's supposed to replace theme-attribute styling everywhere.
So here's the failure chain:
- Edge-to-edge is now forced on
- The old
statusBarColor/windowLightStatusBarattributes are silently ignored - Nothing else in the app sets the icon color explicitly
- The system falls back to defaults that, in our case, made white icons invisible against a light-rendered bar
And it wasn't only Android 15 devices. The same symptom showed up on an Android 13 phone, because MAUI itself was quietly overriding the theme during the Maui.SplashTheme β Maui.MainTheme.NoActionBar transition at startup. Two different bugs, one visible symptom.
The Fix Has Three Layers
There's no single flag that fixes this everywhere. What worked was stacking three independent defenses β each one addresses a different Android version or a different failure mode.
Layer 1 β Tell Android 15 to Back Off
The cleanest fix for the new mandatory edge-to-edge behavior is to opt out of it, restoring the old, predictable status bar rendering.
Create Platforms/Android/Resources/values-v35/styles.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="OptOutEdgeToEdgeEnforcement">
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
</style>
</resources>
The values-v35 folder name isn't arbitrary β it's an Android resource qualifier. Anything inside a values-v35 folder is only loaded on devices running API 35 (Android 15) or higher. On any older device, this entire file is invisible to the system, as if it didn't exist.
That's exactly the problem: MainActivity.cs applies this style unconditionally, on every device, regardless of Android version:
protected override void OnCreate(Bundle? savedInstanceState)
{
Theme?.ApplyStyle(Resource.Style.OptOutEdgeToEdgeEnforcement, force: false);
base.OnCreate(savedInstanceState);
}
Resource.Style.OptOutEdgeToEdgeEnforcement is a compile-time generated reference. If the style named OptOutEdgeToEdgeEnforcement only existed inside values-v35, an Android 13 or 14 device β which never loads that folder β would have no resource by that name at all. The app would crash trying to apply a style that, as far as that device is concerned, doesn't exist.
The fix is a placeholder: define the same style name again, but empty, in the regular Platforms/Android/Resources/values/styles.xml β the folder that's loaded on every Android version, no qualifier needed:
<resources>
<style name="AppTheme" parent="Maui.SplashTheme">
<!-- your existing theme attributes -->
</style>
<style name="OptOutEdgeToEdgeEnforcement">
</style>
</resources>
This gives Android two versions of the same style name to choose from, and it picks the right one automatically based on API level:
- Android 15+ loads
values-v35/styles.xmlβ gets the real attribute, opts out of edge-to-edge - Android 13/14 loads
values/styles.xmlβ gets the empty placeholder, which is a harmless no-op
Same line of C# code, two completely different outcomes, zero crashes either way.
Apply the style before base.OnCreate. Do it after, and it's too late β the window has already been configured.
Worth knowing: Google has flagged this attribute as temporary. It's a bridge, not a permanent solution β which is exactly why it's layer one and not the whole fix.
Layer 2 β StatusBarBehavior via CommunityToolkit.Maui
This is the part that actually future-proofs the app instead of just patching around Android 15. CommunityToolkit.Maui.Behaviors.StatusBarBehavior wraps WindowInsetsControllerCompat on Android (and the equivalent native mechanism on iOS) β it's a cross-platform behavior, not an Android-only workaround, and it's the API Google actually wants you using going forward.
Installing it, if the project doesn't already reference it:
dotnet add package CommunityToolkit.Maui
At the time of writing this pulls in version 15.0.0. CommunityToolkit.Maui.Core comes along as a transitive dependency β you don't need to reference it directly.
Registering it in MauiProgram.cs:
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
#if !MACCATALYST
.UseMauiCommunityToolkit()
#endif
.ConfigureFonts(fonts =>
{
// your font configuration
});
That #if !MACCATALYST isn't boilerplate paranoia β the toolkit's initialization doesn't run on Mac Catalyst builds, so wrap the call the same way if your app targets that platform too.
Using it, StatusBarBehavior exposes two properties that matter here:
StatusBarColorβ aColor, e.g.BlackStatusBarStyleβ an enum:Default,LightContent(white icons, for dark bars), orDarkContent(dark icons, for light bars)
<ContentPage.Behaviors>
<toolkit:StatusBarBehavior
StatusBarColor="Black"
StatusBarStyle="LightContent" />
</ContentPage.Behaviors>
Now the part that actually trips people up. If your app is Shell-routed β multiple ShellContent entries, navigation done through GoToAsync, pages living inside the Shell's own page hierarchy β one StatusBarBehavior on AppShell is enough. It cascades to every route.
Most real-world MAUI apps aren't built that way, though β especially ones migrated from Xamarin.Forms. It's common to have Shell wrapping a single root page (a tabbed home screen, say), while every other screen β edit forms, filter dialogs, detail views β gets pushed with Navigation.PushModalAsync(page) instead of through Shell routing. That's exactly how this app works: AppShell.xaml hosts one ShellContent, and every secondary page goes through a custom navigation helper that calls PushModalAsync directly against the app's INavigation stack.
Pages pushed that way sit outside the Shell's page tree entirely. A behavior attached to AppShell has no path to them β Shell can't cascade something to a page it doesn't know about. So if that's your app's shape too, the root page gets one StatusBarBehavior:
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit">
<ContentPage.Behaviors>
<toolkit:StatusBarBehavior
StatusBarColor="Black"
StatusBarStyle="LightContent" />
</ContentPage.Behaviors>
<!-- rest of your content -->
</ContentPage>
And every modal page pushed outside Shell gets its own copy:
<ContentPage ...>
<ContentPage.Behaviors>
<toolkit:StatusBarBehavior
StatusBarColor="Black"
StatusBarStyle="LightContent" />
</ContentPage.Behaviors>
<!-- your page content -->
</ContentPage>
In a real app this means going through every modal page one by one. In this case, that was 22 of them β edit forms, filter dialogs, evaluation screens. Tedious, but mechanical: one XAML block, repeated. If you're auditing an app for this, grep for wherever your navigation helper calls PushModalAsync β every page on the receiving end of that call needs the behavior.
Layer 3 β Stop Content From Sliding Under the Bars
.NET MAUI 10 quietly flipped the default SafeAreaEdges from Container to None. Combined with edge-to-edge rendering, this means page content can now draw underneath the status bar and navigation bar β even after the status bar itself looks correct again.
Fix it once, globally, instead of setting it on every page:
<Style TargetType="ContentPage" ApplyToDerivedTypes="True">
<Setter Property="SafeAreaEdges" Value="Container" />
</Style>
Drop that into your global styles resource dictionary and every ContentPage β including custom base classes derived from it β goes back to respecting system bar insets.
Verifying It Actually Worked
None of this is worth much without testing across the version range that broke in the first place:
dotnet build -f net10.0-android -c Debug
Then check on real hardware or emulators spanning:
- Android 13β14 β should look exactly like it did before any of this (no regression)
- Android 15+ β status bar visible, black background, white icons, clock and battery readable again
And don't stop at the status bar. Check that SafeAreaEdges="Container" didn't introduce new clipping or unexpected scroll on pages with dark backgrounds or full-bleed layouts.
Why This Is Worth Getting Right
The status bar isn't decoration. It's the only place a user checks whether they have signal, how much battery is left, and whether a background process silently died. When it goes invisible, the app doesn't look broken β it looks like the phone is broken, and that's a much worse impression to leave.
It's also a preview of where Android is heading. Edge-to-edge isn't an Android 15 quirk that'll get walked back β it's the direction the platform has committed to. Apps that adopt WindowInsetsControllerCompat (via StatusBarBehavior) now, instead of leaning on the opt-out flag as a permanent fix, won't be back here again for Android 16 or 17.
The Short Version
Four things, four different failure modes covered:
- Opt out of edge-to-edge on API 35+ so the status bar renders predictably again β and don't forget the empty placeholder style in
values/, or older devices crash - Install and register
CommunityToolkit.Maui, then useStatusBarBehaviorinstead of deprecated theme attributes β this is the part that's actually future-proof - Check how your app navigates. Shell-routed apps need the behavior once, on
AppShell. Apps that push pages viaPushModalAsyncoutside Shell β which is most real-world MAUI apps β need it on every modal page individually - Restore
SafeAreaEdges="Container"globally so content stops sliding under the bars
None of these four alone would have fixed it. Together, the status bar is back β on every Android version we could get our hands on to test.
Comments
Have a question, or found an issue with the code? Drop a comment below — I read and reply to every one.