# Flow Canvas

Source: https://lumeo.nativ.sh/components/flow-canvas

# Flow Canvas

A node/flow editor surface: your own Razor node cards and SVG edges on a pannable, zoomable canvas, with drag-to-move, snapping, a background grid and zoom controls. A first-party engine — no third-party runtime library.

## Installation

.NET CLI PackageReference Lumeo CLI

dotnet add package Lumeo.Flow

One-time app setup (`AddLumeo()`, CSS & JS) is covered in the [installation guide](docs/introduction).

## Usage

@using Lumeo

<FlowCanvas />

**Phase 5 of Lumeo.Flow.** Sub-flows — nodes nested in groups (`ParentId`, `FlowExtent.Parent`, `FlowGroupNode`, `Ctrl`+`G`) — and virtualization for large graphs (`OnlyRenderVisibleNodes`) join resizing, helper lines, the clipboard, loose connections, inline label editing and export. The node, edge and handle shapes you write stay the same.

## When to Use

-   Workflow and automation builders — triggers, conditions and actions as cards joined by edges.
-   Pipelines, dependency and impact maps where the user arranges the graph by hand.
-   Any diagram whose nodes are real Razor content (badges, avatars, buttons), not canvas pixels.

Preview Code

Custom node cards — drag, pan, zoom

Drag a card to move it (the edges follow live, the position is committed once on drop through `@bind-Nodes`). Drag the background to pan, use the wheel to zoom around the pointer, or focus a card and nudge it with the arrow keys (Shift for 10px).

Last committed move

Drag a card to see its committed position.

Viewport

x 0 · y 0 · zoom 1.00

Preview Code

Read-only, background variants & controls

`Readonly` keeps pan and zoom but turns every edit off — including the handles, which drop out of the tab order and go `aria-hidden` while staying visible as ports (toggle it below and tab through the canvas). `FlowBackground` draws dots, lines or crosses that follow the viewport; `FlowControls` can sit in any corner. `SnapToGrid` snaps drags to `SnapGrid` (here 20×20, matching the background). The `a-c` edge below shows a per-edge colour (`Class` + a `--lumeo-flow-edge-stroke` rule, no `!important`) and a solid animated edge (`Animated="true" Dashed="false"` — a travelling dash overlay, the base line stays solid).

DotsLinesCross

Bottom leftTop right

Read-only

Preview Code

Connect handles — pointer and keyboard, with validation

Drag from a source handle (the dot on a card's right edge) to a target handle to propose a connection — `IsValidConnection` runs on every attempt. Here a `trigger` card may only be a source: connecting into one is rejected. Keyboard: Tab to a source handle, press Enter to start connecting (every compatible target handle highlights), Tab to one and press Enter to commit, or Esc to cancel.

Try dragging "Notify sales" onto "New order" — it will be rejected.

Preview Code

Selection and delete

Click selects a card, Shift/Ctrl\-click toggles more into the selection, and shift-dragging the background draws a marquee. Click an edge to select it too. Delete or Backspace removes whatever is selected — nodes, the edges touching them, and any directly selected edge.

Nothing selected.

Preview Code

Minimap, panel and a node toolbar

`FlowMiniMap` shows every node and the current viewport — click or drag it to pan. `FlowPanel` is a plain corner overlay for your own content. `FlowNodeToolbar` floats above the single selected node and follows it while dragging.

Preview Code

Auto-layout — tidy tree / layered

`FlowLayout.Tree` and `FlowLayout.Layered` are pure functions: they take the current nodes and edges and return a new node list with fresh positions — apply it with `Nodes = FlowLayout.Tree(...)`, then `FitViewAsync()`. Both break cycles deterministically, so an accidental loop still lays out instead of hanging, and both handle a duplicate node id in the input without throwing. On a graph too large to fit above your minimum zoom, `FitViewAsync(new FlowFitViewOptions(MinZoom: 0.6, AnchorNodeId: "start"))` centres on that node at 60% instead of zooming out further to show everything. For a `LeftToRight`/`TopToBottom` tree, centring the anchor wastes half the pane on the side the tree never grows into — add `AnchorAlign: FlowAnchorAlign.Start` to pin the anchor's leading edge (left in LTR, right in RTL; always the top vertically) at the fit padding from the pane's own edge instead; `FlowAnchorAlign.End` mirrors it against the trailing edge.

Tidy tree Layered Scramble positions

Preview Code

Anchored fit — AnchorAlign (LU-19)

Fit — Anchor Center Fit — Anchor Start

FlowFitViewOptions.AnchorAlign controls WHERE a clamped anchor lands. Center (default) keeps it mid-pane — on this left-to-right tree that wastes half the pane on the side it never grows into. Start pins the anchor's leading edge (left in LTR) at the fit padding instead.

Preview Code

Undo / redo with FlowHistory

Give the canvas a `FlowHistory` and it records a snapshot after every committed change — drag, keyboard move, connect, delete, an applied layout — and handles Ctrl+Z / Ctrl+Y (or Ctrl+Shift+Z) while the canvas has focus. `FlowControls`' `ShowHistory` adds undo/redo buttons bound to the same history; `ShowSnapToggle` adds a grid toggle.

Drag a card, then use the undo/redo buttons in the canvas (bottom-left) or Ctrl+Z / Ctrl+Y.

Preview Code

Reconnect an edge, touch and pinch

Select an edge (click it) and small grab handles appear at each end — drag one onto a different handle to move that end, validated through `IsValidConnection` the same way a fresh connection is; Esc cancels mid-drag. On a touch screen: one finger pans the background or drags a node (no long-press — it starts immediately), two fingers pinch-zoom anchored on the midpoint between them.

Select the edge, then drag its right-hand end from "Triage" onto "Archive".

Preview Code

Resize, with helper lines

Select a card, then drag a grip — corners resize both axes (Shift keeps the card's aspect ratio), edges resize one. Drag the whole card near another one to see the alignment guide (`HelperLines`).

Preview Code

Clipboard — copy, paste, duplicate

Click a card to focus the canvas, then Ctrl+C / Ctrl+V copies and pastes it (and any edge between two copied cards) at a (20, 20) offset with a new id; Ctrl+D duplicates the current selection directly.

Select a card, then Ctrl+C / Ctrl+V, or Ctrl+D to duplicate.

Preview Code

Loose connections and editable labels

`ConnectionMode.Loose` lets a drag start on either handle and land on either handle — try dragging from a target port to another target port. Double-click "next" to edit it inline (`EdgeLabelEditable`); Enter commits, Esc cancels.

Preview Code

Export and import

`ToDocument()` / `LoadDocumentAsync()` round-trip the whole canvas as JSON; `ExportSvgAsync()` is a pure-vector snapshot; `ExportPngAsync()` rasterizes the live node DOM (best effort — it can return `null` on a tainted canvas).

Export JSON Load Export PNG

Preview Code

Sub-flows — nested groups

A node with a `ParentId` lives inside that group: its `X`/`Y` are relative to the group, so dragging "Checkout" moves everything in it. "Payment" and its cards use `Extent="FlowExtent.Parent"` — try dragging them out. Select cards and press Ctrl+G to group them, Ctrl+Shift+G on a group to dissolve it. Deleting a group deletes what is inside it.

Group selection Ungroup Auto-layout per group

Preview Code

1,500 nodes — only the visible ones are rendered

`OnlyRenderVisibleNodes` mounts just the nodes inside the viewport plus one viewport of margin, and follows pans and zooms with hysteresis. Pan around — the minimap still shows all 1,500. (Fit view shows everything at once, so everything mounts: virtualization helps when the graph is larger than what is on screen.)

1,500 nodes, 1,470 edges — viewport (24, 24) at 100%

## API Reference

### FlowCanvas

### FlowBackground

### FlowControls

### FlowHandle

### FlowMiniMap

### FlowPanel

### FlowNodeToolbar

With two or more nodes selected, one toolbar renders at the SELECTION'S bounding box top (phase 4) instead of nothing.

### FlowNodeResizer

Place inside `NodeTemplate`, next to `FlowHandle`.

### FlowGroupNode

The default chrome for a group node (label, tinted box, resize grips while selected). Render it from `NodeTemplate` for nodes of `Type == FlowGroupNode.GroupType` ("group"); without a `NodeTemplate` the canvas uses it by itself for those nodes.

### FlowLayout (static)

Pure functions, not a component — apply the result with `Nodes = FlowLayout.Tree(...)`.

Member

Description

Tree(nodes, edges, options?, measured?)

Rooted tree/forest: a root per node with no incoming edge, children centred under their parent. A node's depth is the LONGEST path reaching it; it is placed under the single parent that explains that depth (the deepest of its parents, when it has more than one) — never under a shallower parent whose subtree happened to reach it first.

Layered(nodes, edges, options?, measured?, sweeps = 4)

Longest-path ranking (a node's rank is one more than the deepest predecessor), then barycenter ordering within each rank to reduce crossings.

FlowLayoutOptions

`Direction` (`LeftToRight`/`TopToBottom`), `NodeSpacing`, `RankSpacing`, `GroupPadding` (20), `GroupHeaderHeight` (28).

Groups

With nested nodes (`ParentId`) each group's children are laid out on their own, inside the group (relative, inset by `GroupPadding` plus `GroupHeaderHeight`), deepest groups first; a group grows (never shrinks) to fit them; then the top level is laid out with edges that cross a group border counted as edges of that group.

measured

A size per node id — pass `canvas.MeasuredSizes` so a node with no fixed `Width`/`Height` lays out at its real rendered size, not the library default.

Cycles

Both break cycles first (a deterministic DFS back-edge removal, same input → same result) so a cyclic graph still lays out instead of looping.

### FlowHistory

A plain class, not a component — construct one, pass it as `FlowCanvas.History`.

Member

Description

FlowHistory(int capacity = 50)

The oldest snapshot is dropped once `Capacity` is exceeded.

Push(nodes, edges)

Records a new current state; discards any redo future. The canvas calls this itself once per committed change.

Undo() / Redo()

Returns the `Snapshot` to apply, or `null`. The canvas' own `UndoAsync`/`RedoAsync` apply it for you.

CanUndo / CanRedo / Count / Current

State for your own UI (`FlowControls`' `ShowHistory` already wires these two).

Changed

Raised after `Push`/`Undo`/`Redo`/`Clear`.

### Building blocks rendered by FlowCanvas

You never place these yourself; they are listed for completeness.

### FlowNode / FlowEdge

Field

Type

Description

FlowNode.Id

string

Stable identifier; edges reference nodes by it.

FlowNode.X / Y

double

Top-left corner in flow coordinates.

FlowNode.Type / Data

string? / object?

Your discriminator and payload for `NodeTemplate`.

FlowNode.Width / Height

double?

Fixed size; when null the node is sized by its content and measured.

FlowNode.Draggable

bool

Per-node drag switch (combined with the canvas' `NodesDraggable` / `Readonly`).

FlowNode.ParentId

string?

The group node this node sits in. Its `X`/`Y` are then relative to that group's top-left corner, it paints above the group and moves with it. A missing parent or a parent cycle counts as top-level.

FlowNode.Extent

FlowExtent

`None` (default) or `Parent` — keeps the node inside its parent's rect while dragged or moved with the arrow keys.

FlowEdge.Source / Target

string

Node ids. An edge whose node is missing is skipped.

FlowEdge.SourceHandle / TargetHandle

string?

An explicit `FlowHandle` id. Left `null` (the default), the edge anchors to the node's default side — right for a source, left for a target — even when the node renders handles; only naming a handle id opts that edge into handle-based anchoring.

FlowEdge.Type

FlowEdgeType

`Bezier` (default), `SmoothStep`, `Step` or `Straight`.

FlowNode.Connectable / Deletable

bool

Per-node connect/delete switches (combined with the canvas' own switches).

FlowEdge.Label

string?

Renders as a small themed pill at the path midpoint (nowrap, truncates with an ellipsis past `max-w-32`); use `EdgeLabelTemplate` for custom content.

FlowEdge.Animated / Dashed

bool

`Dashed` is a static dashed stroke. `Animated` alone keeps the edge SOLID and adds a small travelling-dash overlay on top (off under `prefers-reduced-motion`); `Animated` + `Dashed` together animate the dash offset of the dashed stroke itself.

FlowEdge.MarkerEnd

string?

Any non-null value draws an arrowhead at the target, coloured to match the edge (a dedicated marker for the selected state; the default marker upgrades to `context-stroke` under `@supports` in engines that implement it). `null` draws none.

FlowEdge.Deletable

bool

Per-edge delete switch.

FlowEdge.Class

string?

Merged onto the edge's `<path>`. Pair with a rule that sets `--lumeo-flow-edge-stroke` (e.g. `.critical { --lumeo-flow-edge-stroke: var(--color-destructive); }`) to recolour this edge — no `!important` needed, the default stroke reads that same custom property.

FlowEdge.Style

string?

Additional inline CSS appended after the library's own — a one-off escape hatch that always wins, including over the selected-state colour.

### Methods (via @ref)

Method

Effect

FitViewAsync(double? padding)

Fits every node into view.

FitViewAsync(FlowFitViewOptions)

Fits with per-call `Padding`/`MinZoom`/`MaxZoom` overrides and an optional `AnchorNodeId`: when the plain fit would need a zoom below `MinZoom`, places that node at `MinZoom` instead of zooming out further — "start readable" on a large graph. `AnchorAlign` (`Center` default, `Start`, `End`) controls WHERE it lands: `Center` keeps the node mid-pane; `Start`/`End` instead pin its leading/trailing edge at the fit padding from the pane's matching edge on both axes — RTL-aware horizontally. Raises `OnFitView` once applied.

GetPaneSizeAsync()

The pane's size measured fresh from the DOM right now (not the last debounced resize report, which can still be in flight after a container resize in the same tick).

ZoomInAsync() / ZoomOutAsync()

One ×1.2 step around the pane centre.

ZoomToAsync(double zoom)

Zooms to a level (clamped to `MinZoom`/`MaxZoom`) around the pane centre.

SetCenterAsync(x, y, zoom?)

Centres a flow point.

SetViewportAsync(FlowViewport)

Moves the canvas to an exact viewport.

ScreenToFlow(x, y) / FlowToScreen(x, y)

Converts between pane-local pixels and flow coordinates.

SelectAsync(IEnumerable<string> nodeIds)

Replaces the selection with these node ids (unknown ids are ignored).

ClearSelectionAsync()

Clears the node and edge selection.

UndoAsync() / RedoAsync()

Applies one step of `History`; a no-op without a `History` or with nothing to undo/redo.

ToDocument()

The current nodes, edges and viewport as one `FlowDocument` (phase 4).

LoadDocumentAsync(FlowDocument)

Replaces nodes/edges/viewport, clears the selection, pushes one `History` snapshot (phase 4).

ExportSvgAsync()

A self-contained vector SVG (edges + node boxes with their label text) built purely from canvas state — never fails (phase 4).

GroupSelectionAsync()

What Ctrl+G does: wraps the selection in a new `"group"` node (bounds + padding + a label row) in the selection's common parent, re-parents the selected nodes into it at unchanged screen positions and selects it (phase 5).

UngroupSelectionAsync()

What Ctrl+Shift+G does: dissolves each selected group (its children move one level up, the group and its own edges are removed); with no group selected, takes the selected nodes out of their group (phase 5).

GetAbsolutePosition(string nodeId)

A node's top-left corner in absolute flow coordinates (a child's own `X`/`Y` are relative to its group), or `null` for an unknown id (phase 5). `FlowGeometry.GetAbsolutePositions(nodes)` does the same for a whole list, without a canvas.

ExportPngAsync(double scale = 1)

Best-effort raster of the live node DOM as a `data:image/png` URL; `null` on failure (a cross-origin image/font is the usual cause), never throws (phase 4).

### Read-only properties (via @ref)

Member

Description

CurrentNodes / CurrentEdges

The nodes/edges as currently rendered — the bound list, or the canvas' own copy.

MeasuredSizes

Every node's measured (width, height) known so far — pass to `FlowLayout`'s `measured` parameter.

CurrentViewport

The viewport as .NET last knows it.

PaneSize

The pane's last KNOWN size (from the most recent resize report or engine registration) — `(0, 0)` before either has happened. For a size guaranteed fresh right now, await `GetPaneSizeAsync()` instead.

## Keyboard & accessibility

-   The canvas is a labelled `role="application"` region; every node is a focusable `role="group"` in node order.
-   ← → ↑ ↓ move the focused node by 1px (by one grid step with `SnapToGrid`); Shift moves 10×. Each press commits like a drop.
-   Esc cancels a drag or a connection in progress (the node/line returns) and clears the selection.
-   Hold Space to pan by dragging anywhere, including over nodes.
-   Click, or Enter/Space on a focused node, selects it and raises `OnNodeClick` (Space does not also scroll the page); Shift/Ctrl\-click toggles it into a multi-selection; shift-dragging the background draws a marquee. Edges are click-selectable the same way.
-   Delete/Backspace removes the current selection (configurable via `DeleteKey`).
-   Handles are tab stops when connecting is actually possible (the canvas is not `Readonly`/locked, `NodesConnectable` is on, and the node's own `Connectable` is on) — otherwise they drop out of the tab order and are `aria-hidden` while staying visible as ports. Enter/Space on a source handle enters "connecting" mode (announced to screen readers and marked on every compatible target handle with `data-flow-handle-valid`); Enter/Space on a target handle commits it, Esc cancels.
-   The controls are labelled buttons; the lock and snap-grid toggles report `aria-pressed`, undo/redo report `disabled` when there is nothing to undo/redo.
-   Ctrl+Z undoes, Ctrl+Y or Ctrl+Shift+Z redoes, while the canvas has focus and a `History` is set.
-   Undo/redo, delete and the arrow-key nudge all skip a keystroke aimed at an editable field — an input, textarea, select, contenteditable region, or anything marked `data-flow-nodrag` (e.g. a node card's own inline-edit box, or an inspector panel rendered through `FlowPanel`) — so typing there never undoes a graph change or deletes the selected node; the field's own native undo/typing keeps working. Esc is not gated.
-   Reconnecting an edge (dragging one of a selected edge's grab handles) is pointer/touch only — there is no keyboard equivalent yet; the handles are `aria-hidden` so a screen reader doesn't announce an unreachable control. The edge itself stays fully keyboard-selectable and deletable.
-   Ctrl+G groups the selection, Ctrl+Shift+G ungroups — both announced to screen readers; a group node's name includes how many nodes it holds ("Payment, 3 nodes") and its role description is "group". Like the other shortcuts they skip a keystroke aimed at an editable field.
-   Touch: one finger pans the background or drags a node (starts immediately, no long-press); two fingers pinch-zoom anchored on the midpoint between them. `touch-action: none` on the pane stops the browser from also scrolling/zooming the page.

## Styling hooks (data-\*)

Attribute

Element

Meaning

data-flow-node="id"

Node host

The positioned wrapper around your template (also carries `data-type`).

data-selected / data-dragging

Node host

Presence-only: the node is selected / being dragged right now.

data-flow-edge

Edge path

With `data-source`, `data-target`, `data-edge-type`.

data-flow-handle

Handle button

With `data-handle-type`, `data-handle-id`, `data-position`.

data-flow-nodrag

Anything in a node

Add it to an element inside your template to stop a press there from dragging the node.

data-flow-ready="done"

Canvas root

Set by the engine once it has mounted and applied the initial fit.

data-flow-handle-valid

Handle button

Presence-only: a structurally compatible target while a connection (pointer or keyboard) is in progress.

data-flow-connecting

Handle button

Presence-only: the source handle of an in-progress keyboard connection.

data-flow-edge-label

Label div

With `data-edge-id`; positioned at the path midpoint.

data-flow-toolbar-for="id"

`FlowNodeToolbar`

The node id it floats above; the engine repositions it live while that node is dragged.

data-parent-id / data-extent

Node host

A nested node's resolved group id, and `"parent"` when it is clamped inside it (phase 5).

data-flow-group

Node host

Presence-only: the node has children (phase 5).

data-flow-virtualized

Canvas root

Presence-only: `OnlyRenderVisibleNodes` is on (phase 5).

data-flow-edge-end

Reconnect grab handle

With `data-edge-id` and `data-end` ("source"/"target"); rendered for a selected, reconnectable edge.

## Related Components

-   [Gantt Chart](/components/gantt) — when the graph is really a schedule over time.
-   [Tree View](/components/tree-view) — strictly hierarchical data without free placement.
