Skip to main content

The Best MAUI Library You Don't Know: Nalu

I’ve been looking for a better Shell navigation experience in .NET MAUI for a while. Shell works, don’t get me wrong. But every time I write GoToAsync("//page?id=5") and cross my fingers, a little part of me dies. String-based routes, fragile parameter passing, no way to intercept back navigation when a user has unsaved changes. It gets the job done, but it could be so much better.

Then I found Nalu.Maui.

TL;DR: Nalu sits on top of Shell and gives you type-safe navigation, unsaved changes protection, high-performance scrolling, custom tab bars, and useful layout helpers. It’s built by Alberto Aldegheri and it’s really good.

I built a full task manager app called TaskFlow to show what Nalu can do. Check it out:

Setting Up Nalu #

Add the NuGet packages and register what you need in MauiProgram.cs. Here’s a minimal setup with just navigation:

builder
    .UseMauiApp<App>()
    .UseNaluNavigation<App>(nav => nav
        .AddPage<HomePageModel, HomePage>()
        .AddPage<DetailPageModel, DetailPage>());

That’s it for basic navigation. In the TaskFlow sample I use the full suite:

builder
    .UseMauiApp<App>()
    .UseNaluNavigation<App>(nav => nav
        .AddPage<DashboardPageModel, DashboardPage>()
        .AddPage<TaskListPageModel, TaskListPage>()
        .AddPage<TaskEditorPageModel, TaskEditorPage>()
        .AddPage<TimeLogPageModel, TimeLogPage>()
        .AddPage<SettingsPageModel, SettingsPage>()
        .AddPage<DiscardChangesPopupModel, DiscardChangesPopup>())
    .UseNaluLayouts()
    .UseNaluControls()
    .UseNaluVirtualScroll()
    .UseNaluTabBar();

Each .UseNalu*() call registers a specific module. You can adopt Nalu incrementally too. Since it’s built on top of Shell, your existing Shell pages keep working. Add Nalu navigation to new pages and migrate at your own pace.

Type-Safe Navigation #

📖 Navigation docs

If you’ve used IQueryAttributable (or the older QueryProperty attribute) to pass parameters between pages, you know it’s not great. Parameters come in as strings, there’s no compile-time safety, and you find out about mismatches at runtime.

Nalu replaces all of that with a fluent API. Navigating to a page:

await _navigationService.GoToAsync(
    Navigation.Relative().Push<TaskEditorPageModel>());

Need to pass a parameter? Use .WithIntent():

await _navigationService.GoToAsync(
    Navigation.Relative().Push<TaskEditorPageModel>()
        .WithIntent(task.Id));

On the receiving end, your page model implements IEnteringAware<T> to receive the parameter with full type safety:

public class TaskEditorPageModel : ObservableObject, IEnteringAware, IEnteringAware<int>
{
    public ValueTask OnEnteringAsync()
    {
        // No parameter — creating a new task
        _editingTaskId = null;
        PageTitle = "New Task";
        return ValueTask.CompletedTask;
    }

    public ValueTask OnEnteringAsync(int taskId)
    {
        // Parameter received — editing an existing task
        _editingTaskId = taskId;
        PageTitle = "Edit Task";
        var task = _taskService.GetTask(taskId);
        // populate fields...
        return ValueTask.CompletedTask;
    }
}

If the types don’t line up, the compiler tells you. Not a runtime crash, not a silent null. Pretty neat.

ILeavingGuard: Protecting Unsaved Changes #

📖 Navigation guards docs

This is one of my favorite features. Ever had a user accidentally swipe back and lose their form data? With vanilla Shell, handling that requires intercepting the back button, the swipe gesture, and programmatic navigation separately. It’s a mess.

Nalu makes it one interface:

public class TaskEditorPageModel : ObservableObject, ILeavingGuard
{
    private bool _isDirty;
    private bool _isSaving;

    public ValueTask<bool> CanLeaveAsync()
    {
        if (!_isDirty || _isSaving) return ValueTask.FromResult(true);

        _dispatcher.Dispatch(PromptUserToDiscardChanges);
        return ValueTask.FromResult(false);

        async void PromptUserToDiscardChanges()
        {
            var discardChanges = await _navigationService
                .ResolveIntentAsync<DiscardChangesPopupModel, bool>(
                    new DiscardIntent());

            if (discardChanges)
            {
                await _navigationService.GoToAsync(
                    Navigation.Relative(
                        NavigationBehavior.DefaultImmediateIgnoreGuards)
                    .Pop());
            }
        }
    }
}

When the user tries to navigate away, CanLeaveAsync kicks in. If there are unsaved changes, it shows a popup asking to confirm. If they choose to stay, navigation is cancelled. If they choose to discard, it pops the page while explicitly ignoring the guard to avoid an infinite loop. Pretty elegant.

TaskFlow showing the unsaved changes popup when trying to navigate away from a task being edited. Also visible: the custom NaluTabBar at the bottom.

VirtualScroll: CollectionView That Actually Performs #

📖 VirtualScroll docs

If you’ve hit performance issues with CollectionView on large lists, you know the pain. Nalu’s VirtualScroll is backed by RecyclerView on Android and UICollectionView on iOS, giving you native-level scrolling performance.

Setting it up in XAML:

<vs:VirtualScroll ItemsSource="{Binding Adapter}"
                  RefreshCommand="{Binding RefreshCommand}"
                  IsRefreshEnabled="True"
                  FadingEdgeLength="80"
                  BackgroundColor="Transparent">
    <vs:VirtualScroll.ItemTemplate>
        <DataTemplate x:DataType="m:TaskItem">
            <nalu:ViewBox Padding="16,4">
                <Border Style="{StaticResource CardStyle}" Padding="0">
                    <!-- Your item layout here -->
                </Border>
            </nalu:ViewBox>
        </DataTemplate>
    </vs:VirtualScroll.ItemTemplate>
</vs:VirtualScroll>

The key difference from CollectionView: instead of binding directly to an ObservableCollection, you create an adapter in your page model:

public ReplaceableObservableCollection<TaskItem> Tasks { get; }
public IVirtualScrollAdapter Adapter { get; }

public TaskListPageModel(INavigationService navigationService, TaskService taskService)
{
    Tasks = new ReplaceableObservableCollection<TaskItem>([]);
    Adapter = VirtualScroll.CreateObservableCollectionAdapter(Tasks);
}

VirtualScroll supports pull-to-refresh, dynamic sizing, section templates, and even carousel mode. The pull-to-refresh pattern uses a callback to signal completion:

[RelayCommand]
private void Refresh(Action completionCallback)
{
    LoadTasks();
    completionCallback();
}

In the TaskFlow demo I combine VirtualScroll with ExpanderViewBox inside each item for collapsible detail sections. Tap a task card and it smoothly expands to show the description and action buttons. The scrolling stays buttery smooth even with hundreds of items.

TaskFlow task list powered by VirtualScroll, showing 200 items with expanded cards, priority color indicators, time logged, and ExpanderViewBox for collapsible details. Custom NaluTabBar visible at the bottom.

Licensing note: VirtualScroll uses a non-commercial license (based on Apache 2.0, but with a non-commercial restriction). If you’re using it in a commercial product, internal tool, or contract work, you’ll need an active GitHub Sponsors subscription. The rest of Nalu (Navigation, Layouts, Controls, Core) is fully open source under Apache 2.0 with no restrictions.

Custom Tab Bar with NaluTabBar #

📖 Navigation docs — TabBar

The default Shell tab bar is… functional. But if you want a custom design, you’re usually looking at a lot of platform-specific renderer work. Nalu’s NaluTabBar lets you build a fully custom tab bar in pure XAML.

The TaskFlow app has a tab bar with a notched cutout shape and a floating circle that animates to the selected tab. Here’s the core of it:

<Border StrokeThickness="0"
        BackgroundColor="{AppThemeBinding Light='#E9E9F0', Dark='#252547'}">
    <Border.StrokeShape>
        <!-- Custom shape that creates the notch cutout -->
        <local:FancyTabBarShape InsetHeight="40" InsetWidth="120" />
    </Border.StrokeShape>
    <Grid x:Name="Buttons" ColumnDefinitions="*,*,*,*" Padding="20,0,20,0">
        <ImageButton Source="{FontImageSource Glyph='&#xe871;',
                     FontFamily='MaterialRound'}"
                     Grid.Column="0" Clicked="IconClicked"/>
        <ImageButton Source="{FontImageSource Glyph='&#xe8f9;',
                     FontFamily='MaterialRound'}"
                     Grid.Column="1" Clicked="IconClicked"/>
        <!-- ... more tabs -->
    </Grid>
</Border>

<!-- Floating selected indicator that animates to the active tab -->
<nalu:ViewBox x:Name="SelectedShape" Padding="4">
    <Border HeightRequest="48" WidthRequest="48"
            StrokeThickness="0" StrokeShape="RoundRectangle 24"
            BackgroundColor="{AppThemeBinding Light='#E9E9F0', Dark='#252547'}">
        <ImageButton x:Name="SelectedButton" Padding="10"
                     Clicked="SelectedButtonClicked"/>
    </Border>
</nalu:ViewBox>

The FancyTabBarShape is a custom Shape that creates the notched cutout in the bar background where the floating circle sits. When you tap a different tab, the circle animates horizontally to that position while the notch follows. All pure XAML and C#, no platform renderers needed. Register with .UseNaluTabBar() and you’re set.

I really recommend watching the video to see this in action, screenshots don’t do the animation justice.

Close-up of the TaskFlow tab bar showing the floating selected indicator with the FancyTabBarShape notch cutout. The active tab pops up above the bar while the others stay inline.

Layout Helpers #

📖 Layouts docs

Beyond navigation and scrolling, Nalu includes some genuinely useful layout tools:

  • ViewBox: A lightweight ContentView replacement with clipping support and scoped ContentBindingContext. Less overhead when you just need a container.
  • ExpanderViewBox: Animated collapsible sections. Set CollapsedHeight and bind IsExpanded, it handles the smooth animation. Used in the TaskFlow demo for expandable task cards.
  • ToggleTemplate: Switch between different DataTemplate views based on a boolean. Great for loading/empty/content states without converter gymnastics.
  • HorizontalWrapLayout: Flowing content that wraps to the next line. Think tag chips or filter buttons.
  • Magnet: A constraint-based layout system for more complex positioning needs.

Built-in Leak Detector #

📖 Core docs

Nalu includes a memory leak detector that monitors page and view model lifetimes during development. When a page is popped from the navigation stack but its view model isn’t garbage collected, you get a warning. Super helpful for catching retain cycles and event handler leaks before your users do.

The Core module also includes NSUrlBackgroundSessionHttpMessageHandler for iOS, which solves the annoying problem of HTTP requests failing when the user backgrounds the app. That one alone has saved me some headaches.

Try It Out #

Install from NuGet:

dotnet add package Nalu.Maui
dotnet add package Nalu.Maui.VirtualScroll

Nalu.Maui is the main package that includes Navigation, Layouts, Controls, and Core, all under the Apache 2.0 license. Nalu.Maui.VirtualScroll is a separate package due to its non-commercial license (commercial use requires a sponsorship).

VirtualScroll depends on Nalu.Maui.Core only, so you can use it without the full navigation system if you just need the scrolling performance.

Check out the Nalu documentation for the full API reference. You can also browse the TaskFlow sample app source code to see everything working together.

Support the Project #

Nalu is open source and actively maintained. If you find it useful, consider sponsoring Alberto on GitHub and giving the repo a star.

I’d love to see what you build with it. Go check it out!