100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
C#

JavaScript Interop

Call JavaScript from C# and C# from JavaScript in Blazor to access browser APIs that .NET can't reach directly.

State & ServicesIntermediate9 min readJul 10, 2026
Analogies

Why Blazor Needs JavaScript Interop

JavaScript interop, or JS interop, is the bridge Blazor uses whenever functionality lives only in the browser and has no .NET equivalent, such as reading the clipboard, drawing to a canvas element, integrating a JS charting library, or triggering a file download, since Blazor's own rendering engine has no direct access to arbitrary browser APIs. Both hosting models route interop calls through IJSRuntime, but Blazor Server sends each call over the existing SignalR connection, adding a network round trip that WASM's in-process calls don't incur.

🏏

Cricket analogy: A translator is needed when an English-speaking commentator interviews a player who only speaks Hindi fluently, just as JS interop translates between the .NET runtime and browser APIs that speak only JavaScript.

Calling JS from C# with IJSRuntime

Calling JavaScript from C# uses IJSRuntime.InvokeAsync<TResult>(identifier, args), where identifier names a globally accessible JS function, typically one defined in a wwwroot/*.js file referenced via a script tag in index.html or App.razor, and args are automatically JSON-serialized. Since .NET 5, IJSObjectReference lets you scope a call to a specific JS module loaded via import(), avoiding global namespace pollution, and since .NET 7's IJSInProcessRuntime, WASM apps can make synchronous JS calls when the async round trip isn't needed.

🏏

Cricket analogy: A captain calls a specific fielder by name to make a play, not just shouting 'someone catch it,' similar to how InvokeAsync targets a specific named JS function rather than something ambiguous.

javascript
// wwwroot/js/canvasInterop.js
export function drawCircle(canvasId, x, y, radius) {
    const ctx = document.getElementById(canvasId).getContext('2d');
    ctx.beginPath();
    ctx.arc(x, y, radius, 0, 2 * Math.PI);
    ctx.stroke();
}
csharp
@inject IJSRuntime JS

@code {
    private IJSObjectReference? _module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            _module = await JS.InvokeAsync<IJSObjectReference>(
                "import", "./js/canvasInterop.js");
            await _module.InvokeVoidAsync("drawCircle", "myCanvas", 100, 100, 40);
        }
    }
}

Calling C# from JS with JSInvokable

The reverse direction—JavaScript calling back into C#—requires a static method decorated with [JSInvokable], invoked from JS via DotNet.invokeMethodAsync('AssemblyName', 'MethodName', ...args), or an instance method invoked through a DotNetObjectReference created with DotNetObjectReference.Create(this) and passed into the JS call, which is the pattern used for callbacks like a JS event listener notifying a Blazor component when a canvas drawing gesture completes. The DotNetObjectReference must be explicitly disposed to release the .NET-side reference once the JS side no longer needs it.

🏏

Cricket analogy: A stadium's third umpire radios a decision back down to the on-field umpire after review, closing the loop the other direction, similar to how JS calls back into C# via DotNet.invokeMethodAsync to complete the round trip.

csharp
[JSInvokable]
public static Task<string> NotifyDrawingComplete(string canvasId)
{
    Console.WriteLine($"Drawing on {canvasId} finished");
    return Task.FromResult("acknowledged");
}
javascript
DotNet.invokeMethodAsync('MyApp', 'NotifyDrawingComplete', 'myCanvas')
    .then(result => console.log(result));

Loading JS as an ES module via await JS.InvokeAsync<IJSObjectReference>("import", "./js/canvasInterop.js") avoids polluting the global window object with function names, and the returned IJSObjectReference should be disposed (it implements IAsyncDisposable) when the component no longer needs the module.

Every argument and return value crossing the JS interop boundary is JSON-serialized, so types like DateTime, decimal precision beyond what JS numbers support, or circular object references can silently lose fidelity or throw serialization errors — test interop calls with the actual data shapes you'll send, not just simple strings and ints.

  • JS interop bridges functionality only available in the browser (canvas, clipboard, third-party JS libraries) that .NET can't reach directly
  • IJSRuntime.InvokeAsync<TResult> calls a named JS function and JSON-serializes the arguments
  • IJSObjectReference scopes calls to an imported JS module, avoiding global namespace pollution
  • [JSInvokable] exposes a static or instance C# method that JavaScript can call back into
  • DotNetObjectReference.Create(this) is required to let JS call back into a specific component instance
  • DotNetObjectReference must be explicitly disposed to release the .NET-side reference
  • Blazor Server interop calls travel over the existing SignalR connection, adding latency WASM doesn't have

Practice what you learned

Was this page helpful?

Topics covered

#BlazorStudyNotes#MicrosoftTechnologies#JavaScriptInterop#JavaScript#Interop#Blazor#Needs#StudyNotes#SkillVeris#ExamPrep