Every .NET MAUI app needs navigation. What most tutorials skip is that choosing between Shell and NavigationPage isn’t just a style preference — it’s an architectural decision that shapes how you handle deep linking, URI routing, tab structures, and back-stack behavior across platforms.
Get it wrong early and you’ll feel it in every feature you build afterward. This guide gives you the real trade-offs.
Table of Contents
- The Fundamentals: What Each System Does
- Shell Deep Dive
- NavigationPage Deep Dive
- URI Routing and Deep Linking
- Passing Data Between Pages
- Platform-Specific Behavior
- Which to Choose: Decision Framework
- Mixing Shell and NavigationPage
- FAQ
- Conclusion
The Fundamentals: What Each System Does
Shell is a top-level navigation container that provides flyout menus, tab bars, and a navigation stack — all driven by a URI-based routing system. It’s declarative, defined in XAML, and gives you app-wide navigation from a single file.
NavigationPage is a simpler stack-based navigation container. You push pages onto a stack and pop them off. No routing, no URI scheme — just a navigation stack with a title bar and a back button.
The Mental Model
- Shell = app architecture (handles your entire app’s navigation structure)
- NavigationPage = navigation container (handles a section of your app’s stack)
Shell can contain NavigationPages. NavigationPage cannot contain Shell.
Shell Deep Dive
Shell lives in AppShell.xaml and is set as the app’s main page:
<!-- AppShell.xaml -->
<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="Home" Icon="home.png">
<ShellContent ContentTemplate="{DataTemplate views:HomePage}" Route="home" />
</FlyoutItem>
<FlyoutItem Title="Products" Icon="products.png">
<Tab Title="All">
<ShellContent ContentTemplate="{DataTemplate views:ProductListPage}" Route="products" />
</Tab>
<Tab Title="Favorites">
<ShellContent ContentTemplate="{DataTemplate views:FavoritesPage}" Route="favorites" />
</Tab>
</FlyoutItem>
</Shell>
Route Registration for Detail Pages
Pages not declared in the Shell XAML (detail/modal pages) are registered in code:
// AppShell.xaml.cs
public AppShell()
{
InitializeComponent();
Routing.RegisterRoute("productdetail", typeof(ProductDetailPage));
Routing.RegisterRoute("checkout", typeof(CheckoutPage));
}
Shell Navigation API
// Navigate to a route
await Shell.Current.GoToAsync("productdetail");
// Navigate with query parameters
await Shell.Current.GoToAsync($"productdetail?id={product.Id}");
// Navigate backward
await Shell.Current.GoToAsync("..");
// Navigate to absolute route (resets stack)
await Shell.Current.GoToAsync("//home");
// Modal navigation
await Shell.Current.GoToAsync("checkout", animate: true);
Receiving Query Parameters
[QueryProperty(nameof(ProductId), "id")]
public partial class ProductDetailViewModel : ObservableObject
{
private int _productId;
public int ProductId
{
get => _productId;
set
{
_productId = value;
LoadProductAsync(value).FireAndForget();
}
}
}
NavigationPage Deep Dive
NavigationPage wraps a root page and manages a navigation stack. You set it as the app’s main page directly:
// App.xaml.cs
public App()
{
InitializeComponent();
MainPage = new NavigationPage(new HomePage());
}
Pushing and Popping Pages
// Push a new page
await Navigation.PushAsync(new ProductDetailPage(productId));
// Push modally
await Navigation.PushModalAsync(new CheckoutPage());
// Go back
await Navigation.PopAsync();
// Go back to root
await Navigation.PopToRootAsync();
// Remove a specific page from the stack
Navigation.RemovePage(somePageInstance);
NavigationPage Properties
You can customize the navigation bar per-page using attached properties:
<ContentPage
NavigationPage.HasNavigationBar="true"
NavigationPage.HasBackButton="true"
NavigationPage.TitleView="{StaticResource CustomTitleView}"
NavigationPage.IconColor="White">
</ContentPage>
When NavigationPage Shines
Simple hierarchical flows without tabs or flyouts. Wizards, onboarding sequences, settings screens with sub-screens — these are pure push/pop patterns where NavigationPage’s simplicity is a feature.
URI Routing and Deep Linking
This is where Shell has a decisive advantage. Shell supports URI-based navigation that maps naturally to push notification deep links and OS-level universal links.
// Handle a push notification deep link
public async Task HandleDeepLink(string url)
{
// url = "myapp://productdetail?id=42"
await Shell.Current.GoToAsync($"productdetail?id=42");
}
// Absolute navigation resets the stack — great for notifications
await Shell.Current.GoToAsync("//home/productdetail?id=42");
With NavigationPage, you have to implement deep linking yourself — parse the URL, decide which pages to push in what order, and rebuild the navigation stack manually. It’s possible but substantially more work.
Passing Data Between Pages
Shell: Query Parameters and Objects
Shell supports passing query string parameters and complex objects:
// Passing a complex object (MAUI 8+)
await Shell.Current.GoToAsync("productdetail", new Dictionary<string, object>
{
{ "Product", selectedProduct }
});
// In the receiving ViewModel
[QueryProperty(nameof(Product), "Product")]
public partial class ProductDetailViewModel : ObservableObject
{
public Product? Product { get; set; }
}
NavigationPage: Constructor Parameters
// Simple and explicit
await Navigation.PushAsync(new ProductDetailPage(product));
// In the page constructor
public ProductDetailPage(Product product)
{
InitializeComponent();
BindingContext = new ProductDetailViewModel(product);
}
Constructor injection is simpler but couples the page to its caller. Shell’s query properties support DI-friendly ViewModels that don’t know how they were navigated to — better for testability.
Platform-Specific Behavior
iOS Differences
- Shell: Tab bar renders at the bottom (iOS HIG-compliant). Flyout renders as a side drawer.
- NavigationPage: Navigation bar renders at the top. Back swipe gesture works natively.
- Both support the iOS back swipe, but Shell’s implementation is more consistent across .NET versions.
Android Differences
- Shell: Flyout renders as a navigation drawer (hamburger menu). Bottom tabs use BottomNavigationView.
- NavigationPage: Uses Toolbar for the top navigation bar. Hardware back button integrates automatically.
Windows Differences
On Windows, Shell flyouts render as NavigationView side panels. Tab bars render as tab controls. NavigationPage renders as a simple content area with a back button. Shell typically looks more native on Windows desktop.
Which to Choose: Decision Framework
Use this as your decision guide:
Choose Shell When:
- Your app has a tab bar, flyout menu, or bottom navigation
- You need deep linking from push notifications or universal links
- You want a URI-based navigation system for testability and flexibility
- Your app has 4+ top-level sections
- You’re building a new app — Shell is Microsoft’s recommended default
Choose NavigationPage When:
- You’re building a simple linear flow (onboarding, wizard, checkout)
- The app is a single navigation hierarchy with no tabs or flyout
- You need fine-grained control over the navigation stack (remove specific pages mid-stack)
- You’re embedding a section of navigation inside a Shell tab
The Default Answer
For any app beyond a simple prototype: start with Shell. It handles tabs, flyouts, routing, and deep linking. You can always simplify later. Adding Shell navigation structure to a NavigationPage-based app after the fact is far more painful than the reverse.
Mixing Shell and NavigationPage
You can embed NavigationPage inside a Shell tab — this is a common and legitimate pattern:
<Tab Title="Products">
<ShellContent ContentTemplate="{DataTemplate views:ProductListPage}"
Route="products" />
</Tab>
When you push from within a Shell tab using Shell.Current.GoToAsync, Shell maintains a per-tab navigation stack automatically. You don’t need to wrap in NavigationPage unless you need NavigationPage-specific features (like NavigationPage.TitleView customizations that Shell doesn’t expose).
Modal Pages Within Shell
// Push a modal that sits above the Shell chrome
await Shell.Current.GoToAsync("checkout", animate: true);
// Or use Navigation.PushModalAsync for pages that need full-screen control
await Navigation.PushModalAsync(new FullScreenVideoPage());
FAQ
Can I change the navigation system mid-project without rewriting everything?
Switching from NavigationPage to Shell mid-project requires updating how every navigation call works (Push/Pop → GoToAsync), restructuring your app startup, and potentially redesigning your ViewModel data-passing approach. It’s doable but plan for a 1–2 week refactor in a medium-sized app.
Does Shell work well with MVVM and dependency injection?
Yes — Shell’s route-based navigation decouples pages from each other, which pairs well with ViewModels injected by the DI container. The [QueryProperty] attribute lets ViewModels receive data without knowing which page navigated to them.
Why does my Shell navigation not animate on Android?
Shell animation on Android uses the platform’s fragment transaction system. If you’re not seeing animations, check whether you’re on a minimum SDK version that supports the transitions, and verify that animate: true is passed to GoToAsync.
Is NavigationPage being deprecated?
Not officially, but Microsoft’s documentation consistently recommends Shell for new apps. NavigationPage will remain for backward compatibility and specific use cases, but don’t expect feature investment in it going forward.
How does Shell handle authentication flows where the user shouldn’t be able to navigate back?
Use absolute routes: await Shell.Current.GoToAsync("//main/home"). The // prefix resets the navigation stack, so the user can’t back-navigate to the login screen. This is the standard pattern for post-login navigation.
Can both Shell and NavigationPage support custom page transition animations?
Yes, but the approach differs. NavigationPage supports custom transitions via INavigationPageSystemGoBackAnimation and platform renderers/handlers. Shell supports transitions via GoToAsync with custom ShellNavigationTransition — added in .NET MAUI 8.
Conclusion
Shell is the right default for the majority of .NET MAUI apps. It handles the hard parts of mobile navigation — tabs, flyouts, deep linking, and back-stack management — with a clean declarative API. NavigationPage remains valuable for simple flows and as an embedded container within Shell tabs.
The decision rule is straightforward: if your app has any form of top-level structure (tabs, menu, drawer), use Shell from day one. If you’re building a single-flow experience with no top-level navigation, NavigationPage is leaner and appropriate.
[INTERNAL_LINK: MVVM in .NET MAUI with CommunityToolkit.Mvvm] — once navigation is settled, pairing it with a solid ViewModel pattern is the next step to a maintainable MAUI app.

Leave a Reply