Handling the Soft Keyboard in .NET MAUI 10: KeyboardAdjust, KeyboardAvoidingView, and Platform Fixes
Stop the soft keyboard from covering inputs in .NET MAUI 10. Practical Android windowSoftInputMode setup, iOS keyboard avoidance, edge-to-edge Android 15 fixes, and a reusable KeyboardAvoidingView pattern that works across both platforms.
To stop the soft keyboard from covering an Entry in .NET MAUI 10, set windowSoftInputMode="adjustResize" on the Android MainActivity and wrap your form in a ScrollView so the framework can scroll the focused input into view. On iOS, add a keyboard-avoiding behavior (or use the MAUI Community Toolkit) because UIKit doesn't reshape the layout for you. That two-line fix solves the classic "keyboard covers my input" bug about 80% of the time. The remaining 20% is edge-to-edge, foldables, and multi-window, all of which we'll walk through below.
Android needs windowSoftInputMode="adjustResize" plus a ScrollView. The framework handles the rest by shrinking the visible area and auto-scrolling to the focused control.
iOS never resizes the layout when the keyboard appears, so you have to translate the page yourself using UIKeyboardWillShow/WillHide or the MAUI Community Toolkit's KeyboardBehavior.
On Android 15+ with edge-to-edge enabled, adjustResize behaves differently. You must consume the IME WindowInsets explicitly, or the keyboard will slide under your content.
Entry.HideKeyboardAsync() and Entry.ShowKeyboardAsync() ship in .NET MAUI 10 and replace the old Focus()/Unfocus() hacks for programmatic keyboard control.
The MAUI Community Toolkit's KeyboardExtensions and SoftKeyboardBehavior cut about 200 lines of boilerplate compared to hand-rolling notifications on both platforms.
Test with the hardware keyboard toggle off. Android emulators and iOS Simulators default to hardware input, which hides the very bug you're trying to fix.
Why keyboard handling is still hard in .NET MAUI
Honestly, I've been shipping MAUI apps in production since the 6.0 preview and the keyboard is still the number-one bug report from our QA team on new form screens. The root cause is that Android and iOS took fundamentally different design decisions decades ago. Android resizes the window when the IME opens; iOS overlays it. .NET MAUI 10 papers over some of that difference, but not enough that you can safely ignore either platform.
On Android, the Window and its content have a well-defined "adjust" behavior controlled per Activity. You pick adjustResize (shrink the layout), adjustPan (pan the whole activity up), or adjustNothing. In edge-to-edge mode (mandatory from Android 15 for apps targeting SDK 35+), Google flipped the default and now expects you to consume the IME inset yourself via WindowInsetsCompat.
On iOS, the keyboard is a separate UIWindow that slides up over your view. UIKit posts UIKeyboardWillShowNotification with the frame, and you're expected to translate your view or adjust contentInset on your scroll view. .NET MAUI doesn't do this automatically for a plain ContentPage. It does do it for a ScrollView containing the focused Entry, but only if the scroll view is the direct parent of the focused control's layout chain. Nest it wrong, and iOS silently gives up.
Cross-platform frameworks that don't reconcile these two models leak the differences to you. .NET MAUI 10 improved things (the SoftInputExtensions API landed in the Community Toolkit v11), but you still need to know both platforms enough to reason about failures. In this guide we'll build up from the simple case (single form, no gotchas) to the ugly ones (edge-to-edge, iPad multi-window, foldables) so you have a mental model that survives real devices.
How do you prevent the keyboard from covering an Entry in .NET MAUI?
The minimum viable fix is a two-step process: configure Android to resize the window, then wrap your form in a ScrollView. Here's the setup we use on every new MAUI page:
The ScrollView is doing real work here. It gives the framework a scrollable region to shrink when the keyboard opens on Android, and on iOS it's what MAUI uses to compute the scroll offset that brings the focused Entry above the keyboard. Skip the ScrollView and you'll get the bug back, even with everything else configured correctly.
Configuring windowSoftInputMode on Android
Android needs an explicit declaration of how the Activity should react to the IME. In .NET MAUI 10 the recommended approach is the [Activity] attribute on MainActivity. That keeps the setting versioned with your code, instead of buried in XML:
// Platforms/Android/MainActivity.cs
using Android.App;
using Android.Content.PM;
using Android.OS;
using Android.Views;
namespace MyApp;
[Activity(
Theme = "@style/Maui.SplashTheme",
MainLauncher = true,
ConfigurationChanges =
ConfigChanges.ScreenSize | ConfigChanges.Orientation |
ConfigChanges.UiMode | ConfigChanges.ScreenLayout |
ConfigChanges.SmallestScreenSize | ConfigChanges.Density,
WindowSoftInputMode = SoftInput.AdjustResize)]
public class MainActivity : MauiAppCompatActivity { }
SoftInput.AdjustResize tells Android to shrink the window's visible area when the keyboard appears. Combined with the ScrollView above, this is enough for the framework to auto-scroll the focused Entry into view. If you need to override the behavior on a per-page basis (for instance, an image-picker page where you want the whole content to pan up instead of squish), call the platform-specific API from OnAppearing:
// MyFormPage.xaml.cs
using Microsoft.Maui.Controls.PlatformConfiguration;
using Microsoft.Maui.Controls.PlatformConfiguration.AndroidSpecific;
using AndroidApp = Microsoft.Maui.Controls.PlatformConfiguration.AndroidSpecific.Application;
public partial class MyFormPage : ContentPage
{
protected override void OnAppearing()
{
base.OnAppearing();
AndroidApp.SetWindowSoftInputModeAdjust(
Application.Current!,
WindowSoftInputModeAdjust.Resize);
}
protected override void OnDisappearing()
{
// Reset to the app default when leaving
AndroidApp.SetWindowSoftInputModeAdjust(
Application.Current!,
WindowSoftInputModeAdjust.Pan);
base.OnDisappearing();
}
}
Building keyboard avoidance on iOS
iOS is where most cross-platform teams get stuck. UIKit does not resize your view when the keyboard appears, so you have to move it. .NET MAUI's built-in behavior scrolls the focused Entry into a ScrollView, but if your form is not inside a ScrollView, or if the layout uses Grid.Row constraints that pin content to the bottom, you'll see the keyboard cover the input.
The clean pattern is to subscribe to keyboard notifications from a page behavior. Here's a minimal iOS-only implementation that translates the page when the keyboard appears and restores it when it hides:
// Platforms/iOS/KeyboardAdjustEffect.cs
using CoreGraphics;
using Foundation;
using Microsoft.Maui.Controls.Platform;
using UIKit;
namespace MyApp.Platforms.iOS;
public class KeyboardAdjustEffect : PlatformEffect
{
NSObject? _willShow, _willHide;
nfloat _originalY;
protected override void OnAttached()
{
if (Container is not UIView view) return;
_originalY = view.Frame.Y;
_willShow = UIKeyboard.Notifications.ObserveWillShow((_, args) =>
{
var kbdFrame = args.FrameEnd;
var window = view.Window ?? UIApplication.SharedApplication
.ConnectedScenes.OfType<UIWindowScene>()
.SelectMany(s => s.Windows).FirstOrDefault(w => w.IsKeyWindow);
if (window is null) return;
// Find the focused input's frame in window coordinates
var focused = FindFirstResponder(view);
if (focused is null) return;
var inputFrame = focused.ConvertRectToView(focused.Bounds, null);
var overlap = inputFrame.Bottom - kbdFrame.Top + 12;
if (overlap <= 0) return;
UIView.Animate(args.AnimationDuration, () =>
{
var f = view.Frame;
view.Frame = new CGRect(f.X, _originalY - overlap, f.Width, f.Height);
});
});
_willHide = UIKeyboard.Notifications.ObserveWillHide((_, args) =>
{
UIView.Animate(args.AnimationDuration, () =>
{
var f = view.Frame;
view.Frame = new CGRect(f.X, _originalY, f.Width, f.Height);
});
});
}
protected override void OnDetached()
{
_willShow?.Dispose(); _willHide?.Dispose();
}
static UIView? FindFirstResponder(UIView root)
{
if (root.IsFirstResponder) return root;
foreach (var s in root.Subviews)
if (FindFirstResponder(s) is { } r) return r;
return null;
}
}
Attach the effect from XAML with a routing effect registration, or (much simpler in production) use the MAUI Community Toolkit's built-in behavior, which we cover next. We only write this by hand when we need custom easing or the toolkit's timing conflicts with an in-flight animation.
Using the MAUI Community Toolkit KeyboardBehavior
Every team I've shipped with has ended up on the MAUI Community Toolkit soft-keyboard extensions. As of Toolkit v11 (August 2026), the API surface is stable and covers both platforms with one XAML attachment. Add the package and register it:
// MauiProgram.cs
using CommunityToolkit.Maui;
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseMauiCommunityToolkit() // <-- registers keyboard behaviors
.ConfigureFonts(fonts => {
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
return builder.Build();
}
Then wire it up in XAML. The SoftKeyboardBehavior is attached to the root layout of the page (usually a Grid) and takes care of iOS translation, Android inset consumption, and (new in v11) the "scroll focused input above keyboard" logic that used to require a ScrollView:
Because the behavior owns the layout translation, you can mix it with tightly constrained UI (pinned toolbars, floating action buttons, sticky headers) without recomputing frames by hand. This is the pattern I recommend for about 90% of forms. If you're already invested in the toolkit for MVVM (see our MVVM Community Toolkit guide), the keyboard behavior slots in naturally alongside ObservableProperty and RelayCommand.
Building a custom KeyboardAvoidingView
When the toolkit's built-in behavior isn't enough, say you need per-field padding, custom easing, or you want to shift only a subset of the layout, write a reusable ContentView. This is our production pattern and it's about 60 lines with all the platform plumbing tucked into partial classes:
// Controls/KeyboardAvoidingView.cs
namespace MyApp.Controls;
public partial class KeyboardAvoidingView : ContentView
{
public static readonly BindableProperty ExtraPaddingProperty =
BindableProperty.Create(nameof(ExtraPadding),
typeof(double), typeof(KeyboardAvoidingView), 0.0);
public double ExtraPadding
{
get => (double)GetValue(ExtraPaddingProperty);
set => SetValue(ExtraPaddingProperty, value);
}
public KeyboardAvoidingView()
{
HandlerChanged += (_, _) => OnHandlerChangedPlatform();
}
partial void OnHandlerChangedPlatform();
partial void OnKeyboardShownPlatform(double keyboardHeight);
partial void OnKeyboardHiddenPlatform();
void ApplyOffset(double offset)
{
this.TranslateTo(0, -offset, 250, Easing.CubicOut);
}
}
// Controls/KeyboardAvoidingView.Android.cs
#if ANDROID
using AndroidX.Core.View;
using Android.Views;
namespace MyApp.Controls;
public partial class KeyboardAvoidingView
{
partial void OnHandlerChangedPlatform()
{
if (Handler?.PlatformView is not Android.Views.View v) return;
ViewCompat.SetOnApplyWindowInsetsListener(v, (view, insets) =>
{
var ime = insets.GetInsets(WindowInsetsCompat.Type.Ime());
if (ime.Bottom > 0) OnKeyboardShownPlatform(ime.Bottom / v.Context!.Resources!.DisplayMetrics!.Density);
else OnKeyboardHiddenPlatform();
return insets;
});
}
partial void OnKeyboardShownPlatform(double h) => ApplyOffset(h + ExtraPadding);
partial void OnKeyboardHiddenPlatform() => ApplyOffset(0);
}
#endif
// Controls/KeyboardAvoidingView.iOS.cs
#if IOS
using CoreGraphics;
using Foundation;
using UIKit;
namespace MyApp.Controls;
public partial class KeyboardAvoidingView
{
NSObject? _show, _hide;
partial void OnHandlerChangedPlatform()
{
_show?.Dispose(); _hide?.Dispose();
_show = UIKeyboard.Notifications.ObserveWillShow((_, a) =>
OnKeyboardShownPlatform(a.FrameEnd.Height));
_hide = UIKeyboard.Notifications.ObserveWillHide((_, _) =>
OnKeyboardHiddenPlatform());
}
partial void OnKeyboardShownPlatform(double h) => ApplyOffset(h + ExtraPadding);
partial void OnKeyboardHiddenPlatform() => ApplyOffset(0);
}
#endif
The pattern uses partial classes with conditional compilation, which keeps the shared API clean and platform code where the compiler can typecheck it. You get one KeyboardAvoidingView to drop into XAML and no runtime dispatch.
How do you dismiss the keyboard in .NET MAUI programmatically?
.NET MAUI 10 shipped Entry.HideKeyboardAsync() and Entry.ShowKeyboardAsync(), which replace the old workaround of calling Unfocus() and hoping the platform got the message. These are cancellation-aware, work identically on Android and iOS, and take care of the animation timing:
// Dismiss on button tap
private async void OnSendClicked(object sender, EventArgs e)
{
await nameEntry.HideKeyboardAsync();
await ViewModel.SubmitAsync();
}
// Dismiss when tapping outside any Entry (put on the root Grid)
private async void OnBackgroundTapped(object sender, TappedEventArgs e)
{
if (this.FindByName<Entry>("nameEntry") is Entry entry && entry.IsFocused)
await entry.HideKeyboardAsync();
}
The tap-to-dismiss pattern is common enough that it's worth encapsulating in a behavior. Attach a TapGestureRecognizer to the page's root layout and iterate the visual tree for any focused InputView descendant. The toolkit ships this as DismissKeyboardOnTapBehavior. On iOS specifically, don't rely on Entry.Completed firing when the user taps outside the field. UIKit only fires Completed for the Return key, and that gotcha has caught many teams by surprise (mine included, on a shipping app that quietly lost half its form submissions for a week).
Edge-to-edge, Android 15+, and IME insets
Android 15 (API 35) forces edge-to-edge on any app targeting SDK 35 or higher. This is the same change that broke status bar overlap and that we covered in our edge-to-edge fix for Android 16 guide. The knock-on effect on the keyboard: adjustResize still fires, but the resized window now includes the navigation bar and IME areas, so your content ends up flush against the top of the keyboard with no padding.
The fix is to consume the IME inset explicitly. Wire up an insets listener on the root DecorView:
// Platforms/Android/MainActivity.cs (inside OnCreate, after base.OnCreate)
using AndroidX.Core.View;
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
var root = Window!.DecorView.FindViewById(Android.Resource.Id.Content)!;
ViewCompat.SetOnApplyWindowInsetsListener(root, (view, insets) =>
{
var bars = insets.GetInsets(
WindowInsetsCompat.Type.SystemBars() |
WindowInsetsCompat.Type.Ime());
view.SetPadding(bars.Left, bars.Top, bars.Right, bars.Bottom);
return WindowInsetsCompat.Consumed;
});
}
This gives you the same behavior as pre-15 Android: content sits above the keyboard, with a safe area at the bottom, while remaining compliant with the edge-to-edge requirement. The MAUI Community Toolkit's SoftKeyboardBehavior v11 does this automatically, so if you're already using it, you get the fix for free.
Testing keyboard behavior across devices
Keyboard testing is one of those areas where emulators lie to you. The Android emulator and iOS Simulator both default to using your Mac or PC's hardware keyboard, which never triggers the soft-keyboard code path. On Android, toggle it with Ctrl + K in the emulator; on iOS, uncheck Connect Hardware Keyboard in the Simulator's I/O menu.
Beyond that, our team's minimum test matrix for any form-heavy screen is:
iPhone SE (small screen, 4.7"). Worst case for input coverage because the keyboard eats about 60% of the screen height.
iPad in Split View (multi-window). The keyboard height differs from full-screen and safe-area insets change.
Android device with gesture navigation. The bottom inset is different from the button-navigation case.
Foldable in unfolded state. The keyboard docks to the bottom of the current display region, not the whole panel; this catches implementations that assume a single window height.
Landscape orientation. The ratio flips, and forms that fit vertically often don't.
UI-automation-wise, Appium and MAUI's UI tests can drive input focus but not always the keyboard visibility, so check the target frameworks for gaps. In practice we rely on device-lab runs for keyboard behavior, and use unit tests only for the view-model logic that reacts to keyboard events.
One more sanity step I've grown fond of: install the CommunityToolkit.Maui GitHub repo's sample gallery on the same devices. When the toolkit's own samples reproduce a bug on your setup, it's almost always a device or OS quirk, not your code. That single check has saved me at least a full afternoon of debugging on more than one project.
Frequently Asked Questions
Why does the keyboard cover my Entry in .NET MAUI even with adjustResize set?
The most common cause is that the focused Entry has no scrollable ancestor. adjustResize shrinks the window, but MAUI needs a ScrollView in the layout chain to scroll the focused input into the newly visible area. Add a ScrollView around your form, or use the MAUI Community Toolkit's SoftKeyboardBehavior.
Does .NET MAUI 10 have a KeyboardAvoidingView like React Native?
Not in the base framework, but the MAUI Community Toolkit v11 ships SoftKeyboardBehavior, which is the equivalent. Attach it to your root layout in XAML and it handles both platforms. If you need per-field control, roll a custom ContentView using partial classes for iOS and Android.
How do I dismiss the keyboard when the user taps outside an input?
Attach a TapGestureRecognizer to the page's root layout and call await entry.HideKeyboardAsync() on the currently focused Entry. The MAUI Community Toolkit's DismissKeyboardOnTapBehavior encapsulates this pattern in one line of XAML.
What is windowSoftInputMode adjustResize vs adjustPan?
adjustResize shrinks the window height when the keyboard appears, letting the framework scroll the focused input into view. adjustPan shifts the whole activity up as a rigid unit, which can push the status bar off-screen. Almost always use adjustResize.
Why is the keyboard behaving differently on Android 15?
Android 15 (API 35) forces edge-to-edge, which changes how window insets are reported. Even with adjustResize, your content ends up flush with the top of the keyboard because the IME inset is no longer consumed automatically. Add a ViewCompat.SetOnApplyWindowInsetsListener to consume both system-bar and IME insets, or use the Community Toolkit v11's built-in handling.
Cross-platform engineering lead who's shipped apps to millions on both Play Store and App Store. Believes shared codebases shouldn't mean shared mediocrity.
A working guide to shipping .NET MAUI 10 apps with Fastlane: match-based iOS signing, API-key auth for TestFlight, staged Play Store rollouts, and Fastfile lanes that survive real teams.
Bind iOS XCFrameworks and Android AARs in .NET MAUI 10 with Objective Sharpie, Metadata.xml transforms, and a single-NuGet package. Real errors, real fixes.
Map stripped .NET MAUI 10 crashes back to source: where dSYMs and R8 mapping.txt land, how to upload to Sentry and Crashlytics, and how to automate it in CI without shipping unsymbolicated builds.