Enhanced Navigation and Form Handling in Blazor

One of the more subtle additions in .NET 8 is enhanced navigation. When enabled, Blazor intercepts standard link clicks and form submissions, fetching the new page via fetch and patching the DOM — no full page reload. The result feels like a single-page application, even when your pages use static server-side rendering.

How Enhanced Navigation Works

Enhanced navigation is enabled by default in .NET 8 Blazor applications. When a user clicks an internal link, Blazor's client-side script:

  1. Intercepts the click event
  2. Makes a fetch request to the target URL
  3. Receives the HTML response
  4. Merges the new content into the existing DOM
  5. Updates the browser's URL and history

This preserves the scroll position, avoids re-downloading CSS and JavaScript, and makes transitions feel instant.

Controlling Enhanced Navigation

Sometimes you need a full page reload — for example, when linking to a non-Blazor page or downloading a file:

razor
<a href="/legacy-page" data-enhance-nav="false">Legacy Page</a>

<a href="/api/report/download" data-enhance-nav="false">Download Report</a>

The data-enhance-nav="false" attribute tells Blazor to let the browser handle the navigation normally.

Disabling for a Section

You can disable enhanced navigation for an entire section of the page:

razor
<div data-enhance-nav="false">
    <a href="/external-app/page1">External Link 1</a>
    <a href="/external-app/page2">External Link 2</a>
</div>

Enhanced Form Handling

Forms can also benefit from enhanced handling. Add the Enhance attribute to an EditForm:

razor
<EditForm Model="model" OnValidSubmit="Save" FormName="settings" Enhance>
    <DataAnnotationsValidator />

    <div>
        <label>Display Name</label>
        <InputText @bind-Value="model.DisplayName" />
        <ValidationMessage For="() => model.DisplayName" />
    </div>

    <button type="submit">Save</button>
</EditForm>

@code {
    [SupplyParameterFromForm]
    private SettingsModel model { get; set; } = new();

    private async Task Save()
    {
        await SettingsService.UpdateAsync(model);
    }
}

Without Enhance, form submission triggers a full page POST and reload. With it, the submission happens via fetch, and only the changed parts of the DOM are updated.

SupplyParameterFromForm

In static SSR, form data arrives via HTTP POST rather than through interactive binding. The [SupplyParameterFromForm] attribute tells Blazor to populate the property from form data:

razor
@page "/register"

<EditForm Model="registration" OnValidSubmit="Register" FormName="register" Enhance>
    <DataAnnotationsValidator />

    <InputText @bind-Value="registration.Username" />
    <InputText @bind-Value="registration.Email" type="email" />
    <InputText @bind-Value="registration.Password" type="password" />

    <button type="submit">Register</button>
</EditForm>

@code {
    [SupplyParameterFromForm]
    private RegistrationModel registration { get; set; } = new();

    private async Task Register()
    {
        await AuthService.RegisterAsync(registration);
        Navigation.NavigateTo("/welcome");
    }
}

This works seamlessly with enhanced form handling — the form posts via fetch, the server processes it, and the response is merged into the DOM.

You can respond to navigation events programmatically using NavigationManager:

Example.cs
@inject NavigationManager Navigation
@implements IDisposable

@code {
    protected override void OnInitialized()
    {
        Navigation.LocationChanged += HandleLocationChanged;
    }

    private void HandleLocationChanged(object? sender, LocationChangedEventArgs e)
    {
        Console.WriteLine($"Navigated to: {e.Location}");
        // e.IsNavigationIntercepted indicates enhanced nav was used
    }

    public void Dispose()
    {
        Navigation.LocationChanged -= HandleLocationChanged;
    }
}

For preventing navigation (e.g., when a form has unsaved changes), use RegisterLocationChangingHandler:

Example.cs
@inject NavigationManager Navigation
@implements IDisposable

@code {
    private IDisposable? registration;
    private bool hasUnsavedChanges;

    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            registration = Navigation.RegisterLocationChangingHandler(OnLocationChanging);
        }
    }

    private ValueTask OnLocationChanging(LocationChangingContext context)
    {
        if (hasUnsavedChanges)
        {
            context.PreventNavigation();
            // Show a confirmation dialog
        }

        return ValueTask.CompletedTask;
    }

    public void Dispose() => registration?.Dispose();
}

Loading Indicators

Enhanced navigation happens asynchronously, so users might not realise the page is loading. Blazor provides a built-in mechanism for showing a loading indicator:

style.css
/* Show a loading bar during enhanced navigation */
.blazor-loading-bar {
    position: fixed;
    top: 0;
    left: 0;
    height: 3px;
    background: #3b82f6;
    width: 0;
    transition: width 0.3s ease;
    z-index: 9999;
}

You can also detect enhanced navigation in JavaScript to show custom loading states.

When Enhanced Navigation Doesn't Apply

Enhanced navigation only works for same-origin links within the Blazor application. It won't intercept:

Practical Considerations

Enhanced navigation is one of those features that "just works" most of the time, but can cause subtle issues. If you have JavaScript that runs on page load (DOMContentLoaded), it won't re-fire on enhanced navigations. Use Blazor's enhancedload event instead:

script.js
Blazor.addEventListener("enhancedload", () => {
    // Reinitialise any JS that depends on page content
    initializeTooltips();
});

Enhanced navigation makes Blazor's static SSR mode feel remarkably close to a SPA, with significantly better initial load performance and SEO characteristics. It's one of the best additions in .NET 8.