Blazor JavaScript Interop

This section documents ASP.NET Core on .NET 10 (LTS), the current release — the minimal hosting model, the middleware pipeline, dependency injection, Minimal APIs, MVC & Razor Pages, Blazor with the current render modes, SignalR and gRPC, EF Core, ASP.NET Core Identity and policy-based authorization, output caching, rate limiting, and Native-AOT-aware building — as described by the official documentation at Microsoft Learn, which is the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. .NET ships a major release every November and its APIs continue to evolve: the examples here target .NET 10 / C# 14.

This section’s bibliography lists the reference material consulted while preparing these pages.

Blazor calls JavaScript for anything the .NET runtime cannot do itself — wrapping an existing JS library, DOM APIs with no C# equivalent, browser-only features — through IJSRuntime, and JavaScript calls back into .NET through [JSInvokable].

IJSRuntime

@inject IJSRuntime JS

@code {
    private async Task CopyAsync(string text)
        => await JS.InvokeVoidAsync("navigator.clipboard.writeText", text);

    private async Task<int> GetWidthAsync()
        => await JS.InvokeAsync<int>("eval", "document.body.clientWidth");
}

InvokeAsync<T> returns a value, InvokeVoidAsync does not. Both are asynchronous even under Interactive Server, where the call is a round trip over the SignalR circuit. See JavaScript interoperability (JS interop).

IJSObjectReference and JS modules

Load an ES module and keep a reference to it instead of calling eval/global functions — the idiomatic way to wrap a library or a page’s own script:

// wwwroot/js/chart.js
export function render(element, data) { /* draw the chart */ }
export function destroy(element) { /* cleanup */ }
private IJSObjectReference? module;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        module = await JS.InvokeAsync<IJSObjectReference>("import", "./js/chart.js");
        await module.InvokeVoidAsync("render", chartElement, data);
    }
}

public async ValueTask DisposeAsync()
{
    if (module is not null)
    {
        await module.InvokeVoidAsync("destroy", chartElement);
        await module.DisposeAsync();
    }
}

Collocated JS

A file named Component.razor.js next to Component.razor is a JS module scoped to that component, served automatically and loaded the same way as any other module — no manual wwwroot placement needed:

// Chart.razor.js
export function render(element, data) { /* ... */ }
module = await JS.InvokeAsync<IJSObjectReference>("import", "./Components/Chart.razor.js");

JS to .NET: [JSInvokable] and DotNetObjectReference

public sealed partial class NotificationBell : IAsyncDisposable
{
    private DotNetObjectReference<NotificationBell>? selfRef;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            selfRef = DotNetObjectReference.Create(this);
            await JS.InvokeVoidAsync("notifications.subscribe", selfRef);
        }
    }

    [JSInvokable]
    public void OnNotificationReceived(string message)
    {
        latest = message;
        StateHasChanged();
    }

    public ValueTask DisposeAsync()
    {
        selfRef?.Dispose();
        return ValueTask.CompletedTask;
    }
}

[JSInvokable] marks an instance method callable via a DotNetObjectReference, or a static method callable globally via DotNet.invokeMethodAsync("AssemblyName", "MethodName", …​) from JS with no reference needed. Always dispose a DotNetObjectReference — it pins the .NET object alive for JS to call back into.

InvokeConstructorAsync, GetValue/SetValue (.NET 10)

NET 10 adds first-class support for constructing a JS object and reading/writing its members without a

hand-written wrapper function:

var mapRef = await JS.InvokeConstructorAsync<IJSObjectReference>("L.Map", containerElement);
await mapRef.SetValueAsync("zoom", 12);
var currentZoom = await mapRef.GetValueAsync<int>("zoom");

Previously this required a small JS shim (export function createMap(el) { return new L.Map(el); }) for every constructor or property access; these APIs cover the common case directly.

Synchronous interop in WebAssembly

Interactive WebAssembly runs in the same browser tab as the JS it calls, so it can skip the async round trip where startup latency matters:

@inject IJSInProcessRuntime JS

private int GetWidth() => JS.Invoke<int>("eval", "document.body.clientWidth");

IJSInProcessRuntime is available only under Interactive WebAssembly — injecting it under Interactive Server throws, since there is no synchronous path over a network connection.

ElementReference and FocusAsync

Capture a reference to a rendered DOM element with @ref to pass it into JS interop calls, or use the built-in FocusAsync extension for the common case of moving keyboard focus without any custom JS:

<input @ref="searchBox" />

@code {
    private ElementReference searchBox;
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender) await searchBox.FocusAsync();
    }
}

Wrapping an existing JS library

The pattern is consistent regardless of the library: write a small JS module exposing the operations the component needs, import it once, keep the IJSObjectReference, and dispose it (and the underlying JS object, if the library needs explicit teardown) in DisposeAsync — exactly as shown for the chart example above.

Interop and prerendering

JS interop calls fail during prerendering (see the double-render trap) because there is no browser DOM yet to call into — document, window, and any injected library are simply undefined. Guard interop calls to run only in OnAfterRender(Async) (which never runs during prerendering) rather than in OnInitializedAsync, or check RendererInfo.IsInteractive first. See Prerender ASP.NET Core Razor components.