Using Xamarin.Forms Shell for Hierarchical Navigation
Learn how to use Xamarin.Forms Shell to create flyout menus, tab bars and URI‑based routing with a simple XAML example, plus limits and common pitfalls.
14 Aug 2025, 00:48 UTC

Quick answer: use Shell to get flyout menus, tab bars and URI‑based routing without manually pushing pages
If you need a consistent navigation structure across pages—such as a side‑menu (flyout) or bottom tabs—Xamarin.Forms Shell provides a built‑in model that handles the navigation stack for you. You define the UI once in XAML, assign a Route to each destination, and then navigate with a single call like Shell.Current.GoToAsync("//details"). The rest of this guide shows a minimal working setup, explains how the mechanism works, and lists the limits and common pitfalls you’ll encounter.
Worked example: a flyout with two tabs and a detail page
- Create a new Xamarin.Forms project (or open an existing one) and ensure the Xamarin.Forms NuGet package is version 4.0.0 or higher.
- Add a file named
AppShell.xamlin the root of the shared project. - Paste the following XAML into
AppShell.xaml:
<Shell xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyApp.AppShell"
FlyoutBehavior="Flyout">
<!-- FlyoutItem defines the side‑menu entry -->
<FlyoutItem Title="Home" Icon="icon_home.png" Route="home">
<!-- TabBar creates bottom tabs inside the flyout item -->
<TabBar>
<Tab Title="Feed" Icon="icon_feed.png" Route="feed">
<ShellContent ContentTemplate="{DataTemplate local:FeedPage}" />
</Tab>
<Tab Title="Profile" Icon="icon_profile.png" Route="profile">
<ShellContent ContentTemplate="{DataTemplate local:ProfilePage}" />
</Tab>
</TabBar>
</FlyoutItem>
<!-- A regular page reachable via a global route -->
<MenuItem Text="Settings" Icon="icon_settings.png" Command="{Binding ShowSettingsCommand}" />
<!-- Global route for a detail page (no visual container needed) -->
<ShellContent Route="details" ContentTemplate="{DataTemplate local:DetailPage}" />
</Shell>
- In
App.xaml.csset the main page to an instance ofAppShell:
public App()
{
InitializeComponent();
MainPage = new AppShell();
}
- Navigate from any page (e.g., a button click) using the Shell static:
await Shell.Current.GoToAsync("//details"); // global route, double‑slash starts from root
// or relative navigation inside the current tab:
await Shell.Current.GoToAsync("feed");
When the app runs, tapping the flyout icon opens the side menu. Selecting “Home” shows the tab bar with Feed and Profile tabs. Choosing Settings executes the bound command, and invoking GoToAsync("//details") pushes the DetailPage onto the navigation stack, with the automatic back button returning to the previous page.
How Shell works under the hood
Shell is not a page itself; it is a special Layout that manages a hierarchy of FlyoutItem, TabBar, and ShellContent objects. At startup Shell reads the XAML, registers every Route attribute into an internal dictionary, and builds the visual tree accordingly. When you call GoToAsync, Shell looks up the route, determines whether the target is global (starts with //) or relative to the current location, and then pushes or pops the appropriate ShellContent while preserving the flyout/tab state.
Limits and version requirements
- Minimum Xamarin.Forms version: 4.0.0. Older versions lack the Shell class; check via
dotnet list package Xamarin.Formsor the NuGet UI in Visual Studio. - No mixing with NavigationPage: You cannot place a traditional
NavigationPageas a direct child ofShelland expect Shell’s back‑button handling to work. If you need a separate navigation stack, embed aNavigationPageinside aShellContent(this creates a child stack, not a replacement for Shell). - Deep linking only works for registered routes: Attempting to navigate to a URI that does not match a registered
Routethrows anInvalidOperationExceptionat runtime. - Route uniqueness: Duplicate route strings across the app cause the same exception when Shell tries to resolve the navigation target.
Common mistakes and how to avoid them
- Forgotten route registration: If you navigate to a page that has no
Routedefined (or you misspelled it), Shell will fail silently in XAML but throw at runtime. Fix: Verify every destination you intend to reach viaGoToAsynchas a uniqueRouteattribute. - Incorrect absolute URL: Using a single slash (
"/details") treats the route as relative to the current location, often landing you in the wrong tab. Fix: Prefix global routes with double slash ("//details"). - Placing raw content directly under
<Shell>: Elements likeLabelorButtonthat are not wrapped in aFlyoutItem,TabBar, orShellContentcause aXamlParseExceptionbecause Shell expects only its known child types. Fix: Always wrap UI content in one of those container elements. - Assuming the back button works across flyout/tabs: The hardware back button exits the app when you are at the root of the flyout; it does not automatically close the flyout menu. Fix: Handle
Shell.Current.FlyoutIsPresentedor overrideOnBackButtonPressedif you need custom behavior.
Practical verification steps
- Check the NuGet version: open the Package Manager Console and run
Get-Package Xamarin.Forms(ordotnet list package Xamarin.Formsin the project folder). Ensure the version number is ≥ 4.0.0. - Build the solution; there should be no XAML compile errors related to Shell.
- Launch the app on an emulator or device. Tap the flyout icon, navigate to a tab, then invoke a navigation call (e.g., from a button) to a route like
"//details". Verify the target page appears and the back button returns you to the previous page. - Test error handling: call
Shell.Current.GoToAsync("//unknown")whereunknownis not registered. Observe the output console for anInvalidOperationExceptionconfirming route registration is required.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.