
Maui Shell Navigation
- 19 installs
- 466 repo stars
- Updated July 25, 2026
- managedcode/dotnet-skills
Helps with ai & agent building tasks.
About
maui-shell-navigation is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- maui-shell-navigation
- AI & Agent Building
- AI-coding skill
Maui Shell Navigation by the numbers
- 19 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #10,587 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/managedcode/dotnet-skills --skill maui-shell-navigationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 19 |
|---|---|
| repo stars | ★ 466 |
| Last updated | July 25, 2026 |
| Repository | managedcode/dotnet-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
.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
NavigationPagewithout Shell (different navigation API)
Inputs
- A .NET MAUI project with
AppShell.xamlas 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
Tabchildren - 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
<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));
}
}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
// 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:
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:
[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:
var parameters = new ShellNavigationQueryParameters
{
{ "animal", selectedAnimal }
};
await Shell.Current.GoToAsync("animaldetails", parameters);Receive via IQueryAttributable:
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?"):
// 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:
<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 |
<!-- Hide the tab bar on a specific page -->
<ContentPage Shell.TabBarIsVisible="False" ... />Flyout Configuration
FlyoutBehavior
Set on Shell: Disabled, Flyout, or Locked.
<Shell FlyoutBehavior="Flyout"> ... </Shell>FlyoutDisplayOptions
Controls how children appear in the flyout:
AsSingleItem(default) — one flyout entry for the groupAsMultipleItems— each childTabgets its own entry
<FlyoutItem Title="Animals" FlyoutDisplayOptions="AsMultipleItems">
<Tab Title="Cats" ... />
<Tab Title="Dogs" ... />
</FlyoutItem>MenuItem (Non-Navigation Flyout Entries)
<MenuItem Text="Log Out"
Command="{Binding LogOutCommand}"
IconImageSource="logout.png" />Back Button Behavior
Customize the back button per page:
<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
// 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:
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
Contentdirectly instead ofContentTemplatewithDataTemplatecreates all pages at Shell init, hurting startup time. Always useContentTemplate. - Duplicate route names:
Routing.RegisterRoutethrowsArgumentExceptionif 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")unlesssomepagewas registered withRouting.RegisterRoute. Visual hierarchy pages use absolute//routes. - Fire-and-forget GoToAsync: Not awaiting
GoToAsynccauses race conditions and silent failures. Alwaysawaitthe 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
GoToAsyncfor all navigation changes. - Forgetting `GetDeferral()` for async guards: Synchronous cancellation in
OnNavigatingworks, but async checks requireGetDeferral()/deferral.Complete()to avoid race conditions.
References
references/shell-navigation-api.md— Full API reference for Shell hierarchy, routes, tabs, flyout, and navigation- .NET MAUI Shell Navigation
- .NET MAUI Shell Tabs
- .NET MAUI Shell Flyout
- .NET MAUI Shell Pages
{
"version": "0.1.0",
"category": "Cross-Platform UI",
"compatibility": "Requires a .NET MAUI project or solution.",
"package_prefix": "Microsoft.Maui"
}
Shell Navigation API Reference
Shell Visual Hierarchy
Shell uses a four-level hierarchy. Each level wraps the one below it:
Shell
├── FlyoutItem / TabBar (top-level navigation grouping)
│ ├── Tab (bottom-tab grouping)
│ │ ├── ShellContent (page slot; points to a ContentPage)
│ │ └── ShellContent (creates top tabs within a bottom tab)
│ └── Tab
└── FlyoutItem / TabBar- FlyoutItem – appears in the flyout menu. Contains one or more
Tabchildren. - TabBar – bottom tab bar with no flyout entry. Use when the app has no flyout.
- Tab – groups
ShellContentobjects. MultipleShellContentin oneTabproduces top tabs. - ShellContent – each represents 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 |
This keeps simple apps concise while allowing full control when needed.
AppShell.xaml Setup
<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>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 within that bottom tab:
<Tab Title="Photos">
<ShellContent Title="Recent" ContentTemplate="{DataTemplate views:RecentPage}" />
<ShellContent Title="Favorites" ContentTemplate="{DataTemplate views:FavoritesPage}" />
</Tab>TabBar Appearance (Attached Properties)
Set these on any page or Shell element:
| Attached Property | Type | Purpose |
|---|---|---|
Shell.TabBarBackgroundColor | Color | Tab bar background |
Shell.TabBarForegroundColor | Color | Foreground / selected icon color |
Shell.TabBarTitleColor | Color | Selected tab title color |
Shell.TabBarUnselectedColor | Color | Unselected tab icon/title color |
Shell.TabBarDisabledColor | Color | Disabled tab color |
Shell.TabBarIsVisible | bool | Show/hide the tab bar |
<ContentPage Shell.TabBarIsVisible="False" ... />Flyout Configuration
FlyoutBehavior
Set on Shell:
<Shell FlyoutBehavior="Flyout"> ... </Shell>Values: Disabled, Flyout, Locked.
FlyoutDisplayOptions
Controls how a FlyoutItem's children appear in the flyout:
<FlyoutItem Title="Animals" FlyoutDisplayOptions="AsMultipleItems">
<Tab Title="Cats" ... />
<Tab Title="Dogs" ... />
</FlyoutItem>AsSingleItem(default) – one flyout entry for the group.AsMultipleItems– each childTabgets its own flyout entry.
Flyout Item Template
Customize appearance with Shell.ItemTemplate. BindingContext exposes Title and FlyoutIcon (FlyoutItem) or Text and IconImageSource (MenuItem):
<Shell.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="Auto,*" Padding="10">
<Image Source="{Binding FlyoutIcon}" HeightRequest="24" />
<Label Grid.Column="1" Text="{Binding Title}" VerticalTextAlignment="Center" />
</Grid>
</DataTemplate>
</Shell.ItemTemplate>Replacing Flyout Content
<Shell.FlyoutContent>
<CollectionView BindingContext="{x:Reference shell}"
ItemsSource="{Binding FlyoutItems}" />
</Shell.FlyoutContent>MenuItem (non-navigation flyout entries)
<MenuItem Text="Log Out"
Command="{Binding LogOutCommand}"
IconImageSource="logout.png" />Route Registration
Shell visual hierarchy items have implicit routes derived from their Route property (or type name). Detail pages not in the hierarchy must be registered:
// In AppShell constructor or MauiProgram
Routing.RegisterRoute("animaldetails", typeof(AnimalDetailsPage));
Routing.RegisterRoute("editanimal", typeof(EditAnimalPage));Navigation with GoToAsync
All programmatic navigation goes through Shell.Current.GoToAsync:
// Absolute – navigate to a specific place in the hierarchy
await Shell.Current.GoToAsync("//animals/cats/domestic");
// Relative – push a registered page onto the navigation stack
await Shell.Current.GoToAsync("animaldetails");
// With query string
await Shell.Current.GoToAsync($"animaldetails?id={animal.Id}");Absolute vs Relative Routes
| Prefix | Meaning |
|---|---|
// | Absolute route from Shell root |
| (none) | Relative; pushes onto the current nav stack |
.. | Go back one level in the navigation stack |
../ | Go back then navigate forward |
// Go back one page
await Shell.Current.GoToAsync("..");
// Go back two pages
await Shell.Current.GoToAsync("../..");
// Go back one page, then navigate to edit
await Shell.Current.GoToAsync("../editanimal");Query Parameters
QueryProperty Attribute
[QueryProperty(nameof(AnimalId), "id")]
public partial class AnimalDetailsPage : ContentPage
{
public string AnimalId { get; set; }
}
// Navigate with query string:
await Shell.Current.GoToAsync($"animaldetails?id={animal.Id}");IQueryAttributable Interface
Preferred for ViewModels — gives you all parameters in one call:
public class AnimalDetailsViewModel : ObservableObject, IQueryAttributable
{
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.TryGetValue("id", out var id))
AnimalId = id.ToString();
}
}The interface works on the page itself or on any object set as the page's BindingContext.
Passing Complex Objects
Use ShellNavigationQueryParameters (dictionary of string → object) to pass objects without serializing to strings:
var parameters = new ShellNavigationQueryParameters
{
{ "animal", selectedAnimal } // pass the object directly
};
await Shell.Current.GoToAsync("animaldetails", parameters);Receive via IQueryAttributable:
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
Animal = query["animal"] as Animal;
}Navigation Events
Override in your AppShell:
protected override void OnNavigating(ShellNavigatingEventArgs args)
{
base.OnNavigating(args);
if (hasUnsavedChanges && args.Source == ShellNavigationSource.Pop)
args.Cancel(); // prevent leaving
}
protected override void OnNavigated(ShellNavigatedEventArgs args)
{
base.OnNavigated(args);
// args.Current, args.Previous, args.Source
}For async checks, use args.GetDeferral() → do work → deferral.Complete().
ShellNavigationSource values: Push, Pop, PopToRoot, Insert, Remove, ShellItemChanged, ShellSectionChanged, ShellContentChanged, Unknown.
Inspecting Navigation State
// Current URI location
ShellNavigationState state = Shell.Current.CurrentState;
string location = state.Location.ToString(); // e.g. "//animals/cats/domestic"
// Current page
Page page = Shell.Current.CurrentPage;
// Navigation stack of the current tab
IReadOnlyList<Page> stack = Shell.Current.Navigation.NavigationStack;Back Button Behavior
Customize the back button per page:
<Shell.BackButtonBehavior>
<BackButtonBehavior Command="{Binding BackCommand}"
IconOverride="back_arrow.png"
TextOverride="Cancel" />
</Shell.BackButtonBehavior>Properties: Command, CommandParameter, IconOverride, TextOverride, IsVisible, IsEnabled.