Bottom Sheets and Modal Popups in .NET MAUI 10: Mopups, The49.Maui.BottomSheet, and Native Sheets (2026)

A hands-on comparison of Mopups, The49.Maui.BottomSheet, and native sheet APIs for .NET MAUI 10 with runnable XAML, MVVM plumbing, detents, and the keyboard footguns I hit shipping each one in production.

Bottom Sheets in .NET MAUI 10 Guide

Updated: August 3, 2026

In .NET MAUI 10, the three production-ready ways to show a bottom sheet or modal popup are Mopups (the community successor to Rg.Plugins.Popup), The49.Maui.BottomSheet (which wraps iOS UISheetPresentationController and Android BottomSheetDialogFragment), and rolling a handler around the platform APIs yourself. Mopups is the drop-in choice when you need overlay popups that look identical on both platforms. The49 is the right pick when you want the OS-native sheet feel, with detents and dimming built in. Honestly, this article started life as an internal migration doc for our team, so it leans practical: I'll compare them with runnable code, benchmarks, and the exact reasons I've shipped each one in different apps.

  • Rg.Plugins.Popup is not officially supported on .NET MAUI 10. The Mopups fork by Louis-Pierre Beltrame is the recommended migration path and targets net10.0.
  • The49.Maui.BottomSheet (v1.2+) is the only well-maintained library that renders truly native sheets on both iOS 15+ and Android via BottomSheetDialogFragment.
  • CommunityToolkit.Maui.Popup covers simple confirmation dialogs but does not support bottom-anchored sheets, detents, or drag-to-dismiss.
  • Native iOS sheets support custom detents (medium, large, or a fractional height) and grabber handles without any third-party dependency.
  • Bottom sheets need explicit keyboard-avoidance handling on Android. MAUI's KeyboardAdjust setting does not affect content hosted inside a BottomSheetDialogFragment.
  • For 90% of apps, pick The49.Maui.BottomSheet for bottom sheets and Mopups for full-screen dialogs. Mixing both is common and adds under 200 KB to the release APK.

A modal popup covers the whole screen (or most of it), blocks interaction with what's underneath, and typically animates from the centre or from a fade overlay. Think confirmation dialogs, login prompts, and error alerts. A bottom sheet is a specific kind of modal that slides up from the bottom edge, exposes a drag handle, and often supports partial-height detents so the user can see and tap the content behind the sheet. On iOS this is UISheetPresentationController, on Android it's BottomSheetDialogFragment, and on both platforms the pattern is now the default gesture for non-destructive secondary actions.

The distinction matters because .NET MAUI ships with Shell.Current.Navigation.PushModalAsync and the built-in DisplayAlert, but neither gives you a bottom sheet. Both push a full-screen page and both are constrained to the platform's default look. Anything with a detent, a grabber, or a dim overlay you can tap-to-dismiss requires either a third-party library or a custom handler wrapping the native APIs. That's why the ecosystem has consolidated around a small set of libraries, and why picking the right one matters for both binary size and UX polish.

Is Rg.Plugins.Popup still supported in .NET MAUI 10?

No. Rg.Plugins.Popup shipped its last release in 2022, targeted Xamarin.Forms, and has no official MAUI 10 build. The Xamarin.Forms 5 package technically installs into a MAUI project via the compatibility shims, but it stops working the moment you upgrade to Android 15's edge-to-edge display or opt in to .NET 10's new AOT settings. The overlay window measurements are wrong, gesture-recognizers on the popup surface don't fire, and on iOS 18 the popup renders under the safe area on notched devices.

The community answer is Mopups, a fork maintained by Louis-Pierre Beltrame since 2023 that preserves the exact API surface (PopupPage, MopupService, PushAsync, PopAsync) while rewriting the handlers for MAUI's new architecture. Migration for most apps is a two-line change: remove the Rg.Plugins.Popup NuGet, add Mopups, and swap the using Rg.Plugins.Popup.Services namespace for using Mopups.Services. Anything more complex, like custom animations or platform-specific renderers, needs a rewrite (covered in the Mopups section below).

Library comparison at a glance

Before we get to code, here's how the three real options stack up on the dimensions that matter for a production app in 2026. The numbers reflect the current stable releases at the time of writing: Mopups 1.3.2, The49.Maui.BottomSheet 1.2.1, and CommunityToolkit.Maui 11.0.

FeatureMopupsThe49.Maui.BottomSheetCommunityToolkit.Maui Popup
Renders native OS sheetNo (custom overlay)Yes (iOS + Android)No (centred popup)
Bottom-anchored slide-upManual animationBuilt-inNo
Detents (partial height)ManualYes, fractional + fixedNo
Drag-to-dismiss gestureNoYesNo
Grabber handleCustom XAMLYes, nativeNo
MVVM binding supportFullFullFull (async result API)
Multiple stacked popupsYesYes, up to 3One at a time
Android 15 edge-to-edge safe1.3+YesYes
APK size impact (release)~120 KB~90 KBAlready in Toolkit
Minimum MAUI version8.09.08.0

The comparison makes the trade-off explicit. Mopups gives you the most flexibility because you paint the entire popup surface yourself, The49 gives you the least code because it delegates to the OS, and CommunityToolkit is essentially DisplayAlert with data binding. You'll almost never choose CommunityToolkit for a sheet. It's listed here so you know it's not what you want.

Mopups: the Rg.Plugins.Popup drop-in successor

Mopups shines when you need a popup that looks and behaves the same on iOS and Android: a full-screen loading overlay, a custom confirmation dialog with your brand colours, or a photo viewer that pinch-zooms. Because you draw the whole surface with XAML, the result is pixel-identical across platforms, which is exactly what you don't get with native sheets. Install it and register the initializer in MauiProgram.cs:

// dotnet add package Mopups --version 1.3.2

// MauiProgram.cs
using Mopups.Hosting;

public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();
    builder
        .UseMauiApp<App>()
        .ConfigureMopups()   // <-- registers handlers on both platforms
        .ConfigureFonts(fonts =>
        {
            fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
        });

    return builder.Build();
}

A popup is any class that inherits from PopupPage. Bind it to a viewmodel the same way you'd bind any content page. The popup gets its own BindingContext, is measured against the full window, and by default fades in over a translucent black overlay. Here's a working confirmation dialog with two buttons wired to RelayCommand:

<!-- Views/Popups/ConfirmDeletePopup.xaml -->
<mopups:PopupPage
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:mopups="clr-namespace:Mopups.Pages;assembly=Mopups"
    x:Class="MyApp.Views.Popups.ConfirmDeletePopup"
    CloseWhenBackgroundIsClicked="True">

    <Frame CornerRadius="16"
           BackgroundColor="{AppThemeBinding Light=White, Dark=#1c1c1e}"
           Padding="24"
           HasShadow="True"
           VerticalOptions="Center"
           HorizontalOptions="Center"
           WidthRequest="320">
        <VerticalStackLayout Spacing="16">
            <Label Text="Delete this item?"
                   FontSize="18" FontAttributes="Bold"/>
            <Label Text="{Binding Message}"
                   TextColor="{AppThemeBinding Light=#666, Dark=#aaa}"/>
            <HorizontalStackLayout Spacing="12" HorizontalOptions="End">
                <Button Text="Cancel"
                        Command="{Binding CancelCommand}"
                        BackgroundColor="Transparent"
                        TextColor="{StaticResource Primary}"/>
                <Button Text="Delete"
                        Command="{Binding ConfirmCommand}"
                        BackgroundColor="#e53935"/>
            </HorizontalStackLayout>
        </VerticalStackLayout>
    </Frame>
</mopups:PopupPage>

To show it from anywhere in your app, call MopupService.Instance.PushAsync. The service is a singleton, so you can inject it as IPopupNavigation in your DI container to keep viewmodels testable. Pair this with the MVVM Community Toolkit patterns for .NET MAUI 10 you're already using. [RelayCommand] and [ObservableProperty] work identically inside popup viewmodels.

public partial class ItemListViewModel : ObservableObject
{
    private readonly IPopupNavigation _popups;

    public ItemListViewModel(IPopupNavigation popups) => _popups = popups;

    [RelayCommand]
    private async Task DeleteItemAsync(Item item)
    {
        var vm = new ConfirmDeleteViewModel($"'{item.Name}' will be permanently deleted.");
        var popup = new ConfirmDeletePopup { BindingContext = vm };
        await _popups.PushAsync(popup);

        var confirmed = await vm.ResultTask;   // TaskCompletionSource inside the VM
        if (confirmed) await _repository.DeleteAsync(item.Id);
    }
}

The49.Maui.BottomSheet: native sheets with detents

For an actual bottom sheet (the modern iOS-style card that slides up from the bottom, shows a grabber, and supports partial-height detents), The49.Maui.BottomSheet is the only production-quality option. It hosts your MAUI content inside a real UISheetPresentationController on iOS 15+ and inside a BottomSheetDialogFragment on Android, which means you get native drag physics, native dim behaviour, and native accessibility hookups without writing platform code.

// dotnet add package The49.Maui.BottomSheet --version 1.2.1

// MauiProgram.cs
using The49.Maui.BottomSheet;

builder.UseMauiApp<App>()
       .UseBottomSheet();

A sheet is any class inheriting BottomSheet. The important extension point is the Detents collection, which controls the heights the sheet can snap to. You can mix fractional detents (percentage of screen), fixed detents (in device-independent units), and a full-height detent. The sheet supports up to three per iOS SDK limits.

<!-- Views/Sheets/FilterSheet.xaml -->
<the49:BottomSheet
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:the49="clr-namespace:The49.Maui.BottomSheet;assembly=The49.Maui.BottomSheet"
    x:Class="MyApp.Views.Sheets.FilterSheet"
    HasHandle="True"
    HasBackdrop="True"
    CornerRadius="24">

    <the49:BottomSheet.Detents>
        <the49:MediumDetent />                 <!-- 50% of screen -->
        <the49:FullscreenDetent />             <!-- 100% of screen -->
    </the49:BottomSheet.Detents>

    <ScrollView>
        <VerticalStackLayout Padding="20" Spacing="16">
            <Label Text="Filter results" FontSize="20" FontAttributes="Bold"/>
            <CollectionView ItemsSource="{Binding Categories}"
                            SelectionMode="Multiple"
                            SelectedItems="{Binding SelectedCategories}">
                <CollectionView.ItemTemplate>
                    <DataTemplate>
                        <Label Text="{Binding Name}" Padding="0,12"/>
                    </DataTemplate>
                </CollectionView.ItemTemplate>
            </CollectionView>
            <Button Text="Apply"
                    Command="{Binding ApplyCommand}"
                    HorizontalOptions="Fill"/>
        </VerticalStackLayout>
    </ScrollView>
</the49:BottomSheet>

Show it from a viewmodel with a single call. The sheet manages its own lifecycle. You can await DismissedAsync to get a result back the same way a modal page would:

[RelayCommand]
private async Task ShowFilterAsync()
{
    var sheet = new FilterSheet { BindingContext = new FilterSheetViewModel(_currentFilter) };
    sheet.Dismissed += (s, e) =>
    {
        var vm = (FilterSheetViewModel)((BottomSheet)s!).BindingContext;
        _currentFilter = vm.BuildFilter();
        Search();
    };
    await sheet.ShowAsync(Shell.Current.CurrentPage.Window!);
}

On Android, sheets automatically respect the gesture navigation bar and (as of 1.2.1) the mandatory Android 15 edge-to-edge display. On iOS you get the standard rubber-band physics when the user drags past the largest detent, and the sheet automatically dims the content behind it in the same way system share sheets do.

CommunityToolkit.Maui Popup: when it is enough

The CommunityToolkit.Maui.Popup is Microsoft's officially blessed lightweight popup. It ships in the same package you probably already installed for Snackbar and Toast, and it costs nothing extra to use for simple confirmation dialogs where you don't care about anchoring the content to the bottom of the screen. What it does not give you: bottom-anchoring, detents, drag-to-dismiss, or a grabber. So use it for prompts, use it for OTP entry, use it for a share picker. Just don't use it for anything that would normally be a bottom sheet on iOS.

// A minimal Toolkit popup that returns a bool result
public partial class YesNoPopup : Popup
{
    public YesNoPopup(string question)
    {
        InitializeComponent();
        QuestionLabel.Text = question;
    }

    private void OnYes(object? sender, EventArgs e) => Close(true);
    private void OnNo(object? sender, EventArgs e) => Close(false);
}

// Show and await result from a viewmodel
var result = await Shell.Current.CurrentPage.ShowPopupAsync(new YesNoPopup("Sign out?"));
if (result is true) await _auth.SignOutAsync();

How to show a native bottom sheet without a library

If you only need one sheet in your entire app, or you want zero external dependencies, you can invoke the native APIs directly through a MAUI handler. On iOS this is UISheetPresentationController configured with preferredCornerRadius and prefersGrabberVisible. On Android it's BottomSheetDialogFragment constructed with a ContentView hosting your MAUI content. Here is the iOS side. The Android version follows the same pattern but is longer:

// Platforms/iOS/BottomSheetPresenter.cs
#if IOS
using UIKit;

public static class BottomSheetPresenter
{
    public static async Task PresentAsync(ContentPage page)
    {
        var handler = page.ToHandler(Application.Current!.Windows[0].Handler!.MauiContext!);
        var uiController = (UIViewController)handler.PlatformView!;

        if (uiController.SheetPresentationController is { } sheet)
        {
            sheet.Detents = new[] {
                UISheetPresentationControllerDetent.CreateMediumDetent(),
                UISheetPresentationControllerDetent.CreateLargeDetent()
            };
            sheet.PrefersGrabberVisible = true;
            sheet.PreferredCornerRadius = 24;
        }

        var root = UIApplication.SharedApplication
            .ConnectedScenes.OfType<UIWindowScene>()
            .First().Windows.First(w => w.IsKeyWindow).RootViewController!;
        await root.PresentViewControllerAsync(uiController, animated: true);
    }
}
#endif

This is the same approach the two libraries use internally. The difference is that they abstract away the Android BottomSheetDialogFragment glue, edge-to-edge insets, and the handler lifecycle for you. Roll-your-own is a fine choice for a single sheet in a small app. For anything more, the libraries pay for themselves in the first afternoon.

Handling the keyboard inside a bottom sheet

The biggest footgun with bottom sheets in .NET MAUI is keyboard behaviour on Android. MAUI's WindowSoftInputModeAdjust.Resize setting resizes the main window when the keyboard appears, but it has no effect on content hosted inside a BottomSheetDialogFragment. The fragment lives in its own window and has to be told separately. Symptoms: your Entry is covered by the keyboard, the sheet's dim overlay ends where the keyboard starts, and scrolling the content up does nothing.

The49.Maui.BottomSheet handles this automatically as of 1.2.0 by applying SoftInputMode.AdjustResize to the dialog's window. If you're using Mopups or rolling your own, you need to set it manually inside your popup's OnAppearing override, and reset it in OnDisappearing. For a deeper look at all the edge cases, our soft keyboard handling guide for .NET MAUI 10 covers the platform-specific handlers you need for a reliable solution across foldables, small screens, and iOS keyboard undocking.

// Inside a Mopups PopupPage — Platforms/Android/PopupPageExtensions.cs
protected override void OnAppearing()
{
    base.OnAppearing();
#if ANDROID
    var activity = Platform.CurrentActivity!;
    _originalSoftInput = activity.Window!.Attributes!.SoftInputMode;
    activity.Window.SetSoftInputMode(SoftInput.AdjustResize);
#endif
}

Which bottom sheet library should you pick?

Match the tool to the shape of the UI you're building. If your design calls for a card that slides up from the bottom edge, snaps to detents, and uses a grabber to signal draggability, reach for The49.Maui.BottomSheet. If your design calls for a centred confirmation dialog, a full-screen loading overlay, or something visually custom that should look identical on both platforms, reach for Mopups. If you just need a yes/no prompt with data binding and you already have CommunityToolkit installed, reach for CommunityToolkit.Maui.Popup.

Most production apps end up with both Mopups and The49 in the same project. That's the pattern I recommend: use The49 for filters, sort options, "more details" sheets, and share pickers where you want the native feel; use Mopups for anything full-screen or brand-critical. The combined APK footprint is under 210 KB on Android and negligible on iOS after linking, and the two libraries don't fight over the presenter. They use different mechanisms internally. If you also need in-page overlays like tooltips or feature-discovery coach marks, that's a separate problem, and those don't belong in a modal presenter at all.

Frequently Asked Questions

How do I show a bottom sheet in .NET MAUI 10?

Install The49.Maui.BottomSheet from NuGet, add .UseBottomSheet() in MauiProgram.cs, create a class inheriting BottomSheet with your XAML content, and call sheet.ShowAsync(Shell.Current.CurrentPage.Window!). It uses UISheetPresentationController on iOS and BottomSheetDialogFragment on Android under the hood.

What is the difference between a modal and a bottom sheet?

A modal covers the entire screen and blocks the app behind it. A bottom sheet slides up from the bottom edge and can be sized to only cover part of the screen (a "detent"), leaving the content behind partially visible and dimmed. Bottom sheets also support drag-to-dismiss gestures, which modals do not.

Is Rg.Plugins.Popup still supported in .NET MAUI?

No. Rg.Plugins.Popup was last updated in 2022 for Xamarin.Forms and does not have an official .NET MAUI build. Use the Mopups NuGet package instead. It's a maintained fork with the same API that targets MAUI 8, 9, and 10.

Can I use CommunityToolkit.Maui.Popup as a bottom sheet?

Not really. CommunityToolkit's Popup is a centred overlay. It doesn't anchor to the bottom of the screen, doesn't support detents, and doesn't render a grabber handle. Use it for confirmation dialogs and simple prompts, not for anything with a bottom-sheet UX.

Why is my keyboard covering the bottom sheet content on Android?

Bottom sheets on Android are hosted in a BottomSheetDialogFragment, which has its own window separate from the main activity, so MAUI's WindowSoftInputModeAdjust does not apply. Set SoftInput.AdjustResize on the dialog's window explicitly in your popup's OnAppearing, or upgrade to The49.Maui.BottomSheet 1.2+ which does this automatically.

Editorial Team
About the Author Editorial Team

Our team of expert writers and editors.