Your Blazor Hybrid header is drawn behind Android's status bar, your bottom tabs behind the
gesture bar, and the env(safe-area-inset-top) you added to fix it computes to
0 — inside an Android WebView the safe-area variables carry no information at all.
Android hands the activity's content view the whole screen, edge to edge, and the WebView inside
it does not translate the window insets into CSS. So the safe-area variables are always zero, and
no stylesheet can learn where the status bar is. The fix is native: register an
OnApplyWindowInsetsListener in MainActivity that pads the content view
by the system-bar insets, and leave the CSS alone.
The error
There is no message — only a screenshot with no header. This is the CSS that asked, from pocket.css; on Android both env() values resolve to zero:
.app-top {
padding: calc(var(--s-3) + env(safe-area-inset-top)) var(--s-4) var(--s-3);
position: sticky; top: 0; z-index: 20;
}
.tabbar {
position: fixed; left: 0; right: 0; bottom: 0;
height: calc(var(--tabbar-h) + env(safe-area-inset-bottom));
padding-bottom: env(safe-area-inset-bottom);
}
Why it happens
env(safe-area-inset-*) is a browser feature. Safari on iPhone fills the variables in
when a page opts into the notch with viewport-fit=cover; Chrome does the same for
display cutouts. A WebView embedded in an app is not that browser. It is a view the activity
sizes, and by default Android sizes it to the full window — under the clock at the top and the
home pill at the bottom — while reporting nothing about either to the page.
The clue that misleads: the same Razor components rendered correctly on Windows, so the CSS looked innocent. It was innocent. "Safe area" is the operating system's business, and on Android the operating system delivers it to your activity as window insets, not to your stylesheet as variables.
The fix
In Platforms/Android/MainActivity.cs, trimmed to the moving parts:
using AndroidX.Core.View;
/* … in OnCreate, after base.OnCreate … */
var content = FindViewById<ViewGroup>(Android.Resource.Id.Content);
if (content is not null)
{
ViewCompat.SetOnApplyWindowInsetsListener(content, new SystemBarsPadding());
}
private sealed class SystemBarsPadding : Java.Lang.Object, IOnApplyWindowInsetsListener
{
// Android.Views.View spelled out: inside a MAUI project, bare "View" is
// Microsoft.Maui.Controls.View — the compiler's first complaint on this file.
public WindowInsetsCompat OnApplyWindowInsets(Android.Views.View? v, WindowInsetsCompat? insets)
{
/* … null checks … */
var bars = insets.GetInsets(WindowInsetsCompat.Type.SystemBars());
v.SetPadding(bars.Left, bars.Top, bars.Right, bars.Bottom);
return WindowInsetsCompat.Consumed!;
}
}
The listener runs whenever the insets change — rotation, keyboard, the gesture bar appearing —
so the padding follows the bars instead of guessing at them. Keep the env() calls in
the CSS: they are still right for the browser host and for iOS, where the variables do work. Two
notes. That fully qualified Android.Views.View is not decoration; the first version
of this file didn't compile, and
CS0104 explains
why. And the older route of asking the window to fit system windows for you stops applying on
Android 15, where edge-to-edge is enforced — the listener works on both sides of that line.
What the AI got wrong: it reached for CSS, because CSS is where the model has seen safe areas handled, and a Windows screenshot with the header intact made CSS look blameless. The lesson web developers don't get to learn on the web: the insets are the OS's to give.
Where it bit us
Season three, Part 2: one UI, three
hosts, in the very first Android screenshot of the season. Tag pocket-02 in
the repo has the listener, and Part 4
later tidied two warnings it left. The lesson: a screenshot found in seconds what reading the CSS
would never have found, because the CSS was correct.
Frequently asked
- Why is env(safe-area-inset-top) zero in a .NET MAUI Blazor Hybrid app on Android?
- Because the safe-area variables are filled in by a browser, and an Android WebView embedded in an app is not one. Android sizes the WebView to the full window and reports the status bar and gesture bar to the activity as window insets, which never reach CSS, so the variables stay at zero.
- How do I keep Blazor Hybrid content out from under the Android status bar?
- Handle it natively in MainActivity: register an OnApplyWindowInsetsListener on the activity's content view with ViewCompat.SetOnApplyWindowInsetsListener, read the system-bar insets from WindowInsetsCompat, and apply them as padding on that view. The WebView then lives inside the padded area and the CSS needs no change.
- Does the insets listener still work on Android 15 edge-to-edge?
- Yes. Android 15 enforces edge-to-edge and ignores the older approach of asking the window to fit system windows, but an OnApplyWindowInsetsListener that pads the content view by the system-bar insets works on both older and newer versions.
More decoded errors in the Fixes category; the season starts at Part 1.