Anchor navigation in a Blazor application

  • Gérald Barré

After loading a page, a browser automatically scrolls to the element identified by its id in the fragment part of the URL. It does the same when you click on an anchor with an href of the kind #element-id.

When you load a page using the URL https://example.com/#header it should automatically scroll the element with the id header into the view. Note that recently, chromium-based browsers support text fragments to highlight a part of the page. The URL looks like https://example.com/#element:~:text=demo.

The second behavior is when you click on a link which only set the fragment, the browser should navigate to the element:

<a href="#header">#example</a>

#Why anchor navigation doesn't work in a Blazor application

The page load behavior doesn't work for a Blazor application except in the pre-rendered mode.

  • In the case of a Blazor Server application, it needs to initialize the SignalR connection and get the data from the server. This happens after the page load, so when the browser looks for the element by id on page load, the element doesn't exist.
  • In the case of a Blazor WebAssembly application, it first loads the .NET runtime and application DLLs before doing any rendering. As for a Blazor Server application, the content of the page is not available on page load.

The second anchor behavior doesn't work by default because Blazor handles the navigation events for routing purposes. The default behavior is therefore disabled.

#Using a component to simulate anchor navigation

As I always say, the solution to everything in Blazor is to create a component. Once again, this is what we'll do in this case.

The component will handle the page load using the OnAfterRenderAsync method. The anchor click can be handled using the NavigationManager.LocationChanged event. In both methods, you can get the current URI using NavigationManager.Uri. You can get the fragment part and parse it to get the element id. Last, you can use a JavaScript function to scroll the element into the view.

Let's create a file named AnchorNavigation.razor with the following content:

@inject IJSRuntime JSRuntime
@inject NavigationManager NavigationManager
@implements IDisposable
@code {
    protected override void OnInitialized()
    {
        NavigationManager.LocationChanged += OnLocationChanged;
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await ScrollToFragment();
    }

    public void Dispose()
    {
        NavigationManager.LocationChanged -= OnLocationChanged;
    }

    private async void OnLocationChanged(object sender, LocationChangedEventArgs e)
    {
        await ScrollToFragment();
    }

    private async Task ScrollToFragment()
    {
        var uri = new Uri(NavigationManager.Uri, UriKind.Absolute);
        var fragment = uri.Fragment;
        if (fragment.StartsWith('#'))
        {
            // Handle text fragment (https://example.org/#test:~:text=foo)
            // https://github.com/WICG/scroll-to-text-fragment/
            var elementId = fragment.Substring(1);
            var index = elementId.IndexOf(":~:", StringComparison.Ordinal);
            if (index > 0)
            {
                elementId = elementId.Substring(0, index);
            }

            if (!string.IsNullOrEmpty(elementId))
            {
                await JSRuntime.InvokeVoidAsync("BlazorScrollToId", elementId);
            }
        }
    }
}

Add the JavaScript function somewhere before the Blazor script:

<script>
    function BlazorScrollToId(id) {
        const element = document.getElementById(id);
        if (element instanceof HTMLElement) {
            element.scrollIntoView({
                behavior: "smooth",
                block: "start",
                inline: "nearest"
            });
        }
    }
</script>

#Demo

You can add the component to the page. I use the LoremNET library to generate a few paragraphs and simulate a large page.

@page "/"

<strong>Table of Contents</strong>
<ul>
    @for (int i = 0; i < 10; i++)
    {
        <li><a href="@GetHref(i)">Header @i</a></li>
    }
</ul>

@for (int i = 0; i < 10; i++)
{
    <h1 id="@GetId(i)">Header @i</h1>
    @LoremNET.Lorem.Paragraph(wordCount: 30, sentenceCount: 10)
}

@* 👇 Integrate the component *@
<AnchorNavigation />

@code{
    string GetId(int i) => "header-" + i;
    string GetHref(int i) => "#" + GetId(i);
}

If you want to prevent content from being hidden underneath a fixed header, you can use the scroll-margin-top CSS property (documentation).

/* Use a more specific selector if possible */
*[id] {
  scroll-margin-top: 5rem;
}

#Conclusion

As usual, creating a component helps to do great things! The anchor navigation is useful to scroll to the meaningful part of a page. If you want this feature to be built-in in Blazor, you can upvote the following GitHub issues:

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee