Full Blazor WebAssembly and JavaScript interop. Over 1,000 strongly typed C# wrappers for browser APIs - create JavaScript objects, access properties, call methods, and handle events the .NET way without writing JavaScript.
Full API Documentation - Complete MDN-style API reference with guides, 1,000+ typed wrapper references, and real C# examples.
- .NET 8, 9, and 10
- Blazor WebAssembly Standalone App
- Blazor Web App - Interactive WebAssembly mode without prerendering
Note: Version 3.x dropped support for .NET 6 and 7. Use version 2.x for those targets.
Important: PublishTrimmed and RunAOTCompilation must be set to false in your project file. Trimming removes types needed for JS interop.
SpawnDev.BlazorJS.WebWorkers is now in a separate repo.
dotnet add package SpawnDev.BlazorJSTwo changes to Program.cs:
using SpawnDev.BlazorJS;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
// Add BlazorJSRuntime
builder.Services.AddBlazorJSRuntime();
// Use BlazorJSRunAsync instead of RunAsync
await builder.Build().BlazorJSRunAsync();Inject into any component or service:
[Inject]
BlazorJSRuntime JS { get; set; }Basic usage:
// Get and Set global properties
var innerHeight = JS.Get<int>("window.innerHeight");
JS.Set("document.title", "Hello World!");
// Call global methods
var item = JS.Call<string?>("localStorage.getItem", "key");
JS.CallVoid("console.log", "Hello from Blazor!");
// Async methods (Promise-returning)
var response = await JS.CallAsync<Response>("fetch", "/api/data");
// Create new JS objects
using var audio = new Audio("song.mp3");
await audio.Play();
// Typed browser API access
using var window = JS.Get<Window>("window");
window.OnResize += Window_OnResize;
// ... later, before disposing:
window.OnResize -= Window_OnResize;
void Window_OnResize() => Console.WriteLine("Window resized!");
// Null-conditional member access
var size = JS.Get<int?>("fruit.options?.size");For full setup details including worker scope detection and WebWorkerService, see the Getting Started Guide.
| Feature | Description | Docs |
|---|---|---|
| 1,000+ Typed Wrappers | Every major browser API - DOM, WebGPU, WebRTC, WebAudio, Crypto, WebXR, and more | API Reference |
| BlazorJSRuntime | Get, Set, Call, CallAsync, New - with null-conditional (?.) support |
Guide |
| JSObject | Base class for typed JS wrappers with automatic disposal | Guide |
| ActionEvent | Type-safe event subscription with += / -= and automatic ref counting |
Guide |
| Callbacks | Pass .NET methods to JS - Create, CreateOne, CallbackGroup | Guide |
| Union Types | TypeScript-style discriminated unions with Match, Map, Reduce | Guide |
| Undefinable | Distinguish null from undefined in JS interop |
Guide |
| TypedArrays | Full typed array support - Uint8Array, Float32Array, ArrayBuffer, etc. | Guide |
| HeapView | Zero-copy data sharing by pinning .NET arrays in WASM memory | Guide |
| Promises | JS Promise wrapper - create from Task, lambda, or TaskCompletionSource | Guide |
| Worker Scopes | Detect Window, DedicatedWorker, SharedWorker, ServiceWorker contexts | Guide |
| Custom Wrappers | Wrap any JS library in typed C# - step-by-step guide | Guide |
| Disposal | Managing JSObject, Callback, and reference lifetimes | Guide |
| EnumString | Bidirectional enum-to-JS-string mapping | Guide |
| Blazor Web App | .NET 8+ compatibility with prerendering support | Guide |
SpawnDev.BlazorJS is a 1:1 mapping to JavaScript. Use the correct call type or it will throw:
| JS Method Returns | C# Call | Wrong |
|---|---|---|
| Value (sync) | JS.Call<T>(), JS.Get<T>() |
CallAsync on sync method throws |
| Promise (async) | JS.CallAsync<T>() |
Call on Promise returns wrong type |
| void (sync) | JS.CallVoid() |
- |
| void Promise (async) | JS.CallVoidAsync() |
CallVoid on async method won't await |
// Sync JS method - use sync call
var total = JS.Call<int>("addNumbers", 20, 22);
// Async JS method or Promise-returning - use async call
var data = await JS.CallAsync<string>("fetchData");
// Async void (Promise with no return value)
await JS.CallVoidAsync("someAsyncVoidMethod");See the BlazorJSRuntime Guide for full details.
using var ws = new WebSocket("wss://echo.websocket.org");
ws.BinaryType = "arraybuffer";
ws.OnOpen += WS_OnOpen;
ws.OnMessage += WS_OnMessage;
ws.OnClose += WS_OnClose;
// ... when done, always unsubscribe before disposing:
ws.OnOpen -= WS_OnOpen;
ws.OnMessage -= WS_OnMessage;
ws.OnClose -= WS_OnClose;
void WS_OnOpen() => ws.Send("Hello!");
void WS_OnMessage(MessageEvent msg) => Console.WriteLine($"Received: {msg.Data}");
void WS_OnClose(CloseEvent e) => Console.WriteLine($"Closed: {e.Code}");using var response = await JS.CallAsync<Response>("fetch", "/api/data");
if (response.Ok)
{
var text = await response.Text();
Console.WriteLine(text);
}using var idbFactory = new IDBFactory();
using var db = await idbFactory.OpenAsync("myDB", 1, (evt) =>
{
using var request = evt.Target;
using var database = request.Result;
database.CreateObjectStore<string, MyData>("store", new IDBObjectStoreCreateOptions { KeyPath = "id" });
});
using var tx = db.Transaction("store", "readwrite");
using var store = tx.ObjectStore<string, MyData>("store");
await store.PutAsync(new MyData { Id = "1", Name = "Test" });using var crypto = new Crypto();
using var subtle = crypto.Subtle;
using var keys = await subtle.GenerateKey<CryptoKeyPair>(
new EcKeyGenParams { Name = "ECDSA", NamedCurve = "P-384" },
false, new[] { "sign", "verify" });
using var signature = await subtle.Sign(
new EcdsaParams { Hash = "SHA-384" }, keys.PrivateKey!, testData);
var valid = await subtle.Verify(
new EcdsaParams { Hash = "SHA-384" }, keys.PublicKey!, signature, testData);using var window = JS.Get<Window>("window");
// Attach event handler - reference counting is automatic
window.OnStorage += HandleStorageEvent;
// Detach - IMPORTANT: always detach before disposing to prevent leaks
window.OnStorage -= HandleStorageEvent;
void HandleStorageEvent(StorageEvent e)
{
Console.WriteLine($"Storage changed: {e.Key}");
}// Wrap any JS library without writing JavaScript
public class Audio : JSObject
{
public Audio(IJSInProcessObjectReference _ref) : base(_ref) { }
public Audio(string url) : base(JS.New("Audio", url)) { }
public string Src { get => JSRef!.Get<string>("src"); set => JSRef!.Set("src", value); }
public double Volume { get => JSRef!.Get<double>("volume"); set => JSRef!.Set("volume", value); }
public Task Play() => JSRef!.CallVoidAsync("play");
public Task Pause() => JSRef!.CallVoidAsync("pause");
public ActionEvent OnEnded { get => new ActionEvent("ended", AddEventListener, RemoveEventListener); set { } }
}See the Custom JSObject Guide for the full walkthrough.
For 930+ more typed wrappers, see the Complete API Reference.
This project uses Playwright .NET for unit testing in a real browser with an actual JavaScript environment.
SpawnDev.BlazorJS.Demo- Demo project with unit test methodsPlaywrightTestRunner- Playwright test runner projectPlaywrightTestRunner/_test.bat/_test.sh- Build and run tests on Windows / Linux.github/workflows/playwright-test-runner.yml- CI testing on GitHub Actions
If you find a bug or missing properties, methods, or JavaScript objects please submit an issue here on GitHub. I will help as soon as possible.
Create a new discussion to show off your projects and post your ideas.
Sponsor us via GitHub Sponsors to give us more time to work on SpawnDev.BlazorJS and other open source projects. Or buy us a cup of coffee via PayPal. All support is greatly appreciated!
Thank you to everyone who has helped support SpawnDev.BlazorJS and related projects financially, by filing issues, and by improving the code. Every bit helps!
BlazorJS and WebWorkers Demo: https://blazorjs.spawndev.com/