Loading...
Loading...
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.
npx skill4agent add dotnet/skills maui-shell-navigationGoToAsyncmaui-data-bindingmaui-dependency-injectionNavigationPageAppShell.xamlContentPageShell
├── FlyoutItem / TabBar (top-level grouping)
│ ├── Tab (bottom-tab grouping)
│ │ ├── ShellContent (page slot → ContentPage)
│ │ └── ShellContent (multiple = top tabs)
│ └── Tab
└── FlyoutItem / TabBarTabShellContentContentPage| You write | Shell creates |
|---|---|
| |
| |
| |
AppShell.xamlShellFlyoutItemTabBarTabShellContentContentTemplateDataTemplateAppShell<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>// AppShell.xaml.cs
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
Routing.RegisterRoute("animaldetails", typeof(AnimalDetailsPage));
Routing.RegisterRoute("editanimal", typeof(EditAnimalPage));
}
}Shell.Current.GoToAsyncawait| Prefix | Meaning |
|---|---|
| Absolute route from Shell root |
| (none) | Relative; pushes onto the current nav stack |
| Go back one level |
| Go back then navigate forward |
// 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");public class AnimalDetailsViewModel : ObservableObject, IQueryAttributable
{
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.TryGetValue("id", out var id))
AnimalId = id.ToString();
}
}[QueryProperty(nameof(AnimalId), "id")]
public partial class AnimalDetailsPage : ContentPage
{
public string AnimalId { get; set; }
}var parameters = new ShellNavigationQueryParameters
{
{ "animal", selectedAnimal }
};
await Shell.Current.GoToAsync("animaldetails", parameters);IQueryAttributablepublic void ApplyQueryAttributes(IDictionary<string, object> query)
{
Animal = query["animal"] as Animal;
}GetDeferral()OnNavigating// 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();
}
}ShellContentTabTabBarFlyoutItemShellContentTab<Tab Title="Photos">
<ShellContent Title="Recent" ContentTemplate="{DataTemplate views:RecentPage}" />
<ShellContent Title="Favorites" ContentTemplate="{DataTemplate views:FavoritesPage}" />
</Tab>| Attached Property | Type | Purpose |
|---|---|---|
| | Tab bar background |
| | Selected icon color |
| | Selected tab title color |
| | Unselected tab icon/title |
| | Show/hide the tab bar |
<!-- Hide the tab bar on a specific page -->
<ContentPage Shell.TabBarIsVisible="False" ... />ShellDisabledFlyoutLocked<Shell FlyoutBehavior="Flyout"> ... </Shell>AsSingleItemAsMultipleItemsTab<FlyoutItem Title="Animals" FlyoutDisplayOptions="AsMultipleItems">
<Tab Title="Cats" ... />
<Tab Title="Dogs" ... />
</FlyoutItem><MenuItem Text="Log Out"
Command="{Binding LogOutCommand}"
IconImageSource="logout.png" /><Shell.BackButtonBehavior>
<BackButtonBehavior Command="{Binding BackCommand}"
IconOverride="back_arrow.png"
TextOverride="Cancel"
IsVisible="True" />
</Shell.BackButtonBehavior>CommandCommandParameterIconOverrideTextOverrideIsVisibleIsEnabled// 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;AppShellprotected override void OnNavigated(ShellNavigatedEventArgs args)
{
base.OnNavigated(args);
// args.Current, args.Previous, args.Source
}ShellNavigationSourcePushPopPopToRootInsertRemoveShellItemChangedShellSectionChangedShellContentChangedUnknownContentContentTemplateDataTemplateContentTemplateRouting.RegisterRouteArgumentExceptionGoToAsync("somepage")somepageRouting.RegisterRoute//GoToAsyncawait//FlyoutItem/Tab/ShellContentGoToAsyncGetDeferral()OnNavigatingGetDeferral()deferral.Complete()references/shell-navigation-api.md