# Mobile Sheet Patterns

Source: https://lumeo.nativ.sh/docs/mobile-sheet-patterns

# Mobile Sheet Patterns

On mobile, a side sheet looks wrong — it crowds the viewport and forces horizontal eye movement. The native pattern is the bottom sheet: a panel that slides up from the bottom edge and dismisses with a downward swipe. Lumeo's [Sheet](components/sheet) and [Drawer](components/drawer) components support bottom-edge presentation, opt-in swipe-to-close, and the `MobileFullscreen` option for service-driven overlays.

## When bottom, when side

Side

Use for

Bottom

Action sheets, filter panels, quick pickers, anything triggered by a thumb-reachable button. Default for mobile.

Right / Left

Desktop and tablet. On phones, only for full-height navigation drawers triggered by a hamburger.

Top

Rare — notification trays, search overlays. Avoid for forms (keyboard pushes the sheet off-screen).

## Responsive side switching

One overlay, two presentations. Inject `IResponsiveService` and pick the side from `IsMobile` so the same component renders as a right-side sheet on desktop and a bottom sheet on phone. Subscribe to `ViewportChanged` so a tablet rotation flips the layout without re-opening the sheet.

@inject IResponsiveService Viewport
@implements IDisposable

<Sheet @bind-Open="\_open">
    <SheetContent Side="@(Viewport.IsMobile ? Lumeo.Side.Bottom : Lumeo.Side.Right)" Size="SheetContent.SheetSize.Default">
        <SheetHeader><SheetTitle>Filters</SheetTitle></SheetHeader>
        <div class="flex-1 overflow-y-auto p-6">
            @\* filter UI \*@
        </div>
    </SheetContent>
</Sheet>

@code {
    private bool \_open;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (!firstRender) return;
        await Viewport.EnsureInitialisedAsync();
        Viewport.ViewportChanged += OnViewportChanged;
        StateHasChanged();
    }

    private void OnViewportChanged(ViewportInfo \_) => InvokeAsync(StateHasChanged);
    public void Dispose() => Viewport.ViewportChanged -= OnViewportChanged;
}

## Mobile-fullscreen for service overlays

When you open a Dialog, Sheet, or Drawer through `OverlayService`, set `OverlayOptions.MobileFullscreen = true` to force the overlay to fill the viewport below the `MobileBreakpoint` (default 768px). The size / side parameters still apply on tablet and desktop — this is a phone-only override.

@inject IOverlayService Overlays

<Button OnClick="OpenFilters">Filters</Button>

@code {
    private Task OpenFilters() =>
        Overlays.ShowSheetAsync<FilterPanel>(
            title: "Filters",
            side: Lumeo.Side.Right,
            size: SheetSize.Default,
            options: new OverlayOptions { MobileFullscreen = true });
}

## Swipe-to-close (opt-in)

Swipe-to-dismiss is **off by default** because it conflicts with scrollable content. Opt in with `SwipeToClose="true"` on `SheetContent` for sheets whose content fits the viewport without internal scrolling (filter panels, picker menus, action sheets).

<Sheet @bind-Open="\_open">
    <SheetContent Side="Lumeo.Side.Bottom" SwipeToClose="true">
        <SheetHeader><SheetTitle>Sort by</SheetTitle></SheetHeader>
        @\* compact list — fits without scrolling, so swipe is safe \*@
    </SheetContent>
</Sheet>

<!-- form sheet: keep swipe OFF (default) so a scroll gesture doesn't dismiss it -->
<Sheet @bind-Open="\_formOpen">
    <SheetContent Side="Lumeo.Side.Bottom" PreventClose="true">
        @\* user must use the explicit Cancel or Save buttons \*@
    </SheetContent>
</Sheet>

## Safe areas and the keyboard

Bottom sheets must respect the home indicator on iOS and the navigation pill on Android. Wrap the footer in [SafeArea](components/safe-area) with `Bottom="true"` so the primary action stays tappable. When the soft keyboard appears, the sheet's `flex` layout keeps the footer above the keyboard — but only if the body uses `flex-1 overflow-y-auto`.

<Sheet @bind-Open="\_open">
    <SheetContent Side="Lumeo.Side.Bottom">
        <div class="flex flex-col flex-1 min-h-0">
            <SheetHeader><SheetTitle>New message</SheetTitle></SheetHeader>

            <div class="flex-1 overflow-y-auto px-4 py-3">
                <Textarea @bind-Value="\_body" Rows="8" />
            </div>

            <SafeArea Bottom="true">
                <SheetFooter>
                    <Button Variant="Button.ButtonVariant.Outline"
                            OnClick="() => \_open = false">Cancel</Button>
                    <Button OnClick="SendAsync">Send</Button>
                </SheetFooter>
            </SafeArea>
        </div>
    </SheetContent>
</Sheet>

## See also

-   [Sheet](components/sheet)
-   [Drawer](components/drawer) — non-modal mobile drawer
-   [Safe Area](components/safe-area)
-   [Swipe Actions](components/swipe-actions)
