maui
maui copied to clipboard
[.NET10] SafeArea Epic
📐 Proposal: SafeAreaInsets Markup Extension for .NET MAUI
Summary
Introduce a markup extension to make it easy to apply safe area insets to layout padding and margin in .NET MAUI applications. This approach avoids introducing new controls or layout behaviors and enables developers to apply platform-specific insets declaratively and flexibly.
Motivation
- MAUI currently lacks a simple, declarative way to apply safe area insets per side.
- Existing options like
IgnoreSafeAreaare coarse and often require nested layouts. - A markup extension provides XAML-friendly, composable access to platform insets without requiring changes to the layout system.
Design Goals
- ✅ Declarative: usable directly in
PaddingorMargin - ✅ Composable: works alongside static offsets
- ✅ Flexible: side-specific inset application
Core Proposal
SafeAreaInsetsExtension Markup Extension
A markup extension that returns a Thickness representing safe area padding for the selected sides.
Enum: SafeAreaSides
[Flags]
public enum SafeAreaSides
{
None = 0,
Left = 1,
Top = 2,
Right = 4,
Bottom = 8,
All = Left | Top | Right | Bottom
}
Markup Extension API
public class SafeAreaInsetsExtension
{
public SafeAreaSides Sides { get; set; } = SafeAreaSides.None;
public Thickness Offset { get; set; } = new Thickness(0);
public Thickness ProvideValue(IServiceProvider serviceProvider);
}
Behavior
- For each specified side, the extension fetches the current platform safe area inset.
- It adds the corresponding value from
Offsetto allow fine-tuning. - If a side is not included in
Sides, only theOffsetis applied for that side.
Usage Examples
Apply safe area to the top and bottom only:
<Grid Padding="{SafeAreaInsets Sides=Top|Bottom}" />
Add 5px static padding to the left, and safe area padding to the top:
<Grid Padding="{SafeAreaInsets Sides=Top, Offset='5,,0,0'}" />
Add 5px static padding to the left, right, bottom, and safe area padding to the top:
<Grid Padding="{SafeAreaInsets Sides=Top, Offset='5'}" />
Apply all safe areas:
<ContentView Padding="{SafeAreaInsets Sides=All}" />
Platform Behavior
| Platform | Notes |
|---|---|
| iOS/macOS | Applies top/bottom/left/right safe insets (status bar, home indicator, etc.) |
| Android | Maps to WindowInsetsCompat.getInsets(Type.systemBars()) by default |
| Keyboard | Not included in base spec (see below) |
Optional Next Step: SafeAreaGroups for Advanced Inset Control
To support more granular control (e.g., include keyboard insets or gesture areas), the extension could later support specifying inset groups.
Future Enum: SafeAreaGroup
[Flags]
public enum SafeAreaGroup
{
None = 0,
SystemBars = 1,
Cutouts = 2,
IME = 4,
Gestures = 8,
All = SystemBars | Cutouts | IME | Gestures,
Default = SystemBars | Cutouts
}
Extension API (Optional Future)
public SafeAreaGroup Groups { get; set; } = SafeAreaGroup.Default;
public SafeAreaGroup? TopGroup { get; set; }
public SafeAreaGroup? BottomGroup { get; set; }
public SafeAreaGroup? LeftGroup { get; set; }
public SafeAreaGroup? RightGroup { get; set; }
Example Usage (Optional Future)
<Grid Padding="{SafeAreaInsets Sides=Top|Bottom,
TopGroup=Cutouts,
BottomGroup=IME,
Offset='0,,0,'}" />
Alternate Paths To Explore
- Just create a SafeAreaInsets property on controls that willl host children controls (ScrollView, ContentView, Layout, etc...). Markup extentions might be too hard to make work since you can apply them everywhere.
- Just make
IgnoreSafeAreawork on Android for now and explore a better option in .NET 11
and you can apply this to elements at any level, right ?
I like the proposal, this was actually the very first suggestion that was made to implement safe area
Some remarks...
- ProvideValue should be an implicit interface implementation (not public)
Sides=Top|Bottomshould beSides='Top,Bottom'(that's how we do flags in XAML)
not sure about the 'future' part. I'd split the feature in 2, the obvious (let's do it), and keep the idea for future extensions in the future.
this is an easy grab for anyone who want to start contributing. It'll require a XamlC counterpart, but @simonrozsival or I will take care of it later
One thing that a markup extension will do is prevent this support from happening in C#. Sure, there can be another way in C#, but this spec just supports XAML.
I do like the alternative where:
IgnoreSafeAreais where the MAUI framework just does it all for you, but if you don't like it- then
IContainerView.SafeAreaInsetswill give you the option for full control
The core proposal is mostly OK because I think most users don't care about the number and more the "this part of the screen I want to...". However, it is XAML only and the way it is structured is Padding="{VALUE}" seems to indicate that this is a real value, however, it is more calculated during layout depending on where it is in the UI.
Also, it is really hard to use since there will be no or limited intellisense.
If the view moves, the safe area moves and thus the padding changes. Having the SafeAreaInsets property does mean that you will have to update yourself if you want a calculated value. But at least you know it is happening.
However, either way feels like we are abusing the padding property. I think a slightly better way would be with a separate propert[y|ies]:
<Grid Padding="20"
SafeAreaBehavior.Top="None"
SafeAreaBehavior.Left="SystemBars,Cutouts"
SafeAreaBehavior.Bottom="All" />
The way I see this is:
- no matter what, there will be a padding of 20 or a min 20 padding
- the top will be be free for me to put a button
- the bottom is protected and MAUI will make sure there is a space
As a result, I can have all of the spec without caring about the actual value no messing with other properties that may have been animated or chnaged based on some other feature (like a11y). If your UI scales, you don't want your padding to be based on the safe areas.
This implementation does not really take away from the first two points:
- IgnoreSafeAreas can be a switch to turn it onn or off, and this controls it.
- the safe area insets is still super useful if you need the actual numbers. This also can tie into that the inset is now no longer "everything" but just what you specified in the behaviors.
I realise the IgnoreSafeAreas propoerty i not in the nice attached property, but we can always move it and obsolete things - or we can keep them like that. It could be SafeAreaBehavior.Ignore="True"...
Maybe the IgnoreSafeAreas can be a markup extension such that:
<Grid IgnoreSafeAreas="{SafeAreaBehavior Top='SystemBars,Cutout', Left=All, Bottom=SystemBars}" />
If we make the property type for
SafeAreaBehavior.Topbe a more complex type (just like withGridLength), it may be able to be extended in future (not sure if additional data is needed besides behavior)
We can make the property attached so it is all in a single place (or not), but the property type can be
enum SafeAreaComponent
{
// pretend tha safe area is not there and put content directly in the space
// this is the same as IgnoreSafeAreas=true (i think)
None = 0,
// don't put content in the status and nav bar areas
SystemBars = 1,
// don't put content under the cutout or notch
Cutouts = 2,
// don't put content under the keyboard
IME = 4, // optional
// kee the gesture areas free so the user does not try tap something that would cause the OS to respond
Gestures = 8,
// don't put anything in any area that the OS may have something else in
// this is the same as IgnoreSafeArea=false
All = SystemBars | Cutouts | IME | Gestures,
// don't put content under the status, nav and cutout
// this is what most users would want... maybe...
Default = SystemBars | Cutouts
}
How's the value provider gonna provide values dynamically? Would it return some kind of binding?
What would be the need of having Top,Bottom,Left,Right here?
It seems an overkill to me: I usually want to avoid the system bars no matter where they are.
So I'd go with a new property like SafeAreaPadding with a new type SafeAreaTypes.
What about ScrollView, I guess the padding applies to the scrollable area, right?
What about CollectionView?
Regarding the keyboard, we should also do something to auto-scroll to the focused field after messing up with the layout while the keyboard was showing:
- https://github.com/nalu-development/nalu/blob/main/Source/Nalu.Maui.Core/Platforms/Android/SoftKeyboardManager.Android.cs#L55
- https://github.com/nalu-development/nalu/blob/main/Source/Nalu.Maui.Core/Platforms/iOS/SoftKeyboardManager.iOS.cs#L330-L351
@mattleibow @PureWeen
Can we also in this perhaps look at two additional attached properties that allows us to apply padding and margin to elements that match the insets for that edge? Something like a ApplySafeAreaPadding and ApplySafeAreaMargin that use the same flags setup as what you're suggesting. That way you can more easily manage the content you're wanting to bleed into the safe area.
E.g. for a sheet coming out from the bottom you want the sheet background itself to ignore the insets (using IgnoreSafeArea), but you'd also want the content of the sheet to have a bottom padding so it doesn't remain hidden behind platform elements in the insets. That would then be achieved with:
<Grid SafeAreaGuides.IgnoreSafeArea="None,None,None,All" SafeAreaGuides.ApplySafeAreaPadding="None,None,None,All" />
Something like this, conceptually;
public static readonly BindableProperty ApplySafeAreaMarginProperty =
BindableProperty.CreateAttached(
"ApplySafeAreaMargin",
typeof(typeof(SafeAreaGroup[])),
typeof(SafeAreaGuides),
false,
propertyChanged: OnSafeAreaChanged
);
public static readonly BindableProperty ApplySafeAreaPaddingProperty =
BindableProperty.CreateAttached(
"ApplySafeAreaPadding",
typeof(typeof(SafeAreaGroup[])),
typeof(SafeAreaGuides),
false,
propertyChanged: OnSafeAreaChanged
);
public static bool GetApplySafeAreaMargin(BindableObject view) =>
(SafeAreaGroup[])view.GetValue(ApplySafeAreaMarginProperty);
public static void SetApplySafeAreaMargin(BindableObject view, SafeAreaGroup[] value) =>
view.SetValue(ApplySafeAreaMarginProperty, value);
public static bool GetApplySafeAreaPadding(BindableObject view) =>
(SafeAreaGroup[])view.GetValue(ApplySafeAreaPaddingProperty);
public static void SetApplySafeAreaPadding(BindableObject view, SafeAreaGroup[] value) =>
view.SetValue(ApplySafeAreaPaddingProperty, value);
private static void OnSafeAreaChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is not Layout layout || newValue is not SafeAreaGroup[] applySafeArea)
return;
if (!applySafeArea)
return;
#if IOS
if (!UIDevice.CurrentDevice.CheckSystemVersion(11, 0))
return;
var insets = UIApplication.SharedApplication.Delegate.GetWindow().SafeAreaInsets;
var shouldApplyMargins = GetApplySafeAreaMargin(layout);
// Todo: replace with an actual check if these should be set or not.
var left = shouldApplyMargins == Left ? insets.Left : 0;
var top =shouldApplyMargins == Top ? insets.Top : 0;
var right = shouldApplyMargins == Right ? insets.Right : 0;
var bottom = shouldApplyMargins == Bottom ? insets.Bottom : 0;
// Add that margin to any margin we manually applied on the element.
layout.Margin = new Thickness(layout.Margin.Left + left, layout.Margin.Top + top,
layout.Margin.Right + right, layout.Margin.Bottom + bottom);
var shouldApplyPaddings = GetApplySafeAreaPadding(layout);
// Todo: replace with an actual check if these should be set or not.
left = shouldApplyPaddings == Left ? insets.Left : 0;
top = shouldApplyPaddings == Top ? insets.Top : 0;
right = shouldApplyPaddings == Right ? insets.Right : 0;
bottom = shouldApplyPaddings == Bottom ? insets.Bottom : 0;
// Add that padding to any padding we manually applied on the element.
layout.Padding = new Thickness(layout.Padding.Left + left, layout.Padding.Top + top,
layout.Padding.Right + right, layout.Padding.Bottom + bottom);
#endif
}
Can we also in this perhaps look at two additional attached properties that allows us to apply padding and margin to elements that match the insets for that edge? Something like a
ApplySafeAreaPaddingandApplySafeAreaMarginthat use the same flags setup as what you're suggesting. That way you can more easily manage the content you're wanting to bleed into the safe area.E.g. for a sheet coming out from the bottom you want the sheet background itself to ignore the insets (using
IgnoreSafeArea), but you'd also want the content of the sheet to have a bottom padding so it doesn't remain hidden behind platform elements in the insets. That would then be achieved with:Something like this, conceptually; public static readonly BindableProperty ApplySafeAreaMarginProperty = BindableProperty.CreateAttached( "ApplySafeAreaMargin", typeof(typeof(SafeAreaGroup[])), typeof(SafeAreaGuides), false, propertyChanged: OnSafeAreaChanged );
public static readonly BindableProperty ApplySafeAreaPaddingProperty = BindableProperty.CreateAttached( "ApplySafeAreaPadding", typeof(typeof(SafeAreaGroup[])), typeof(SafeAreaGuides), false, propertyChanged: OnSafeAreaChanged );
public static bool GetApplySafeAreaMargin(BindableObject view) => (SafeAreaGroup[])view.GetValue(ApplySafeAreaMarginProperty);
public static void SetApplySafeAreaMargin(BindableObject view, SafeAreaGroup[] value) => view.SetValue(ApplySafeAreaMarginProperty, value);
public static bool GetApplySafeAreaPadding(BindableObject view) => (SafeAreaGroup[])view.GetValue(ApplySafeAreaPaddingProperty);
public static void SetApplySafeAreaPadding(BindableObject view, SafeAreaGroup[] value) => view.SetValue(ApplySafeAreaPaddingProperty, value);
private static void OnSafeAreaChanged(BindableObject bindable, object oldValue, object newValue) { if (bindable is not Layout layout || newValue is not SafeAreaGroup[] applySafeArea) return;
if (!applySafeArea) return;
#if IOS if (!UIDevice.CurrentDevice.CheckSystemVersion(11, 0)) return;
var insets = UIApplication.SharedApplication.Delegate.GetWindow().SafeAreaInsets; var shouldApplyMargins = GetApplySafeAreaMargin(layout); // Todo: replace with an actual check if these should be set or not. var left = shouldApplyMargins == Left ? insets.Left : 0; var top =shouldApplyMargins == Top ? insets.Top : 0; var right = shouldApplyMargins == Right ? insets.Right : 0; var bottom = shouldApplyMargins == Bottom ? insets.Bottom : 0; // Add that margin to any margin we manually applied on the element. layout.Margin = new Thickness(layout.Margin.Left + left, layout.Margin.Top + top, layout.Margin.Right + right, layout.Margin.Bottom + bottom); var shouldApplyPaddings = GetApplySafeAreaPadding(layout); // Todo: replace with an actual check if these should be set or not. left = shouldApplyPaddings == Left ? insets.Left : 0; top = shouldApplyPaddings == Top ? insets.Top : 0; right = shouldApplyPaddings == Right ? insets.Right : 0; bottom = shouldApplyPaddings == Bottom ? insets.Bottom : 0; // Add that padding to any padding we manually applied on the element. layout.Padding = new Thickness(layout.Padding.Left + left, layout.Padding.Top + top, layout.Padding.Right + right, layout.Padding.Bottom + bottom);#endif }
- In theory could you achieve this with Nested Layouts?
<ContentView SafeAreaGuides.IgnoreSafeArea="None,None,None,All" >
<Grid SafeAreaGuides.IgnoreSafeArea="None,None,None,None">
- the plan is to add more APIs to this down the road as we see a need. One other approach we're looking at is just exposing the raw values of these SafeAreas. So, you'd be able to just Query Window for them or we'd have a DynamicResource, this way users would just have a raw value they could use and apply to anything
- In theory could you achieve this with Nested Layouts? <ContentView SafeAreaGuides.IgnoreSafeArea="None,None,None,All" > <Grid SafeAreaGuides.IgnoreSafeArea="None,None,None,None">
- the plan is to add more APIs to this down the road as we see a need. One other approach we're looking at is just exposing the raw values of these SafeAreas. So, you'd be able to just Query Window for them or we'd have a DynamicResource, this way users would just have a raw value they could use and apply to anything
True, but I was always taught to have as little nesting of elements as possible ;-) Re: providing a DynamicResource that represents the insets, you'd still always have to use that and then set things like Padding/Margin on a nested element to make it additive, right? I just feel like the overall need for nesting all sorts of UI elements for something as simple as a Margin or Padding could be more minimized.
Suggestion for Extension to Support AppShell
Considering the current SafeAreaGuides.IgnoreSafeArea proposal for .NET 10, it would be valuable to include specific AppShell support in the implementation. AppShell is a fundamental component in MAUI's navigation architecture and presents unique challenges related to safe area management.
Context and Need
AppShell in .NET MAUI maps to native components like UITabBarController on iOS and BottomNavigationView on Android. Currently, when edge-to-edge is enabled, there is inconsistent behavior between AppShell and regular pages, where AppShell still adds content insets while regular pages may not.
Implementation Proposal
The SafeAreaGuides.IgnoreSafeArea attached property should work natively with the following AppShell components:
1. ShellContent and ContentPage
<Shell>
<TabBar>
<Tab>
<ShellContent SafeAreaGuides.IgnoreSafeArea="All">
<ContentPage Title="Main Page" />
</ShellContent>
</Tab>
</TabBar>
</Shell>
2. FlyoutItem and FlyoutHeader
<Shell>
<FlyoutItem SafeAreaGuides.IgnoreSafeArea="Top,Bottom">
<Tab Title="Menu Item" />
</FlyoutItem>
<Shell.FlyoutHeader>
<Grid SafeAreaGuides.IgnoreSafeArea="Top">
<!-- Header content -->
</Grid>
</Shell.FlyoutHeader>
</Shell>
3. Modals and Page Presentation
A specific identified problem is that modals presented through AppShell don't fill the entire screen when edge-to-edge is enabled. The implementation should ensure that:
await Shell.Current.GoToAsync("//modal", animate: true);
Respects the IgnoreSafeArea settings defined on modal pages.
Benefits of Integration
- Visual Consistency: Ensure all parts of the application using AppShell have consistent safe area behavior
- Granular Control: Allow developers to individually control how different Shell elements (TabBar, FlyoutMenu, pages) interact with safe areas
- Future Preparation: With Android 16 completely removing the edge-to-edge opt-out option, AppShell needs to be prepared to manage safe areas natively
In theory could you achieve this with Nested Layouts?
the plan is to add more APIs to this down the road as we see a need. One other approach we're looking at is just exposing the raw values of these SafeAreas. So, you'd be able to just Query Window for them or we'd have a DynamicResource, this way users would just have a raw value they could use and apply to anything
True, but I was always taught to have as little nesting of elements as possible ;-) Re: providing a
DynamicResourcethat represents the insets, you'd still always have to use that and then set things like Padding/Margin on a nested element to make it additive, right? I just feel like the overall need for nesting all sorts of UI elements for something as simple as a Margin or Padding could be more minimized.
yea, we definitely want to add more APIs later, so, just checking that I understood your scenario and this would at least allow a way for now.
True, but I was always taught to have as little nesting of elements as possible ;-) Re: providing a
DynamicResourcethat represents the insets, you'd still always have to use that and then set things like Padding/Margin on a nested element to make it additive, right? I just feel like the overall need for nesting all sorts of UI elements for something as simple as a Margin or Padding could be more minimized.
Yes, not super sure how we'd expose this. One thought would just be doing it similar to android like you could just query Window for these values and then we could just have some event that fires if these changes. At which point you can do whatever you want with them. That's most likely more of a net 11 time frame but it's an option for the future.
just exposing the raw values of these SafeAreas.
Yes please, this is the fastest and shortest code and path, and also the best option to layout our screen as we want. No more mix nor SafeAreaBehavior please. Let those "helpers" go in the toolkit.
3 simple TopSystemBar, BottomSystemBar and TopNotch rectangle properties and we're done.
Btw what are the current workarounds for net9 ?