28 March 2026

React Flow Auto Layout: How to Automatically Position Nodes for Workflow Editors

React Flow gives you full control over where nodes are placed — which is great until you realize you don't want to place them yourself. For workflow editors, manually positioning every node is tedious and fragile. A layout algorithm handles it for you, so you can focus on the logic instead.

Mini Map

There are established libraries for this — Dagre, ELK, and D3-Hierarchy — but they're designed for specific graph structures and may not fit the needs of a workflow editor. That's why I designed one from scratch.

What the Algorithm Does

I needed a layout algorithm tailored to workflow editors — not a general-purpose graph layout. Specifically, I wanted a function that could take a workflow structure like this:

React Flow workflow diagram showing node A branching into three parallel paths with nested forks that merge back into node C

...automatically calculate each node's position, update the original flow with those coordinates, and return the total dimensions of the workflow:

React Flow workflow diagram showing the calculated x and y coordinates for each node along with the total dimensions of the workflow: L=20, R=24 and H=48

In this article I'll walk through how that algorithm works — the reasoning behind the key decisions, the constraints that shaped the solution, and how all the pieces fit together.

The Structure of a Workflow

Before diving into how the algorithm works, we first need to understand how the workflow is structured.

At its most basic level, the workflow can be seen as a sequence of nodes.

React Flow workflow diagram showing a simple linear sequence of four nodes A, B, C and D connected vertically

At certain points, a node can create a fork, where the flow branches into multiple paths, with each path acting as a separate flow that eventually merges back into the main one.

React Flow workflow diagram showing node A splitting into two parallel branches B-C-D and F-G that merge back into node E

Since each path is itself a flow, it can also create its own fork, forming a recursive structure.

React Flow workflow diagram showing a recursive fork structure where node A splits into two branches and the right branch F further splits into two nested parallel paths G and I that merge back into node E

Code Representation

To represent this workflow in code, we define a TypeScript interface describing what a node looks like.

export interface Node {
  id: string;
  position: {
    x: number;
    y: number;
  };
  size: {
    w: number;
    h: number;
  };
  next: Node[];
  prev: Node[];
}

Configuration

To keep the layout flexible, the distance between nodes is controlled by a configurable gap. This gap defines how much horizontal and vertical spacing should be left between each node.

interface Config {
  gap: {
    x: number;
    y: number;
  };
}

export const config: Config = {
  gap: {
    x: 4,
    y: 4,
  },
};

The Notation We'll Use

Before diving into the algorithm, let's define the notation we'll use throughout the explanation.

Node

Each node is represented by a single letter, such as AA, BB, or CC.

Diagram showing a single workflow node A representing the basic building block of a React Flow workflow

Flow

A flow is identified by the letter of its root node, followed by the word flow.
For example, a flow starting at node AA is written as A flowA\ \mathrm{flow}.

Diagram illustrating the flow notation used in the algorithm, showing A flow as the entire workflow on the left, and on the right how it breaks down into B flow and F flow as two separate nested flows within a fork

Fork

A fork is defined by the letter of the node that creates it, followed by the word fork.
For instance, if node AA creates a fork, we'll refer to it as A forkA\ \mathrm{fork}.

Diagram illustrating the fork notation used in the algorithm, showing A fork as the branching section created by node A containing two parallel paths that merge back into node E

Attributes

We'll use subscripts to represent the geometric attributes of nodes, flows, and forks.

  • AXA_X: The x-coordinate of node AA.
  • AYA_Y: The y-coordinate of node AA.
  • AWA_W: The width of node AA.
  • AHA_H: The height of node AA.
  • ALA_L: The distance from the left edge of node AA to the center of the workflow.
  • ARA_R: The distance from the right edge of node AA to the center of the workflow.

Diagram showing the geometric attributes of a node and its fork in the layout algorithm, with node A at position x=6 y=0 and A fork having left distance of 8, right distance of 12 and height of 20

When we're talking about forks, we use two additional attributes:

  • A forkSA\ \mathrm{fork}_S: The start of the fork — the distance between its left edge and the center of its first flow.
  • A forkEA\ \mathrm{fork}_E: The end of the fork — the distance between its left edge and the center of its last flow.

Diagram showing the start and end attributes of A fork, with A fork start equal to 2 representing the distance to the center of the first flow on the left, and A fork end equal to 14 representing the distance to the center of the last flow on the rights

The Algorithm Explained

Now that the main concepts are clear, we can start exploring how the algorithm works. Throughout this explanation, we’ll use the example below as our reference.

Complex React Flow workflow diagram used as the reference example throughout the article, showing multiple levels of nested forks starting from node A at position x=0 y=0 and ending at node Q

Computing positions

To compute node positions, we can simplify the problem by treating each fork as a single element.

Diagram showing the reference workflow simplified into its two main forks, A fork and I fork, each treated as a single element to make position calculation easier

Next, we need to identify what information is required to calculate the positions of the nodes and forks. Specifically, we'll need the dimensions of the flow and each fork, as shown below:

Diagram showing the calculated dimensions of the reference workflow, with A fork having L=8 R=12 H=20, I fork having L=12 R=8 H=20, and the total workflow dimensions being L=12 R=12 H=68

With these dimensions, computing positions becomes straightforward. The calculations are as follows:

  • AX=X+A flowLAW/2A_X = X + A\ \mathrm{flow}_L - A_W / 2
  • A forkX=X+A flowLA forkLA\ \mathrm{fork}_X = X + A\ \mathrm{flow}_L - A\ \mathrm{fork}_L
  • IX=X+A flowLIW/2I_X = X + A\ \mathrm{flow}_L - I_W / 2
  • I forkX=X+A flowLI forkLI\ \mathrm{fork}_X = X + A\ \mathrm{flow}_L - I\ \mathrm{fork}_L
  • QX=X+A flowLQW/2Q_X = X + A\ \mathrm{flow}_L - Q_W / 2
  • AY=YA_Y = Y
  • A forkY=AY+AH+config.gap.yA\ \mathrm{fork}_Y = A_Y + A_H + config.gap.y
  • IY=A forkY+A forkH+config.gap.yI_Y = A\ \mathrm{fork}_Y + A\ \mathrm{fork}_H + config.gap.y
  • I forkY=IY+IH+config.gap.yI\ \mathrm{fork}_Y = I_Y + I_H + config.gap.y
  • QY=I forkY+I forkH+config.gap.yQ_Y = I\ \mathrm{fork}_Y + I\ \mathrm{fork}_H + config.gap.y

The function responsible for computing these positions can be defined as:

function setPositions(
  node: Node,
  position: {
    x: number;
    y: number;
  },
  dimensions: DimensionsFlow,
) {
  // ...
}

The dimensions object contains the dimensions of the flow and its forks, and is defined as shown below:

interface DimensionsFlow {
  flow: {
    l: number;
    r: number;
    h: number;
  };
  forks: Record<string, DimensionsFork>;
}

interface DimensionsFork {
  fork: {
    l: number;
    r: number;
    h: number;
  };
  flows: Record<string, DimensionsFlow>;
}

Finally, the positions of all nested flows within each fork are computed recursively, with a new call made for each flow using its corresponding parameters.

Diagram showing the positions computed recursively for each nested flow, with B flow starting at x=4 y=8, C flow at x=12 y=8, J flow at x=0 y=40 and K flow at x=16 y=40

Computing dimensions

Before we can compute the positions, we first need to create the dimensions object. This object can be generated using the following function:

function getDimensionsFlow(node: Node): DimensionsFlow {
  // ...
}

This function internally computes the dimensions of each fork by calling:

function getDimensionsFork(node: Node): DimensionsFork {
  // ...
}

Using the data returned from these forks, it then becomes straightforward to calculate the overall dimensions of the flow with the following formulas:

  • A flowL=max(AW/2,A forkL,IW/2,I forkL,QW/2)A\ \mathrm{flow}_L = \max(A_W / 2, A\ \mathrm{fork}_L, I_W / 2, I\ \mathrm{fork}_L, Q_W / 2)
  • A flowR=max(AW/2,A forkR,IW/2,I forkR,QW/2)A\ \mathrm{flow}_R = \max(A_W / 2, A\ \mathrm{fork}_R, I_W / 2, I\ \mathrm{fork}_R, Q_W / 2)
  • A flowH=AH+A forkH+IH+I forkH+QH+4config.gap.yA\ \mathrm{flow}_H = A_H + A\ \mathrm{fork}_H + I_H + I\ \mathrm{fork}_H + Q_H + 4 * config.gap.y

Diagram showing the dimensions of A fork with L=8 R=12 H=20 and I fork with L=12 R=8 H=20 used by the getDimensionsFlow function to calculate the overall workflow layout

The getDimensionsFork function, in turn, calls getDimensionsFlow to compute the dimensions of each flow within the fork. Using this information, it calculates the dimensions of the fork as follows:

  • A forkW=B flowL+B flowR+config.gap.x+C flowL+C flowRA\ \mathrm{fork}_W = B\ \mathrm{flow}_L + B\ \mathrm{flow}_R + config.gap.x + C\ \mathrm{flow}_L + C\ \mathrm{flow}_R
  • A forkS=B flowLA\ \mathrm{fork}_S = B\ \mathrm{flow}_L
  • A forkE=A forkWC flowRA\ \mathrm{fork}_E = A\ \mathrm{fork}_W - C\ \mathrm{flow}_R
  • A forkL=(A forkS+A forkE)/2A\ \mathrm{fork}_L = (A\ \mathrm{fork}_S + A\ \mathrm{fork}_E) / 2
  • A forkR=A forkWA forkLA\ \mathrm{fork}_R = A\ \mathrm{fork}_W - A\ \mathrm{fork}_L
  • A flowH=max(B flowH,C flowH)A\ \mathrm{flow}_H = \max(B\ \mathrm{flow}_H, C\ \mathrm{flow}_H)

Diagram showing how the dimensions of B flow with L=2 R=2 H=20 and C flow with L=6 R=6 H=20 are used by the getDimensionsFork function to compute the dimensions of A fork

Final steps

Now that both functions are defined, the final step is to combine them. The function below uses them together to compute the position of each node and return the overall dimensions of the flow.

export function autoLayout(root: Node): { l: number; r: number; h: number } {
  const dimensions = getDimensionsFlow(root);
  setPositions(root, { x: 0, y: 0 }, dimensions);
  return {
    l: dimensions.flow.l,
    r: dimensions.flow.r,
    h: dimensions.flow.h,
  };
}

Explore the Code

The implementation lives in two GitHub repositories, a Node.js project with just the layout algorithm, and a React project with it integrated into a basic workflow editor built on top of React Flow.

Frequently Asked Questions

React Flow Auto Layout: How to Automatically Position Nodes for Workflow Editors