Getting Started
One package, one namespace and one stylesheet - no services to register. Then a BlowCanvas
in a container that has a height is a working node editor over your own List<BlowNode>
and List<BlowEdge>.
Install
The package runs on .NET 8, 9 and 10, in Blazor WebAssembly, Blazor Server, a Blazor Web App and Blazor
Hybrid. In a Web App it belongs in the project that holds the pages with a canvas - the .Client
project, for pages rendered with InteractiveWebAssembly.
dotnet add package Bit.Blow
Add the namespace to _Imports.razor. Everything in the library - the components, the model,
layout, history and the clipboard - lives in it:
@using Bit.BlowLink the stylesheet
From the host page - Components/App.razor in a Blazor Web App, wwwroot/index.html
in a standalone WebAssembly or a Hybrid app. The link is required: the library does not inject its
stylesheet, so leaving it out gives an unstyled canvas rather than an error. Put your own overrides after it.
<link rel="stylesheet" href="_content/Bit.Blow/bit-blow.css" />
On .NET 9 and later, href="@Assets["_content/Bit.Blow/bit-blow.css"]" in App.razor
serves the fingerprinted copy. The script module, _content/Bit.Blow/bit-blow.js, needs no tag:
the component imports it the first time it needs it.
A first flow
The canvas fills its container (width: 100%; height: 100%), so the container needs a height -
or give the canvas one directly with Style="height: 600px". The graph is plain C#: the canvas
edits the lists you hand it in place as the user works.
<div style="height: 400px"> <BlowCanvas @ref="_flow" Nodes="_nodes" Edges="_edges" FitViewOnInit="true"> <BlowBackground /> <BlowControls /> </BlowCanvas> </div> @code { private BlowCanvas? _flow; private List<BlowNode> _nodes = [ new BlowNode { Id = "1", Type = "input", Label = "Order placed", Position = new(0, 0) }, new BlowNode { Id = "2", Label = "Payment", Position = new(0, 100) }, new BlowNode { Id = "3", Type = "output", Label = "Shipped", Position = new(0, 200) }, ]; private List<BlowEdge> _edges = [ new BlowEdge { Id = "e1-2", Source = "1", Target = "2" }, new BlowEdge { Id = "e2-3", Source = "2", Target = "3", Animated = true }, ]; }
That is a working editor. Nodes drag; dragging from a handle to another node connects them; clicking selects, Shift+click adds to the selection and Shift+drag on the background draws a selection box; Delete removes the selection; the wheel zooms toward the cursor, dragging the background pans and two fingers pinch. Tab reaches every node and edge, Enter selects, the arrow keys move the selection and Escape clears it.
Reacting to edits
Your state is always the graph on screen. What the canvas raises tells you when something worth knowing has happened:
NodesChangedandEdgesChangedonce an edit is complete - a drag released, a resize ended, a connection made, something deleted or pasted. Not on every pointer move.OnNodeDrag,OnNodeResizeandOnMovewhile a gesture runs, for live readouts.OnSelectionChangewhen the selected set actually changes.OnConnect,OnReconnectandOnDeletefor the specific edits. A connection adds its own edge;OnConnectis for side effects.
When your own code changes the graph outside a render of your component - a timer, a message from a server -
call _flow.Refresh(remeasure: true), or InvokeAsync(StateHasChanged) on your component.
Calling the canvas from code
Capture the canvas with @ref for the imperative API - the counterpart of React Flow's
useReactFlow():
_flow.FitView(); await _flow.SetCenterAsync(x: 400, y: 200, zoom: 1.5, durationMs: 300); BlowXYPosition point = _flow.ScreenToFlowPosition(e.ClientX, e.ClientY); _flow.AddNodes(node); await _flow.DeleteElementsAsync(nodes: [node]); BlowNode? found = _flow.GetNode("2"); BlowSnapshot snapshot = _flow.ToObject(); await _flow.RestoreAsync(snapshot);
Where to go next
- Basic Flow - every default gesture on one canvas.
- Custom Nodes - your own node markup, with handles wherever you put them.
- All Examples - more than eighty working pages, most of them ports of React Flow's own.
- MCP server - point an AI agent at this site and it reads the library instead of guessing.