pyreon

@pyreon/flow — API Reference

Generated from flow's src/manifest.ts — the same source that powers llms.txt and MCP get_api. Do not edit this page by hand; edit the manifest. For the conceptual guide, see flow.

Reactive flow diagrams for Pyreon. Signal-native nodes and edges, pan/zoom via pointer events + CSS transforms, auto-layout from a built-in engine — no elkjs, no D3, no layout dependency at all, and deterministic (the same graph always yields the same positions). Each node mounts exactly once across the lifetime of the graph; drags and selection patches are O(1) via per-node reactive accessors, so a 60fps drag in a 1000-node graph stays cheap.

Peer dependencies: @pyreon/runtime-dom — install alongside this package.

Multiplatform

Tier: Service backend — the API is shared; the native runtimes host it

one API with a web engine and a Swift/Kotlin port: createFlow/useFlow, the whole FlowInstance surface and all seven layouts lower to PyreonFlowState, and <Flow instance> lowers to the interactive PyreonFlowView (static custom node/edge maps, connection lines, handles, resizing, toolbars, background, controls, minimap, panels, colorMode including system, gestures, keyboard commands, culling, portable inline styles). The native engines replay shared scenarios against the web engine as oracle, and the iOS/Android device suites assert the rendered chrome and interactions. A <path d> in a custom edge or connection line lowers for any SVG path data, an inline <svg> in a renderer draws its shapes natively scaled by the viewBox, and plain <div>/<p>/<span> lower where the layout provably matches the browser. Browser-only by member and warned by name: FlowLayersContext, flowStyles, CSS-dependent renderers (a class/style, inline-flow mixes, CSS selectors), SVG <text>/gradients/transform, and renderer maps computed at runtime; @pyreon/flow/webview hosts the unchanged browser renderer for those.

See Multiplatform for the capability matrix and Multiplatform libraries for every package's tier.

Features

  • createFlow<TData> generic over node data shape

  • useFlow(config) component-scoped wrapper that auto-disposes on unmount

  • Custom node/edge renderers with reactive accessor props

  • Pan/zoom via pointer events + CSS transforms (no D3)

  • Auto-layout from the built-in engine — fed each node's EFFECTIVE (measured) box, not a 150×40 phantom

  • Measured node dimensions: per-node ResizeObserver records real rendered boxes + <Handle> dot centers; explicit width/height → measured → default precedence drives edges, layout, fitView, snap lines, minimap, culling

  • Handle-anchored edges: edge.sourceHandle/targetHandle attach at the MEASURED <Handle> dot center (unknown ids dev-warn + fall back to the first handle); handle-less nodes get floating natural-angle endpoints

  • Edge modes: bezier / smoothstep / step / straight / waypoints + per-edge pathOptions (curvature, borderRadius, offset) + config.defaultEdgeOptions flow-wide defaults

  • toJSON / fromJSON round-trip serialization

  • Configurable edge markers (arrow / arrowclosed, per-edge markerStart/markerEnd, deduped <defs>)

  • Complete --pyreon-flow-* CSS-var theming (25 variables, light-mode fallbacks — one CSS block re-skins nodes/edges/handles/controls/minimap)

  • Render virtualization via onlyRenderVisibleElements (cull off-screen nodes/edges, re-filter on pan/zoom)

  • Opt-out object-snapping (snapToObjects: false) — skips the O(N)/frame helper-line scan, ~3-4x faster drags on large graphs

Complete example

A full, end-to-end usage of the package:

import { createFlow, useFlow, Flow, Background, Controls, MiniMap, Handle, Position, type NodeComponentProps } from '@pyreon/flow'

// createFlow is generic over node `data` shape — typed consumers
// pass their data type explicitly and `node.data.kind` narrows
// correctly without any index signature on the data interface.
interface WorkflowData {
  kind: 'trigger' | 'filter' | 'transform' | 'notify'
  label: string
}

const flow = createFlow<WorkflowData>({
  nodes: [
    { id: '1', type: 'custom', position: { x: 0, y: 0 }, data: { kind: 'trigger', label: 'Start' } },
    { id: '2', type: 'custom', position: { x: 200, y: 100 }, data: { kind: 'notify', label: 'End' } },
  ],
  edges: [{ id: 'e1', source: '1', target: '2', animated: true }],
})

flow.addNode({ id: '3', type: 'custom', position: { x: 100, y: 200 }, data: { kind: 'transform', label: 'New' } })
flow.addEdge({ source: '1', target: '3' })
await flow.layout('layered', { direction: 'RIGHT', nodeSpacing: 50, layerSpacing: 100 })
// `direction`/`layerSpacing`/`edgeRouting` apply to layered/tree only —
// `force`/`stress`/`radial`/`box`/`rectpacking` silently ignore them.
// `nodeSpacing` is the only LayoutOptions field respected by every algorithm.

// Prefer `useFlow` inside components — it's `createFlow` + auto-dispose
// on unmount. Use `createFlow` only for singleton/app-store flows that
// outlive the component tree.
const MyDiagram = () => {
  const flow = useFlow<WorkflowData>({ nodes: [], edges: [] })
  return <Flow instance={flow}><Background /></Flow>
}

// Custom node renderers — every prop except `id` is a REACTIVE
// ACCESSOR (`() => T`), not a plain value. Read inside reactive
// scopes (JSX expression thunks, effect, computed) so the node
// patches in place when ANY underlying state changes. Each node
// mounts EXACTLY ONCE across the lifetime of the graph regardless
// of how many drags, selection clicks, or updateNode mutations
// happen — internally <Flow> uses <For> keyed by node.id plus
// per-node accessors that read live state from instance.nodes().
function CustomNode(props: NodeComponentProps<WorkflowData>) {
  return (
    <div
      class={props.selected() ? 'selected' : ''}
      style={() => `cursor: ${props.dragging() ? 'grabbing' : 'grab'}`}
    >
      <Handle type="target" position={Position.Left} />
      {props.data().label}
      <Handle type="source" position={Position.Right} />
    </div>
  )
}

<Flow instance={flow} nodeTypes={{ custom: CustomNode }}>
  <Background variant="dots" gap={20} />
  <Controls position="bottom-left" />
  <MiniMap nodeColor={(node) => '#6366f1'} />
</Flow>

// Serialization round-trips for sidecar editors / persistence:
const json = flow.toJSON()         // { nodes, edges, viewport }
flow.fromJSON({ nodes, edges })    // restore from saved state

Exports

SymbolKindSummary
createFlowfunctionCreate a reactive flow instance.
useFlowhookComponent-scoped wrapper around createFlow — identical shape plus an implicit onUnmount(() => flow.dispose()).
FlowcomponentMain flow container.
BackgroundcomponentGrid background inside a <Flow>.
ControlscomponentZoom / fit-view button cluster plus a live zoom-level readout.
MiniMapcomponentOverview minimap of the full graph.
HandlecomponentConnection handle on a custom node — exposes a connectable point that edges attach to.
PanelcomponentOverlay panel positioned absolutely relative to the flow viewport.
NodeResizercomponentRender drag handles inside a custom node to resize it.
NodeToolbarcomponentA floating toolbar placed beside its host node (default position: "top", offset 8px).
flowStylesconstantThe package stylesheet as ONE plain string — every .pyreon-flow-* rule with its --pyreon-flow-* custom-property fall
EdgeLabelRenderercomponentHTML edge labels for CUSTOM edges.
BaseEdgecomponentThe visible part of a CUSTOM edge: its stroke, markers and an optional label (React Flow <BaseEdge>).
EdgeTextcomponentA text label at a point in flow coordinates, in the built-in edge-label style (11px, --pyreon-flow-edge-label).
ViewportPortalcomponentRenders arbitrary HTML in flow coordinates: the children pan and zoom with the graph (React Flow <ViewportPortal>).
MarkerType / PositionconstantThe two flow enums.
edge-path-helpersfunctionSVG-path builders for CUSTOM edge components.
computeLayoutfunctionAuto-layout from the built-in engine.

API

createFlow function

<TData = Record<string, unknown>>(config: FlowConfig<TData>) => FlowInstance<TData>

Create a reactive flow instance. Generic over node data shape — createFlow<MyData>(...) returns FlowInstance<MyData> so node.data.kind narrows correctly without an [key: string]: unknown index signature on consumer types. Defaults to Record<string, unknown> when no generic is supplied. The returned instance owns signal-native nodes / edges and exposes CRUD, selection, viewport (zoom / pan / fitView), and auto-layout from the built-in engine (pure, synchronous, deterministic — no chunk to fetch). Pan / zoom uses pointer events + CSS transforms — no D3.

Example

// Generic over node data shape — typed consumers get strong narrowing
interface WorkflowData {
  kind: 'trigger' | 'filter' | 'transform' | 'notify'
  label: string
}

const flow = createFlow<WorkflowData>({
  nodes: [
    { id: '1', type: 'custom', position: { x: 0, y: 0 }, data: { kind: 'trigger', label: 'Start' } },
    { id: '2', type: 'custom', position: { x: 200, y: 100 }, data: { kind: 'notify', label: 'End' } },
  ],
  edges: [{ id: 'e1', source: '1', target: '2', animated: true }],
})

// node.data.kind narrows to the typed union, not unknown
const trigger = flow.findNodes((n) => n.data.kind === 'trigger')

flow.addNode({ id: '3', type: 'custom', position: { x: 100, y: 200 }, data: { kind: 'transform', label: 'New' } })
await flow.layout('layered', { direction: 'RIGHT', nodeSpacing: 50, layerSpacing: 100 })
// LayoutOptions applicability: direction / layerSpacing / edgeRouting apply to layered/tree only;
// force/stress/radial/box/rectpacking silently ignore them. nodeSpacing applies to all algorithms.
const json = flow.toJSON(); flow.fromJSON(json)       // round-trip serialization

Common mistakes

  • Calling pushHistory() before every mutation out of habit — mutations checkpoint themselves (autoHistory, default on) and a manual push right before one is deduped; set autoHistory: false only if you want the manual model back.

  • Reading flow.nodes() inside an event handler to get a snapshot — that is a TRACKED read; use getNodes() / getEdges() / getViewport() for plain reads.

  • Computing a drop position from clientX - rect.left by hand — screenToFlowPosition already folds in the container rect, pan and zoom (and flowToScreenPosition is its inverse).

  • Passing isValidConnection as a prop on <Flow> (React Flow) — here it is a createFlow config key, alongside connectionRadius.

  • Hunting for a interactionWidth prop on <Flow> — hit width is per edge (interactionWidth) with a config default (edgeInteractionWidth, 20px); a click near a hairline edge already lands on it.

  • Building a "figma-like" canvas by intercepting pointer events yourself — selectionOnDrag: true + panOnDrag: [1, 2] is the whole recipe, and deleteKeys / multiSelectionKey / selectionKey cover the key remaps.

  • Giving a child node (parentId) an ABSOLUTE position — a child position is relative to its parent; read getAbsolutePosition(id) when you need the canvas coordinate.

  • Wiring your own drag math to keep a child inside its group — extent: 'parent' clamps it and expandParent: true grows the group instead.

  • Forgetting to declare @pyreon/runtime-dom in consumer app deps — flow's JSX emits _tpl() which needs runtime-dom imports

  • Reading NodeComponentProps.data / .selected / .dragging as plain values — all three are REACTIVE ACCESSORS: props.data(), props.selected(), props.dragging()

  • Calling props.data() OUTSIDE a reactive scope — captures the value once at component setup, defeating the per-node reactivity. Read it inside JSX expression thunks, effect, or computed

  • Adding [key: string]: unknown index signature to your node data interface — no longer needed now that createFlow is generic. Pass createFlow<MyData>(...) instead

  • Setting LayoutOptions.direction (or layerSpacing, or edgeRouting) on a force / stress / radial / box / rectpacking layout and expecting a directional result — these options are namespaced under ELK's layered / tree pipelines and silently ignored by the geometric algorithms. Dev-mode console.warn fires when this happens

  • Missing <Flow nodeTypes={{ key: Component }}> registration — node.type strings dispatch to that map, unregistered types fall through to the default renderer

  • Using createFlow inside a component body without onUnmount(() => flow.dispose()) — prefer useFlow which auto-disposes

  • Using direction: 'row' on flow's containing Element layout — Pyreon Element accepts 'inline' / 'rows' / 'reverseInline' / 'reverseRows', not CSS flex-direction values like 'row' or 'column'

  • Confusing markerEnd: null with omitting it — null is the explicit "no end arrow" opt-out that overrides config.defaultMarkerEnd; OMITTING it falls back to the flow default (a closed arrowhead). Set config.defaultMarkerEnd: null to make every edge arrowless by default

  • Expecting onlyRenderVisibleElements to cull an edge whose line crosses the viewport while BOTH its endpoint nodes are off-screen — only nodes (and the edges touching at least one visible node) are kept; a long edge spanning two off-screen nodes is culled (rare; matches React Flow)

  • Assuming object-snapping rescans the graph per frame — snapToObjects (default true) precomputes candidate guide lines ONCE at drag start (an O(N) pass) and each drag frame is a flat primitive-array scan; co-dragged nodes of a multi-drag are excluded as candidates (they move rigidly with the pointer). Set snapToObjects: false to skip object snapping entirely when you don't need helper-line alignment

  • Setting explicit width/height on every node "so layout works" — unnecessary: the renderer MEASURES each node's real box (and <Handle> dot centers) and feeds the effective size (explicit → measured → 150×40 default) to edges, layout(), fitView, snap lines, and the minimap. Explicit sizes are only needed for pre-render/headless layout (SSR, standalone computeLayout) — and they OVERRIDE the measurement, so a stale hardcoded size beats the real one

  • Putting pathOptions fields on the wrong edge type — curvature applies to bezier only, borderRadius+offset to smoothstep, offset to step; straight and waypoint routes ignore all of them. Flow-wide defaults go in config.defaultEdgeOptions (per-edge values, including an explicit markerEnd: null, always win)

See also: useFlow · FlowInstance · Flow


useFlow hook

<TData = Record<string, unknown>>(config: FlowConfig<TData>) => FlowInstance<TData>

Component-scoped wrapper around createFlow — identical shape plus an implicit onUnmount(() => flow.dispose()). Prefer inside component bodies; use createFlow directly only for flows owned outside the component tree (app stores, singletons, SSR-shared state) where you'll dispose at the correct lifecycle point yourself.

Example

// Component-scoped flow — auto-disposes when the component unmounts.
// Identical shape to createFlow, plus an implicit onUnmount(() => flow.dispose()).
const MyDiagram = () => {
  const flow = useFlow<WorkflowData>({
    nodes: [{ id: '1', position: { x: 0, y: 0 }, data: { kind: 'trigger', label: 'Start' } }],
    edges: [],
  })
  return (
    <Flow instance={flow}>
      <Background />
    </Flow>
  )
}

Common mistakes

  • Using useFlow outside a component body — the onUnmount hook registration requires an active component setup context, same constraint as every useX hook

  • Using createFlow inside a component and forgetting onUnmount(() => flow.dispose()) — that was the footgun useFlow exists to prevent

  • Storing the returned instance in a module-level variable — bypasses the auto-dispose guarantee; use createFlow for that pattern

See also: createFlow


Flow component

(props: FlowComponentProps) => VNodeChild

Main flow container. Accepts a FlowInstance via the instance prop plus optional nodeTypes / edgeTypes maps for custom renderers, style / class, and ariaLabel (the accessible name for the focusable canvas — defaults to "Flow diagram"; the container is role="group" + tabindex=0, so set a specific name like "Pipeline editor"). Internally uses <For> keyed by node.id plus per-node reactive accessors that read live state from instance.nodes() — each node mounts EXACTLY ONCE across the lifetime of the graph regardless of drags, selection clicks, or updateNode mutations. A 60fps drag in a 1000-node graph stays O(1) per frame. JSX components are NOT generic at the call site (<Flow<MyData> /> is invalid JSX); FlowProps.instance is typed as FlowInstance<any> so typed consumers can pass FlowInstance<MyData> without casting. ACCESSIBILITY (React Flow keyboard model): every node and edge is a focus stop after the canvas — Enter/Space select, the arrow keys move a node by 10 units (Shift: 100, one undo entry per press), Delete/Escape/undo shortcuts bubble to the canvas; nodes are role="group" + aria-roledescription="node" (optional ariaLabel), edges are named buttons, both described by visually-hidden instructions, and a polite live region announces selection changes and keyboard moves. flowStyles themes :focus-visible and honours prefers-reduced-motion. Opt out with focusable: false per element, nodesFocusable / edgesFocusable per default, or disableKeyboardA11y: true.

Example

<Flow instance={flow} ariaLabel="Pipeline editor" nodeTypes={{ custom: MyNode }} edgeTypes={{ arrow: ArrowEdge }}>
  <Background variant="dots" gap={20} />
  <Controls position="bottom-left" />
  <MiniMap nodeColor={(node) => '#6366f1'} />
</Flow>

// Custom node renderer — every prop except id is a REACTIVE ACCESSOR
function MyNode(props: NodeComponentProps<WorkflowData>) {
  return (
    <div
      class={props.selected() ? 'selected' : ''}
      style={() => `cursor: ${props.dragging() ? 'grabbing' : 'grab'}`}
    >
      {props.data().label}
    </div>
  )
}

Common mistakes

  • Setting an inline outline: none on the canvas or nodes to hide the click ring — it hides KEYBOARD focus too; the UA and flowStyles already scope the ring to :focus-visible, so a pointer click draws none.

  • Skipping flowStyles and wondering why the focus ring is the browser default and animated edges still dash under reduced motion — the themed ring and the prefers-reduced-motion rules live in that stylesheet.

  • Asserting keyboard a11y with hasAttribute — a node with focusable: false still HAS tabindex (-1); assert the value.

  • <Flow<MyData> /> is invalid JSX — the component is not generic at the call site; pass a typed FlowInstance<MyData> via instance prop

  • Missing nodeTypes entry for a node.type string — falls through to the default renderer

  • Mutating instance.nodes() return value directly — use instance.addNode / updateNode / removeNode so the internal signals fire

See also: createFlow · Background · Controls · MiniMap · Handle


Background component

(props?: { variant?: "dots" | "lines" | "cross"; gap?: number; size?: number; color?: string }) => VNodeChild

Grid background inside a <Flow>. Place as a direct child. variant is "dots" (default), "lines", or "cross"; gap is the pattern spacing (default 20); size is the dot radius / line thickness (default 1); color sets the pattern color (default "#ddd"). Renders as an SVG <pattern> at the back of the z-order.

Example

<Flow instance={flow}>
  <Background variant="dots" gap={24} size={1} color="#e5e7eb" />
</Flow>

See also: Flow · Controls · MiniMap


Controls component

(props?: { showZoomIn?: boolean; showZoomOut?: boolean; showFitView?: boolean; showLock?: boolean; position?: "top-left" | "top-right" | "bottom-left" | "bottom-right" }) => VNodeChild

Zoom / fit-view button cluster plus a live zoom-level readout. Renders absolutely inside the flow viewport at the configured corner (default "bottom-left"). showZoomIn / showZoomOut / showFitView default to true and dispatch to instance.zoomIn() / zoomOut() / fitView(); showLock (default false) adds a lock/unlock toggle. Each button is title-labelled for accessibility.

Example

<Flow instance={flow}>
  <Controls position="bottom-right" showLock />
</Flow>

See also: Flow · Background · MiniMap


MiniMap component

(props?: { nodeColor?: string | ((node: FlowNode) => string); maskColor?: string; width?: number; height?: number; style?: string; class?: string }) => VNodeChild

Overview minimap of the full graph. nodeColor is a flat color string OR a per-node color function (default a --pyreon-flow-minimap-node CSS var); maskColor fills the area outside the current viewport (default a --pyreon-flow-minimap-mask var). width / height size the minimap box (default 200 × 150). Clicks on the minimap recenter the main viewport.

Example

<Flow instance={flow}>
  <MiniMap nodeColor={(node) => node.data.highlighted ? '#f59e0b' : '#6366f1'} />
</Flow>

See also: Flow · Background · Controls


Handle component

(props: { type: "source" | "target"; position: Position; id?: string; offset?: number; style?: string; class?: string }) => VNodeChild

Connection handle on a custom node — exposes a connectable point that edges attach to. type picks direction ("source" emits edges, "target" receives), position is a Position enum (Top / Right / Bottom / Left). Provide a distinct id when a node has multiple source or target handles so edges can reference the specific one via edge.sourceHandle / edge.targetHandle — the edge then anchors at the dot's MEASURED rendered center (the NodeLayer records every dot via its per-node ResizeObserver), so restyling/repositioning a dot via style / class moves the edge attachment with it. An edge with no handle id uses the node's first handle of the right type; an unknown id anchors at the first handle and dev-warns once naming the known ids.

Example

function CustomNode(props: NodeComponentProps<MyData>) {
  return (
    <div>
      <Handle type="target" position={Position.Left} />
      {props.data().label}
      {/* Same side, distinct offsets (percent along the side) so the dots don't stack */}
      <Handle type="source" position={Position.Right} id="out-primary" offset={30} />
      <Handle type="source" position={Position.Right} id="out-fallback" offset={70} />
    </div>
  )
}

// Edge referencing a specific source handle by id — anchors at that dot's
// MEASURED rendered center, so the offset moves the attachment point too
flow.addEdge({ source: '1', sourceHandle: 'out-primary', target: '2' })

Common mistakes

  • Multiple source / target handles on one node without distinct id values — edges cannot disambiguate which handle they connect to

  • Two handles on the SAME side without distinct offset values — both render at that side's center and visually stack; give same-side siblings distinct offsets (percent along the side, default 50) and the measured-dot anchoring moves each edge attachment with its dot

  • Nesting a <Handle> inside a non-node component (a <Background> child, a <Panel>, etc.) — the connection machinery expects handles to live inside a node renderer

See also: Flow · Position


Panel component

(props: { position?: "top-left" | "top-right" | "bottom-left" | "bottom-right"; style?: string; class?: string; children?: VNodeChild }) => VNodeChild

Overlay panel positioned absolutely relative to the flow viewport. Use for toolbars, legend badges, or contextual action buttons. Pass any JSX as children — the panel is a plain positioned container, not a predefined chrome component. style / class customise the container.

Example

<Flow instance={flow}>
  <Panel position="top-right">
    <button onClick={() => flow.fitView()}>Fit</button>
    <button onClick={() => flow.toJSON()}>Export</button>
  </Panel>
</Flow>

See also: Flow · Controls


NodeResizer component

NodeResizer(props: { nodeId: string; instance: FlowInstance; minWidth?: number; minHeight?: number; handleSize?: number; showEdgeHandles?: boolean }) => VNodeChild

Render drag handles inside a custom node to resize it. Draws absolutely-positioned corner handles (nw/ne/sw/se) — plus edge handles when showEdgeHandles — that drag via pointer-capture (no document listeners), convert the client delta by the current viewport.zoom, and call instance.updateNode(nodeId, { width, height, position }). w/n-side drags also shift position so the opposite edge stays fixed. Defaults: minWidth 50, minHeight 30, handleSize 8px.

Example

import { NodeResizer } from '@pyreon/flow'

const ResizableNode = (props) => (
  <div style={{ position: 'relative' }}>          {/* required — see mistakes */}
    <NodeResizer nodeId={props.id} instance={flow} minWidth={80} />
    {props.data.label}
  </div>
)

Common mistakes

  • Expecting it to read the flow from context like React Flow — it does NOT; you MUST pass instance={flow} (your createFlow() handle) AND nodeId explicitly.

  • Mounting it in a node whose host element is not position: relative — the handles are position: absolute and will anchor to the wrong ancestor. Wrap the node content in a position: relative element.

See also: NodeToolbar · Handle · useFlow


NodeToolbar component

NodeToolbar(props: { nodeId?: string; position?: 'top' | 'bottom' | 'left' | 'right'; align?: 'start' | 'center' | 'end'; offset?: number; showOnSelect?: boolean; selected?: boolean | (() => boolean); style?: string; class?: string; children?: VNodeChild }) => VNodeChild

A floating toolbar placed beside its host node (default position: "top", offset 8px). Returns a REACTIVE thunk that reads selected and renders null when showOnSelect (default true) and the node is not selected — so it shows/hides with live selection. Put action buttons for a node (delete, duplicate, edit) here.

Example

import { NodeToolbar } from '@pyreon/flow'

const NodeWithToolbar = (props) => (
  <div style={{ position: 'relative' }}>
    <NodeToolbar selected={props.selected}>       {/* pass the accessor */}
      <button onClick={() => flow.removeNode(props.id)}>Delete</button>
    </NodeToolbar>
    {props.data.label}
  </div>
)

Common mistakes

  • Expecting it to escape node clipping like React Flow — it is NOT a portal; it renders inline as an absolutely-positioned div, so an ancestor overflow: hidden CLIPS it. The host node must be position: relative.

  • Passing a bare boolean selected={someValue} — that snapshots selection and never updates. Pass the reactive accessor (the custom node's props.selected, which is () => boolean) so show/hide tracks live selection.

See also: NodeResizer · Handle


flowStyles constant

flowStyles: string

The package stylesheet as ONE plain string — every .pyreon-flow-* rule with its --pyreon-flow-* custom-property fallbacks (26 theme vars, all documented under "Theming"). Inject it once per document (<style>{flowStyles}</style>, a useHead style, or your CSS pipeline); it is an inert string with no side effects, so sideEffects: false tree-shakes it out of apps that ship their own CSS. Theme by overriding the variables on an ancestor, never by editing the string.

Example

import { flowStyles } from '@pyreon/flow'

// once, near the root
<style>{flowStyles}</style>
<div style="--pyreon-flow-edge: #7c3aed; --pyreon-flow-node-bg: #111">
  <Flow instance={flow} />
</div>

Common mistakes

  • Forgetting to inject it — nodes render unstyled and edges invisible (the SVG has no stroke), which reads as a geometry bug.

  • Injecting it per <Flow> instance — it is document-scoped; N copies are N identical stylesheets.

See also: Flow · Background


EdgeLabelRenderer component

EdgeLabelRenderer(props: { children?: VNodeChild }) => VNodeChild

HTML edge labels for CUSTOM edges. SVG <text> cannot hold buttons, inputs or wrapped rich text, so a custom edge wraps its label in this component: the children are portaled into a <div> layer INSIDE the viewport (they pan and zoom with the graph) and position themselves with the EdgeComponentProps.labelX / labelY accessors (the built-in label anchor for the same edge). The layer is pointer-events: none; an interactive label opts back in with pointer-events: all plus the nopan class so a click on it does not start a canvas pan. Renders nothing outside a mounted <Flow> and on the server. React Flow <EdgeLabelRenderer>.

Example

function LabeledEdge(props: EdgeComponentProps) {
  return (
    <>
      <path d={() => getBezierPath({ ... }).path} style="fill: none; stroke: #999;" />
      <EdgeLabelRenderer>
        <div
          class="nopan"
          style={() => `position: absolute; pointer-events: all; transform: translate(-50%, -50%) translate(${props.labelX()}px, ${props.labelY()}px);`}
        >
          <button onClick={() => flow.removeEdge(props.edge.id!)}>×</button>
        </div>
      </EdgeLabelRenderer>
    </>
  )
}

Common mistakes

  • Rendering the label as a plain <div> inside the edge — it lands inside the <svg>, which browsers do not render as HTML.

  • Positioning with left/top in pixels — the layer is INSIDE the zoomed viewport, so use a transform: translate(labelX, labelY) in flow units; the viewport transform does the rest.

  • Forgetting pointer-events: all + nopan on an interactive label — the layer ignores the pointer, and a click that reaches the canvas starts a pan.

See also: NodeToolbar · Flow


BaseEdge component

BaseEdge(props: BaseEdgeProps) => VNodeChild

The visible part of a CUSTOM edge: its stroke, markers and an optional label (React Flow <BaseEdge>). path is SVG path data, typically from getBezierPath / getSmoothStepPath; the default stroke is --pyreon-flow-edge at 1.5px, and a style string REPLACES that default wholesale, as on any SVG element. label + labelX + labelY add an <EdgeText>. The flow already wraps every edge in a wider invisible hit path, so BaseEdge draws only what you see. Lowers natively (a native stroke plus label; markerStart / markerEnd are url(#…) references with no native meaning and are reported).

Example

function Wire(props: EdgeComponentProps) {
  const curve = () => getBezierPath({ sourceX: props.sourceX(), sourceY: props.sourceY(), targetX: props.targetX(), targetY: props.targetY() })
  return <BaseEdge path={curve().path} label="wire" labelX={curve().labelX} labelY={curve().labelY} />
}

Common mistakes

  • Passing a style that sets only opacity and expecting the default stroke to remain — style replaces the default declaration, so no stroke means no line.

  • Setting label without labelX / labelY — the label has no position and is not drawn.

  • Reading props.sourceX without calling it — the edge props are accessors (props.sourceX()).

See also: EdgeText · EdgeLabelRenderer · edge-path-helpers


EdgeText component

EdgeText(props: { x: number; y: number; label: string; style?: string }) => VNodeChild

A text label at a point in flow coordinates, in the built-in edge-label style (11px, --pyreon-flow-edge-label). React Flow <EdgeText>. For interactive or rich labels use <EdgeLabelRenderer>, which renders HTML. Lowers natively.

Example

<EdgeText x={edge().labelX} y={edge().labelY} label="42 ms" />

Common mistakes

  • Putting buttons or wrapped text in it — it is SVG <text>; use <EdgeLabelRenderer> for HTML.

See also: BaseEdge · EdgeLabelRenderer


ViewportPortal component

ViewportPortal(props: { children?: VNodeChild }) => VNodeChild

Renders arbitrary HTML in flow coordinates: the children pan and zoom with the graph (React Flow <ViewportPortal>). Position them with a transform: translate(x, y) in flow units. Renders nothing outside a mounted <Flow> and on the server. WEB-ONLY: CSS positioning has no native meaning, so the compiler reports it and drops it; use <EdgeLabelRenderer>, <NodeToolbar> or <Panel> on native.

Example

<Flow instance={flow}>
  <ViewportPortal>
    <div style="position: absolute; transform: translate(200px, 80px)">Annotation</div>
  </ViewportPortal>
</Flow>

Common mistakes

  • Positioning with left / top in screen pixels — the layer is inside the zoomed viewport, so use flow units.

See also: EdgeLabelRenderer · Panel


MarkerType / Position constant

enum MarkerType { Arrow = 'arrow', ArrowClosed = 'arrowclosed' } · enum Position { Top = 'top', Right = 'right', Bottom = 'bottom', Left = 'left' }

The two flow enums. MarkerType is the edge-arrowhead shape — Arrow (open stroked chevron) or ArrowClosed (filled triangle, the default edge-end marker). Position is a node's handle/edge attachment side — Top/Right/Bottom/Left — consumed by the edge-path helpers and getHandlePosition. Both match React Flow's enums exactly.

Example

import { MarkerType, Position } from '@pyreon/flow'

const edges = [{ id: 'e1', source: 'a', target: 'b',
  markerEnd: { type: MarkerType.ArrowClosed } }]

See also: edge-path-helpers · Handle


edge-path-helpers function

getBezierPath / getSmoothStepPath / getStraightPath / getStepPath / getWaypointPath / getEdgePath => { path: string; labelX: number; labelY: number } · getEffectiveDimensions / getFloatingEndpoints / getNodeIntersection / getSmartHandlePositions / resolveHandleAnchor (anchoring) · collectEdgeMarkers / resolveEdgeMarkers / resolveMarker / markerId / DEFAULT_MARKER_END (markers) · DEFAULT_NODE_WIDTH / DEFAULT_NODE_HEIGHT · getHandlePosition / getSmartHandlePositions

SVG-path builders for CUSTOM edge components. getBezierPath, getSmoothStepPath, getStraightPath, getStepPath, getWaypointPath take a single OPTIONS object ({ sourceX, sourceY, sourcePosition?, targetX, targetY, targetPosition?, … }) and return an EdgePathResult object { path, labelX, labelY }. getEdgePath(type, sourceX, sourceY, sourcePos, targetX, targetY, targetPos) is the POSITIONAL-arg dispatcher (unknown type → bezier). getHandlePosition(position, nodeX, nodeY, nodeW, nodeH) returns the { x, y } anchor on a node edge; getSmartHandlePositions(sourceNode, targetNode) auto-picks the closest facing sides.

Example

import { getBezierPath } from '@pyreon/flow'

const MyEdge = (props) => {
  const { path, labelX, labelY } = getBezierPath({
    sourceX: props.sourceX, sourceY: props.sourceY,
    targetX: props.targetX, targetY: props.targetY,
  })
  return <path d={path} />
}

Common mistakes

  • Destructuring the return as a TUPLE (const [path, labelX, labelY] = getBezierPath(...)) — React Flow returns an array, but @pyreon/flow returns an OBJECT { path, labelX, labelY }. Destructure by NAME.

  • Calling getEdgePath / getHandlePosition with an options object — those two take POSITIONAL args (unlike the five object-param helpers). getStraightPath also takes no *Position params.

See also: MarkerType / Position · Handle


computeLayout function

computeLayout<TData>(nodes: FlowNode<TData>[], edges: FlowEdge[], algorithm?: 'layered' | 'force' | 'stress' | 'tree' | 'radial' | 'box' | 'rectpacking', options?: { direction?: 'UP' | 'DOWN' | 'LEFT' | 'RIGHT'; nodeSpacing?: number; layerSpacing?: number; edgeRouting?: 'orthogonal' | 'splines' | 'polyline' }) => Promise<Array<{ id: string; position: { x: number; y: number } }>>

Auto-layout from the built-in engine. Runs algorithm (default layered) over the graph and returns a NEW array of { id, position } pairs (positions only). The engine is PURE and synchronous — the same graph always produces the same positions — but the function stays async because that is the published signature, and it leaves room to move heavy layouts to a worker later without another breaking change.

Example

import { computeLayout } from '@pyreon/flow'

const positioned = await computeLayout(flow.nodes(), flow.edges(), 'layered', { direction: 'DOWN' })
for (const { id, position } of positioned) flow.updateNode(id, { position })

Common mistakes

  • Forgetting to await it — computeLayout returns a Promise even though the engine underneath is synchronous.

  • Expecting it to move your nodes — it does NOT mutate nodes; it returns only { id, position } pairs (no width/height/data). Map the positions back onto your node objects (e.g. via updateNode).

  • Passing direction / layerSpacing / edgeRouting to a non-layered algorithm — those apply only to layered (and direction to tree); ELK silently ignores them (a dev-mode warning fires).

See also: createFlow · useFlow


Package-level notes

Note: LayoutOptions.direction / layerSpacing / edgeRouting apply to layered/tree only — force/stress/radial/box/rectpacking silently ignore them. nodeSpacing is the only field respected by every algorithm. Dev-mode console.warn fires when an option is set on an algorithm that ignores it.

Mount once: Each node mounts exactly once across the lifetime of the graph — drags, selection, and updateNode mutations patch via per-node reactive accessors, not remount.

Peer dep rationale: @pyreon/runtime-dom is required in consumer apps because flow JSX components emit _tpl() / _bind() calls — declare it as a direct dependency, not a transitive one.

JSX generics: Pyreon JSX components cannot be parameterised at the call site (<Flow<MyData> /> is not valid JSX). FlowProps.instance is typed as FlowInstance<any> so typed consumers can pass their FlowInstance<MyData> without casting.

Effective dimensions: Every geometry consumer (edges, layout(), fitView, snap lines, minimap, culling) resolves node size as explicit width/height → MEASURED rendered box → 150×40 default. Nodes need no explicit sizes in the browser; explicit sizes override the measurement, and headless computeLayout (no DOM) sees explicit-or-default only.

Theming: All renderer colors go through --pyreon-flow-* CSS custom properties (25 vars) with light-mode fallbacks — set them on the container to re-skin; SVG colors are applied via style (never presentation attributes, where var() is invalid).

Flow Diagrams — API Reference