Skip to content

Andreas973/Maui.BottomSheet

Β 
Β 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

184 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ“± Plugin.Maui.BottomSheet

πŸš€ Show native BottomSheets with .NET MAUI!

NuGet License Platform Support

✨ Features

  • 🧭 Built-in NavigationService - Seamless navigation between bottom sheets
  • πŸ“„ Flexible Content - Open any ContentPage or View as BottomSheet
  • 🎨 Customizable Layout - Create BottomSheets in any layout
  • πŸ“‹ Configurable Header - Full header customization support
  • πŸ—οΈ MVVM Support - Complete MVVM pattern integration
  • 🎯 Native Performance - Platform-specific implementations for optimal UX

🎯 Quick Demo

Check out the sample project to see the API in action!

🌍 Platform Support

Mobile

πŸ“± Apple iOS 15+

iPhone iOS Demo iPad iPad Demo

πŸ€– Android API 21+

Phone Android Demo Tablet Android Tablet Demo

Desktop

πŸ’» MacCatalyst 15+

MacCatalyst Demo

πŸͺŸ Windows

Windows Demo

πŸš€ Quick Start

1️⃣ Installation

Install the NuGet package:

dotnet add package Plugin.Maui.BottomSheet

2️⃣ Setup

Enable the plugin in your MauiProgram.cs:

using Plugin.Maui.BottomSheet.Hosting;

var builder = MauiApp.CreateBuilder();
builder
    .UseMauiApp<App>()
    .UseBottomSheet() // πŸ‘ˆ Add this line
    .ConfigureFonts(fonts =>
    {
        fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
        fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
    });

#if DEBUG
builder.Logging.AddDebug();
#endif

return builder.Build();

3️⃣ Basic Usage

Add the XAML namespace:

xmlns:bottomsheet="http://pluginmauibottomsheet.com"

Create a simple bottom sheet:

<bottomsheet:BottomSheet x:Name="MyBottomSheet" IsOpen="{Binding IsSheetOpen}">
    <bottomsheet:BottomSheetContent>
        <Label Text="Hello from Bottom Sheet!" />
    </bottomsheet:BottomSheetContent>
</bottomsheet:BottomSheet>

Set peek height to view height (peek height changes dynamically as view height changes)

<mauibottomsheet:BottomSheet
    PeekHeight="{mauibottomsheet:PeekView View={x:Reference ContentBox}}"
    States="Peek">
    <mauibottomsheet:BottomSheetContent>
        <mauibottomsheet:BottomSheetContent.Content>
            <Grid RowDefinitions="300">
                <BoxView x:Name="ContentBox" Color="Orange" />
            </Grid>
        </mauibottomsheet:BottomSheetContent.Content>
    </mauibottomsheet:BottomSheetContent>
</mauibottomsheet:BottomSheet>

or

<mauibottomsheet:BottomSheet
    PeekHeight="{mauibottomsheet:PeekView ContentBox}"
    States="Peek">
    <mauibottomsheet:BottomSheetContent>
        <mauibottomsheet:BottomSheetContent.ContentTemplate>
            <DataTemplate>
                <Grid RowDefinitions="300">
                    <BoxView x:Name="ContentBox" Color="Orange" />
                </Grid>
            </DataTemplate>
        </mauibottomsheet:BottomSheetContent.ContentTemplate>
    </mauibottomsheet:BottomSheetContent>
</mauibottomsheet:BottomSheet>

Caution

The peek height is calculated using MAUI's measurement behavior. Different platforms may return different heights for the same layout. You should not set a ScrollView as your peek view reference. Use the root of your ScrollView instead. This is especially true when using VerticalStackLayout and Grid with Star rows. To observe the differences, launch the Plugin.BottomSheet.Tests.Maui.Ui.Application test project and open the various peek BottomSheets

πŸ“– API Reference

πŸ”§ Core Properties

Property Type Description
IsModal bool Enable/disable interaction with content under BottomSheet
IsCancelable bool Allow user to close via gestures or background click
HasHandle bool Show/hide the drag handle
ShowHeader bool Show/hide the header section
IsOpen bool Control open/close state
IsDraggable bool Enable/disable drag gestures (useful for drawing)
States List<BottomSheetState> Allowed states (Peek, Medium, Large)
CurrentState BottomSheetState Current display state
PeekHeight double Height when in peek state (iOS 16+)
Padding double Internal padding
BackgroundColor Color Background color
CornerRadius float Top corner radius
WindowBackgroundColor Color Window background (modal only)
SizeMode BottomSheetSizeMode Represents a size mode for the bottom sheet where the height is adjusted dynamically to fit the content displayed within it or a fixed value based on the number of states it can display.

Size Modes

State Description
States Represents a size mode for the bottom sheet where the height is set to a fixed value based on the number of states it can display. This mode ensures the bottom sheet is fully expanded rather than dynamically adjusting its size to-fit content.
FitToContent A size mode indicating that the bottom sheet dynamically adjusts its height to precisely fit the content it displays. In this mode, the bottom sheet wraps around its contents and does not occupy any more vertical space than necessary.

Caution

Mode FitToContent is not compatible with the States and CurrentState property. Do not mix these two properties. Requires iOS 16+. On prior versions of iOS, the medium state will be used instead.

🎭 BottomSheet States

State Description
Peek Fractional height (customizable)
Medium Half screen height
Large Full screen height

🎨 Header Configuration

Property Type Description
TitleText string Title text displayed in header
TopLeftButton Button Custom button for top-left position
TopRightButton Button Custom button for top-right position
HeaderDataTemplate DataTemplate Custom view template (overrides other header properties)
Content View Direct view content
HeaderAppearance BottomSheetHeaderButtonAppearanceMode Controls which buttons are displayed
ShowCloseButton bool Show built-in close button
CloseButtonPosition CloseButtonPosition Position of close button (Left/Right)

Caution

Be careful when using Content because the Content will be created even if the BottomSheet isn't open and this may have a negative performance impact. Content should only be used with navigation and not in BottomSheets added directly to a Layout.

πŸ“± Content Configuration

Property Type Description
Content View Direct view content
ContentTemplate DataTemplate Template for lazy loading

Caution

Be careful when using Content because the Content will be created even if the BottomSheet isn't open and this may have a negative performance impact. Content should only be used with navigation and not in BottomSheets added directly to a Layout.

🎨 Styling Options

BottomSheet Style Properties

Property Type Description
HeaderStyle BottomSheetHeaderStyle Styling configuration for header elements

BottomSheet Header Style Properties

Property Type Description
TitleTextColor Color Color of the title text
TitleTextFontSize double Font size of the title text
TitleTextFontAttributes FontAttributes Font attributes (Bold, Italic, None)
TitleTextFontFamily string Font family name for title text
TitleTextFontAutoScalingEnabled bool Enable automatic font scaling
CloseButtonHeightRequest double Requested height for close button
CloseButtonWidthRequest double Requested width for close button
CloseButtonTintColor Color Tint color for close button icon

πŸŽͺ Advanced Examples

πŸ“‹ Complete XAML Example

<bottomsheet:BottomSheet
    x:Name="AdvancedBottomSheet"
    Padding="20"
    CornerRadius="20"
    HasHandle="True"
    IsCancelable="True"
    IsDraggable="True"
    IsModal="True"
    IsOpen="{Binding IsOpen}"
    ShowHeader="True"
    States="Peek,Medium,Large"
    CurrentState="Medium"
    WindowBackgroundColor="Black">

    <!-- Header Configuration -->
    <bottomsheet:BottomSheet.Header>
        <bottomsheet:BottomSheetHeader
            CloseButtonPosition="Right"
            HeaderAppearance="LeftAndRightButton"
            ShowCloseButton="True"
            TitleText="My Awesome Sheet">
            <bottomsheet:BottomSheetHeader.TopLeftButton>
                <Button Command="{Binding LeftButtonCommand}" Text="Cancel" />
            </bottomsheet:BottomSheetHeader.TopLeftButton>
            <bottomsheet:BottomSheetHeader.TopRightButton>
                <Button Command="{Binding RightButtonCommand}" Text="Save" />
            </bottomsheet:BottomSheetHeader.TopRightButton>
        </bottomsheet:BottomSheetHeader>
    </bottomsheet:BottomSheet.Header>

    <!-- Content with Peek Height Behavior -->
    <bottomsheet:BottomSheetContent>
        <bottomsheet:BottomSheetContent.ContentTemplate>
            <DataTemplate>
                <VerticalStackLayout>
                    <!-- Peek Content -->
                    <ContentView>
                        <ContentView.Behaviors>
                            <bottomsheet:BottomSheetPeekBehavior />
                        </ContentView.Behaviors>
                        <Label Text="This content is visible in peek mode" />
                    </ContentView>

                    <!-- Full Content -->
                    <Grid>
                        <Label Text="This content appears when expanded" />
                        <!-- Your content here -->
                    </Grid>
                </VerticalStackLayout>
            </DataTemplate>
        </bottomsheet:BottomSheetContent.ContentTemplate>
    </bottomsheet:BottomSheetContent>

    <!-- Styling -->
    <bottomsheet:BottomSheet.BottomSheetStyle>
        <bottomsheet:BottomSheetStyle>
            <bottomsheet:BottomSheetStyle.HeaderStyle>
                <bottomsheet:BottomSheetHeaderStyle
                    TitleTextColor="Blue"
                    TitleTextFontSize="18"
                    TitleTextFontAttributes="Bold"
                    CloseButtonTintColor="Red"/>
            </bottomsheet:BottomSheetStyle.HeaderStyle>
        </bottomsheet:BottomSheetStyle>
    </bottomsheet:BottomSheet.BottomSheetStyle>
</bottomsheet:BottomSheet>

πŸ“‹ Easy and fast popups with content based size

<mauibottomsheet:BottomSheet
    SizeMode="FitToContent">
    <mauibottomsheet:BottomSheetContent>
        <mauibottomsheet:BottomSheetContent.Content>
            <Grid RowDefinitions="300">
                <BoxView x:Name="ContentBox" Color="Orange" />
            </Grid>
        </mauibottomsheet:BottomSheetContent.Content>
    </mauibottomsheet:BottomSheetContent>
</mauibottomsheet:BottomSheet>

🎨 Global Styling

<Style TargetType="bottomsheet:BottomSheet">
    <Setter Property="BottomSheetStyle">
        <Setter.Value>
            <bottomsheet:BottomSheetStyle>
                <bottomsheet:BottomSheetStyle.HeaderStyle>
                    <bottomsheet:BottomSheetHeaderStyle
                        CloseButtonHeightRequest="40"
                        CloseButtonTintColor="LightBlue"
                        CloseButtonWidthRequest="40"
                        TitleTextColor="Blue"
                        TitleTextFontAttributes="Bold"
                        TitleTextFontAutoScalingEnabled="True"
                        TitleTextFontFamily="OpenSansRegular"
                        TitleTextFontSize="20" />
                </bottomsheet:BottomSheetStyle.HeaderStyle>
            </bottomsheet:BottomSheetStyle>
        </Setter.Value>
    </Setter>
</Style>

🧭 Navigation System

πŸš€ Setup Navigation

Register your bottom sheets for navigation:

// Basic registration
builder.Services.AddBottomSheet<UserProfilePage>("UserProfile");

// With ViewModel binding
builder.Services.AddBottomSheet<UserProfileSheet, UserProfileViewModel>("UserProfile");

// With default configuration
builder.Services.AddBottomSheet<UserProfilePage>("UserProfile", (sheet, page) =>
{
    sheet.States = [BottomSheetState.Medium, BottomSheetState.Large];
    sheet.CurrentState = BottomSheetState.Large;
    sheet.ShowHeader = true;
    sheet.Header = new BottomSheetHeader()
    {
        TitleText = page.Title,
    };
});

🎯 Using Navigation Service

public class MainViewModel
{
    private readonly IBottomSheetNavigationService _navigationService;

    public MainViewModel(IBottomSheetNavigationService navigationService)
    {
        _navigationService = navigationService;
    }

    // Navigate to a bottom sheet
    public async Task ShowUserProfile()
    {
        await _navigationService.NavigateToAsync("UserProfile");
    }

    // Navigate with parameters
    public async Task ShowUserProfile(int userId)
    {
        var parameters = new BottomSheetNavigationParameters
        {
            { "UserId", userId },
            { "Mode", "Edit" },
            { "ShowActions", true }
        };

        await _navigationService.NavigateToAsync("UserProfile", parameters);
    }

    // Navigate with parameters and custom configuration
    public async Task ShowProductDetails(Product product)
    {
        var parameters = new BottomSheetNavigationParameters
        {
            { "Product", product },
            { "Category", product.CategoryId },
            { "ReadOnly", false }
        };

        await _navigationService.NavigateToAsync("ProductDetails", parameters, configure: sheet =>
        {
            sheet.Header.TitleText = product.Name;
            sheet.CurrentState = BottomSheetState.Large;
            sheet.HasHandle = true;
        });
    }

    // Navigate with ViewModel and parameters
    public async Task ShowEditForm<T>(T model, string formType)
    {
        var parameters = new BottomSheetNavigationParameters
        {
            { "Model", model },
            { "FormType", formType },
            { "Timestamp", DateTime.Now }
        };

        await _navigationService.NavigateToAsync<EditFormViewModel>("EditForm", parameters);
    }

    // Navigate with complex object parameters
    public async Task ShowOrderSummary(Order order, List<OrderItem> items, decimal total)
    {
        var parameters = new BottomSheetNavigationParameters
        {
            { "Order", order },
            { "OrderItems", items },
            { "Total", total },
            { "Currency", "USD" },
            { "CanEdit", order.Status == OrderStatus.Draft }
        };

        await _navigationService.NavigateToAsync("OrderSummary", parameters);
    }

    // Go back with result parameters
    public async Task CloseWithResult()
    {
        var resultParameters = new BottomSheetNavigationParameters
        {
            { "Result", "Success" },
            { "ModifiedData", GetModifiedData() },
            { "HasChanges", true }
        };

        await _navigationService.GoBackAsync(resultParameters);
    }

    // Clear all sheets with notification parameters
    public async Task ClearBottomSheetStackAsync()
    {
        var parameters = new BottomSheetNavigationParameters
        {
            { "CloseReason", "ForceClose" },
            { "SaveState", true }
        };

        await _navigationService.ClearBottomSheetStack(parameters);
    }
}

πŸ”„ Navigation Lifecycle

Implement INavigationAware for lifecycle events:

public class UserProfileViewModel : INavigationAware
{
    public void OnNavigatedTo(IBottomSheetNavigationParameters parameters)
    {
        // Sheet opened
        var userId = parameters.GetValue<string>("UserId");
        LoadUserData(userId);
    }

    public void OnNavigatedFrom(IBottomSheetNavigationParameters parameters)
    {
        // Sheet closed or another sheet opened
        SaveChanges();
    }
}

Tip

Not only can the BottomSheet’s ViewModel implement INavigationAware, but so can the ViewModel of the parent view that launched the BottomSheet. This allows the parent to respond when:

  • The first BottomSheet is opened (OnNavigatedFrom)
  • The last BottomSheet is closed (OnNavigatedTo)

This is especially useful for suspending or restoring state in the parent view while modals are active.

πŸ›‘οΈ Navigation Confirmation

Implement confirmation dialogs:

public class EditUserViewModel : IConfirmNavigationAsync
{
    public async Task<bool> CanNavigateAsync(IBottomSheetNavigationParameters? parameters)
    {
        if (HasUnsavedChanges)
        {
            return await Shell.Current.CurrentPage.DisplayAlert(
                "Unsaved Changes",
                "You have unsaved changes. Do you want to discard them?",
                "Discard",
                "Cancel");
        }
        return true;
    }
}

Important

When using the IsOpen to manage the visibility of a BottomSheet, the parent ViewModel can implement INavigationAware to respond to the lifecycle of the BottomSheet. This allows the parent ViewModel to handle actions when:

Scenario INavigationAware on Parent Triggered?
BottomSheet opened via navigation ❌ No
BottomSheet opened without navigation (IsOpen) βœ… Yes

🎯 Event Handling

πŸ“± Commands

public class MyViewModel
{
    public ICommand TopLeftButtonCommand { get; }
    public ICommand TopRightButtonCommand { get; }
    public ICommand OpeningCommand { get; }
    public ICommand OpenedCommand { get; }
    public ICommand ClosingCommand { get; }
    public ICommand ClosedCommand { get; }
}

🎭 Events

MyBottomSheet.Opening += OnOpening;
MyBottomSheet.Opened += OnOpened;
MyBottomSheet.Closing += OnClosing;
MyBottomSheet.Closed += OnClosed;

πŸ”§ Platform-Specific Features

πŸ€– Android

🎨 Custom Themes

// Set custom theme before opening
MyBottomSheet.On<Android>().SetTheme(Resource.Style.My_Awesome_BottomSheetDialog);

πŸ“ Size Constraints

// Code approach
MyBottomSheet.On<Android>().SetMaxHeight(800);
MyBottomSheet.On<Android>().SetMaxWidth(600);
MyBottomSheet.On<Android>().SetMargin(new Thickness(10, 0, 10, 0));
MyBottomSheet.On<Android>().SetHalfExpanedRatio = 0.8f;
MyBottomSheet.On<Android>().SetShouldRemoveExpandedCorners = false;
<!-- XAML approach -->
xmlns:androidBottomsheet="http://pluginmauibottomsheet.com/platformconfiguration/android"

<bottomsheet:BottomSheet
    androidBottomsheet:BottomSheet.MaxWidth="300"
    androidBottomsheet:BottomSheet.Margin="10,0,10,0"
    androidBottomsheet:BottomSheet.HalfExpandedRatio="0.8"
    androidBottomsheet:BottomSheet.ShouldRemoveExpandedCorners="false">
    <!-- Content -->
</bottomsheet:BottomSheet>

🎨 Edge-to-Edge Support

EdgeToEdge support is built-in and enabled by default. If you create your own theme make sure to derive from ThemeOverlay.MaterialComponents.BottomSheetDialog and that navigationBarColor is translucent.

To disable:

<item name="enableEdgeToEdge">false</item>

πŸ’» MacCatalyst

By design, sheets are always modal on macOS.

πŸͺŸ Windows

To ensure the same user experience (UX) across desktop applications, Windows sheets are always modal too. The sheet implementation mimics Microsoft.UI.Xaml.Controls.ContentDialog therefore some default properties are set.

Property ResourceKey Value
BorderThickness ContentDialogBorderWidth 1
MinWidth ContentDialogMinWidth 320
MinHeight ContentDialogMinHeight 184
MaxWidth ContentDialogMaxWidth 548
MaxHeight ContentDialogMaxHeight 756

πŸ“ Size Constraints

// Code approach
MyBottomSheet.On<Windows>().SetMaxHeight(800);
MyBottomSheet.On<Windows>().SetMaxWidth(600);
MyBottomSheet.On<Windows>().SetMinHeight(800);
MyBottomSheet.On<Windows>().SetMinWidth(600);
<!-- XAML approach -->
xmlns:windowsBottomsheet="http://pluginmauibottomsheet.com/platformconfiguration/windows"

<bottomsheet:BottomSheet
    androidBottomsheet:BottomSheet.MinWidth="300"
    androidBottomsheet:BottomSheet.MinHeight="300"
    androidBottomsheet:BottomSheet.MaxWidth="300"
    androidBottomsheet:BottomSheet.MaxHeight="300">
    <!-- Content -->
</bottomsheet:BottomSheet>

πŸ“± Platform Considerations

Feature iOS Android MacCatalyst Windows
Edge-to-Edge βœ… βœ… ❌ ❌
PeekHeight iOS 16+ βœ… ❌ ❌
Custom Themes ❌ βœ… ❌ ❌
Modal Only ❌ ❌ βœ… βœ…

πŸ”„ Lifecycle Events

πŸ“± Platform Lifecycle Integration

using Microsoft.Maui.LifecycleEvents;
using Plugin.Maui.BottomSheet.LifecycleEvents;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .UseBottomSheet();

        #if ANDROID
        builder.ConfigureLifecycleEvents(events =>
        {
            events.AddAndroid(android =>
            {
                android.OnBottomSheetBackPressed(activity =>
                {
                    // Handle back button press
                    Debug.WriteLine("BottomSheet back button pressed");
                });
            });
        });
        #endif

        return builder.Build();
    }
}

🚨 Important Notes & Best Practices

🎯 Performance Tips

  • Use ContentTemplate instead of Content for better performance
  • Implement lazy loading for complex content
  • Consider using INavigationAware for proper lifecycle management

πŸ”„ Navigation Best Practices

  • Don't mix IsOpen with navigation methods
  • Always implement IConfirmNavigation for forms with user input

XAML and .NET HotReload

  • XAML and .NET HotReload are not supported for BottomSheet
  • If you face any issues please open an issue and describe your setup(Rider, VSCode, Visual Studio, Platform, OS etc.)

πŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

πŸ†˜ Support


Made with ❀️ for the .NET MAUI community

About

Native BottomSheets in .Net Maui!

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages

  • C# 100.0%