loading
Note:
This is not a router Blazor's own router owns navigation inside the app - keep using NavigationManager.NavigateTo for that. What this API adds is the ability to see the history list rather than only push onto it. The interception half of the spec, which exists so that a router can take over navigations, is deliberately not wrapped: a second router competing with Blazor's is not something this library should make easy.

Can you go back?

CanGoBack / CanGoForward

The reason this class exists. history.length counts the whole session including other people's sites, so it cannot distinguish a freshly opened tab from one with a page behind it - which is why an in-app back button built on History is either always enabled and sometimes does nothing, or navigates and traps the user in a loop. These two are the direct answer, and they are what a back button's disabled state should be bound to.

Razor
// exactly what an in-app back button should be:
<button disabled="@(_canGoBack is false)" @onclick="() => navigation.Back()">Back</button>

@code {
    private bool _canGoBack;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender is false) return;

        _canGoBack = await navigation.CanGoBack();
        StateHasChanged();
    }
}
Live sample
canGoBack / canGoForward output
Results will appear here when you interact with the samples.

Read the history list

GetEntries / GetCurrentEntry

Every entry belonging to this document, oldest first - the same list the browser's own back and forward buttons walk. Entries from other origins are not exposed at all, which is why this needs no permission. Key identifies the slot and survives leaving and returning; Id identifies the individual visit and changes each time.

C#
NavigationEntry[] entries = await navigation.GetEntries();

NavigationEntry? current = await navigation.GetCurrentEntry();

foreach (var entry in entries)
{
    Console.WriteLine($"{entry.Index}: {entry.Url} (key {entry.Key})");
}
Live sample
entries output
Results will appear here when you interact with the samples.

Traverse

Back / Forward / TraverseTo

Back and Forward move one step and return false rather than throwing when there is nowhere to go - a back button pressed at the start of the list is a normal outcome, not an exception. TraverseTo jumps straight to a remembered key however far away it is, which is how a 'back to results' button should work: not by calling Back a guessed number of times, and not by pushing a duplicate entry on top of the stack.

C#
// remember where the user was, then return there later
var listEntry = await navigation.GetCurrentEntry();
_listKey = listEntry?.Key;

// ... the user wanders off through several pages ...

await navigation.TraverseTo(_listKey!);   // straight back, one step

bool moved = await navigation.Back();     // false when there was nowhere to go
Live sample
traverse output
Results will appear here when you interact with the samples.

Per-entry state

UpdateCurrentEntry / GetCurrentState

State attached to a history entry survives a reload and a traversal, which makes it the right place for 'where was the user in this view' - a scroll offset, an open panel, a filter selection - as opposed to application data. UpdateCurrentEntry writes it without navigating and without adding an entry, so it is safe to call on every change; unlike History.ReplaceState it does not make you restate the URL to do so.

C#
await navigation.UpdateCurrentEntry(new ViewState(Filter: "open", Scroll: 420));

var state = await navigation.GetCurrentState<ViewState>();

public record ViewState(string Filter, int Scroll);
Live sample
Filter

Write a value, then reload the page: the state comes back, and no history entry was added.

state output
Results will appear here when you interact with the samples.

Observe changes

SubscribeCurrentEntryChange / SubscribeNavigateSuccess / SubscribeNavigateError

currententrychange is the one to use instead of popstate: it fires for every kind of change - a traversal, a push, a replace, an in-place state update - rather than only for traversals, and NavigationType says which it was. Navigating around this site with the buttons above, or with the browser's own back button, will drive it.

Razor
@implements IAsyncDisposable
@inject Bit.Butil.Navigation navigation

@code {
    private string? _lastChange;
    private ButilSubscription? _subscription;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender is false) return;

        _subscription = await navigation.SubscribeCurrentEntryChange(info =>
            InvokeAsync(() =>
            {
                _lastChange = info.NavigationType;
                StateHasChanged();
            }));
    }

    public async ValueTask DisposeAsync()
    {
        if (_subscription is not null) await _subscription.DisposeAsync();
    }
}
Live sample
currententrychange output
Results will appear here when you interact with the samples.

API reference

Member
Signature
Description
IsSupported
ValueTask<bool> IsSupported()
True when the runtime exposes window.navigation.
CanGoBack
ValueTask<bool> CanGoBack()
True when there is an entry behind the current one to traverse to.
CanGoForward
ValueTask<bool> CanGoForward()
True when there is an entry ahead of the current one.
GetCurrentEntry
ValueTask<NavigationEntry?> GetCurrentEntry()
The entry the document is currently showing.
GetEntries
ValueTask<NavigationEntry[]> GetEntries()
Every entry in this document's session history, oldest first.
GetCurrentState
ValueTask<T?> GetCurrentState<T>()
The state object stored on the current entry, deserialized to T.
Back
ValueTask<bool> Back()
Traverses one entry backwards; false when there was nowhere to go.
Forward
ValueTask<bool> Forward()
Traverses one entry forwards; false when there was nowhere to go.
TraverseTo
ValueTask<bool> TraverseTo(string key)
Jumps to the entry with the given key; false when it is no longer in the list.
Navigate
ValueTask<bool> Navigate(string url, object? state = null, NavigationHistoryBehavior history = Auto)
Navigates to a URL, loading the document. Use NavigationManager.NavigateTo for in-app routing.
Reload
ValueTask<bool> Reload(object? state = null)
Reloads the current entry, optionally replacing its state.
UpdateCurrentEntry
ValueTask<bool> UpdateCurrentEntry(object? state)
Replaces the state on the current entry without navigating and without adding an entry.
SubscribeCurrentEntryChange
ValueTask<ButilSubscription> SubscribeCurrentEntryChange(Action<NavigationEventInfo>)
Raised whenever the current entry changes, with the type of change.
SubscribeNavigateSuccess
ValueTask<ButilSubscription> SubscribeNavigateSuccess(Action<NavigationEventInfo>)
Raised when a navigation completes successfully.
SubscribeNavigateError
ValueTask<ButilSubscription> SubscribeNavigateError(Action<NavigationEventInfo>)
Raised when a navigation fails, with the reason in Message.
Note:
History or Navigation? Reach for History when you need to work with a browser that predates Baseline 2026, or when all you do is push and replace. Reach for this when you need to read the list rather than only add to it - which includes every case where a button's enabled state depends on whether the navigation would actually go anywhere.
An unhandled error has occurred. Reload 🗙