Agent skill

maui-shell-navigation

Guide for implementing Shell-based navigation in .NET MAUI apps. Covers AppShell setup, visual hierarchy (FlyoutItem, TabBar, Tab, ShellContent), URI-based navigation with GoToAsync, route registration, query parameters, back navigation, flyout and tab configuration, navigation events, and navigation guards. Use when: setting up Shell navigation, adding tabs or flyout menus, navigating between pages with GoToAsync, passing parameters between pages, registering routes, customizing back button behavior, or guarding navigation with confirmation dialogs. Do not use for: deep linking from external URLs (see .NET MAUI deep linking documentation), data binding on pages (use maui-data-binding), dependency injection setup (use maui-dependency-injection), or NavigationPage-only apps that don't use Shell.

Stars 302
Forks 22

Install this agent skill to your Project

npx add-skill https://github.com/managedcode/dotnet-skills/tree/main/catalog/Frameworks/Official-DotNet-MAUI/skills/maui-shell-navigation

SKILL.md

.NET MAUI Shell Navigation

Implement page navigation in .NET MAUI apps using Shell. Shell provides URI-based navigation, a flyout menu, tab bars, and a four-level visual hierarchy — all configured declaratively in XAML.

When to Use

  • Setting up top-level app navigation with tabs or a flyout menu
  • Navigating between pages programmatically with GoToAsync
  • Passing data between pages via query parameters or object parameters
  • Registering detail-page routes for push navigation
  • Guarding navigation with confirmation dialogs (e.g., unsaved changes)
  • Customizing back button behavior per page

When Not to Use

  • Deep linking from external URLs or app links — see .NET MAUI deep linking docs
  • Data binding on navigation target pages — use maui-data-binding
  • Dependency injection for pages and view models — use maui-dependency-injection
  • Apps using NavigationPage without Shell (different navigation API)

Inputs

  • A .NET MAUI project with AppShell.xaml as the root shell
  • Pages (ContentPage) to navigate between
  • Route names for detail pages not in the visual hierarchy

Shell Visual Hierarchy

Shell uses a four-level hierarchy. Each level wraps the one below it:

Shell
 ├── FlyoutItem / TabBar          (top-level grouping)
 │    ├── Tab                     (bottom-tab grouping)
 │    │    ├── ShellContent        (page slot → ContentPage)
 │    │    └── ShellContent        (multiple = top tabs)
 │    └── Tab
 └── FlyoutItem / TabBar
  • FlyoutItem — appears in the flyout menu; contains Tab children
  • TabBar — bottom tab bar with no flyout entry
  • Tab — groups ShellContent; multiple children produce top tabs
  • ShellContent — each points to a ContentPage

Implicit Conversion

You can omit intermediate wrappers. Shell auto-wraps:

You write Shell creates
ShellContent only FlyoutItem > Tab > ShellContent
Tab only FlyoutItem > Tab
ShellContent in TabBar TabBar > Tab > ShellContent

Workflow: Set Up AppShell

  1. Define AppShell.xaml inheriting from Shell
  2. Add FlyoutItem or TabBar elements for top-level navigation
  3. Add Tab elements for bottom tabs; nest multiple ShellContent for top tabs
  4. Always use ContentTemplate with DataTemplate so pages load on demand
  5. Register detail-page routes in the AppShell constructor
xml
<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       xmlns:views="clr-namespace:MyApp.Views"
       x:Class="MyApp.AppShell"
       FlyoutBehavior="Flyout">

    <FlyoutItem Title="Animals" Icon="animals.png">
        <Tab Title="Cats">
            <ShellContent Title="Domestic"
                          ContentTemplate="{DataTemplate views:DomesticCatsPage}" />
            <ShellContent Title="Wild"
                          ContentTemplate="{DataTemplate views:WildCatsPage}" />
        </Tab>
        <Tab Title="Dogs" Icon="dogs.png">
            <ShellContent ContentTemplate="{DataTemplate views:DogsPage}" />
        </Tab>
    </FlyoutItem>

    <TabBar>
        <ShellContent Title="Home" Icon="home.png"
                      ContentTemplate="{DataTemplate views:HomePage}" />
        <ShellContent Title="Settings" Icon="settings.png"
                      ContentTemplate="{DataTemplate views:SettingsPage}" />
    </TabBar>
</Shell>
csharp
// AppShell.xaml.cs
public partial class AppShell : Shell
{
    public AppShell()
    {
        InitializeComponent();
        Routing.RegisterRoute("animaldetails", typeof(AnimalDetailsPage));
        Routing.RegisterRoute("editanimal", typeof(EditAnimalPage));
    }
}

Workflow: Navigate with GoToAsync

All programmatic navigation uses Shell.Current.GoToAsync. Always await the call.

Route Prefixes

Prefix Meaning
// Absolute route from Shell root
(none) Relative; pushes onto the current nav stack
.. Go back one level
../ Go back then navigate forward

Navigation Examples

csharp
// 1. Absolute — switch to a specific hierarchy location
await Shell.Current.GoToAsync("//animals/cats/domestic");

// 2. Relative — push a registered detail page
await Shell.Current.GoToAsync("animaldetails");

// 3. With query string parameters
await Shell.Current.GoToAsync($"animaldetails?id={animal.Id}");

// 4. Go back one page
await Shell.Current.GoToAsync("..");

// 5. Go back two pages
await Shell.Current.GoToAsync("../..");

// 6. Go back one page, then push a different page
await Shell.Current.GoToAsync("../editanimal");

Workflow: Pass Data Between Pages

Option 1: IQueryAttributable (Preferred)

Implement on ViewModels to receive all parameters in one call:

csharp
public class AnimalDetailsViewModel : ObservableObject, IQueryAttributable
{
    public void ApplyQueryAttributes(IDictionary<string, object> query)
    {
        if (query.TryGetValue("id", out var id))
            AnimalId = id.ToString();
    }
}

Option 2: QueryProperty Attribute

Apply directly on the page class:

csharp
[QueryProperty(nameof(AnimalId), "id")]
public partial class AnimalDetailsPage : ContentPage
{
    public string AnimalId { get; set; }
}

Option 3: Complex Objects via ShellNavigationQueryParameters

Pass objects without serializing to strings:

csharp
var parameters = new ShellNavigationQueryParameters
{
    { "animal", selectedAnimal }
};
await Shell.Current.GoToAsync("animaldetails", parameters);

Receive via IQueryAttributable:

csharp
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
    Animal = query["animal"] as Animal;
}

Workflow: Guard Navigation

Use GetDeferral() in OnNavigating for async checks (e.g., "save unsaved changes?"):

csharp
// In AppShell.xaml.cs
protected override async void OnNavigating(ShellNavigatingEventArgs args)
{
    base.OnNavigating(args);
    if (hasUnsavedChanges && args.Source == ShellNavigationSource.Pop)
    {
        var deferral = args.GetDeferral();
        bool discard = await ShowConfirmationDialog();
        if (!discard)
            args.Cancel();
        deferral.Complete();
    }
}

Tab Configuration

Bottom Tabs

Multiple ShellContent (or Tab) children inside a TabBar or FlyoutItem produce bottom tabs.

Top Tabs

Multiple ShellContent children inside a single Tab produce top tabs:

xml
<Tab Title="Photos">
    <ShellContent Title="Recent"    ContentTemplate="{DataTemplate views:RecentPage}" />
    <ShellContent Title="Favorites" ContentTemplate="{DataTemplate views:FavoritesPage}" />
</Tab>

Tab Bar Appearance

Attached Property Type Purpose
Shell.TabBarBackgroundColor Color Tab bar background
Shell.TabBarForegroundColor Color Selected icon color
Shell.TabBarTitleColor Color Selected tab title color
Shell.TabBarUnselectedColor Color Unselected tab icon/title
Shell.TabBarIsVisible bool Show/hide the tab bar
xml
<!-- Hide the tab bar on a specific page -->
<ContentPage Shell.TabBarIsVisible="False" ... />

Flyout Configuration

FlyoutBehavior

Set on Shell: Disabled, Flyout, or Locked.

xml
<Shell FlyoutBehavior="Flyout"> ... </Shell>

FlyoutDisplayOptions

Controls how children appear in the flyout:

  • AsSingleItem (default) — one flyout entry for the group
  • AsMultipleItems — each child Tab gets its own entry
xml
<FlyoutItem Title="Animals" FlyoutDisplayOptions="AsMultipleItems">
    <Tab Title="Cats" ... />
    <Tab Title="Dogs" ... />
</FlyoutItem>

MenuItem (Non-Navigation Flyout Entries)

xml
<MenuItem Text="Log Out"
          Command="{Binding LogOutCommand}"
          IconImageSource="logout.png" />

Back Button Behavior

Customize the back button per page:

xml
<Shell.BackButtonBehavior>
    <BackButtonBehavior Command="{Binding BackCommand}"
                       IconOverride="back_arrow.png"
                       TextOverride="Cancel"
                       IsVisible="True" />
</Shell.BackButtonBehavior>

Properties: Command, CommandParameter, IconOverride, TextOverride, IsVisible, IsEnabled.

Inspecting Navigation State

csharp
// Current URI location
string location = Shell.Current.CurrentState.Location.ToString();

// Current page
Page page = Shell.Current.CurrentPage;

// Navigation stack of the current tab
IReadOnlyList<Page> stack = Shell.Current.Navigation.NavigationStack;

Navigation Events

Override in AppShell:

csharp
protected override void OnNavigated(ShellNavigatedEventArgs args)
{
    base.OnNavigated(args);
    // args.Current, args.Previous, args.Source
}

ShellNavigationSource values: Push, Pop, PopToRoot, Insert, Remove, ShellItemChanged, ShellSectionChanged, ShellContentChanged, Unknown.

Common Pitfalls

  • Eager page creation: Using Content directly instead of ContentTemplate with DataTemplate creates all pages at Shell init, hurting startup time. Always use ContentTemplate.
  • Duplicate route names: Routing.RegisterRoute throws ArgumentException if a route name matches an existing route or a visual hierarchy route. Every route must be unique across the app.
  • Relative routes without registration: You cannot GoToAsync("somepage") unless somepage was registered with Routing.RegisterRoute. Visual hierarchy pages use absolute // routes.
  • Fire-and-forget GoToAsync: Not awaiting GoToAsync causes race conditions and silent failures. Always await the call.
  • Wrong absolute route path: Absolute routes must match the full path through the visual hierarchy (//FlyoutItem/Tab/ShellContent). Wrong paths produce silent no-ops, not exceptions.
  • Manipulating Tab.Stack directly: The navigation stack is read-only. Use GoToAsync for all navigation changes.
  • Forgetting GetDeferral() for async guards: Synchronous cancellation in OnNavigating works, but async checks require GetDeferral() / deferral.Complete() to avoid race conditions.

References

Expand your agent's capabilities with these related and highly-rated skills.

managedcode/dotnet-skills

dotnet-project-setup

Create or reorganize .NET solutions with clean project boundaries, repeatable SDK settings, and a maintainable baseline for libraries, apps, tests, CI, and local development.

302 22
Explore
managedcode/dotnet-skills

csharp-scripts

Run single-file C# programs as scripts (file-based apps) for quick experimentation, prototyping, and concept testing. Use when the user wants to write and execute a small C# program without creating a full project.

302 22
Explore
managedcode/dotnet-skills

dotnet-pinvoke

Correctly call native (C/C++) libraries from .NET using P/Invoke and LibraryImport. Covers function signatures, string marshalling, memory lifetime, SafeHandle, and cross-platform patterns. USE FOR: writing new P/Invoke or LibraryImport declarations, reviewing or debugging existing native interop code, wrapping a C or C++ library for use in .NET, diagnosing crashes, memory leaks, or corruption at the managed/native boundary. DO NOT USE FOR: COM interop, C++/CLI mixed-mode assemblies, or pure managed code with no native dependencies.

302 22
Explore
managedcode/dotnet-skills

nuget-trusted-publishing

Set up NuGet trusted publishing (OIDC) on a GitHub Actions repo — replaces long-lived API keys with short-lived tokens. USE FOR: trusted publishing, NuGet OIDC, keyless NuGet publish, migrate from NuGet API key, NuGet/login, secure NuGet publishing. DO NOT USE FOR: publishing to private feeds or Azure Artifacts (OIDC is nuget.org only). INVOKES: shell (powershell or bash), edit, create, ask_user for guided repo setup.

302 22
Explore
managedcode/dotnet-skills

dotnet-legacy-aspnet

Maintain classic ASP.NET applications on .NET Framework, including Web Forms, older MVC, and legacy hosting patterns, while planning realistic modernization boundaries.

302 22
Explore
managedcode/dotnet-skills

dotnet-code-review

Review .NET changes for bugs, regressions, architectural drift, missing tests, incorrect async or disposal behavior, and platform-specific pitfalls before you approve or merge them.

302 22
Explore

Didn't find tool you were looking for?

Be as detailed as possible for better results