Custom Edge Paths
Create custom edge rendering with JavaScript functions for unique connection styles.
Overview
By default, FlowState uses curved Bézier edges. You can replace this with custom edge path functions to create straight lines, stepped connections, or any SVG path style.
How It Works
- Create a JavaScript function that takes
fromandtopositions - Return an SVG path string
- Expose the function on the
windowobject - Set
JsEdgePathFunctionNameon FlowCanvas
Unity-Style Edges (Complete Example)
This example creates edges with horizontal extensions and rounded corners, similar to Unity’s node editor.
1. Create graphLine.js
function createUnityStylePath(from, to) {
const HORIZONTAL_OFFSET = 50;
const CORNER_RADIUS = 10;
const p1 = { x: from.x + HORIZONTAL_OFFSET, y: from.y };
const p2 = { x: to.x - HORIZONTAL_OFFSET, y: to.y };
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const radius = Math.min(CORNER_RADIUS, dist / 2);
const ux = dx / dist;
const uy = dy / dist;
return `M ${from.x} ${from.y}
L ${p1.x - radius} ${p1.y}
Q ${p1.x} ${p1.y}, ${p1.x + radius * ux} ${p1.y + radius * uy}
L ${p2.x - radius * ux} ${p2.y - radius * uy}
Q ${p2.x} ${p2.y}, ${p2.x + radius} ${p2.y}
L ${to.x} ${to.y}`;
}
window.EdgePathFunc = createUnityStylePath;
2. Load the JavaScript
Place the file in wwwroot and load it:
@inject IJSRuntime JSRuntime
@code {
protected override async Task OnInitializedAsync()
{
await JSRuntime.InvokeAsync<IJSObjectReference>(
"import",
"/_content/YourProject/graphLine.js"
);
}
}
3. Use in FlowCanvas
<FlowCanvas Graph="graph"
JsEdgePathFunctionName="EdgePathFunc"
Height="100vh"
Width="100vw">
<BackgroundContent>
<FlowBackground class="grid-bg"/>
</BackgroundContent>
</FlowCanvas>
Straight Line Edges
Simple straight connections:
function createStraightPath(from, to) {
return `M ${from.x} ${from.y} L ${to.x} ${to.y}`;
}
window.StraightEdge = createStraightPath;
Stepped Edges
Orthogonal/stepped connections:
function createSteppedPath(from, to) {
const midX = (from.x + to.x) / 2;
return `M ${from.x} ${from.y}
L ${midX} ${from.y}
L ${midX} ${to.y}
L ${to.x} ${to.y}`;
}
window.SteppedEdge = createSteppedPath;
Bezier Curves (Custom)
Custom Bézier curve implementation:
function createCustomBezier(from, to) {
const dx = to.x - from.x;
const dy = to.y - from.y;
const dist = Math.sqrt(dx * dx + dy * dy);
// Control point offset based on distance
const offset = Math.min(200, dist * 0.5);
const c1x = from.x - offset;
const c1y = from.y;
const c2x = to.x + offset;
const c2y = to.y;
return `M ${from.x} ${from.y} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${to.x} ${to.y}`;
}
window.CustomBezier = createCustomBezier;
Animated Edges
Add animation with SVG attributes (CSS or SMIL):
function createAnimatedPath(from, to) {
const path = `M ${from.x} ${from.y} L ${to.x} ${to.y}`;
// Path is returned, animation handled via CSS
return path;
}
window.AnimatedEdge = createAnimatedPath;
Then style with CSS:
.edge {
stroke-dasharray: 10 5;
animation: dash 1s linear infinite;
}
@keyframes dash {
to {
stroke-dashoffset: -15;
}
}
Curved with Horizontal Exit
Edges that exit horizontally before curving:
function createHorizontalCurve(from, to) {
const EXIT_DISTANCE = 30;
const fromExit = { x: from.x - EXIT_DISTANCE, y: from.y };
const toEntry = { x: to.x + EXIT_DISTANCE, y: to.y };
const dx = toEntry.x - fromExit.x;
const dy = toEntry.y - fromExit.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const controlOffset = dist * 0.3;
const c1x = fromExit.x - controlOffset;
const c1y = fromExit.y;
const c2x = toEntry.x + controlOffset;
const c2y = toEntry.y;
return `M ${from.x} ${from.y}
L ${fromExit.x} ${fromExit.y}
C ${c1x} ${c1y}, ${c2x} ${c2y}, ${toEntry.x} ${toEntry.y}
L ${to.x} ${to.y}`;
}
window.HorizontalCurve = createHorizontalCurve;
Circuit Board Style
Sharp corners for a technical look:
function createCircuitPath(from, to) {
const SEGMENT_LENGTH = 40;
const midY = (from.y + to.y) / 2;
const p1 = { x: from.x - SEGMENT_LENGTH, y: from.y };
const p2 = { x: from.x - SEGMENT_LENGTH, y: midY };
const p3 = { x: to.x + SEGMENT_LENGTH, y: midY };
const p4 = { x: to.x + SEGMENT_LENGTH, y: to.y };
return `M ${from.x} ${from.y}
L ${p1.x} ${p1.y}
L ${p2.x} ${p2.y}
L ${p3.x} ${p3.y}
L ${p4.x} ${p4.y}
L ${to.x} ${to.y}`;
}
window.CircuitEdge = createCircuitPath;
Position Parameters
The from and to parameters are objects with x and y properties:
function myEdgeFunction(from, to) {
// from = { x: 100, y: 50 } // Output socket position
// to = { x: 300, y: 150 } // Input socket position
// Return SVG path string
return `M ${from.x} ${from.y} L ${to.x} ${to.y}`;
}
SVG Path Commands
Quick reference for SVG path commands:
// Move to
`M ${x} ${y}`
// Line to
`L ${x} ${y}`
// Cubic Bézier curve
`C ${c1x} ${c1y}, ${c2x} ${c2y}, ${x} ${y}`
// Quadratic Bézier curve
`Q ${cx} ${cy}, ${x} ${y}`
// Arc
`A ${rx} ${ry} ${rotation} ${largeArc} ${sweep} ${x} ${y}`
Testing Your Edge Function
- Create the JavaScript file
- Load it in your component
- Reference it in FlowCanvas
- Create nodes and connect them
- Observe the edge rendering
Complete Working Example
// wwwroot/myEdges.js
function createMyCustomEdge(from, to) {
// Your custom logic here
const dx = to.x - from.x;
const dy = to.y - from.y;
// Example: Wavy path
const midX = (from.x + to.x) / 2;
const wave = Math.sin(dx / 50) * 20;
return `M ${from.x} ${from.y}
Q ${midX} ${from.y + wave}, ${to.x} ${to.y}`;
}
window.MyCustomEdge = createMyCustomEdge;
export function Load() {
console.log("Custom edges loaded");
}
@page "/custom-edges"
@inject IJSRuntime JSRuntime
<FlowCanvas Graph="graph"
JsEdgePathFunctionName="MyCustomEdge"
Height="100vh"
Width="100vw">
<BackgroundContent>
<FlowBackground class="grid-bg"/>
</BackgroundContent>
</FlowCanvas>
@code {
FlowGraph graph = new();
protected override async Task OnInitializedAsync()
{
await JSRuntime.InvokeAsync<IJSObjectReference>(
"import",
"/myEdges.js"
);
graph.RegisterNode<MyNode>();
}
}
See Also
- FlowCanvas - JsEdgePathFunctionName parameter
- Getting Started - GraphViewportUnity example
- Styling Guide - Edge styling with CSS
- Custom Sockets - Vertical socket direction for top-to-bottom flows
Edge Labels and Custom Content
Add text or interactive Blazor content directly on top of edge paths.
Simple Text Label
Use the Label parameter to render a text string centered at the midpoint of the edge:
<!-- In FlowCanvas, edges are added programmatically via FlowGraph.ConnectAsync.
After connecting, set the Label on the returned EdgeInfo.Parameters. -->
@code {
private async Task OnLoaded()
{
var source = await graph.CreateNodeAsync<MySourceNode>(100, 100, []);
var target = await graph.CreateNodeAsync<MyTargetNode>(400, 100, []);
await Task.Delay(100);
var (edge, _) = await graph.ConnectAsync(source.Id, target.Id, "Output", "Input");
if (edge != null)
edge.Parameters[nameof(FlowEdge.Label)] = "Data";
}
}
The label is rendered inside the SVG coordinate space so it automatically follows canvas pan and zoom.
Rich Content (Buttons, Icons)
Use the LabelContent RenderFragment for arbitrary Blazor content. The content is placed inside an SVG <foreignObject> element at the path midpoint:
<!-- Custom edge type with a delete button -->
@code {
// Build the RenderFragment and store it in the Parameters dict
private RenderFragment MakeDeleteButton(string edgeId) => __builder =>
{
__builder.OpenElement(0, "button");
__builder.AddAttribute(1, "class", "edge-delete-btn");
__builder.AddAttribute(2, "onclick",
EventCallback.Factory.Create<MouseEventArgs>(this, () => DeleteEdge(edgeId)));
__builder.AddContent(3, "✕");
__builder.CloseElement();
};
private async Task OnLoaded()
{
var (edge, _) = await graph.ConnectAsync(src.Id, tgt.Id, "Output", "Input");
if (edge != null)
{
edge.Parameters[nameof(FlowEdge.LabelContent)] = MakeDeleteButton(edge.Id);
edge.Parameters[nameof(FlowEdge.LabelWidth)] = 32;
edge.Parameters[nameof(FlowEdge.LabelHeight)] = 32;
}
}
private async Task DeleteEdge(string id) => await graph.RemoveEdgeAsync(id);
}
.edge-delete-btn {
width: 28px;
height: 28px;
border-radius: 50%;
border: 1px solid rgba(255,255,255,0.25);
background: rgba(30,30,30,0.85);
color: #ef4444;
font-size: 12px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.edge-delete-btn:hover {
background: rgba(239,68,68,0.2);
}
Label Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
Label | string? | null | Simple SVG text rendered at path midpoint |
LabelContent | RenderFragment? | null | Arbitrary Blazor content (takes precedence over Label) |
LabelWidth | int | 120 | Width in px of the foreignObject container |
LabelHeight | int | 40 | Height in px of the foreignObject container |
Note:
LabelContentandLabelare only rendered after the edge path is computed — typically the frame after the canvas loads. There is no visual flash; the label simply appears once the path midpoint is known.
Vertical Edge Paths
For graphs using Direction="SocketDirection.Vertical" sockets (anchors on top/bottom of nodes), use a vertical Bézier path instead of the default horizontal one:
// wwwroot/graphLine.js
function createVerticalPath(from, to) {
const dy = to.y - from.y;
const dist = Math.abs(dy) + Math.abs(to.x - from.x);
const offset = Math.min(150, dist * 0.45);
const c1 = { x: from.x, y: from.y + offset };
const c2 = { x: to.x, y: to.y - offset };
return `M ${from.x} ${from.y} C ${c1.x} ${c1.y}, ${c2.x} ${c2.y}, ${to.x} ${to.y}`;
}
window.VerticalEdgeFunc = createVerticalPath;
<FlowCanvas Graph="graph"
JsEdgePathFunctionName="VerticalEdgeFunc"
Height="100vh"
Width="100vw">
</FlowCanvas>
See examples/SharedNodesLibrary/GraphViewportVertical.razor for a complete working example with vertical nodes and labeled edges.