JavaScript Interop in Blazor: When and How to Call JS from .NET
Blazor handles most UI concerns without JavaScript, but there are times when you need the browser's native APIs — clipboard access, geolocation, third-party JS libraries, or DOM manipulation that Blazor doesn't abstract. JavaScript interop bridges that gap cleanly.
Calling JavaScript from .NET
The IJSRuntime service is your entry point. Inject it and call InvokeVoidAsync (for functions that don't return a value) or InvokeAsync<T> (for those that do).
@inject IJSRuntime JS
<button @onclick="CopyToClipboard">Copy</button>
@code {
private async Task CopyToClipboard()
{
await JS.InvokeVoidAsync("navigator.clipboard.writeText", "Copied text");
}
}
For functions that return values:
@code {
private async Task<int> GetWindowWidth()
{
return await JS.InvokeAsync<int>("eval", "window.innerWidth");
}
}
Using eval is quick for one-offs, but for anything more substantial, define your JavaScript in a separate file.
Organising JavaScript with Modules
The preferred approach is to use JavaScript modules — they're isolated, cacheable, and avoid polluting the global scope.
Create a .js file alongside your component or in wwwroot:
export function copyText(text) {
return navigator.clipboard.writeText(text);
}
export function readText() {
return navigator.clipboard.readText();
}
Import and use it with IJSObjectReference:
@inject IJSRuntime JS
@implements IAsyncDisposable
<button @onclick="Copy">Copy</button>
@code {
private IJSObjectReference? module;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
module = await JS.InvokeAsync<IJSObjectReference>(
"import", "./js/clipboard.js");
}
}
private async Task Copy()
{
if (module is not null)
{
await module.InvokeVoidAsync("copyText", "Hello from Blazor");
}
}
public async ValueTask DisposeAsync()
{
if (module is not null)
{
await module.DisposeAsync();
}
}
}
Collocated JavaScript Files
In .NET 8+, you can place a .js file next to your component and Blazor will bundle it automatically:
Components/
MyComponent.razor
MyComponent.razor.js
export function highlight(element) {
element.style.backgroundColor = "#fef3c7";
}
The module path follows the pattern ./_content/{AssemblyName}/{ComponentPath}.razor.js or for the main project, ./{ComponentPath}.razor.js. Import it in OnAfterRenderAsync as shown above.
Calling .NET from JavaScript
Sometimes JavaScript needs to call back into .NET — for example, when a JS event fires and you want to handle it in C#.
Instance method calls:
@inject IJSRuntime JS
@code {
private DotNetObjectReference<MyComponent>? objRef;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
objRef = DotNetObjectReference.Create(this);
await JS.InvokeVoidAsync("registerCallback", objRef);
}
}
[JSInvokable]
public void OnEventFromJs(string data)
{
Console.WriteLine($"Received: {data}");
StateHasChanged();
}
public void Dispose() => objRef?.Dispose();
}
// In your JS file
function registerCallback(dotNetRef) {
document.addEventListener("customEvent", (e) => {
dotNetRef.invokeMethodAsync("OnEventFromJs", e.detail);
});
}
Static method calls:
[JSInvokable]
public static string GetAppVersion()
{
return "1.0.0";
}
// In JS
const version = await DotNet.invokeMethodAsync("MyApp", "GetAppVersion");
Working with Element References
To pass a DOM element to JavaScript, use ElementReference:
<canvas @ref="canvasRef" width="400" height="300"></canvas>
@code {
private ElementReference canvasRef;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JS.InvokeVoidAsync("initCanvas", canvasRef);
}
}
}
The ElementReference is only valid after the component has rendered — attempting to use it before OnAfterRender will fail.
Timing and Prerendering
JavaScript interop is not available during prerendering. If you call IJSRuntime during OnInitializedAsync in a prerendered component, you'll get an InvalidOperationException. Always defer JS calls to OnAfterRenderAsync:
// Wrong - will fail during prerendering
protected override async Task OnInitializedAsync()
{
var width = await JS.InvokeAsync<int>("getWidth"); // Throws!
}
// Correct - safe during prerendering
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var width = await JS.InvokeAsync<int>("getWidth"); // Works
}
}
When to Avoid JS Interop
Before reaching for JavaScript, check whether Blazor already provides what you need. Common traps include using JS for focus management (Blazor has FocusAsync), for navigation (use NavigationManager), or for local storage (consider a .NET wrapper package). Every JS interop call has overhead — especially in Server mode where it requires a round trip over SignalR. Use it when you must, not as a default.