
Maui Safe Area
- 35 installs
- 163 repo stars
- Updated July 6, 2026
- davidortinau/maui-skills
Handles .NET MAUI safe area and edge-to-edge layout for .NET 10+ via SafeAreaEdges, SafeAreaRegions, per-edge control, and keyboard avoidance.
About
Guides .NET MAUI safe area and edge-to-edge layout for .NET 10+ using SafeAreaEdges, SafeAreaRegions, per-edge control, keyboard avoidance and Blazor Hybrid CSS safe areas. A developer uses it when laying out MAUI UI around notches and system bars.
- SafeAreaEdges property and SafeAreaRegions enum with per-edge control
- Keyboard avoidance and Blazor Hybrid CSS safe areas
Maui Safe Area by the numbers
- 35 all-time installs (skills.sh)
- Ranked #648 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/davidortinau/maui-skills --skill maui-safe-areaAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 35 |
|---|---|
| repo stars | ★ 163 |
| Last updated | July 6, 2026 |
| Repository | davidortinau/maui-skills ↗ |
What it does
Handles .NET MAUI safe area and edge-to-edge layout for .NET 10+ via SafeAreaEdges, SafeAreaRegions, per-edge control, and keyboard avoidance.
Files
Safe Area — Gotchas & Best Practices (.NET 10+)
Breaking Changes
⚠️ .NET 9 → 10: ContentPage default changed to None
ContentPage now defaults to edge-to-edge (None) on all platforms. In .NET 9, Android ContentPage behaved like Container. If your Android content goes under the status bar after upgrading, add SafeAreaEdges="Container" explicitly.
<!-- ❌ .NET 10 default — content goes under status bar on Android -->
<ContentPage>
<!-- ✅ Restore .NET 9 Android behavior -->
<ContentPage SafeAreaEdges="Container">⚠️ WindowSoftInputModeAdjust.Resize migration
If you used WindowSoftInputModeAdjust.Resize in .NET 9, you may need SafeAreaEdges="All" on the ContentPage to maintain keyboard avoidance.
Common Gotchas
1. Layouts default to Container, not None
For true edge-to-edge, set SafeAreaEdges="None" on both the page and layouts:
<!-- ❌ Grid still respects system bars (its default is Container) -->
<ContentPage SafeAreaEdges="None">
<Grid>
<Image Source="bg.jpg" Aspect="AspectFill" />
</Grid>
</ContentPage>
<!-- ✅ Both page and layout set to None -->
<ContentPage SafeAreaEdges="None">
<Grid SafeAreaEdges="None">
<Image Source="bg.jpg" Aspect="AspectFill" />
</Grid>
</ContentPage>2. SoftInput on ScrollView has no effect
ScrollView manages its own content insets. Wrap it in a Grid/StackLayout instead:
<!-- ❌ SoftInput is ignored on ScrollView -->
<ScrollView SafeAreaEdges="SoftInput">
<!-- ✅ Set SoftInput on the wrapper layout -->
<Grid SafeAreaEdges="Container, Container, Container, SoftInput">
<ScrollView> ... </ScrollView>
</Grid>3. Default is not None
Default = "use platform defaults for this control type." None = "edge-to-edge." They differ significantly on ScrollView (iOS maps Default to automatic content insets).
4. Blazor Hybrid double-padding
<!-- ❌ XAML safe area + CSS env() = double padding -->
<ContentPage SafeAreaEdges="Container">
<BlazorWebView ... /> <!-- CSS also uses env(safe-area-inset-*) -->
<!-- ✅ Pick ONE approach — CSS is recommended for Blazor -->
<ContentPage SafeAreaEdges="None">
<BlazorWebView ... /> <!-- CSS handles all insets with env() -->5. iOS Shell/NavigationPage edge-to-edge
Content won't extend behind the nav bar unless you set a transparent background AND hide the separator:
<Shell Shell.BackgroundColor="#80000000"
Shell.NavBarHasShadow="False" />Decision Framework
| Scenario | SafeAreaEdges value |
|---|---|
| Forms, critical inputs | All |
| Photo viewer, video player, game | None (on page AND layout) |
| Scrollable content with fixed header/footer | Container |
| Chat/messaging with bottom input bar | Per-edge: Container, Container, Container, SoftInput |
| Blazor Hybrid app | None on page, CSS env() for insets |
Migration Quick Reference
| Legacy (.NET 9) | New (.NET 10+) |
|---|---|
ios:Page.UseSafeArea="True" | SafeAreaEdges="Container" |
IgnoreSafeArea="True" | SafeAreaEdges="None" |
WindowSoftInputModeAdjust.Resize | SafeAreaEdges="All" on ContentPage |
Legacy properties still work but are obsolete.
Best Practices
1. Combine `SafeAreaEdges` with `Padding` — safe area handles insets, Padding adds visual spacing:
<ContentPage SafeAreaEdges="All">
<VerticalStackLayout Padding="20">
<!-- Both applied — no conflict -->
</VerticalStackLayout>
</ContentPage>2. Use per-control settings for mixed layouts (edge-to-edge header + safe body + keyboard-aware footer).
3. For Blazor Hybrid, prefer CSS env() and leave SafeAreaEdges="None". Add viewport-fit=cover to the <meta viewport> tag.
4. Test on notched devices (iPhone X+, Android cutouts), tablets in landscape, and varying screen sizes.
Checklist
- [ ] Android upgrade:
SafeAreaEdges="Container"added if content goes under status bar - [ ] Edge-to-edge:
Noneset on both page and layout - [ ] ScrollView keyboard avoidance uses wrapper Grid, not ScrollView's own
SafeAreaEdges - [ ] Blazor Hybrid: using either XAML or CSS safe areas, not both
- [ ]
viewport-fit=coverin Blazor'sindex.html<meta viewport>tag - [ ] Legacy
UseSafeArea/IgnoreSafeAreamigrated toSafeAreaEdges
Safe Area API Reference
SafeAreaRegions Enum (flags)
[Flags]
public enum SafeAreaRegions
{
None = 0, // Edge-to-edge — no safe area padding
SoftInput = 1 << 0, // Pad to avoid keyboard
Container = 1 << 1, // Stay out of bars/notch, flow under keyboard
Default = -1, // Platform default for the control type
All = 1 << 15 // Obey all safe area insets (most restrictive)
}SoftInput and Container are flags and can be combined: SafeAreaRegions.Container | SafeAreaRegions.SoftInput = respect bars AND keyboard.
SafeAreaEdges Struct
public readonly struct SafeAreaEdges
{
public SafeAreaRegions Left { get; }
public SafeAreaRegions Top { get; }
public SafeAreaRegions Right { get; }
public SafeAreaRegions Bottom { get; }
// Uniform — same value for all edges
public SafeAreaEdges(SafeAreaRegions uniformValue)
// Horizontal/Vertical
public SafeAreaEdges(SafeAreaRegions horizontal, SafeAreaRegions vertical)
// Per-edge
public SafeAreaEdges(SafeAreaRegions left, SafeAreaRegions top,
SafeAreaRegions right, SafeAreaRegions bottom)
}Static presets: SafeAreaEdges.None, SafeAreaEdges.All, SafeAreaEdges.Default
XAML Type Converter
The XAML type converter follows Thickness-like syntax with comma-separated values:
<!-- Uniform: all edges = Container -->
SafeAreaEdges="Container"
<!-- Horizontal, Vertical -->
SafeAreaEdges="Container, SoftInput"
<!-- Left, Top, Right, Bottom -->
SafeAreaEdges="Container, Container, Container, SoftInput"Controls That Support SafeAreaEdges
| Control | Default value | Notes |
|---|---|---|
ContentPage | None | Edge-to-edge. Breaking change from .NET 9 Android. |
Layout (Grid, StackLayout, etc.) | Container | Respects bars/notch, flows under keyboard |
ScrollView | Default | iOS: maps to UIScrollViewContentInsetAdjustmentBehavior.Automatic. Only Container and None have effect. |
ContentView | None | Inherits parent behavior |
Border | None | Inherits parent behavior |
Usage Pattern Examples
Edge-to-edge content (background images, immersive UIs)
<ContentPage SafeAreaEdges="None">
<Grid SafeAreaEdges="None">
<Image Source="background.jpg" Aspect="AspectFill" />
<VerticalStackLayout Padding="20"
VerticalOptions="End">
<Label Text="Overlay text"
TextColor="White"
FontSize="24" />
</VerticalStackLayout>
</Grid>
</ContentPage>Respect all safe areas (forms, critical content)
<ContentPage SafeAreaEdges="All">
<VerticalStackLayout Padding="20">
<Label Text="Safe content" FontSize="18" />
<Entry Placeholder="Enter text" />
<Button Text="Submit" />
</VerticalStackLayout>
</ContentPage>Keyboard-aware chat/messaging layout
<ContentPage>
<Grid RowDefinitions="*,Auto"
SafeAreaEdges="Container, Container, Container, SoftInput">
<ScrollView Grid.Row="0">
<VerticalStackLayout Padding="20" Spacing="10">
<Label Text="Messages" FontSize="24" />
</VerticalStackLayout>
</ScrollView>
<Border Grid.Row="1"
BackgroundColor="LightGray"
Padding="20">
<HorizontalStackLayout Spacing="10">
<Entry Placeholder="Type a message..."
HorizontalOptions="Fill" />
<Button Text="Send" />
</HorizontalStackLayout>
</Border>
</Grid>
</ContentPage>Mixed layout — edge-to-edge header, safe body
<ContentPage SafeAreaEdges="None">
<Grid RowDefinitions="Auto,*,Auto">
<!-- Header: edge-to-edge behind status bar -->
<Grid BackgroundColor="Primary">
<Label Text="App Header"
TextColor="White"
Margin="20,40,20,20" />
</Grid>
<!-- Body: respect safe areas -->
<ScrollView Grid.Row="1" SafeAreaEdges="All">
<VerticalStackLayout Padding="20">
<Label Text="Main content" />
</VerticalStackLayout>
</ScrollView>
<!-- Footer: keyboard-aware -->
<Grid Grid.Row="2"
SafeAreaEdges="SoftInput"
BackgroundColor="LightGray"
Padding="20">
<Entry Placeholder="Type a message..." />
</Grid>
</Grid>
</ContentPage>Programmatic (C#)
var page = new ContentPage
{
SafeAreaEdges = SafeAreaEdges.All
};
var grid = new Grid
{
// Per-edge: Container on top/left/right, SoftInput on bottom
SafeAreaEdges = new SafeAreaEdges(
left: SafeAreaRegions.Container,
top: SafeAreaRegions.Container,
right: SafeAreaRegions.Container,
bottom: SafeAreaRegions.SoftInput)
};Blazor Hybrid Setup
Recommended approach
1. Set the page to edge-to-edge (default in .NET 10):
<ContentPage SafeAreaEdges="None">
<BlazorWebView HostPage="wwwroot/index.html">
<BlazorWebView.RootComponents>
<RootComponent Selector="#app" ComponentType="{x:Type local:Routes}" />
</BlazorWebView.RootComponents>
</BlazorWebView>
</ContentPage>2. Add `viewport-fit=cover` in index.html to let CSS access safe area insets:
<meta name="viewport" content="width=device-width, initial-scale=1.0,
maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />3. Use CSS `env()` functions for safe area insets:
/* Status bar spacer for iOS */
.status-bar-safe-area {
display: none;
}
@supports (-webkit-touch-callout: none) {
.status-bar-safe-area {
display: flex;
position: sticky;
top: 0;
height: env(safe-area-inset-top);
background-color: var(--header-bg, #f7f7f7);
width: 100%;
z-index: 1;
}
}
/* General safe area padding */
body {
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}Available CSS environment variables:
env(safe-area-inset-top)— status bar, notch, Dynamic Islandenv(safe-area-inset-bottom)— home indicator, navigation barenv(safe-area-inset-left)— landscape left edgeenv(safe-area-inset-right)— landscape right edge
Migration from Legacy APIs
From ios:Page.UseSafeArea (iOS-only)
<!-- .NET 9 (legacy) -->
<ContentPage xmlns:ios="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;assembly=Microsoft.Maui.Controls"
ios:Page.UseSafeArea="True">
<!-- .NET 10+ (cross-platform) -->
<ContentPage SafeAreaEdges="Container">From Layout.IgnoreSafeArea
<!-- .NET 9 (legacy) -->
<Grid IgnoreSafeArea="True">
<!-- .NET 10+ -->
<Grid SafeAreaEdges="None">The legacy properties still work but are obsolete. IgnoreSafeArea="True" maps internally to SafeAreaRegions.None.
Platform-Specific Behavior Details
iOS & Mac Catalyst
- Safe area insets include: status bar, navigation bar, tab bar, notch/Dynamic
Island, home indicator
SoftInputincludes the keyboard when visible- Insets update automatically on rotation and UI visibility changes
ScrollViewwithDefaultmaps toUIScrollViewContentInsetAdjustmentBehavior.Automatic
Reading safe area insets at runtime (iOS only):
using Microsoft.Maui.Controls.PlatformConfiguration;
using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;
Thickness insets = On<iOS>().SafeAreaInsets();
// insets.Top, insets.Bottom, insets.Left, insets.RightTransparent navigation bar for edge-to-edge under nav bar:
<!-- Shell -->
<Shell Shell.BackgroundColor="#80000000"
Shell.NavBarHasShadow="False" />
<!-- NavigationPage -->
<NavigationPage BarBackgroundColor="#80000000"
ios:NavigationPage.HideNavigationBarSeparator="True" />Android
- Safe area insets include: system bars (status/navigation) and display cutouts
SoftInputincludes the soft keyboard- Behavior varies by Android version and edge-to-edge settings
- MAUI uses
WindowInsetsCompatandWindowInsetsAnimationCompatinternally