# React Flow Documentation
What is React Flow?
React Flow is a library that allows you to create interactive, node-based user
interfaces: flowcharts, diagrams, visual programming tools, and workflows inside
your react applications. It supports theming, custom nodes and edges, a library
of shadcn UI components, and offers a large collection of examples for rapid development.
Developers can leverage the React Flow Pro platform for advanced features like
real-time collaboration, complex layouts, and enhanced performance, making it
suitable for both simple and large-scale, production-ready visual applications.
## Learn
### Quick Start
This page will take you from zero to a working React Flow app in a few minutes. If you
just want to have a look around and get an impression of React Flow, check out our
interactive no-code [Playground](https://play.reactflow.dev/).
#### Installation
First, spin up a new React project however you like -- we recommend using
[Vite](https://vitejs.dev/)
```bash copy npm2yarn
npm init vite my-react-flow-app -- --template react
```
Next `cd` into your new project folder and add
[`@xyflow/react`](https://npmjs.com/package/@xyflow/react) as a dependency
```bash copy npm2yarn
npm install @xyflow/react
```
Lastly, spin up the dev server and you're good to go!
#### Usage
We will render the [` `](/api-reference/react-flow) component from the
`@xyflow/react` package. That and defining a handful of [node](/api-reference/types/node)
objects, [edge](/api-reference/types/edge) objects and
[event handlers](/api-reference/react-flow#event-handlers) are all we need to get
something going! Get rid of everything inside `App.jsx` and add the following:
```jsx "import '@xyflow/react/dist/style.css';" "width: '100vw', height: '100vh'"
import { useState, useCallback } from 'react';
import { ReactFlow, applyNodeChanges, applyEdgeChanges, addEdge } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
const initialNodes = [
{ id: 'n1', position: { x: 0, y: 0 }, data: { label: 'Node 1' } },
{ id: 'n2', position: { x: 0, y: 100 }, data: { label: 'Node 2' } },
];
const initialEdges = [{ id: 'n1-n2', source: 'n1', target: 'n2' }];
export default function App() {
const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState(initialEdges);
const onNodesChange = useCallback(
(changes) => setNodes((nodesSnapshot) => applyNodeChanges(changes, nodesSnapshot)),
[],
);
const onEdgesChange = useCallback(
(changes) => setEdges((edgesSnapshot) => applyEdgeChanges(changes, edgesSnapshot)),
[],
);
const onConnect = useCallback(
(params) => setEdges((edgesSnapshot) => addEdge(params, edgesSnapshot)),
[],
);
return (
`. This prop takes an object mapping message keys to strings or functions. Keys include:
| Key | Default Value |
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `node.a11yDescription.default` | Press enter or space to select a node. Press delete to remove it and escape to cancel. |
| `node.a11yDescription.keyboardDisabled` | Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel. |
| `node.a11yDescription.ariaLiveMessage` | `Moved selected node {direction}. New position, x: {x}, y: {y}` |
| `edge.a11yDescription.default` | Press enter or space to select an edge. You can then press delete to remove it or escape to cancel. |
| `controls.ariaLabel` | Control Panel |
| `controls.zoomIn.ariaLabel` | Zoom In |
| `controls.zoomOut.ariaLabel` | Zoom Out |
| `controls.fitView.ariaLabel` | Fit View |
| `controls.interactive.ariaLabel` | Toggle Interactivity |
| `minimap.ariaLabel` | Mini Map |
| `handle.ariaLabel` | Handle |
For example, to provide custom or localized text:
```js
const ariaLabels = {
'node.a11yDescription.default': 'Press [Enter] to select this node',
'node.a11yDescription.keyboardDisabled': 'Keyboard navigation is disabled',
};
;
```
This tells React Flow to use your text instead of the defaults. By supplying localized strings via [ariaLabelConfig](/api-reference/react-flow#arialabelconfig), you ensure screen readers announce messages in the user’s language.
#### WCAG 2.1 AA
React Flow provides features that can help you meet key WCAG 2.1 AA criteria when properly implemented:
* **Keyboard:** React Flow supports keyboard operability with `Tab` navigation to nodes and edges, interaction via `Enter`/`Space`, and arrow key movement for nodes. These features help satisfy requirements for keyboard accessibility.
* **Screen Reader:** With semantic ARIA roles and labels (e.g. `role="group"`, `aria-label`, and `aria-roledescription`), React Flow enables you to create meaningfully announced graphical nodes/edges. Edge components include a customizable `aria-label` and nodes can be given appropriate `aria-label` text.
* **ARIA Live Regions:** Dynamic updates are announced through an `aria-live` region. The `A11yDescriptions` component includes an element with `aria-live="assertive"` that notifies users of node movements, helping you meet requirements for status messages.
* **Instructions and Focus Management:** React Flow provides contextual help with clear instructions like "Press enter or space to select a node…". The automatic focus management ensures nodes scroll into view when focused, helping satisfy requirements for input assistance.
This guide is helpful for learning about [ARIA best practices](https://www.w3.org/WAI/ARIA/apg/practices/read-me-first/).
### Computing Flows
For this guide we assume that you already know about the [core
concepts](/learn/concepts/core-concepts) of React Flow and how to implement
[custom nodes](/learn/customization/custom-nodes).
Usually with React Flow, developers handle their data outside of React Flow by sending it somewhere else, like on a server or a database. Instead, in this guide we'll show you how to compute data flows directly inside of React Flow. You can use this for updating a node based on connected data, or for building an app that runs entirely inside the browser.
#### What are we going to build?
By the end of this guide, you will build an interactive flow graph that generates a color out of three separate number input fields (red, green and blue), and determines whether white or black text would be more readable on that background color.
Example: learn/computing-6
##### App.jsx
```jsx
import { useCallback } from 'react';
import {
ReactFlow,
Background,
useNodesState,
useEdgesState,
addEdge,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import NumberInput from './NumberInput';
import ColorPreview from './ColorPreview';
import Lightness from './Lightness';
import Log from './Log';
const nodeTypes = {
NumberInput,
ColorPreview,
Lightness,
Log,
};
const initialNodes = [
{
type: 'NumberInput',
id: '1',
data: { label: 'Red', value: 255 },
position: { x: 0, y: 0 },
},
{
type: 'NumberInput',
id: '2',
data: { label: 'Green', value: 0 },
position: { x: 0, y: 100 },
},
{
type: 'NumberInput',
id: '3',
data: { label: 'Blue', value: 115 },
position: { x: 0, y: 200 },
},
{
type: 'ColorPreview',
id: 'color',
position: { x: 150, y: 50 },
data: {
label: 'Color',
value: { r: undefined, g: undefined, b: undefined },
},
},
{
type: 'Lightness',
id: 'lightness',
position: { x: 350, y: 75 },
},
{
id: 'log-1',
type: 'Log',
position: { x: 500, y: 0 },
data: { label: 'Use black font', fontColor: 'black' },
},
{
id: 'log-2',
type: 'Log',
position: { x: 500, y: 140 },
data: { label: 'Use white font', fontColor: 'white' },
},
];
const initialEdges = [
{
id: '1-color',
source: '1',
target: 'color',
targetHandle: 'red',
},
{
id: '2-color',
source: '2',
target: 'color',
targetHandle: 'green',
},
{
id: '3-color',
source: '3',
target: 'color',
targetHandle: 'blue',
},
{
id: 'color-lightness',
source: 'color',
target: 'lightness',
},
{
id: 'lightness-log-1',
source: 'lightness',
sourceHandle: 'light',
target: 'log-1',
},
{
id: 'lightness-log-2',
source: 'lightness',
sourceHandle: 'dark',
target: 'log-2',
},
];
function ReactiveFlow() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(params) => setEdges((eds) => addEdge(params, eds)),
[],
);
return (
);
}
export default ReactiveFlow;
```
##### ColorPreview\.jsx
```jsx
import { useEffect } from 'react';
import {
Handle,
Position,
useNodeConnections,
useNodesData,
useReactFlow,
} from '@xyflow/react';
function CustomHandle({ id, label, onChange }) {
const connections = useNodeConnections({
handleType: 'target',
handleId: id,
});
const nodeData = useNodesData(connections?.[0].source);
useEffect(() => {
onChange(nodeData?.data ? nodeData.data.value : 0);
}, [nodeData]);
return (
{label}
);
}
function ColorPreview({ id, data }) {
const { updateNodeData } = useReactFlow();
return (
{
updateNodeData(id, (node) => {
return { value: { ...node.data.value, r: value } };
});
}}
/>
{
updateNodeData(id, (node) => {
return { value: { ...node.data.value, g: value } };
});
}}
/>
{
updateNodeData(id, (node) => {
return { value: { ...node.data.value, b: value } };
});
}}
/>
);
}
export default ColorPreview;
```
##### Lightness.jsx
```jsx
import { useState, useEffect } from 'react';
import {
Handle,
Position,
useNodeConnections,
useNodesData,
useReactFlow,
} from '@xyflow/react';
function LightnessNode({ id }) {
const { updateNodeData } = useReactFlow();
const connections = useNodeConnections({ handleType: 'target' });
const nodesData = useNodesData(connections?.[0].source);
const [lightness, setLightness] = useState('dark');
useEffect(() => {
if (nodesData.data?.value) {
const color = nodesData.data.value;
const isLight =
0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b >= 128;
setLightness(isLight ? 'light' : 'dark');
const newNodeData = isLight
? { light: color, dark: null }
: { light: null, dark: color };
updateNodeData(id, newNodeData);
} else {
setLightness('dark');
updateNodeData(id, { light: null, dark: { r: 0, g: 0, b: 0 } });
}
}, [nodesData, updateNodeData]);
return (
);
}
export default LightnessNode;
```
##### Log.jsx
```jsx
import { Handle, useNodeConnections, useNodesData } from '@xyflow/react';
function Log({ data }) {
const connections = useNodeConnections({ handleType: 'target' });
const nodeData = useNodesData(connections?.[0].source);
const color = nodeData.data
? nodeData.data[connections?.[0].sourceHandle]
: null;
return (
{color ? data.label : 'Do nothing'}
);
}
export default Log;
```
##### NumberInput.jsx
```jsx
import { useCallback, useState } from 'react';
import { Handle, Position, useReactFlow } from '@xyflow/react';
function NumberInput({ id, data }) {
const { updateNodeData } = useReactFlow();
const [number, setNumber] = useState(data.value);
const onChange = useCallback((evt) => {
const cappedNumber = Math.min(255, Math.max(0, evt.target.value));
setNumber(cappedNumber);
updateNodeData(id, { value: cappedNumber });
}, []);
return (
);
}
export default NumberInput;
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
.react-flow__node {
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.12);
border-radius: 10px;
font-size: 12px;
}
.number-input {
padding: 10px;
background: #fff;
border: 1px solid #eee;
border-radius: 10px;
}
.node {
height: 150px;
width: 150px;
display: flex;
flex-direction: column;
justify-content: space-around;
border-radius: 10px;
}
.handle {
position: relative;
top: 15px;
}
.label {
margin-left: 10px;
mix-blend-mode: difference;
color: white;
font-weight: bold;
}
.lightness-node {
width: 100px;
height: 100px;
display: flex;
flex-direction: column;
align-items: end;
justify-content: center;
text-align: center;
border-radius: 10px;
}
.log-node {
width: 80px;
height: 80px;
word-wrap: break-word;
padding: 5px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
#### Creating custom nodes
Let's start by creating a custom input node (`NumberInput.js`) and add three instances of it. We will be using a controlled ` ` and limit it to integer numbers between 0 - 255 inside the `onChange` event handler.
```jsx
import { useCallback, useState } from 'react';
import { Handle, Position } from '@xyflow/react';
function NumberInput({ id, data }) {
const [number, setNumber] = useState(0);
const onChange = useCallback((evt) => {
const cappedNumber = Math.round(
Math.min(255, Math.max(0, evt.target.value)),
);
setNumber(cappedNumber);
}, []);
return (
);
}
export default NumberInput;
```
Next, we'll add a new custom node (`ColorPreview.js`) with one target handle for each color channel and a background that displays the resulting color. We can use `mix-blend-mode: 'difference';` to make the text color always readable.
Whenever you have multiple handles of the same kind on a single node, don't
forget to give each one a separate id!
Let's also add edges going from the input nodes to the color node to our
`initialEdges` array while we are at it.
Example: learn/computing
##### App.jsx
```jsx
import { useCallback } from 'react';
import {
ReactFlow,
Background,
useNodesState,
useEdgesState,
addEdge,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import NumberInput from './NumberInput';
import ColorPreview from './ColorPreview';
const nodeTypes = {
NumberInput,
ColorPreview,
};
const initialNodes = [
{
type: 'NumberInput',
id: '1',
data: { label: 'Red' },
position: { x: 0, y: 0 },
},
{
type: 'NumberInput',
id: '2',
data: { label: 'Green' },
position: { x: 0, y: 100 },
},
{
type: 'NumberInput',
id: '3',
data: { label: 'Blue' },
position: { x: 0, y: 200 },
},
{
type: 'ColorPreview',
id: 'color',
position: { x: 150, y: 50 },
data: { label: 'Color' },
},
];
const initialEdges = [
{
id: '1-color',
source: '1',
target: 'color',
targetHandle: 'red',
},
{
id: '2-color',
source: '2',
target: 'color',
targetHandle: 'green',
},
{
id: '3-color',
source: '3',
target: 'color',
targetHandle: 'blue',
},
];
function ReactiveFlow() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(params) => setEdges((eds) => addEdge(params, eds)),
[],
);
return (
);
}
export default ReactiveFlow;
```
##### ColorPreview\.jsx
```jsx
import { Handle, Position } from '@xyflow/react';
function ColorPreview() {
const color = { r: 0, g: 0, b: 0 };
return (
);
}
export default ColorPreview;
```
##### NumberInput.jsx
```jsx
import { useCallback, useState } from 'react';
import { Handle, Position } from '@xyflow/react';
function NumberInput({ id, data }) {
const [number, setNumber] = useState(0);
const onChange = useCallback((evt) => {
const cappedNumber = Math.round(
Math.min(255, Math.max(0, evt.target.value)),
);
setNumber(cappedNumber);
}, []);
return (
);
}
export default NumberInput;
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
.react-flow__node {
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.12);
border-radius: 10px;
font-size: 12px;
}
.number-input {
padding: 10px;
background: #fff;
border: 1px solid #eee;
border-radius: 10px;
}
.node {
height: 150px;
width: 150px;
display: flex;
flex-direction: column;
justify-content: space-around;
border-radius: 10px;
}
.handle {
position: relative;
top: 15px;
}
.label {
margin-left: 10px;
mix-blend-mode: difference;
color: white;
font-weight: bold;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
#### Computing data
How do we get the data from the input nodes to the color node? This is a two step process that involves two hooks created for this exact purpose:
1. Store each number input value inside the node's `data` object with help of the [`updateNodeData`](/api-reference/types/react-flow-instance#update-node-data) callback.
2. Find out which nodes are connected by using [`useNodeConnections`](/api-reference/hooks/use-node-connections) and then use [`useNodesData`](/api-reference/hooks/use-nodes-data) for receiving the data from the connected nodes.
##### Step 1: Writing values to the data object
First let's add some initial values for the input nodes inside the `data` object in our `initialNodes` array and use them as an initial state for the input nodes.
Then we'll grab the function [`updateNodeData`](/api-reference/types/react-flow-instance#update-node-data) from the [`useReactFlow`](/api-reference/hooks/use-react-flow) hook and use it to update the `data` object of the node with a new value whenever the input changes.
By default, the data you pass to [`updateNodeData`](/api-reference/types/react-flow-instance#update-node-data) will be merged with the old data object. This makes it easier to do partial updates and saves you in case you forget to add `{...data}`. You can pass `{ replace: true }` as an option to replace the object instead.
Example: learn/computing-2
##### App.jsx
```jsx
import { useCallback } from 'react';
import {
ReactFlow,
Background,
useNodesState,
useEdgesState,
addEdge,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import NumberInput from './NumberInput';
import ColorPreview from './ColorPreview';
const nodeTypes = {
NumberInput,
ColorPreview,
};
const initialNodes = [
{
type: 'NumberInput',
id: '1',
data: { label: 'Red', value: 255 },
position: { x: 0, y: 0 },
},
{
type: 'NumberInput',
id: '2',
data: { label: 'Green', value: 0 },
position: { x: 0, y: 100 },
},
{
type: 'NumberInput',
id: '3',
data: { label: 'Blue', value: 115 },
position: { x: 0, y: 200 },
},
{
type: 'ColorPreview',
id: 'color',
position: { x: 150, y: 50 },
data: { label: 'Color' },
},
];
const initialEdges = [
{
id: '1-color',
source: '1',
target: 'color',
targetHandle: 'red',
},
{
id: '2-color',
source: '2',
target: 'color',
targetHandle: 'green',
},
{
id: '3-color',
source: '3',
target: 'color',
targetHandle: 'blue',
},
];
function ReactiveFlow() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(params) => setEdges((eds) => addEdge(params, eds)),
[],
);
return (
);
}
export default ReactiveFlow;
```
##### ColorPreview\.jsx
```jsx
import { Handle, Position } from '@xyflow/react';
function ColorPreview() {
const color = { r: 0, g: 0, b: 0 };
return (
);
}
export default ColorPreview;
```
##### NumberInput.jsx
```jsx
import { useCallback, useState } from 'react';
import { Handle, Position, useReactFlow } from '@xyflow/react';
function NumberInput({ id, data }) {
const { updateNodeData } = useReactFlow();
const [number, setNumber] = useState(data.value);
const onChange = useCallback((evt) => {
const cappedNumber = Math.min(255, Math.max(0, evt.target.value));
setNumber(cappedNumber);
updateNodeData(id, { value: cappedNumber });
}, []);
return (
);
}
export default NumberInput;
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
.react-flow__node {
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.12);
border-radius: 10px;
font-size: 12px;
}
.number-input {
padding: 10px;
background: #fff;
border: 1px solid #eee;
border-radius: 10px;
}
.node {
height: 150px;
width: 150px;
display: flex;
flex-direction: column;
justify-content: space-around;
border-radius: 10px;
}
.handle {
position: relative;
top: 15px;
}
.label {
margin-left: 10px;
mix-blend-mode: difference;
color: white;
font-weight: bold;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
When dealing with input fields you don't want to use a nodes `data` object
as UI state directly.
There is a delay in updating the data object and the cursor might jump around
erratically and lead to unwanted inputs.
##### Step 2: Getting data from connected nodes
We start by determining all connections for each node with the [`useNodeConnections`](/api-reference/hooks/use-node-connections) hook and then fetching the data for the first connected node with [`updateNodeData`](/api-reference/types/react-flow-instance#update-node-data).
Note that each handle can have multiple nodes connected to it and you might
want to restrict the number of connections to a single handle inside your
application. Check out the [connection limit
example](/examples/nodes/connection-limit) to see how to do that.
And there you go! Try changing the input values and see the color change
in real time.
Example: learn/computing-3
##### App.jsx
```jsx
import { useCallback } from 'react';
import {
ReactFlow,
Background,
useNodesState,
useEdgesState,
addEdge,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import NumberInput from './NumberInput';
import ColorPreview from './ColorPreview';
const nodeTypes = {
NumberInput,
ColorPreview,
};
const initialNodes = [
{
type: 'NumberInput',
id: '1',
data: { label: 'Red', value: 255 },
position: { x: 0, y: 0 },
},
{
type: 'NumberInput',
id: '2',
data: { label: 'Green', value: 0 },
position: { x: 0, y: 100 },
},
{
type: 'NumberInput',
id: '3',
data: { label: 'Blue', value: 115 },
position: { x: 0, y: 200 },
},
{
type: 'ColorPreview',
id: 'color',
position: { x: 150, y: 50 },
data: { label: 'Color' },
},
];
const initialEdges = [
{
id: '1-color',
source: '1',
target: 'color',
targetHandle: 'red',
},
{
id: '2-color',
source: '2',
target: 'color',
targetHandle: 'green',
},
{
id: '3-color',
source: '3',
target: 'color',
targetHandle: 'blue',
},
];
function ReactiveFlow() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(params) => setEdges((eds) => addEdge(params, eds)),
[],
);
return (
);
}
export default ReactiveFlow;
```
##### ColorPreview\.jsx
```jsx
import {
Handle,
Position,
useNodesData,
useNodeConnections,
} from '@xyflow/react';
function ColorPreview() {
const redConnections = useNodeConnections({
handleType: 'target',
handleId: 'red',
});
const redNodeData = useNodesData(redConnections?.[0].source);
const greenConnections = useNodeConnections({
handleType: 'target',
handleId: 'green',
});
const greenNodeData = useNodesData(greenConnections?.[0].source);
const blueConnections = useNodeConnections({
handleType: 'target',
handleId: 'blue',
});
const blueNodeData = useNodesData(blueConnections?.[0].source);
const color = {
r: redNodeData?.data ? redNodeData.data.value : 0,
g: greenNodeData?.data ? greenNodeData.data.value : 0,
b: blueNodeData?.data ? blueNodeData.data.value : 0,
};
return (
);
}
export default ColorPreview;
```
##### NumberInput.jsx
```jsx
import { useCallback, useState } from 'react';
import { Handle, Position, useReactFlow } from '@xyflow/react';
function NumberInput({ id, data }) {
const { updateNodeData } = useReactFlow();
const [number, setNumber] = useState(data.value);
const onChange = useCallback((evt) => {
const cappedNumber = Math.min(255, Math.max(0, evt.target.value));
setNumber(cappedNumber);
updateNodeData(id, { value: cappedNumber });
}, []);
return (
);
}
export default NumberInput;
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
.react-flow__node {
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.12);
border-radius: 10px;
font-size: 12px;
}
.number-input {
padding: 10px;
background: #fff;
border: 1px solid #eee;
border-radius: 10px;
}
.node {
height: 150px;
width: 150px;
display: flex;
flex-direction: column;
justify-content: space-around;
border-radius: 10px;
}
.handle {
position: relative;
top: 15px;
}
.label {
margin-left: 10px;
mix-blend-mode: difference;
color: white;
font-weight: bold;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### Improving the code
It might seem awkward to get the connections first, and then the data separately for each handle. For nodes with multiple handles like these, you should consider creating a custom handle component that isolates connection states and node data binding. We can create one inline.
```jsx filename="ColorPreview.js"
// {...}
function CustomHandle({ id, label, onChange }) {
const connections = useNodeConnections({
handleType: 'target',
handleId: id,
});
const nodeData = useNodesData(connections?.[0].source);
useEffect(() => {
onChange(nodeData?.data ? nodeData.data.value : 0);
}, [nodeData]);
return (
{label}
);
}
```
We can promote color to local state and declare each handle like this:
```jsx filename="ColorPreview.js"
// {...}
function ColorPreview() {
const [color, setColor] = useState({ r: 0, g: 0, b: 0 });
return (
setColor((c) => ({ ...c, r: value }))}
/>
setColor((c) => ({ ...c, g: value }))}
/>
setColor((c) => ({ ...c, b: value }))}
/>
);
}
export default ColorPreview;
```
#### Getting more complex
Now we have a simple example of how to pipe data through React Flow. What if we want to do something more complex, like transforming the data along the way? Or even take different paths? We can do that too!
##### Continuing the flow
Let's extend our flow. Start by adding an output ` ` to the color node and remove the local component state.
Because there are no inputs fields on this node, we don't need to keep a local
state at all. We can just read and update the node's `data` object directly.
Next, we add a new node (`Lightness.js`) that takes in a color object and determines if it is either a light or dark color. We can use the [relative luminance formula](https://en.wikipedia.org/wiki/Relative_luminance#Relative_luminance_and_%22gamma_encoded%22_colorspaces)
`luminance = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b`
to calculate the perceived brightness of a color (0 being the darkest and 255 being the brightest). We can assume everything >= 128 is a light color.
Example: learn/computing-4
##### App.jsx
```jsx
import { useCallback } from 'react';
import {
ReactFlow,
Background,
useNodesState,
useEdgesState,
addEdge,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import NumberInput from './NumberInput';
import ColorPreview from './ColorPreview';
import Lightness from './Lightness';
const nodeTypes = {
NumberInput,
ColorPreview,
Lightness,
};
const initialNodes = [
{
type: 'NumberInput',
id: '1',
data: { label: 'Red', value: 255 },
position: { x: 0, y: 0 },
},
{
type: 'NumberInput',
id: '2',
data: { label: 'Green', value: 0 },
position: { x: 0, y: 100 },
},
{
type: 'NumberInput',
id: '3',
data: { label: 'Blue', value: 115 },
position: { x: 0, y: 200 },
},
{
type: 'ColorPreview',
id: 'color',
position: { x: 150, y: 50 },
data: {
label: 'Color',
value: { r: undefined, g: undefined, b: undefined },
},
},
{
type: 'Lightness',
id: 'lightness',
position: { x: 350, y: 75 },
},
];
const initialEdges = [
{
id: '1-color',
source: '1',
target: 'color',
targetHandle: 'red',
},
{
id: '2-color',
source: '2',
target: 'color',
targetHandle: 'green',
},
{
id: '3-color',
source: '3',
target: 'color',
targetHandle: 'blue',
},
{
id: 'color-lightness',
source: 'color',
target: 'lightness',
},
];
function ReactiveFlow() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(params) => setEdges((eds) => addEdge(params, eds)),
[],
);
return (
);
}
export default ReactiveFlow;
```
##### ColorPreview\.jsx
```jsx
import { useEffect } from 'react';
import {
Handle,
Position,
useNodeConnections,
useNodesData,
useReactFlow,
} from '@xyflow/react';
function CustomHandle({ id, label, onChange }) {
const connections = useNodeConnections({
handleType: 'target',
handleId: id,
});
const nodeData = useNodesData(connections?.[0].source);
useEffect(() => {
onChange(nodeData?.data ? nodeData.data.value : 0);
}, [nodeData]);
return (
{label}
);
}
function ColorPreview({ id, data }) {
const { updateNodeData } = useReactFlow();
return (
{
updateNodeData(id, (node) => {
return { value: { ...node.data.value, r: value } };
});
}}
/>
{
updateNodeData(id, (node) => {
return { value: { ...node.data.value, g: value } };
});
}}
/>
{
updateNodeData(id, (node) => {
return { value: { ...node.data.value, b: value } };
});
}}
/>
);
}
export default ColorPreview;
```
##### Lightness.jsx
```jsx
import { useState, useEffect } from 'react';
import {
Handle,
Position,
useNodeConnections,
useNodesData,
} from '@xyflow/react';
function LightnessNode() {
const connections = useNodeConnections({ handleType: 'target' });
const nodesData = useNodesData(connections?.[0].source);
const [lightness, setLightness] = useState('dark');
useEffect(() => {
if (nodesData?.data) {
const color = nodesData.data.value;
setLightness(
0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b >= 128
? 'light'
: 'dark',
);
} else {
setLightness('dark');
}
}, [nodesData]);
return (
This color is
{lightness}
);
}
export default LightnessNode;
```
##### NumberInput.jsx
```jsx
import { useCallback, useState } from 'react';
import { Handle, Position, useReactFlow } from '@xyflow/react';
function NumberInput({ id, data }) {
const { updateNodeData } = useReactFlow();
const [number, setNumber] = useState(data.value);
const onChange = useCallback((evt) => {
const cappedNumber = Math.min(255, Math.max(0, evt.target.value));
setNumber(cappedNumber);
updateNodeData(id, { value: cappedNumber });
}, []);
return (
);
}
export default NumberInput;
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
.react-flow__node {
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.12);
border-radius: 10px;
font-size: 12px;
}
.number-input {
padding: 10px;
background: #fff;
border: 1px solid #eee;
border-radius: 10px;
}
.node {
height: 150px;
width: 150px;
display: flex;
flex-direction: column;
justify-content: space-around;
border-radius: 10px;
}
.handle {
position: relative;
top: 15px;
}
.label {
margin-left: 10px;
mix-blend-mode: difference;
color: white;
font-weight: bold;
}
.lightness-node {
width: 100px;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
border-radius: 10px;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### Conditional branching
What if we would like to take a different path in our flow based on the perceived lightness? Let's give our lightness node two source handles `light` and `dark` and separate the node `data` object by source handle IDs. This is needed if you have multiple source handles to distinguish between each source handle's data.
But what does it mean to "take a different route"? One solution would be to assume that `null` or `undefined` data hooked up to a target handle is considered a "stop". In our case we can write the incoming color into `data.values.light` if it's a light color and into `data.values.dark` if it's a dark color and set the respective other value to `null`.
Don't forget to add `flex-direction: column;` and `align-items: end;` to reposition the handle labels.
Example: learn/computing-5
##### App.jsx
```jsx
import { useCallback } from 'react';
import {
ReactFlow,
Background,
useNodesState,
useEdgesState,
addEdge,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import NumberInput from './NumberInput';
import ColorPreview from './ColorPreview';
import Lightness from './Lightness';
const nodeTypes = {
NumberInput,
ColorPreview,
Lightness,
};
const initialNodes = [
{
type: 'NumberInput',
id: '1',
data: { label: 'Red', value: 255 },
position: { x: 0, y: 0 },
},
{
type: 'NumberInput',
id: '2',
data: { label: 'Green', value: 0 },
position: { x: 0, y: 100 },
},
{
type: 'NumberInput',
id: '3',
data: { label: 'Blue', value: 115 },
position: { x: 0, y: 200 },
},
{
type: 'ColorPreview',
id: 'color',
position: { x: 150, y: 50 },
data: {
label: 'Color',
value: { r: undefined, g: undefined, b: undefined },
},
},
{
type: 'Lightness',
id: 'lightness',
position: { x: 350, y: 75 },
},
];
const initialEdges = [
{
id: '1-color',
source: '1',
target: 'color',
targetHandle: 'red',
},
{
id: '2-color',
source: '2',
target: 'color',
targetHandle: 'green',
},
{
id: '3-color',
source: '3',
target: 'color',
targetHandle: 'blue',
},
{
id: 'color-lightness',
source: 'color',
target: 'lightness',
},
];
function ReactiveFlow() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(params) => setEdges((eds) => addEdge(params, eds)),
[],
);
return (
);
}
export default ReactiveFlow;
```
##### ColorPreview\.jsx
```jsx
import { useEffect } from 'react';
import {
Handle,
Position,
useNodeConnections,
useNodesData,
useReactFlow,
} from '@xyflow/react';
function CustomHandle({ id, label, onChange }) {
const connections = useNodeConnections({
type: 'target',
handleId: id,
});
const nodeData = useNodesData(connections?.[0].source);
useEffect(() => {
onChange(nodeData?.data ? nodeData.data.value : 0);
}, [nodeData]);
return (
{label}
);
}
function ColorPreview({ id, data }) {
const { updateNodeData } = useReactFlow();
return (
{
updateNodeData(id, (node) => {
return { value: { ...node.data.value, r: value } };
});
}}
/>
{
updateNodeData(id, (node) => {
return { value: { ...node.data.value, g: value } };
});
}}
/>
{
updateNodeData(id, (node) => {
return { value: { ...node.data.value, b: value } };
});
}}
/>
);
}
export default ColorPreview;
```
##### Lightness.jsx
```jsx
import { useState, useEffect } from 'react';
import {
Handle,
Position,
useNodeConnections,
useNodesData,
useReactFlow,
} from '@xyflow/react';
function LightnessNode({ id }) {
const { updateNodeData } = useReactFlow();
const connections = useNodeConnections({ handleType: 'target' });
const nodesData = useNodesData(connections?.[0].source);
const [lightness, setLightness] = useState('dark');
useEffect(() => {
if (nodesData?.data) {
const color = nodesData.data.value;
const isLight =
0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b >= 128;
setLightness(isLight ? 'light' : 'dark');
const newNodeData = isLight
? { light: color, dark: null }
: { light: null, dark: color };
updateNodeData(id, newNodeData);
} else {
setLightness('dark');
updateNodeData(id, { light: null, dark: { r: 0, g: 0, b: 0 } });
}
}, [nodesData, updateNodeData]);
return (
);
}
export default LightnessNode;
```
##### NumberInput.jsx
```jsx
import { useCallback, useState } from 'react';
import { Handle, Position, useReactFlow } from '@xyflow/react';
function NumberInput({ id, data }) {
const { updateNodeData } = useReactFlow();
const [number, setNumber] = useState(data.value);
const onChange = useCallback((evt) => {
const cappedNumber = Math.min(255, Math.max(0, evt.target.value));
setNumber(cappedNumber);
updateNodeData(id, { value: cappedNumber });
}, []);
return (
);
}
export default NumberInput;
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
.react-flow__node {
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.12);
border-radius: 10px;
font-size: 12px;
}
.number-input {
padding: 10px;
background: #fff;
border: 1px solid #eee;
border-radius: 10px;
}
.node {
height: 150px;
width: 150px;
display: flex;
flex-direction: column;
justify-content: space-around;
border-radius: 10px;
}
.handle {
position: relative;
top: 15px;
}
.label {
margin-left: 10px;
mix-blend-mode: difference;
color: white;
font-weight: bold;
}
.lightness-node {
width: 100px;
height: 100px;
display: flex;
flex-direction: column;
align-items: end;
justify-content: center;
text-align: center;
border-radius: 10px;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
Cool! Now we only need a last node to see if it actually works... We can create a custom debugging node (`Log.js`) that displays the hooked up data, and we're done!
Example: learn/computing-6
##### App.jsx
```jsx
import { useCallback } from 'react';
import {
ReactFlow,
Background,
useNodesState,
useEdgesState,
addEdge,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import NumberInput from './NumberInput';
import ColorPreview from './ColorPreview';
import Lightness from './Lightness';
import Log from './Log';
const nodeTypes = {
NumberInput,
ColorPreview,
Lightness,
Log,
};
const initialNodes = [
{
type: 'NumberInput',
id: '1',
data: { label: 'Red', value: 255 },
position: { x: 0, y: 0 },
},
{
type: 'NumberInput',
id: '2',
data: { label: 'Green', value: 0 },
position: { x: 0, y: 100 },
},
{
type: 'NumberInput',
id: '3',
data: { label: 'Blue', value: 115 },
position: { x: 0, y: 200 },
},
{
type: 'ColorPreview',
id: 'color',
position: { x: 150, y: 50 },
data: {
label: 'Color',
value: { r: undefined, g: undefined, b: undefined },
},
},
{
type: 'Lightness',
id: 'lightness',
position: { x: 350, y: 75 },
},
{
id: 'log-1',
type: 'Log',
position: { x: 500, y: 0 },
data: { label: 'Use black font', fontColor: 'black' },
},
{
id: 'log-2',
type: 'Log',
position: { x: 500, y: 140 },
data: { label: 'Use white font', fontColor: 'white' },
},
];
const initialEdges = [
{
id: '1-color',
source: '1',
target: 'color',
targetHandle: 'red',
},
{
id: '2-color',
source: '2',
target: 'color',
targetHandle: 'green',
},
{
id: '3-color',
source: '3',
target: 'color',
targetHandle: 'blue',
},
{
id: 'color-lightness',
source: 'color',
target: 'lightness',
},
{
id: 'lightness-log-1',
source: 'lightness',
sourceHandle: 'light',
target: 'log-1',
},
{
id: 'lightness-log-2',
source: 'lightness',
sourceHandle: 'dark',
target: 'log-2',
},
];
function ReactiveFlow() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(params) => setEdges((eds) => addEdge(params, eds)),
[],
);
return (
);
}
export default ReactiveFlow;
```
##### ColorPreview\.jsx
```jsx
import { useEffect } from 'react';
import {
Handle,
Position,
useNodeConnections,
useNodesData,
useReactFlow,
} from '@xyflow/react';
function CustomHandle({ id, label, onChange }) {
const connections = useNodeConnections({
handleType: 'target',
handleId: id,
});
const nodeData = useNodesData(connections?.[0].source);
useEffect(() => {
onChange(nodeData?.data ? nodeData.data.value : 0);
}, [nodeData]);
return (
{label}
);
}
function ColorPreview({ id, data }) {
const { updateNodeData } = useReactFlow();
return (
{
updateNodeData(id, (node) => {
return { value: { ...node.data.value, r: value } };
});
}}
/>
{
updateNodeData(id, (node) => {
return { value: { ...node.data.value, g: value } };
});
}}
/>
{
updateNodeData(id, (node) => {
return { value: { ...node.data.value, b: value } };
});
}}
/>
);
}
export default ColorPreview;
```
##### Lightness.jsx
```jsx
import { useState, useEffect } from 'react';
import {
Handle,
Position,
useNodeConnections,
useNodesData,
useReactFlow,
} from '@xyflow/react';
function LightnessNode({ id }) {
const { updateNodeData } = useReactFlow();
const connections = useNodeConnections({ handleType: 'target' });
const nodesData = useNodesData(connections?.[0].source);
const [lightness, setLightness] = useState('dark');
useEffect(() => {
if (nodesData.data?.value) {
const color = nodesData.data.value;
const isLight =
0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b >= 128;
setLightness(isLight ? 'light' : 'dark');
const newNodeData = isLight
? { light: color, dark: null }
: { light: null, dark: color };
updateNodeData(id, newNodeData);
} else {
setLightness('dark');
updateNodeData(id, { light: null, dark: { r: 0, g: 0, b: 0 } });
}
}, [nodesData, updateNodeData]);
return (
);
}
export default LightnessNode;
```
##### Log.jsx
```jsx
import { Handle, useNodeConnections, useNodesData } from '@xyflow/react';
function Log({ data }) {
const connections = useNodeConnections({ handleType: 'target' });
const nodeData = useNodesData(connections?.[0].source);
const color = nodeData.data
? nodeData.data[connections?.[0].sourceHandle]
: null;
return (
{color ? data.label : 'Do nothing'}
);
}
export default Log;
```
##### NumberInput.jsx
```jsx
import { useCallback, useState } from 'react';
import { Handle, Position, useReactFlow } from '@xyflow/react';
function NumberInput({ id, data }) {
const { updateNodeData } = useReactFlow();
const [number, setNumber] = useState(data.value);
const onChange = useCallback((evt) => {
const cappedNumber = Math.min(255, Math.max(0, evt.target.value));
setNumber(cappedNumber);
updateNodeData(id, { value: cappedNumber });
}, []);
return (
);
}
export default NumberInput;
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
.react-flow__node {
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.12);
border-radius: 10px;
font-size: 12px;
}
.number-input {
padding: 10px;
background: #fff;
border: 1px solid #eee;
border-radius: 10px;
}
.node {
height: 150px;
width: 150px;
display: flex;
flex-direction: column;
justify-content: space-around;
border-radius: 10px;
}
.handle {
position: relative;
top: 15px;
}
.label {
margin-left: 10px;
mix-blend-mode: difference;
color: white;
font-weight: bold;
}
.lightness-node {
width: 100px;
height: 100px;
display: flex;
flex-direction: column;
align-items: end;
justify-content: center;
text-align: center;
border-radius: 10px;
}
.log-node {
width: 80px;
height: 80px;
word-wrap: break-word;
padding: 5px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
#### Summary
You have learned how to move data through the flow and transform it along the way.
All you need to do is
1. store data inside the node's `data` object with help of [`updateNodeData`](/api-reference/types/react-flow-instance#update-node-data) callback.
2. find out which nodes are connected by using [`useNodeConnections`](/api-reference/hooks/use-node-connections) and then use [`useNodesData`](/api-reference/hooks/use-nodes-data) for receiving the data from the connected nodes.
You can implement branching for example by interpreting incoming data that is undefined as a "stop". As a side note, most flow graphs that also have a branching usually separate the triggering of nodes from the actual data hooked up to the nodes. Unreal Engines Blueprints are a good example for this.
One last note before you go: you should find a consistent way of structuring
all your node data, instead of mixing ideas like we did just now. This means
for example, if you start working with splitting data by handle ID you should
do it for all nodes, regardless whether they have multiple handles or not.
Being able to make assumptions about the structure of your data throughout
your flow will make life a lot easier.
### Devtools and Debugging
This is an ongoing experiment on implementing our own React Flow devtools. While we are
working on the actual package, we'd love to hear about your feedback and ideas on
[Discord](https://discord.gg/Bqt6xrs) or via mail at info@xyflow.com.
React Flow can often seem like a magic black box, but in reality you can reveal quite a
lot about its internal state if you know where to look. In this guide we will show you
three different ways to reveal the internal state of your flow:
* A ` ` component that shows the current position and zoom level of the
viewport.
* A ` ` component that reveals the state of each node.
* A ` ` that wraps your flow's `onNodesChange` handler and logs each change
as it is dispatched.
While we find these tools useful for making sure React Flow is working properly, you might
also find them useful for debugging your applications as your flows and their interactions
become more complex.
Example: learn/devtools
##### App.tsx
```tsx
import { useCallback } from 'react';
import {
ReactFlow,
addEdge,
useEdgesState,
useNodesState,
type Edge,
type OnConnect,
type Node,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import DevTools from './Devtools';
const initNodes: Node[] = [
{
id: '1a',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
},
{
id: '2a',
data: { label: 'Node 2' },
position: { x: 100, y: 120 },
},
{
id: '3a',
data: { label: 'Node 3' },
position: { x: 400, y: 120 },
},
];
const initEdges: Edge[] = [
{ id: 'e1-2', source: '1a', target: '2a' },
{ id: 'e1-3', source: '1a', target: '3a' },
];
const fitViewOptions = { padding: 0.5 };
function Flow() {
const [nodes, , onNodesChange] = useNodesState(initNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
const onConnect: OnConnect = useCallback(
(params) => setEdges((eds) => addEdge(params, eds)),
[setEdges],
);
return (
);
}
export default Flow;
```
##### ChangeLogger.tsx
```tsx
import { useEffect, useRef, useState } from 'react';
import {
useStore,
useStoreApi,
type OnNodesChange,
type NodeChange,
} from '@xyflow/react';
type ChangeLoggerProps = {
color?: string;
limit?: number;
};
type ChangeInfoProps = {
change: NodeChange;
};
function ChangeInfo({ change }: ChangeInfoProps) {
const id = 'id' in change ? change.id : '-';
const { type } = change;
return (
node id: {id}
{type === 'add' ? JSON.stringify(change.item, null, 2) : null}
{type === 'dimensions'
? `dimensions: ${change.dimensions?.width} × ${change.dimensions?.height}`
: null}
{type === 'position'
? `position: ${change.position?.x.toFixed(
1,
)}, ${change.position?.y.toFixed(1)}`
: null}
{type === 'remove' ? 'remove' : null}
{type === 'select' ? (change.selected ? 'select' : 'unselect') : null}
);
}
export default function ChangeLogger({ limit = 20 }: ChangeLoggerProps) {
const [changes, setChanges] = useState
([]);
const onNodesChangeIntercepted = useRef(false);
const onNodesChange = useStore((s) => s.onNodesChange);
const store = useStoreApi();
useEffect(() => {
if (!onNodesChange || onNodesChangeIntercepted.current) {
return;
}
onNodesChangeIntercepted.current = true;
const userOnNodesChange = onNodesChange;
const onNodesChangeLogger: OnNodesChange = (changes) => {
userOnNodesChange(changes);
setChanges((oldChanges) => [...changes, ...oldChanges].slice(0, limit));
};
store.setState({ onNodesChange: onNodesChangeLogger });
}, [onNodesChange, limit]);
return (
Change Logger
{changes.length === 0 ? (
<>no changes triggered>
) : (
changes.map((change, index) => (
))
)}
);
}
```
##### Devtools.tsx
```tsx
import {
useState,
type Dispatch,
type SetStateAction,
type ReactNode,
type HTMLAttributes,
} from 'react';
import { Panel } from '@xyflow/react';
import NodeInspector from './NodeInspector';
import ChangeLogger from './ChangeLogger';
import ViewportLogger from './ViewportLogger';
export default function DevTools() {
const [nodeInspectorActive, setNodeInspectorActive] = useState(true);
const [changeLoggerActive, setChangeLoggerActive] = useState(true);
const [viewportLoggerActive, setViewportLoggerActive] = useState(true);
return (
Node Inspector
Change Logger
Viewport Logger
{changeLoggerActive &&
}
{nodeInspectorActive &&
}
{viewportLoggerActive &&
}
);
}
function DevToolButton({
active,
setActive,
children,
...rest
}: {
active: boolean;
setActive: Dispatch>;
children: ReactNode;
} & HTMLAttributes) {
return (
setActive((a) => !a)}
className={active ? 'active' : ''}
{...rest}
>
{children}
);
}
```
##### NodeInspector.tsx
```tsx
import {
useNodes,
ViewportPortal,
useReactFlow,
type XYPosition,
} from '@xyflow/react';
export default function NodeInspector() {
const { getInternalNode } = useReactFlow();
const nodes = useNodes();
return (
{nodes.map((node) => {
const internalNode = getInternalNode(node.id);
if (!internalNode) {
return null;
}
const absPosition = internalNode?.internals.positionAbsolute;
return (
);
})}
);
}
type NodeInfoProps = {
id: string;
type: string;
selected: boolean;
position: XYPosition;
absPosition: XYPosition;
width?: number;
height?: number;
data: any;
};
function NodeInfo({
id,
type,
selected,
position,
absPosition,
width,
height,
data,
}: NodeInfoProps) {
if (!width || !height) {
return null;
}
return (
id: {id}
type: {type}
selected: {selected ? 'true' : 'false'}
position: {position.x.toFixed(1)}, {position.y.toFixed(1)}
dimensions: {width} × {height}
data: {JSON.stringify(data, null, 2)}
);
}
```
##### ViewportLogger.tsx
```tsx
import { Panel, useStore } from '@xyflow/react';
export default function ViewportLogger() {
const viewport = useStore(
(s) =>
`x: ${s.transform[0].toFixed(2)}, y: ${s.transform[1].toFixed(
2,
)}, zoom: ${s.transform[2].toFixed(2)}`,
);
return {viewport} ;
}
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
.react-flow__devtools {
--border-radius: 4px;
--highlight-color: rgba(238, 58, 115, 1);
--font: monospace, sans-serif;
border-radius: var(--border-radius);
font-size: 11px;
font-family: var(--font);
}
.react-flow__devtools button {
background: white;
border: none;
padding: 5px 15px;
color: #222;
font-weight: bold;
font-size: 12px;
cursor: pointer;
font-family: var(--font);
background-color: #f4f4f4;
border-right: 1px solid #ddd;
}
.react-flow__devtools button:hover {
background: var(--highlight-color);
opacity: 0.8;
color: white;
}
.react-flow__devtools button.active {
background: var(--highlight-color);
color: white;
}
.react-flow__devtools button:first-child {
border-radius: var(--border-radius) 0 0 var(--border-radius);
}
.react-flow__devtools button:last-child {
border-radius: 0 var(--border-radius) var(--border-radius) 0;
border-right: none;
}
.react-flow__devtools-changelogger {
pointer-events: none;
position: relative;
top: 50px;
left: 20px;
font-family: var(--font);
}
.react-flow__devtools-title {
font-weight: bold;
margin-bottom: 5px;
}
.react-flow__devtools-nodeinspector {
pointer-events: none;
font-family: monospace, sans-serif;
font-size: 10px;
}
.react-flow__devtools-nodeinfo {
top: 5px;
}
```
##### index.html
```html
React Flow Example
```
##### index.tsx
```tsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
We encourage you to copy any or all of the components from this example into your own
projects and modify them to suit your needs: each component works independently!
#### Node Inspector
The ` ` component makes use of our
[`useNodes`](/api-reference/hooks/use-nodes) hook to access all the nodes in the flow.
Typically we discourage using this hook because it will trigger a re-render any time *any*
of your nodes change, but that's exactly what makes it so useful for debugging!
The `width` and `height` properties are added to each node by React Flow after it has
measured the node's dimensions. We pass those dimensions, as well as other information
like the node's id and type, to a custom ` ` component.
We make use of the [` `](/api-reference/components/viewport-portal)
component to let us render the inspector into React Flow's viewport. That means it's
content will be positioned and transformed along with the rest of the flow as the user
pans and zooms.
#### Change Logger
Any change to your nodes and edges that originates from React Flow itself is communicated
to you through the `onNodesChange` and `onEdgesChange` callbacks. If you are working with
a controlled flow (that means you're managing the nodes and edges yourself), you need to
apply those changes to your state in order to keep everything in sync.
The ` ` component wraps your user-provided `onNodesChange` handler with a
custom function that intercepts and logs each change as it is dispatched. We can do this
by using the [`useStore`](/api-reference/hooks/use-store) and
[`useStoreApi`](/api-reference/hooks/use-store-api) hooks to access the store and and then
update React Flow's internal state accordingly. These two hooks give you powerful access
to React Flow's internal state and methods.
Beyond debugging, using the ` ` can be a great way to learn more about how
React Flow works and get you thinking about the different functionality you can build on
top of each change.
You can find documentation on the [`NodeChange`](/api-reference/types/node-change) and
[`EdgeChange`](/api-reference/types/edge-change) types in the API reference.
#### Viewport Logger
The ` ` is the simplest example of what state you can pull out of React
Flow's store if you know what to look for. The state of the viewport is stored internally
under the `transform` key (a name we inherited from
[d3-zoom](https://d3js.org/d3-zoom#zoomTransform)). This component extracts the `x`, `y`,
and `zoom` components of the transform and renders them into a
[` `](/api-reference/components/panel) component.
#### Let us know what you think
As mentioned above, if you have any feedback or ideas on how to improve the devtools,
please let us know on [Discord](https://discord.gg/Bqt6xrs) or via mail at
. If you build your own devtools using these ideas, we'd love to hear about
it!
### Hooks and Providers
React Flow provides several [hooks](/api-reference/hooks) and a context provider
for you to enhance the functionality of your flow. These tools help you to
manage state, access internal methods, and create custom components more
effectively.
#### ReactFlowProvider
The ReactFlowProvider is a context provider that allows you to access the
internal state of the flow, such as nodes, edges, and viewport, from anywhere in
your component tree even outside the [`ReactFlow`](/api-reference/react-flow)
component. It is typically used at the top level of your application.
There are several cases where you might need to use the
[`ReactFlowProvider`](/api-reference/react-flow-provider) component:
* Many of the [hooks](/api-reference/hooks) we provide rely on this component to
work.
* You want to access the internal state of the flow outside of the `ReactFlow`
component.
* You are working with multiple flows on a page.
* You are using a client-side router.
Example: examples/misc/provider
##### App.jsx
```jsx
import React, { useCallback } from 'react';
import {
Background,
ReactFlow,
ReactFlowProvider,
useNodesState,
useEdgesState,
addEdge,
Controls,
} from '@xyflow/react';
import Sidebar from './Sidebar';
import '@xyflow/react/dist/style.css';
const initialNodes = [
{
id: 'provider-1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
},
{ id: 'provider-2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
{ id: 'provider-3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
{ id: 'provider-4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
];
const initialEdges = [
{
id: 'provider-e1-2',
source: 'provider-1',
target: 'provider-2',
animated: true,
},
{ id: 'provider-e1-3', source: 'provider-1', target: 'provider-3' },
];
const ProviderFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(params) => setEdges((els) => addEdge(params, els)),
[],
);
return (
);
};
export default ProviderFlow;
```
##### Sidebar.jsx
```jsx
import React, { useCallback } from 'react';
import { useStore } from '@xyflow/react';
const transformSelector = (state) => state.transform;
export default ({ nodes, setNodes }) => {
const transform = useStore(transformSelector);
const selectAll = useCallback(() => {
setNodes((nds) =>
nds.map((node) => {
return {
...node,
selected: true,
};
}),
);
}, [setNodes]);
return (
This is an example of how you can access the internal state outside of the
ReactFlow component.
Zoom & pan transform
[{transform[0].toFixed(2)}, {transform[1].toFixed(2)}, {transform[2].toFixed(2)}]
Nodes
{nodes.map((node) => (
Node {node.id} - x: {node.position.x.toFixed(2)}, y:{' '}
{node.position.y.toFixed(2)}
))}
select all nodes
);
};
```
##### xy-theme.css
```css
/* xyflow theme files. Delete these to start from our base */
.react-flow {
--xy-background-color: #f7f9fb;
/* Custom Variables */
--xy-theme-selected: #f57dbd;
--xy-theme-hover: #c5c5c5;
--xy-theme-edge-hover: black;
--xy-theme-color-focus: #e8e8e8;
/* Built-in Variables see https://reactflow.dev/learn/customization/theming */
--xy-node-border-default: 1px solid #ededed;
--xy-node-boxshadow-default:
0px 3.54px 4.55px 0px #00000005, 0px 3.54px 4.55px 0px #0000000d,
0px 0.51px 1.01px 0px #0000001a;
--xy-node-border-radius-default: 8px;
--xy-handle-background-color-default: #ffffff;
--xy-handle-border-color-default: #aaaaaa;
--xy-edge-label-color-default: #505050;
}
.react-flow.dark {
--xy-node-boxshadow-default:
0px 3.54px 4.55px 0px rgba(255, 255, 255, 0.05),
/* light shadow */ 0px 3.54px 4.55px 0px rgba(255, 255, 255, 0.13),
/* medium shadow */ 0px 0.51px 1.01px 0px rgba(255, 255, 255, 0.2); /* smallest shadow */
--xy-theme-color-focus: #535353;
}
/* Customizing Default Theming */
.react-flow__node {
box-shadow: var(--xy-node-boxshadow-default);
border-radius: var(--xy-node-border-radius-default);
background-color: var(--xy-node-background-color-default);
display: flex;
justify-content: center;
align-items: center;
text-align: center;
padding: 10px;
font-size: 12px;
flex-direction: column;
border: var(--xy-node-border-default);
color: var(--xy-node-color, var(--xy-node-color-default));
}
.react-flow__node.selectable:focus {
box-shadow: 0px 0px 0px 4px var(--xy-theme-color-focus);
border-color: #d9d9d9;
}
.react-flow__node.selectable:focus:active {
box-shadow: var(--xy-node-boxshadow-default);
}
.react-flow__node.selectable:hover,
.react-flow__node.draggable:hover {
border-color: var(--xy-theme-hover);
}
.react-flow__node.selectable.selected {
border-color: var(--xy-theme-selected);
box-shadow: var(--xy-node-boxshadow-default);
}
.react-flow__node-group {
background-color: rgba(207, 182, 255, 0.4);
border-color: #9e86ed;
}
.react-flow__edge.selectable:hover .react-flow__edge-path,
.react-flow__edge.selectable.selected .react-flow__edge-path {
stroke: var(--xy-theme-edge-hover);
}
.react-flow__handle {
background-color: var(--xy-handle-background-color-default);
}
.react-flow__handle.connectionindicator:hover {
pointer-events: all;
border-color: var(--xy-theme-edge-hover);
background-color: white;
}
.react-flow__handle.connectionindicator:focus,
.react-flow__handle.connectingfrom,
.react-flow__handle.connectingto {
border-color: var(--xy-theme-edge-hover);
}
.react-flow__node-resizer {
border-radius: 0;
border: none;
}
.react-flow__resize-control.handle {
background-color: #ffffff;
border-color: #9e86ed;
border-radius: 0;
width: 5px;
height: 5px;
}
/*
Custom Example CSS - This CSS is to improve the example experience.
You can remove it if you want to use the default styles.
New Theme Classes:
.xy-theme__button - Styles for buttons.
.xy-theme__input - Styles for text inputs.
.xy-theme__checkbox - Styles for checkboxes.
.xy-theme__select - Styles for dropdown selects.
.xy-theme__label - Styles for labels.
Use these classes to apply consistent theming across your components.
*/
:root {
--color-primary: #ff0073;
--color-background: #fefefe;
--color-hover-bg: #f6f6f6;
--color-disabled: #76797e;
}
.xy-theme__button-group {
display: flex;
align-items: center;
.xy-theme__button:first-child {
border-radius: 100px 0 0 100px;
}
.xy-theme__button:last-child {
border-radius: 0 100px 100px 0;
margin: 0;
}
}
/* Custom Button Styling */
.xy-theme__button {
display: inline-flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 1rem;
border-radius: 100px;
border: 1px solid var(--color-primary);
background-color: var(--color-background);
color: var(--color-primary);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
box-shadow: var(--xy-node-boxshadow-default);
cursor: pointer;
}
.xy-theme__button.active {
background-color: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.xy-theme__button.active:hover,
.xy-theme__button.active:active {
background-color: var(--color-primary);
opacity: 0.9;
}
.xy-theme__button:hover {
background-color: var(--xy-controls-button-background-color-hover-default);
}
.xy-theme__button:active {
background-color: var(--color-hover-bg);
}
.xy-theme__button:disabled {
color: var(--color-disabled);
opacity: 0.8;
cursor: not-allowed;
border: 1px solid var(--color-disabled);
}
.xy-theme__button > span {
margin-right: 0.2rem;
}
/* Add gap between adjacent buttons */
.xy-theme__button + .xy-theme__button {
margin-left: 0.3rem;
}
/* Example Input Styling */
.xy-theme__input {
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-primary);
border-radius: 7px;
background-color: var(--color-background);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
font-size: 1rem;
color: inherit;
}
.xy-theme__input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(255, 0, 115, 0.3);
}
/* Specific Checkbox Styling */
.xy-theme__checkbox {
appearance: none;
-webkit-appearance: none;
width: 1.25rem;
height: 1.25rem;
border-radius: 7px;
border: 2px solid var(--color-primary);
background-color: var(--color-background);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
cursor: pointer;
display: inline-block;
vertical-align: middle;
margin-right: 0.5rem;
}
.xy-theme__checkbox:checked {
background-color: var(--color-primary);
border-color: var(--color-primary);
}
.xy-theme__checkbox:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(255, 0, 115, 0.3);
}
/* Dropdown Styling */
.xy-theme__select {
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-primary);
border-radius: 50px;
background-color: var(--color-background);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
font-size: 1rem;
color: inherit;
margin-right: 0.5rem;
box-shadow: var(--xy-node-boxshadow-default);
}
.xy-theme__select:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(255, 0, 115, 0.3);
}
.xy-theme__label {
margin-top: 10px;
margin-bottom: 3px;
display: inline-block;
}
```
##### index.css
```css
@import url('./xy-theme.css');
html,
body {
margin: 0;
font-family: sans-serif;
box-sizing: border-box;
}
#app {
width: 100vw;
height: 100vh;
}
.providerflow {
flex-direction: column;
display: flex;
flex-grow: 1;
height: 100%;
}
.providerflow aside {
border-left: 1px solid #eee;
padding: 15px 10px;
font-size: 12px;
background: #fff;
}
.providerflow aside .description {
margin-bottom: 10px;
}
.providerflow aside .title {
font-weight: 700;
margin-bottom: 5px;
}
.providerflow aside .transform {
margin-bottom: 20px;
}
.providerflow .reactflow-wrapper {
flex-grow: 1;
}
.providerflow .selectall {
margin-top: 10px;
}
@media screen and (min-width: 768px) {
.providerflow {
flex-direction: row;
}
.providerflow aside {
width: 20%;
max-width: 250px;
height: 200px;
}
}
```
#### useReactFlow
The [`useReactFlow`](/api-reference/hooks/use-react-flow) hook provides access
to the [`ReactFlowInstance`](/api-reference/types/react-flow-instance) and its
methods. It allows you to manipulate nodes, edges, and the viewport
programmatically.
This example illustrates how to use the `useReactFlow` hook.
Example: examples/misc/use-react-flow-hook
##### App.jsx
```jsx
import React, { useCallback } from 'react';
import {
Background,
ReactFlow,
ReactFlowProvider,
addEdge,
useNodesState,
useEdgesState,
} from '@xyflow/react';
import Buttons from './Buttons';
import '@xyflow/react/dist/style.css';
const initialNodes = [
{
id: '1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
},
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
];
const initialEdges = [
{
id: 'e1-2',
source: '1',
target: '2',
},
{ id: 'e1-3', source: '1', target: '3' },
];
const ProviderFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(params) => setEdges((els) => addEdge(params, els)),
[],
);
return (
);
};
export default ProviderFlow;
```
##### Buttons.jsx
```jsx
import React from 'react';
import { useStoreApi, useReactFlow, Panel } from '@xyflow/react';
const panelStyle = {
color: '#777',
fontSize: 12,
};
const buttonStyle = {
fontSize: 12,
marginRight: 5,
marginTop: 5,
};
export default () => {
const store = useStoreApi();
const { zoomIn, zoomOut, setCenter } = useReactFlow();
const focusNode = () => {
const { nodeLookup } = store.getState();
const nodes = Array.from(nodeLookup).map(([, node]) => node);
if (nodes.length > 0) {
const node = nodes[0];
const x = node.position.x + node.measured.width / 2;
const y = node.position.y + node.measured.height / 2;
const zoom = 1.85;
setCenter(x, y, { zoom, duration: 1000 });
}
};
return (
This is an example of how you can use the zoom pan helper hook
focus node
zoom in
zoom out
);
};
```
##### xy-theme.css
```css
/* xyflow theme files. Delete these to start from our base */
.react-flow {
--xy-background-color: #f7f9fb;
/* Custom Variables */
--xy-theme-selected: #f57dbd;
--xy-theme-hover: #c5c5c5;
--xy-theme-edge-hover: black;
--xy-theme-color-focus: #e8e8e8;
/* Built-in Variables see https://reactflow.dev/learn/customization/theming */
--xy-node-border-default: 1px solid #ededed;
--xy-node-boxshadow-default:
0px 3.54px 4.55px 0px #00000005, 0px 3.54px 4.55px 0px #0000000d,
0px 0.51px 1.01px 0px #0000001a;
--xy-node-border-radius-default: 8px;
--xy-handle-background-color-default: #ffffff;
--xy-handle-border-color-default: #aaaaaa;
--xy-edge-label-color-default: #505050;
}
.react-flow.dark {
--xy-node-boxshadow-default:
0px 3.54px 4.55px 0px rgba(255, 255, 255, 0.05),
/* light shadow */ 0px 3.54px 4.55px 0px rgba(255, 255, 255, 0.13),
/* medium shadow */ 0px 0.51px 1.01px 0px rgba(255, 255, 255, 0.2); /* smallest shadow */
--xy-theme-color-focus: #535353;
}
/* Customizing Default Theming */
.react-flow__node {
box-shadow: var(--xy-node-boxshadow-default);
border-radius: var(--xy-node-border-radius-default);
background-color: var(--xy-node-background-color-default);
display: flex;
justify-content: center;
align-items: center;
text-align: center;
padding: 10px;
font-size: 12px;
flex-direction: column;
border: var(--xy-node-border-default);
color: var(--xy-node-color, var(--xy-node-color-default));
}
.react-flow__node.selectable:focus {
box-shadow: 0px 0px 0px 4px var(--xy-theme-color-focus);
border-color: #d9d9d9;
}
.react-flow__node.selectable:focus:active {
box-shadow: var(--xy-node-boxshadow-default);
}
.react-flow__node.selectable:hover,
.react-flow__node.draggable:hover {
border-color: var(--xy-theme-hover);
}
.react-flow__node.selectable.selected {
border-color: var(--xy-theme-selected);
box-shadow: var(--xy-node-boxshadow-default);
}
.react-flow__node-group {
background-color: rgba(207, 182, 255, 0.4);
border-color: #9e86ed;
}
.react-flow__edge.selectable:hover .react-flow__edge-path,
.react-flow__edge.selectable.selected .react-flow__edge-path {
stroke: var(--xy-theme-edge-hover);
}
.react-flow__handle {
background-color: var(--xy-handle-background-color-default);
}
.react-flow__handle.connectionindicator:hover {
pointer-events: all;
border-color: var(--xy-theme-edge-hover);
background-color: white;
}
.react-flow__handle.connectionindicator:focus,
.react-flow__handle.connectingfrom,
.react-flow__handle.connectingto {
border-color: var(--xy-theme-edge-hover);
}
.react-flow__node-resizer {
border-radius: 0;
border: none;
}
.react-flow__resize-control.handle {
background-color: #ffffff;
border-color: #9e86ed;
border-radius: 0;
width: 5px;
height: 5px;
}
/*
Custom Example CSS - This CSS is to improve the example experience.
You can remove it if you want to use the default styles.
New Theme Classes:
.xy-theme__button - Styles for buttons.
.xy-theme__input - Styles for text inputs.
.xy-theme__checkbox - Styles for checkboxes.
.xy-theme__select - Styles for dropdown selects.
.xy-theme__label - Styles for labels.
Use these classes to apply consistent theming across your components.
*/
:root {
--color-primary: #ff0073;
--color-background: #fefefe;
--color-hover-bg: #f6f6f6;
--color-disabled: #76797e;
}
.xy-theme__button-group {
display: flex;
align-items: center;
.xy-theme__button:first-child {
border-radius: 100px 0 0 100px;
}
.xy-theme__button:last-child {
border-radius: 0 100px 100px 0;
margin: 0;
}
}
/* Custom Button Styling */
.xy-theme__button {
display: inline-flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 1rem;
border-radius: 100px;
border: 1px solid var(--color-primary);
background-color: var(--color-background);
color: var(--color-primary);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
box-shadow: var(--xy-node-boxshadow-default);
cursor: pointer;
}
.xy-theme__button.active {
background-color: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.xy-theme__button.active:hover,
.xy-theme__button.active:active {
background-color: var(--color-primary);
opacity: 0.9;
}
.xy-theme__button:hover {
background-color: var(--xy-controls-button-background-color-hover-default);
}
.xy-theme__button:active {
background-color: var(--color-hover-bg);
}
.xy-theme__button:disabled {
color: var(--color-disabled);
opacity: 0.8;
cursor: not-allowed;
border: 1px solid var(--color-disabled);
}
.xy-theme__button > span {
margin-right: 0.2rem;
}
/* Add gap between adjacent buttons */
.xy-theme__button + .xy-theme__button {
margin-left: 0.3rem;
}
/* Example Input Styling */
.xy-theme__input {
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-primary);
border-radius: 7px;
background-color: var(--color-background);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
font-size: 1rem;
color: inherit;
}
.xy-theme__input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(255, 0, 115, 0.3);
}
/* Specific Checkbox Styling */
.xy-theme__checkbox {
appearance: none;
-webkit-appearance: none;
width: 1.25rem;
height: 1.25rem;
border-radius: 7px;
border: 2px solid var(--color-primary);
background-color: var(--color-background);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
cursor: pointer;
display: inline-block;
vertical-align: middle;
margin-right: 0.5rem;
}
.xy-theme__checkbox:checked {
background-color: var(--color-primary);
border-color: var(--color-primary);
}
.xy-theme__checkbox:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(255, 0, 115, 0.3);
}
/* Dropdown Styling */
.xy-theme__select {
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-primary);
border-radius: 50px;
background-color: var(--color-background);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
font-size: 1rem;
color: inherit;
margin-right: 0.5rem;
box-shadow: var(--xy-node-boxshadow-default);
}
.xy-theme__select:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(255, 0, 115, 0.3);
}
.xy-theme__label {
margin-top: 10px;
margin-bottom: 3px;
display: inline-block;
}
```
##### index.css
```css
@import url('./xy-theme.css');
html,
body {
margin: 0;
font-family: sans-serif;
box-sizing: border-box;
}
#app {
width: 100vw;
height: 100vh;
}
```
### Multiplayer
Node-based UIs are often used to create applications that are visual and explorative in
their nature. For collaborative features, this usually means that there is no satisfying
“middle-ground” solution like sparsely synchronising the state between users, because it
would break visual consistency or make collaborative exploration more complicated. Often,
a full live multiplayer system needs to be put in place.
In our [Collaborative Flow Pro Example](/examples/interaction/collaborative), we show a
realtime multiplayer collaboration flow using React Flow and
[Yjs](https://github.com/yjs/yjs). In this guide, we will explore some best practices for
integrating multiplayer collaboration in your React Flow application.
#### What are multiplayer applications?
Although **realtime collaboration** would be the correct term here, we refer to it as
**multiplayer**, which was most likely popularized in this context by Figma. Besides being
more intuitive for marketing purposes, it also does a better job of communicating the
commitment to creating something truly realtime by putting it in the same category as
competitive video games.
To figure out what degree of collaboration your application supports, you just need to
look at how closely it resembles a multiplayer game. These features likely include:
* No manual saving is required; all users share a world where actions are persisted.
* Any changes made by other users are immediately visible to you.
* Not only completed actions, but transitive states are synced too (e.g. dragging a node,
not only when the node is dropped).
* You can see other users (e.g. their cursors or viewport positions)
##### A word of caution
Making multiplayer applications is hard! When users are collaboratively editing shared
objects, conflicts, delays, connection drops and concurrent operations are expected. The
hardest challenge in multiplayer apps is **conflict-resolution**: you always need some
kind of sync engine that is able to merge any changes coming from the server (or other
clients) into the (often optimistic) client state. You need to gracefully handle conflicts
and disconnects. You need to build a reliable network layer.
The simplest sync engine for multiplayer would be a "first-come-first-served" approach,
and failed requests and messages sent being ignored. If you try to manually handle this,
you will see a quite vast variety of edge cases emerge. Networks are slow, clients
disconnect, and the order of actions matters.
##### Local-first and CRDTs
Depending on how far you want to take handling conflicts and disconnects, you very quickly
land in **local-first** territory. When your sync engine allows a user to be disconnected
for an indefinite amount of time, you have a **local-first** application. If you would
like a good primer on this topic we recommend
[Ink & Switch's essay](https://www.inkandswitch.com/essay/local-first/) that coined the
term.
A **CRDT** (*Conflict-free Replicated Data Type*) is a data structure that lets multiple
users edit the same data simultaneously on different devices, then automatically merges
all changes without conflicts—no central server needed. Instead of just storing the
current value of the data, CRDTs keep operation history on every connected client, so any
replica can merge changes into the same final state, regardless of the order updates
arrive. They are thus solving the conflicts and disconnects by being multi-replica synced
data structures that can work completely offline, and automatically upload, fetch, and
reconcile their state on reconnection.
A couple of popular solutions are [Yjs](https://github.com/yjs/yjs),
[Automerge](https://automerge.org/) and [Loro](https://loro.dev).
#### What React Flow state should I sync?
Among your first considerations should be **how** you want to sync **what** parts of the
local state of your app.
##### Ephemeral vs Durable
While there are many ways to sync state between clients, we can categorize them into two
groups: ephemeral and durable (aka atomic) changes. **Ephemeral changes are not
persisted** and neither its consistency nor its correctness is very important for the
functionality of the application. These are things like cursor or viewport positions or
any transient interaction state. Missing some of these updates will not break anything and
in case of a connection loss, a client can just listen for the newest changes and restore
any lost functionality.
On the other hand, **updates to the nodes & edges should be persistent and consistent!**
Imagine if Alice deletes a node and Bob misses this update. Now if Bob subsequently moves
the node, we would have an inconsistent application state. For this kind of data we have
to use a solution that handles disconnects more aggressively and is able to discard
impossible actions like moving a deleted node.
At the core of a multiplayer React Flow application is syncing the nodes and edges. However
not all parts of the state should be synced. For instance, what nodes and edges are selected
should be different for each client. Also there are some fields that are relevant for the
certain library functions like `dragging`, `resizing` or `measured`.
This is an overview of what parts are recommended to sync and what not.
##### Nodes
| Field | Durable | Ephemeral | Explanation |
| ----------------- | ------- | --------- | ------------------------------------- |
| `id` | ✅ | ❌ | Important, always needs to be in-sync |
| `type` | ✅ | ❌ | Important, always needs to be in-sync |
| `data` | ✅ | ❌ | Important, always needs to be in-sync |
| `position` | ✅ | ✳️ | Important, includes transient state |
| `width`, `height` | ✅ | ✳️ | Important, includes transient state |
| `dragging` | ❌ | ✅ | Transient interaction state |
| `resizing` | ❌ | ✅ | Transient interaction state |
| `selected` | ❌ | ❌ | Per-user UI state |
| `measured` | ❌ | ❌ | Computed from DOM |
##### Edges
| Field | Durable | Ephemeral | Explanation |
| -------------- | ------- | --------- | ------------------------------------- |
| `id` | ✅ | ❌ | Important, always needs to be in-sync |
| `type` | ✅ | ❌ | Important, always needs to be in-sync |
| `data` | ✅ | ❌ | Important, always needs to be in-sync |
| `source` | ✅ | ❌ | Important, always needs to be in-sync |
| `target` | ✅ | ❌ | Important, always needs to be in-sync |
| `sourceHandle` | ✅ | ❌ | Important, always needs to be in-sync |
| `targetHandle` | ✅ | ❌ | Important, always needs to be in-sync |
| `selected` | ❌ | ❌ | Per-user UI state |
##### Connections and Cursors
Good examples of ephemeral data are
[`connections`](https://reactflow.dev/api-reference/hooks/use-connection) (the transient
part of edges being created) and cursors.
Sharing each user's connections status improves visual consistency across clients, but
losing this state on disconnect doesn't affect the correctness of the shared data: it's
purely presentational.
The same applies to cursors. They help create the immersion of a shared workspace, but
they're not essential to the application's integrity. As long as the nodes and edges
remain in sync, cursor state can be safely discarded.
Both can be shared as purely ephemeral state. If you want to reduce the frequency of live
updates, you can debounce the updates and smooth the movements of other users' cursors.
You can use a library like
[perfect-cursors](https://github.com/steveruizok/perfect-cursors) to smooth the movements
of other users' cursors.
#### Third Party Libraries and Services
We experimented with different multiplayer backend solutions to understand their best use
cases for React Flow apps. What we found out, is that there's no one-size-fits-all
solution. The choice between CRDT-based local-first libraries (like
[Yjs](https://yjs.dev/) and [Jazz](https://jazz.tools)) and server-authoritative
approaches (like [Supabase](https://supabase.com/) and [Convex](https://convex.dev/)) will
change the way you approach building multiplayer React Flow applications:
* **CRDT libraries** will make your life easier around offline-first
multiplayer support, giving you for-free automatic conflict resolution, but in the case
where your application is also structured around a database, you will need to implement
your own adapters between the CRDT library and the database.
* **Server-authoritative solutions** are easier to use with a database and
classical application logic, but will complicate your life around offline-first
multiplayer support, conflict resolution, and development complexity.
| Name | Architecture | Offline Support | Conflict Resolution | Notes |
| ------------------------------------------------- | ------------------------------- | -------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [**Yjs**](https://yjs.dev/) | CRDT (Local-first) | ✅ Full offline support | Automatic (CRDT) | Collaborative editing, offline-first CRDT for local-first apps, with a [React Flow Pro Example](/examples/interaction/collaborative) |
| [**Jazz**](https://jazz.tools/) | CRDT (Local-first) | ✅ Full offline support | Automatic (CRDT) | A full, offline-first database solution that handles multiplayer natively. |
| [**Supabase**](https://supabase.com/) | Server-authoritative | ❌ Requires connection | Manual/RLS policies | Classic SQL-based database solution, with realtime support, but no CRDT (conflict resolution must be implemented manually). |
| [**Convex**](https://convex.dev/) | Server-authoritative | ❌ Requires connection | [Optimistic](https://docs.convex.dev/database/advanced/occ) | TypeScript-first, database solution with real-time queries and a sync-engine with some conflict resolution capabilities. [Can be paired with Automerge](https://stack.convex.dev/automerge-and-convex) |
| [**Liveblocks**](https://liveblocks.io/) | Server-authoritative, CRDT-like | 🟡 Partial offline support | Uses CRDTs under the hood | Real-time collaboration platform with hosted proprietary backend. [React Flow Example](https://liveblocks.io/examples/collaborative-flowchart/zustand-flowchart). |
| [**Velt**](https://velt.dev/libraries/react-flow) | CRDT with managed backend | ✅ Full offline support | Uses CRDTs under the hood | Collaborative editing platform with managed backend, with a dedicated [React Flow Library](https://docs.velt.dev/realtime-collaboration/crdt/setup/reactflow) |
| [**Automerge**](https://automerge.org/) | CRDT (Local-first) | ✅ Full offline support | Automatic (CRDT) | The canonical CRDT library. Can be used together with [Convex](https://stack.convex.dev/automerge-and-convex) and other databases. |
| [**Loro**](https://loro.dev/) | CRDT (Local-first) | ✅ Full offline support | Automatic (CRDT) | A fast CRDT library in Rust and WebAssembly with history tracking and version control. |
### Performance
When dealing with a large number of nodes or complex components, managing performance can
be challenging. Here are a few effective strategies to optimize the performance of React
Flow.
#### Use memoization
One of the main reasons for performance issues in React Flow is unnecessary re-renders.
Since node movements trigger frequent state updates, this can lead to performance
bottlenecks, especially in larger diagrams.
##### Memoize components
Components provided as props to the `` component, including custom node and
edge components, should either be memoized using `React.memo` or declared outside the
parent component. This ensures that React does not create a new reference for the
component on every render, which would otherwise trigger unnecessary re-renders.
```tsx
const NodeComponent = memo(() => {
return {data.label}
;
});
```
##### Memoize functions
Similarly, functions passed as props to `` should be memoized using
`useCallback`. This prevents React from creating a new function reference on every render,
which could also trigger unnecessary re-renders. Additionally, arrays and objects like
`defaultEdgeOptions` or `snapGrid` should be memoized using `useMemo` to prevent
unnecessary re-renders.
```tsx
import React, { useCallback } from 'react';
const MyDiagram = () => {
const onNodeClick = useCallback((event, node) => {
console.log('Node clicked:', node);
}, []);
return ;
};
export default MyDiagram;
```
#### Avoid accessing nodes in components
One of the most common performance pitfalls in React Flow is directly accessing
the `nodes` or `edges` in the components or the viewport. These objects change frequently
during operations like dragging, panning, or zooming, which can cause unnecessary
re-renders of components that depend on them.
For example, if you fetch the entire `nodes` array from the store and filter it to display
selected node IDs, this approach can lead to performance degradation. Every update to
the `nodes` array triggers a re-render of all dependent components, even if the change is
unrelated to the selected nodes.
##### Inefficient example
```tsx
const SelectedNodeIds = () => {
// ❌ This will cause unnecessary re-renders!
const nodes = useStore((state) => state.nodes);
const selectedNodeIds = nodes.filter((node) => node.selected).map((node) => node.id);
return (
{selectedNodeIds.map((id) => (
{id}
))}
);
};
```
In this example, every update to the `nodes` array causes the `SelectedNodeIds` component
to re-render, even if the selection hasn’t changed.
##### Optimized solution
To avoid unnecessary re-renders, store the selected nodes in a separate field in your
state (using Zustand, Redux, or any other state management solution). This ensures that
the component only re-renders when the selection changes.
```tsx
const SelectedNodeIds = () => {
const selectedNodeIds = useStore((state) => state.selectedNodeIds);
return (
{selectedNodeIds.map((id) => (
{id}
))}
);
};
```
By decoupling the selected nodes from the `nodes` array, you prevent unnecessary updates
and improve performance. For more information, view our
[State Management guide](/learn/advanced-use/state-management).
#### Collapse large node trees
If your node tree is deeply nested, rendering all nodes at once can be inefficient.
Instead, show only a limited number of nodes and allow users to expand them as needed. You
can do this by modifying the node’s `hidden` property dynamically to toggle visibility.
```tsx
const handleNodeClick = (targetNode) => {
if (targetNode.data.children) {
setNodes((prevNodes) =>
prevNodes.map((node) =>
targetNode.data.children.includes(node.id)
? { ...node, hidden: !node.hidden }
: node,
),
);
}
};
```
By hiding nodes initially and rendering them only when expanded, we optimize performance
while maintaining usability.
#### Simplify node and edge styles
If you've optimized performance in every other way, and you are still finding performance
issues with large numbers of nodes, complex CSS styles, particularly those involving
animations, shadows, or gradients, can significantly impact performance. Consider reducing
complexity on your node styles in these cases.
#### Additional resources
Here are a few helpful resources on performance in React Flow that you can check out:
* [Guide to Optimize React Flow Project Performance](https://www.synergycodes.com/blog/guide-to-optimize-react-flow-project-performance)
* [Tuning Edge Animations ReactFlow Optimal Performance](https://liambx.com/blog/tuning-edge-animations-reactflow-optimal-performance)
* [5 Ways to Optimize React Flow in 10 minutes](https://www.youtube.com/watch?v=8M2qZ69iM20)
### Server Side Rendering
### Server side rendering, server side generation
Server side rendering is supported since React Flow 12
This is an advanced use case and assumes you are already familiar with React Flow. If you're new to React Flow, check out our [getting started guide](/learn/getting-started/installation-and-requirements).
In this guide you will learn how to configure React Flow to render a flow on the server, which will allow you to
* Display static HTML diagrams in documentation
* Render React Flow diagrams in non-js environments
* Dynamically generate open graph images that appear as embeds when sharing a link to your flow
(If you want to download an image of your flow, there's an easier way to do that on the client-side in our [download image example](/examples/misc/download-image).)
##### Node dimensions
You need to configure a few things to make React Flow work on the server, the most important being the node dimensions. React Flow only renders nodes if they have a width and height. Usually you pass nodes without a specific `width` and `height`, they are then measured and the dimensions get written to `measured.width` and `measured.height`. Since we can't measure the dimensions on the server, we need to pass them explicitly. This can be done with the `width` and `height` or the `initialWidth` and `initialHeight` node properties.
```js
const nodes = [
{
id: '1',
type: 'default',
position: { x: 0, y: 0 },
data: { label: 'Node 1' },
width: 100,
height: 50,
},
];
```
React Flow now knows the dimensions of the node and can render it on the server. The `width` and `height` properties are used as an inline style for the node. If you expect nodes to have different dimensions on the client or if the dimensions should by dynamic based on the content, you can use the `initialWidth` and `initialHeight` properties. They are only used for the first render (on the server or on the client) as long as the nodes are not measured and `measured.width` and `measured.height` are not set.
There are two ways to specify node dimensions for server side rendering:
1. `width` and `height` for static dimensions that are known in advance and don't
change.
2. `initialWidth` and `initialHeight` for dynamic dimensions that are not known in
advance or change.
##### Handle positions
You probably also want to render the edges on the server. On the client, React Flow checks the positions of the handles and stores that information to draw the edges. Since we can't measure the handle positions on the server, we need to pass this information, too. This can be done with the `handles` property of a node.
```js
const nodes: Node[] = [
{
id: '1',
type: 'default',
position: { x: 0, y: 0 },
data: { label: 'Node 1' },
width: 100,
height: 50,
handles: [
{
type: 'target',
position: Position.Top,
x: 100 / 2,
y: 0,
},
{
type: 'source',
position: Position.Bottom,
x: 100 / 2,
y: 50,
},
],
},
];
```
With this additional information, React Flow knows enough about the handles to render the edges on the server. If you are fine with just rendering the nodes, you can skip this step.
##### Using `fitView` on the server
If you know the dimensions of the React Flow container itself, you can even use `fitView` on the server. For this, you need to pass the `width` and `height` of the container to the `ReactFlow` component.
```js
```
This will calculate the viewport and set the `transform` on the server in order to include all nodes in the viewport.
##### Usage with the ``
If you are using the `ReactFlowProvider`, you can pass `initialNodes`, `initialEdges` and optional wrapper dimensions (`initialWidth` and `initialHeight`) and `fitView` to the provider.
```js
```
The `initial-` prefix means that these values are only used for the first render. After that, the provider will use the `nodes` and `edges` from the context.
##### Creating static HTML
If you want to create static HTML, you can use the `renderToStaticMarkup` function from React. This will render the React Flow component to a string of HTML. You can then use this string to create a static HTML file or send it as a response to an HTTP request.
```js
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { ReactFlow, Background } from '@xyflow/react';
function toHTML({ nodes, edges, width, height }) {
const html = renderToStaticMarkup(
React.createElement(
ReactFlow,
{
nodes,
edges,
width,
height,
minZoom: 0.2,
fitView: true,
},
React.createElement(Background, null),
),
);
return html;
}
```
### Using a State Management Library
For this guide we assume that you already know about the [core
concepts](/learn/concepts/core-concepts) of React Flow and how to implement
[custom nodes](/learn/customization/custom-nodes). You should also be familiar
with the concepts of state management libraries and how to use them.
In this guide, we explain how to use React Flow with the state management library [Zustand](https://github.com/pmndrs/zustand). We will build a small app where each node features a color chooser that updates its background color. We chose Zustand for this guide because React Flow already uses it internally, but you can easily use other state management libraries such as [Redux](https://redux.js.org/), [Recoil](https://recoiljs.org/) or [Jotai](https://jotai.org/)
As demonstrated in previous guides and examples, React Flow can easily be used with a local component state to manage nodes and edges in your diagram. However, as your application grows and you need to update the state from within individual nodes, managing this state can become more complex. Instead of passing functions through the node's data field, you can use a [React context](https://reactjs.org/docs/context.html) or integrate a state management library like Zustand, as outlined in this guide.
#### Install Zustand
As mentioned above we are using Zustand in this example. Zustand is a bit like Redux: you have a central store with actions to alter your state and hooks to access your state. You can install Zustand via:
```bash copy npm2yarn
npm install --save zustand
```
#### Create a store
Zustand lets you create a hook for accessing the values and functions of your store. We put the `nodes` and `edges` and the `onNodesChange`, `onEdgesChange`, `onConnect`, `setNodes` and `setEdges` functions in the store to get the basic interactivity for our graph:
Example: learn/state-management
##### App.tsx
```tsx
import React from 'react';
import { useShallow } from 'zustand/react/shallow';
import { ReactFlow } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import useStore from './store';
const selector = (state) => ({
nodes: state.nodes,
edges: state.edges,
onNodesChange: state.onNodesChange,
onEdgesChange: state.onEdgesChange,
onConnect: state.onConnect,
});
function Flow() {
const { nodes, edges, onNodesChange, onEdgesChange, onConnect } = useStore(
useShallow(selector),
);
return (
);
}
export default Flow;
```
##### edges.ts
```ts
import { type Edge } from '@xyflow/react';
export const initialEdges = [
{ id: 'e1-2', source: '1', target: '2' },
{ id: 'e2-3', source: '2', target: '3' },
] as Edge[];
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### index.html
```html
React Flow Example
```
##### index.tsx
```tsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### nodes.ts
```ts
import { type AppNode } from './types';
export const initialNodes = [
{
id: '1',
type: 'input',
data: { label: 'Input' },
position: { x: 250, y: 25 },
},
{
id: '2',
data: { label: 'Default' },
position: { x: 100, y: 125 },
},
{
id: '3',
type: 'output',
data: { label: 'Output' },
position: { x: 250, y: 250 },
},
] as AppNode[];
```
##### store.ts
```ts
import { create } from 'zustand';
import { addEdge, applyNodeChanges, applyEdgeChanges } from '@xyflow/react';
import { initialNodes } from './nodes';
import { initialEdges } from './edges';
import { type AppState } from './types';
// this is our useStore hook that we can use in our components to get parts of the store and call actions
const useStore = create((set, get) => ({
nodes: initialNodes,
edges: initialEdges,
onNodesChange: (changes) => {
set({
nodes: applyNodeChanges(changes, get().nodes),
});
},
onEdgesChange: (changes) => {
set({
edges: applyEdgeChanges(changes, get().edges),
});
},
onConnect: (connection) => {
set({
edges: addEdge(connection, get().edges),
});
},
setNodes: (nodes) => {
set({ nodes });
},
setEdges: (edges) => {
set({ edges });
},
}));
export default useStore;
```
##### types.ts
```ts
import {
type Edge,
type Node,
type OnNodesChange,
type OnEdgesChange,
type OnConnect,
} from '@xyflow/react';
export type AppNode = Node;
export type AppState = {
nodes: AppNode[];
edges: Edge[];
onNodesChange: OnNodesChange;
onEdgesChange: OnEdgesChange;
onConnect: OnConnect;
setNodes: (nodes: AppNode[]) => void;
setEdges: (edges: Edge[]) => void;
};
```
That's the basic setup. We now have a store with nodes and edges that can handle the changes (dragging, selecting or removing a node or edge) triggered by React Flow. When you take a look at the `App.tsx` file, you can see that it's kept nice and clean. All the data and actions are now part of the store and can be accessed with the `useStore` hook.
#### Implement a color change action
We add a new `updateNodeColor` action to update the `data.color` field of a specific node. For this we pass the node id and the new color to the action, iterate over the nodes and update the matching one with the new color:
```ts
updateNodeColor: (nodeId: string, color: string) => {
set({
nodes: get().nodes.map((node) => {
if (node.id === nodeId) {
// it's important to create a new object here, to inform React Flow about the changes
return { ...node, data: { ...node.data, color } };
}
return node;
}),
});
};
```
This new action can now be used in a React component like this:
```tsx
const updateNodeColor = useStore((s) => s.updateNodeColor);
...
updateNodeColor(nodeId, color)} />;
```
#### Add a color chooser node
In this step we implement the `ColorChooserNode` component and call the `updateNodeColor` when the user changes the color. The custom part of the color chooser node is the color input.
```jsx
updateNodeColor(id, evt.target.value)}
className="nodrag"
/>
```
We add the `nodrag` class name so that the user doesn't drag the node by mistake when changing the color and call the `updateNodeColor` in the `onChange` event handler.
Example: learn/state-management-2
##### App.tsx
```tsx
import React from 'react';
import { useShallow } from 'zustand/react/shallow';
import { ReactFlow } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import useStore from './store';
import ColorChooserNode from './ColorChooserNode';
const nodeTypes = { colorChooser: ColorChooserNode };
const selector = (state) => ({
nodes: state.nodes,
edges: state.edges,
onNodesChange: state.onNodesChange,
onEdgesChange: state.onEdgesChange,
onConnect: state.onConnect,
});
function Flow() {
const { nodes, edges, onNodesChange, onEdgesChange, onConnect } = useStore(
useShallow(selector),
);
return (
);
}
export default Flow;
```
##### ColorChooserNode.tsx
```tsx
import React from 'react';
import { Handle, type NodeProps, Position } from '@xyflow/react';
import useStore from './store';
import { type ColorNode } from './types';
function ColorChooserNode({ id, data }: NodeProps) {
const updateNodeColor = useStore((state) => state.updateNodeColor);
return (
);
}
export default ColorChooserNode;
```
##### edges.ts
```ts
import { type Edge } from '@xyflow/react';
export const initialEdges = [
{ id: 'e1-2', source: '1', target: '2' },
{ id: 'e2-3', source: '2', target: '3' },
] as Edge[];
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### index.html
```html
React Flow Example
```
##### index.tsx
```tsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### nodes.ts
```ts
import { type AppNode } from './types';
export const initialNodes = [
{
id: '1',
type: 'colorChooser',
data: { color: '#4FD1C5' },
position: { x: 250, y: 25 },
},
{
id: '2',
type: 'colorChooser',
data: { color: '#F6E05E' },
position: { x: 100, y: 125 },
},
{
id: '3',
type: 'colorChooser',
data: { color: '#B794F4' },
position: { x: 250, y: 250 },
},
] as AppNode[];
```
##### store.ts
```ts
import { create } from 'zustand';
import { addEdge, applyNodeChanges, applyEdgeChanges } from '@xyflow/react';
import { initialNodes } from './nodes';
import { initialEdges } from './edges';
import { type AppNode, type AppState, type ColorNode } from './types';
function isColorChooserNode(node: AppNode): node is ColorNode {
return node.type === 'colorChooser';
}
// this is our useStore hook that we can use in our components to get parts of the store and call actions
const useStore = create((set, get) => ({
nodes: initialNodes,
edges: initialEdges,
onNodesChange: (changes) => {
set({
nodes: applyNodeChanges(changes, get().nodes),
});
},
onEdgesChange: (changes) => {
set({
edges: applyEdgeChanges(changes, get().edges),
});
},
onConnect: (connection) => {
set({
edges: addEdge(connection, get().edges),
});
},
setNodes: (nodes) => {
set({ nodes });
},
setEdges: (edges) => {
set({ edges });
},
updateNodeColor: (nodeId, color) => {
set({
nodes: get().nodes.map((node) => {
if (node.id === nodeId && isColorChooserNode(node)) {
// it's important to create a new object here, to inform React Flow about the changes
return { ...node, data: { ...node.data, color } };
}
return node;
}),
});
},
}));
export default useStore;
```
##### types.ts
```ts
import {
type Edge,
type Node,
type OnNodesChange,
type OnEdgesChange,
type OnConnect,
type BuiltInNode,
} from '@xyflow/react';
export type ColorNode = Node<
{
color: string;
},
'colorChooser'
>;
export type AppNode = ColorNode | BuiltInNode;
export type AppState = {
nodes: AppNode[];
edges: Edge[];
onNodesChange: OnNodesChange;
onEdgesChange: OnEdgesChange;
onConnect: OnConnect;
setNodes: (nodes: AppNode[]) => void;
setEdges: (edges: Edge[]) => void;
updateNodeColor: (nodeId: string, color: string) => void;
};
```
You can now click on a color chooser and change the background of a node.
### Testing
There are plenty of options to test a React application. If you want to test a React Flow application, we recommend to use [Cypress](https://www.cypress.io/) or [Playwright](https://playwright.dev/). React Flow needs to measure nodes in order to render edges and for that relies on rendering DOM elements.
#### Using Cypress or Playwright
If you are using Cypress or Playwright no additional setup is needed. You can refer to the getting started guide for [Cypress here](https://docs.cypress.io/guides/getting-started/installing-cypress) and for [Playwright here](https://playwright.dev/docs/intro).
#### Using Jest
If you are using [Jest](https://jestjs.io/), you need to mock some features in order to be able to run your tests. You can do that by adding this file to your project. Calling `mockReactFlow()` in a `setupTests` file (or inside a `beforeEach`) will trigger the necessary overrides.
```ts
// To make sure that the tests are working, it's important that you are using
// this implementation of ResizeObserver and DOMMatrixReadOnly
class ResizeObserver {
callback: globalThis.ResizeObserverCallback;
constructor(callback: globalThis.ResizeObserverCallback) {
this.callback = callback;
}
observe(target: Element) {
this.callback([{ target } as globalThis.ResizeObserverEntry], this);
}
unobserve() {}
disconnect() {}
}
class DOMMatrixReadOnly {
m22: number;
constructor(transform: string) {
const scale = transform?.match(/scale\(([1-9.])\)/)?.[1];
this.m22 = scale !== undefined ? +scale : 1;
}
}
// Only run the shim once when requested
let init = false;
export const mockReactFlow = () => {
if (init) return;
init = true;
global.ResizeObserver = ResizeObserver;
// @ts-ignore
global.DOMMatrixReadOnly = DOMMatrixReadOnly;
Object.defineProperties(global.HTMLElement.prototype, {
offsetHeight: {
get() {
return parseFloat(this.style.height) || 1;
},
},
offsetWidth: {
get() {
return parseFloat(this.style.width) || 1;
},
},
});
(global.SVGElement as any).prototype.getBBox = () => ({
x: 0,
y: 0,
width: 0,
height: 0,
});
};
```
If you want to test mouse events with jest (for example inside your custom nodes), you need to disable `d3-drag` as it does not work outside of the browser:
```js
```
### Usage with TypeScript
React Flow is written in TypeScript because we value the additional safety barrier it provides.
We export all the types you need for correctly typing data structures and functions you pass to the React Flow component. We also provide a way to extend the types of nodes and edges.
#### Basic usage
Let's start with the most basic types you need for a simple starting point. Typescript might already infer some of these types, but we will define them explicitly nonetheless.
```tsx
import { useState, useCallback } from 'react';
import {
ReactFlow,
addEdge,
applyNodeChanges,
applyEdgeChanges,
type Node,
type Edge,
type FitViewOptions,
type OnConnect,
type OnNodesChange,
type OnEdgesChange,
type OnNodeDrag,
type DefaultEdgeOptions,
} from '@xyflow/react';
const initialNodes: Node[] = [
{ id: '1', data: { label: 'Node 1' }, position: { x: 5, y: 5 } },
{ id: '2', data: { label: 'Node 2' }, position: { x: 5, y: 100 } },
];
const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }];
const fitViewOptions: FitViewOptions = {
padding: 0.2,
};
const defaultEdgeOptions: DefaultEdgeOptions = {
animated: true,
};
const onNodeDrag: OnNodeDrag = (_, node) => {
console.log('drag event', node.data);
};
function Flow() {
const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState(initialEdges);
const onNodesChange: OnNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[setNodes],
);
const onEdgesChange: OnEdgesChange = useCallback(
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[setEdges],
);
const onConnect: OnConnect = useCallback(
(connection) => setEdges((eds) => addEdge(connection, eds)),
[setEdges],
);
return (
);
}
```
##### Custom nodes
When working with [custom nodes](/learn/customization/custom-nodes) you have the possibility to pass a custom `Node` type (or your `Node` union) to the `NodeProps` type. There are basically two ways to work with custom nodes:
1. If you have **multiple custom nodes**, you want to pass a specific `Node` type as a generic to the `NodeProps` type:
```tsx filename="NumberNode.tsx"
import type { Node, NodeProps } from '@xyflow/react';
type NumberNode = Node<{ number: number }, 'number'>;
export default function NumberNode({ data }: NodeProps) {
return A special number: {data.number}
;
}
```
⚠️ If you specify the node data separately, you need to use `type` (an `interface` would not work here):
```ts
type NumberNodeData = { number: number };
type NumberNode = Node;
```
2. If you have **one custom node** that renders different content based on the node type, you want to pass your `Node` union type as a generic to `NodeProps`:
```tsx filename="CustomNode.tsx"
import type { Node, NodeProps } from '@xyflow/react';
type NumberNode = Node<{ number: number }, 'number'>;
type TextNode = Node<{ text: string }, 'text'>;
type AppNode = NumberNode | TextNode;
export default function CustomNode({ data }: NodeProps) {
if (data.type === 'number') {
return A special number: {data.number}
;
}
return A special text: {data.text}
;
}
```
##### Custom edges
For [custom edges](/learn/customization/custom-edges) you have the same possibility as for custom nodes.
```tsx filename="CustomEdge.tsx"
import { getStraightPath, BaseEdge, type EdgeProps, type Edge } from '@xyflow/react';
type CustomEdge = Edge<{ value: number }, 'custom'>;
export default function CustomEdge({
id,
sourceX,
sourceY,
targetX,
targetY,
}: EdgeProps) {
const [edgePath] = getStraightPath({ sourceX, sourceY, targetX, targetY });
return ;
}
```
#### Advanced usage
When creating complex applications with React Flow, you will have a number of custom nodes & edges, each with different kinds of data attached to them.
When we operate on these nodes & edges through built in functions and hooks, we have to make sure that we [narrow down](https://www.typescriptlang.org/docs/handbook/2/narrowing.html)
the types of nodes & edges to prevent runtime errors.
##### `Node` and `Edge` type unions
You will see many functions, callbacks and hooks (even the ReactFlow component itself) that expect a `NodeType` or `EdgeType` generic. These generics are
[unions](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types) of all the different types of nodes & edges you have in your application.
As long as you have typed the data objects correctly (see previous section), you can use their exported type.
If you use any of the built-in nodes ('input', 'output', 'default') or edges
('straight', 'step', 'smoothstep', 'bezier'), you can add the `BuiltInNode` and
`BuiltInEdge` types exported from `@xyflow/react` to your union type.
```tsx
import type { BuiltInNode, BuiltInEdge } from '@xyflow/react';
// Custom nodes
import NumberNode from './NumberNode';
import TextNode from './TextNode';
// Custom edge
import EditableEdge from './EditableEdge';
export type CustomNodeType = BuiltInNode | NumberNode | TextNode;
export type CustomEdgeType = BuiltInEdge | EditableEdge;
```
##### Functions passed to ` `
To receive correct types for callback functions, you can pass your union types to the `ReactFlow` component.
By doing that you will have to type your callback functions explicitly.
```tsx
import { type OnNodeDrag } from '@xyflow/react';
// ...
// Pass your union type here ...
const onNodeDrag: OnNodeDrag = useCallback((_, node) => {
if (node.type === 'number') {
// From here on, Typescript knows that node.data
// is of type { num: number }
console.log('drag event', node.data.number);
}
}, []);
const onNodesChange: OnNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[setNodes],
);
```
##### Hooks
The type unions can also be used to type the return values of many hooks.
```tsx filename="FlowComponent.tsx"
import { useReactFlow, useNodeConnections, useNodesData, useStore } from '@xyflow/react';
export default function FlowComponent() {
// returned nodes and edges are correctly typed now
const { getNodes, getEdges } = useReactFlow();
// You can type useStore by typing the selector function
const nodes = useStore((s: ReactFlowState) => s.nodes);
const connections = useNodeConnections({
handleType: 'target',
});
const nodesData = useNodesData(connections?.[0].source);
nodeData.forEach(({ type, data }) => {
if (type === 'number') {
// This is type safe because we have narrowed down the type
console.log(data.number);
}
});
// ...
}
```
##### Type guards
There are multiple ways you can define [type guards](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#typeof-type-guards) in Typescript.
One way is to define type guard functions like `isNumberNode` or `isTextNode` to filter out specific nodes from a list of nodes.
```tsx
function isNumberNode(node: CustomNodeType): node is NumberNode {
return node.type === 'number';
}
// numberNodes is of type NumberNode[]
const numberNodes = nodes.filter(isNumberNode);
```
### Uncontrolled Flow
There are two ways to use React Flow - controlled or uncontrolled. Controlled means, that you are in control of the state of the nodes and edges. In an uncontrolled flow the state of the nodes and edges is handled by React Flow internally. In this part we will show you how to work with an uncontrolled flow.
An implementation of an uncontrolled flow is simpler, because you don't need to pass any handlers:
Example: learn/uncontrolled
##### App.jsx
```jsx
import { ReactFlow } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { defaultNodes } from './nodes';
import { defaultEdges } from './edges';
const edgeOptions = {
animated: true,
style: {
stroke: 'white',
},
};
const connectionLineStyle = { stroke: 'white' };
export default function Flow() {
return (
);
}
```
##### edges.js
```js
export const defaultEdges = [{ id: 'ea-b', source: 'a', target: 'b' }];
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### nodes.js
```js
export const defaultNodes = [
{
id: 'a',
type: 'input',
data: { label: 'Node A' },
position: { x: 250, y: 25 },
},
{
id: 'b',
data: { label: 'Node B' },
position: { x: 100, y: 125 },
},
{
id: 'c',
type: 'output',
data: { label: 'Node C' },
position: { x: 250, y: 250 },
},
];
```
As you can see, we are passing `defaultEdgeOptions` to define that edges are animated. This is helpful, because you can't use the `onConnect` handler anymore to pass custom options to a newly created edge. Try to connect "Node B" with "Node C" and you see that the new edge is animated.
#### Updating nodes and edges
Since you don't have nodes and edges in your local state, you can't update them directly. To do so, you need to use the [React Flow instance](/api-reference/types/react-flow-instance) that comes with functions for updating the internal state. You can receive the instance via the `onInit` callback or better by using the [`useReactFlow` hook](/api-reference/hooks/use-react-flow). Let's create a button that adds a new node at a random position. For this, we are wrapping our flow with the [`ReactFlowProvider`](/api-reference/react-flow-provider) and use the [`addNodes` function](/api-reference/types/react-flow-instance#nodes-and-edges).
The `Flow` component in this example is wrapped with the `ReactFlowProvider`
to use the `useReactFlow` hook.
Example: learn/uncontrolled-2
##### App.jsx
```jsx
import { useCallback } from 'react';
import { ReactFlow, ReactFlowProvider, useReactFlow } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { defaultNodes } from './nodes';
import { defaultEdges } from './edges';
const edgeOptions = {
animated: true,
style: {
stroke: 'white',
},
};
const connectionLineStyle = { stroke: 'white' };
let nodeId = 0;
function Flow() {
const reactFlowInstance = useReactFlow();
const onClick = useCallback(() => {
const id = `${++nodeId}`;
const newNode = {
id,
position: {
x: Math.random() * 500,
y: Math.random() * 500,
},
data: {
label: `Node ${id}`,
},
};
reactFlowInstance.addNodes(newNode);
}, []);
return (
<>
add node
>
);
}
export default function () {
return (
);
}
```
##### edges.js
```js
export const defaultEdges = [{ id: 'ea-b', source: 'a', target: 'b' }];
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
.btn-add {
position: absolute;
z-index: 10;
top: 10px;
left: 10px;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### nodes.js
```js
export const defaultNodes = [
{
id: 'a',
type: 'input',
data: { label: 'Node A' },
position: { x: 250, y: 25 },
},
{
id: 'b',
data: { label: 'Node B' },
position: { x: 100, y: 125 },
},
{
id: 'c',
type: 'output',
data: { label: 'Node C' },
position: { x: 250, y: 250 },
},
];
```
### Whiteboard Features
React Flow is designed for building node-based UIs like workflow editors, flowcharts and
diagrams. Even if React Flow is not made for creating whiteboard applications, you might
want to integrate common whiteboard features. These examples show how to add drawing
capabilities to your applications when you need to annotate or sketch alongside your nodes
and edges.
#### Examples
##### ✏️ Freehand draw (Pro)
Draw smooth curves on your React Flow pane. Useful for annotations or sketching around
existing nodes.
**Features:**
* Mouse/touch drawing
* Adjustable brush size and color
* converts drawn paths into custom nodes
**Common uses:**
* Annotating flowcharts
* Adding notes to diagrams
* Sketching ideas around nodes
! THIS IS A PRO EXAMPLE. SUBSCRIBE TO TO ACCESS PRO EXAMPLES !
##### 🎯 Lasso selection
Select multiple elements by drawing a freeform selection area with an option to include
partially selected elements.
**Features:**
* Freeform selection shapes
* partial selection of elements
**Common uses:**
* Selecting nodes and annotations together
* Complex selections in mixed content
Example: examples/whiteboard/lasso-selection
##### App.jsx
```jsx
import { useCallback, useState } from 'react';
import {
ReactFlow,
useNodesState,
useEdgesState,
addEdge,
Controls,
Background,
Panel,
} from '@xyflow/react';
import { Lasso } from './Lasso';
import '@xyflow/react/dist/style.css';
const initialNodes = [
{
id: '1',
position: { x: 0, y: 0 },
data: { label: 'Hello' },
},
{
id: '2',
position: { x: 300, y: 0 },
data: { label: 'World' },
},
];
const initialEdges = [];
export default function LassoSelectionFlow() {
const [nodes, _, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params) => setEdges((els) => addEdge(params, els)), []);
const [partial, setPartial] = useState(false);
const [isLassoActive, setIsLassoActive] = useState(true);
return (
{isLassoActive && }
setIsLassoActive(true)}
>
Lasso Mode
setIsLassoActive(false)}
>
Selection Mode
setPartial((p) => !p)}
className="xy-theme__checkbox"
/>
Partial selection
);
}
```
##### Lasso.tsx
```tsx
import { useRef, type PointerEvent } from 'react';
import { useReactFlow, useStore } from '@xyflow/react';
import { getSvgPathFromStroke } from './utils';
type NodePoints = ([number, number] | [number, number, number])[];
type NodePointObject = Record;
export function Lasso({ partial }: { partial: boolean }) {
const { flowToScreenPosition, setNodes } = useReactFlow();
const { width, height, nodeLookup } = useStore((state) => ({
width: state.width,
height: state.height,
nodeLookup: state.nodeLookup,
}));
const canvas = useRef(null);
const ctx = useRef(null);
const nodePoints = useRef({});
const pointRef = useRef<[number, number][]>([]);
function handlePointerDown(e: PointerEvent) {
(e.target as HTMLCanvasElement).setPointerCapture(e.pointerId);
const points = pointRef.current;
const nextPoints = [...points, [e.pageX, e.pageY]] satisfies [number, number][];
pointRef.current = nextPoints;
nodePoints.current = {};
for (const node of nodeLookup.values()) {
const { x, y } = node.internals.positionAbsolute;
const { width = 0, height = 0 } = node.measured;
const points = [
[x, y],
[x + width, y],
[x + width, y + height],
[x, y + height],
] satisfies NodePoints;
nodePoints.current[node.id] = points;
}
ctx.current = canvas.current?.getContext('2d');
if (!ctx.current) return;
ctx.current.lineWidth = 1;
ctx.current.fillStyle = 'rgba(0, 89, 220, 0.08)';
ctx.current.strokeStyle = 'rgba(0, 89, 220, 0.8)';
}
function handlePointerMove(e: PointerEvent) {
if (e.buttons !== 1) return;
const points = pointRef.current;
const nextPoints = [...points, [e.pageX, e.pageY]] satisfies [number, number][];
pointRef.current = nextPoints;
const path = new Path2D(getSvgPathFromStroke(nextPoints));
if (!ctx.current) return;
ctx.current.clearRect(0, 0, width, height);
ctx.current.fill(path);
ctx.current.stroke(path);
const nodesToSelect = new Set();
for (const [nodeId, points] of Object.entries(nodePoints.current)) {
if (partial) {
// Partial selection: select node if any point is in the path
for (const point of points) {
const { x, y } = flowToScreenPosition({ x: point[0], y: point[1] });
if (ctx.current.isPointInPath(path, x, y)) {
nodesToSelect.add(nodeId);
break;
}
}
} else {
// Full selection: select node only if all points are in the path
let allPointsInPath = true;
for (const point of points) {
const { x, y } = flowToScreenPosition({ x: point[0], y: point[1] });
if (!ctx.current.isPointInPath(path, x, y)) {
allPointsInPath = false;
break;
}
}
if (allPointsInPath) {
nodesToSelect.add(nodeId);
}
}
}
setNodes((nodes) =>
nodes.map((node) => ({
...node,
selected: nodesToSelect.has(node.id),
})),
);
}
function handlePointerUp(e: PointerEvent) {
(e.target as HTMLCanvasElement).releasePointerCapture(e.pointerId);
pointRef.current = [];
if (ctx.current) {
ctx.current.clearRect(0, 0, width, height);
}
}
return (
);
}
```
##### xy-theme.css
```css
/* xyflow theme files. Delete these to start from our base */
.react-flow {
--xy-background-color: #f7f9fb;
/* Custom Variables */
--xy-theme-selected: #f57dbd;
--xy-theme-hover: #c5c5c5;
--xy-theme-edge-hover: black;
--xy-theme-color-focus: #e8e8e8;
/* Built-in Variables see https://reactflow.dev/learn/customization/theming */
--xy-node-border-default: 1px solid #ededed;
--xy-node-boxshadow-default:
0px 3.54px 4.55px 0px #00000005, 0px 3.54px 4.55px 0px #0000000d,
0px 0.51px 1.01px 0px #0000001a;
--xy-node-border-radius-default: 8px;
--xy-handle-background-color-default: #ffffff;
--xy-handle-border-color-default: #aaaaaa;
--xy-edge-label-color-default: #505050;
}
.react-flow.dark {
--xy-node-boxshadow-default:
0px 3.54px 4.55px 0px rgba(255, 255, 255, 0.05),
/* light shadow */ 0px 3.54px 4.55px 0px rgba(255, 255, 255, 0.13),
/* medium shadow */ 0px 0.51px 1.01px 0px rgba(255, 255, 255, 0.2); /* smallest shadow */
--xy-theme-color-focus: #535353;
}
/* Customizing Default Theming */
.react-flow__node {
box-shadow: var(--xy-node-boxshadow-default);
border-radius: var(--xy-node-border-radius-default);
background-color: var(--xy-node-background-color-default);
display: flex;
justify-content: center;
align-items: center;
text-align: center;
padding: 10px;
font-size: 12px;
flex-direction: column;
border: var(--xy-node-border-default);
color: var(--xy-node-color, var(--xy-node-color-default));
}
.react-flow__node.selectable:focus {
box-shadow: 0px 0px 0px 4px var(--xy-theme-color-focus);
border-color: #d9d9d9;
}
.react-flow__node.selectable:focus:active {
box-shadow: var(--xy-node-boxshadow-default);
}
.react-flow__node.selectable:hover,
.react-flow__node.draggable:hover {
border-color: var(--xy-theme-hover);
}
.react-flow__node.selectable.selected {
border-color: var(--xy-theme-selected);
box-shadow: var(--xy-node-boxshadow-default);
}
.react-flow__node-group {
background-color: rgba(207, 182, 255, 0.4);
border-color: #9e86ed;
}
.react-flow__edge.selectable:hover .react-flow__edge-path,
.react-flow__edge.selectable.selected .react-flow__edge-path {
stroke: var(--xy-theme-edge-hover);
}
.react-flow__handle {
background-color: var(--xy-handle-background-color-default);
}
.react-flow__handle.connectionindicator:hover {
pointer-events: all;
border-color: var(--xy-theme-edge-hover);
background-color: white;
}
.react-flow__handle.connectionindicator:focus,
.react-flow__handle.connectingfrom,
.react-flow__handle.connectingto {
border-color: var(--xy-theme-edge-hover);
}
.react-flow__node-resizer {
border-radius: 0;
border: none;
}
.react-flow__resize-control.handle {
background-color: #ffffff;
border-color: #9e86ed;
border-radius: 0;
width: 5px;
height: 5px;
}
/*
Custom Example CSS - This CSS is to improve the example experience.
You can remove it if you want to use the default styles.
New Theme Classes:
.xy-theme__button - Styles for buttons.
.xy-theme__input - Styles for text inputs.
.xy-theme__checkbox - Styles for checkboxes.
.xy-theme__select - Styles for dropdown selects.
.xy-theme__label - Styles for labels.
Use these classes to apply consistent theming across your components.
*/
:root {
--color-primary: #ff0073;
--color-background: #fefefe;
--color-hover-bg: #f6f6f6;
--color-disabled: #76797e;
}
.xy-theme__button-group {
display: flex;
align-items: center;
.xy-theme__button:first-child {
border-radius: 100px 0 0 100px;
}
.xy-theme__button:last-child {
border-radius: 0 100px 100px 0;
margin: 0;
}
}
/* Custom Button Styling */
.xy-theme__button {
display: inline-flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 1rem;
border-radius: 100px;
border: 1px solid var(--color-primary);
background-color: var(--color-background);
color: var(--color-primary);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
box-shadow: var(--xy-node-boxshadow-default);
cursor: pointer;
}
.xy-theme__button.active {
background-color: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.xy-theme__button.active:hover,
.xy-theme__button.active:active {
background-color: var(--color-primary);
opacity: 0.9;
}
.xy-theme__button:hover {
background-color: var(--xy-controls-button-background-color-hover-default);
}
.xy-theme__button:active {
background-color: var(--color-hover-bg);
}
.xy-theme__button:disabled {
color: var(--color-disabled);
opacity: 0.8;
cursor: not-allowed;
border: 1px solid var(--color-disabled);
}
.xy-theme__button > span {
margin-right: 0.2rem;
}
/* Add gap between adjacent buttons */
.xy-theme__button + .xy-theme__button {
margin-left: 0.3rem;
}
/* Example Input Styling */
.xy-theme__input {
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-primary);
border-radius: 7px;
background-color: var(--color-background);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
font-size: 1rem;
color: inherit;
}
.xy-theme__input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(255, 0, 115, 0.3);
}
/* Specific Checkbox Styling */
.xy-theme__checkbox {
appearance: none;
-webkit-appearance: none;
width: 1.25rem;
height: 1.25rem;
border-radius: 7px;
border: 2px solid var(--color-primary);
background-color: var(--color-background);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
cursor: pointer;
display: inline-block;
vertical-align: middle;
margin-right: 0.5rem;
}
.xy-theme__checkbox:checked {
background-color: var(--color-primary);
border-color: var(--color-primary);
}
.xy-theme__checkbox:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(255, 0, 115, 0.3);
}
/* Dropdown Styling */
.xy-theme__select {
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-primary);
border-radius: 50px;
background-color: var(--color-background);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
font-size: 1rem;
color: inherit;
margin-right: 0.5rem;
box-shadow: var(--xy-node-boxshadow-default);
}
.xy-theme__select:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(255, 0, 115, 0.3);
}
.xy-theme__label {
margin-top: 10px;
margin-bottom: 3px;
display: inline-block;
}
```
##### index.css
```css
@import url('./xy-theme.css');
html,
body {
margin: 0;
font-family: sans-serif;
box-sizing: border-box;
}
#app {
width: 100vw;
height: 100vh;
}
.tool-overlay {
pointer-events: auto;
position: absolute;
top: 0;
left: 0;
z-index: 4;
height: 100%;
width: 100%;
transform-origin: top left;
touch-action: none;
}
.lasso-controls {
display: flex;
align-items: center;
gap: 10px;
}
.lasso-controls button {
width: 150px;
}
.lasso-controls label {
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
}
```
##### utils.ts
```ts
import getStroke from "perfect-freehand";
export const pathOptions = {
size: 7,
thinning: 0.5,
smoothing: 0.5,
streamline: 0.5,
easing: (t: number) => t,
start: {
taper: 0,
easing: (t: number) => t,
cap: true,
},
end: {
taper: 0.1,
easing: (t: number) => t,
cap: true,
},
};
export function getSvgPathFromStroke(stroke: number[][]) {
if (!stroke.length) return "";
const d = stroke.reduce(
(acc, [x0, y0], i, arr) => {
const [x1, y1] = arr[(i + 1) % arr.length];
acc.push(x0, y0, ",", (x0 + x1) / 2, (y0 + y1) / 2);
return acc;
},
["M", ...stroke[0], "Q"],
);
d.push("Z");
return d.join(" ");
}
export function pointsToPath(points: [number, number, number][], zoom = 1) {
const stroke = getStroke(points, {
...pathOptions,
size: pathOptions.size * zoom,
});
return getSvgPathFromStroke(stroke);
}
```
##### 🧹 Eraser
Remove items by "erasing" over them. Uses collision detection to determine what to delete.
**Features:**
* Collision-based erasing
* Visual eraser cursor
**Common uses:**
* Removing parts of a flow
Example: examples/whiteboard/eraser
##### App.jsx
```jsx
import { useCallback, useState } from 'react';
import {
ReactFlow,
useNodesState,
useEdgesState,
addEdge,
Controls,
Background,
Panel,
} from '@xyflow/react';
import { ErasableNode } from './ErasableNode';
import { ErasableEdge } from './ErasableEdge';
import { Eraser } from './Eraser';
import '@xyflow/react/dist/style.css';
const initialNodes = [
{
id: '1',
type: 'erasable-node',
position: { x: 0, y: 0 },
data: { label: 'Hello' },
},
{
id: '2',
type: 'erasable-node',
position: { x: 300, y: 0 },
data: { label: 'World' },
},
];
const initialEdges = [
{
id: '1->2',
type: 'erasable-edge',
source: '1',
target: '2',
},
];
const nodeTypes = {
'erasable-node': ErasableNode,
};
const edgeTypes = {
'erasable-edge': ErasableEdge,
};
const defaultEdgeOptions = {
type: 'erasable-edge',
};
export default function EraserFlow() {
const [nodes, _, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params) => setEdges((els) => addEdge(params, els)), []);
const [isEraserActive, setIsEraserActive] = useState(true);
return (
{isEraserActive && }
setIsEraserActive(true)}
>
Eraser Mode
setIsEraserActive(false)}
>
Selection Mode
);
}
```
##### ErasableEdge.tsx
```tsx
import {
BaseEdge,
type EdgeProps,
type Edge,
getSmoothStepPath,
useInternalNode,
} from '@xyflow/react';
import { ErasableNodeType } from './ErasableNode';
export type ErasableEdgeType = Edge<{ toBeDeleted?: boolean }, 'erasable-edge'>;
export function ErasableEdge({
id,
source,
sourceX,
sourceY,
target,
targetX,
targetY,
data,
}: EdgeProps) {
const [edgePath] = getSmoothStepPath({ sourceX, sourceY, targetX, targetY });
const sourceNode = useInternalNode(source);
const targetNode = useInternalNode(target);
const toBeDeleted =
data?.toBeDeleted || sourceNode?.data.toBeDeleted || targetNode?.data.toBeDeleted;
return ;
}
```
##### ErasableNode.tsx
```tsx
import { Handle, Position, type Node, type NodeProps } from '@xyflow/react';
export type ErasableNodeType = Node<
{ toBeDeleted?: boolean; label?: string },
'erasable-node'
>;
export function ErasableNode({
data: { label, toBeDeleted },
}: NodeProps) {
return (
);
}
```
##### Eraser.tsx
```tsx
import { useRef, useEffect, type PointerEvent } from 'react';
import {
useEdges,
useNodes,
useReactFlow,
useStore,
type ReactFlowState,
} from '@xyflow/react';
import getStroke from 'perfect-freehand';
import { polylineIntersectsRectangle, pathsIntersect } from './utils';
import { ErasableNodeType } from './ErasableNode';
import { ErasableEdgeType } from './ErasableEdge';
// Type definitions for path coordinates
// - can be 2D or 3D points (with pressure) for freehand strokes for instance
type PathPoints = ([number, number] | [number, number, number])[];
type IntersectionData = {
id: string;
type?: string;
points?: PathPoints;
rect?: { x: number; y: number; width: number; height: number };
};
type TimestampedPoint = {
point: [number, number];
timestamp: number;
};
// Threshold distance for detecting intersections between paths
const intersectionThreshold = 5;
// Distance between points to sample for edge intersection detection
// This is a trade-off between performance and accuracy
const sampleDistance = 150;
const pathOptions = {
size: Math.max(10, intersectionThreshold),
thinning: 0.5,
smoothing: 0.5,
streamline: 0.5,
easing: (t: number) => t,
start: {
taper: true,
},
end: {
taper: 0,
},
};
const storeSelector = (state: ReactFlowState) => ({
width: state.width,
height: state.height,
});
/**
* Eraser component that provides an overlay canvas for erasing nodes and edges.
* Draws a visual trail and detects intersections with flow elements to mark them for deletion.
*/
export function Eraser() {
const { width, height } = useStore(storeSelector);
const { screenToFlowPosition, deleteElements, getInternalNode, setNodes, setEdges } =
useReactFlow();
const nodes = useNodes();
const edges = useEdges();
const canvas = useRef(null);
const ctx = useRef();
// Cached intersection data for performance during dragging
const nodeIntersectionData = useRef([]);
const edgeIntersectionData = useRef([]);
const trailPoints = useRef([]);
const animationFrame = useRef(0);
const isDrawing = useRef(false);
useEffect(() => {
return () => {
if (animationFrame.current) {
cancelAnimationFrame(animationFrame.current);
}
};
}, []);
function handlePointerDown(e: PointerEvent) {
(e.target as HTMLCanvasElement).setPointerCapture(e.pointerId);
isDrawing.current = true;
trailPoints.current = [
{
point: [e.pageX, e.pageY],
timestamp: Date.now(),
},
];
nodeIntersectionData.current = [];
for (const node of nodes) {
const internalNode = getInternalNode(node.id);
if (!internalNode) continue;
const { x, y } = internalNode.internals.positionAbsolute;
const { width = 0, height = 0 } = internalNode.measured;
nodeIntersectionData.current.push({
id: node.id,
type: node.type,
rect: { x, y, width, height },
});
}
edgeIntersectionData.current = [];
for (const edge of edges) {
const path = document.querySelector(
`.react-flow__edge[data-id="${edge.id}"] path`,
);
if (!path) continue;
const length = path.getTotalLength();
const steps = length / Math.max(10, length / sampleDistance);
const points: [number, number][] = [];
for (let i = 0; i <= length + steps; i += steps) {
const point = path.getPointAtLength(i);
points.push([point.x, point.y]);
}
edgeIntersectionData.current.push({
id: edge.id,
type: edge.type,
points,
});
}
ctx.current = canvas.current?.getContext('2d');
if (!ctx.current) return;
ctx.current.lineWidth = 1;
if (animationFrame.current) {
cancelAnimationFrame(animationFrame.current);
}
animate();
}
function handlePointerMove(e: PointerEvent) {
if (e.buttons !== 1) return;
trailPoints.current.push({
point: [e.pageX, e.pageY],
timestamp: Date.now(),
});
const points = trailPoints.current.map((tp) => tp.point);
if (!ctx.current || points.length < 2) return;
const flowPoints = points.map(([x, y]) => {
const flowPos = screenToFlowPosition({ x, y });
return [flowPos.x, flowPos.y] as [number, number];
});
const nodesToDelete = new Set();
const edgesToDelete = new Set();
for (const nodeInfo of nodeIntersectionData.current) {
let intersects = false;
if (nodeInfo.type === 'freehand' && nodeInfo.points) {
intersects = pathsIntersect(
flowPoints,
nodeInfo.points as [number, number][],
intersectionThreshold,
);
} else if (nodeInfo.rect) {
intersects = polylineIntersectsRectangle(flowPoints, nodeInfo.rect);
}
if (intersects) {
nodesToDelete.add(nodeInfo.id);
}
}
for (const edgeInfo of edgeIntersectionData.current) {
let intersects = false;
if (edgeInfo.points) {
intersects = pathsIntersect(
flowPoints,
edgeInfo.points as [number, number][],
intersectionThreshold,
);
}
if (intersects) {
edgesToDelete.add(edgeInfo.id);
}
}
setNodes((nodes: ErasableNodeType[]) =>
nodes.map((node) => {
if (nodesToDelete.has(node.id)) {
return {
...node,
data: {
...node.data,
toBeDeleted: true,
},
};
}
return node;
}),
);
setEdges((edges: ErasableEdgeType[]) =>
edges.map((edge) => {
if (edgesToDelete.has(edge.id)) {
return {
...edge,
data: {
...edge.data,
toBeDeleted: true,
},
};
}
return edge;
}),
);
}
function handlePointerUp(e: PointerEvent) {
(e.target as HTMLCanvasElement).releasePointerCapture(e.pointerId);
deleteElements({
nodes: nodes.filter((node) => node.data.toBeDeleted),
edges: edges.filter((edge) => edge.data?.toBeDeleted),
});
trailPoints.current = [];
isDrawing.current = false;
if (!animationFrame.current) {
animate();
}
}
function drawTrail() {
if (!ctx.current || !canvas.current) return;
ctx.current.clearRect(0, 0, canvas.current.width, canvas.current.height);
if (trailPoints.current.length < 2) return;
const strokePoints: [number, number, number][] = trailPoints.current.map(
({ point }) => [point[0], point[1], 0.5],
);
const stroke = getStroke(strokePoints, pathOptions);
if (stroke.length < 2) return;
ctx.current.fillStyle = '#ef4444';
ctx.current.globalAlpha = 0.6;
ctx.current.beginPath();
stroke.forEach(([x, y], i) => {
if (i === 0) {
ctx.current!.moveTo(x, y);
} else {
ctx.current!.lineTo(x, y);
}
});
ctx.current.closePath();
ctx.current.fill();
ctx.current.globalAlpha = 1.0;
}
function removeOldPoints() {
const now = Date.now();
const cutoffTime = now - 100;
trailPoints.current = trailPoints.current.filter((tp) => tp.timestamp > cutoffTime);
}
function animate() {
removeOldPoints();
drawTrail();
if (isDrawing.current || trailPoints.current.length > 0) {
animationFrame.current = requestAnimationFrame(animate);
}
}
return (
);
}
```
##### xy-theme.css
```css
/* xyflow theme files. Delete these to start from our base */
.react-flow {
--xy-background-color: #f7f9fb;
/* Custom Variables */
--xy-theme-selected: #f57dbd;
--xy-theme-hover: #c5c5c5;
--xy-theme-edge-hover: black;
--xy-theme-color-focus: #e8e8e8;
/* Built-in Variables see https://reactflow.dev/learn/customization/theming */
--xy-node-border-default: 1px solid #ededed;
--xy-node-boxshadow-default:
0px 3.54px 4.55px 0px #00000005, 0px 3.54px 4.55px 0px #0000000d,
0px 0.51px 1.01px 0px #0000001a;
--xy-node-border-radius-default: 8px;
--xy-handle-background-color-default: #ffffff;
--xy-handle-border-color-default: #aaaaaa;
--xy-edge-label-color-default: #505050;
}
.react-flow.dark {
--xy-node-boxshadow-default:
0px 3.54px 4.55px 0px rgba(255, 255, 255, 0.05),
/* light shadow */ 0px 3.54px 4.55px 0px rgba(255, 255, 255, 0.13),
/* medium shadow */ 0px 0.51px 1.01px 0px rgba(255, 255, 255, 0.2); /* smallest shadow */
--xy-theme-color-focus: #535353;
}
/* Customizing Default Theming */
.react-flow__node {
box-shadow: var(--xy-node-boxshadow-default);
border-radius: var(--xy-node-border-radius-default);
background-color: var(--xy-node-background-color-default);
display: flex;
justify-content: center;
align-items: center;
text-align: center;
padding: 10px;
font-size: 12px;
flex-direction: column;
border: var(--xy-node-border-default);
color: var(--xy-node-color, var(--xy-node-color-default));
}
.react-flow__node.selectable:focus {
box-shadow: 0px 0px 0px 4px var(--xy-theme-color-focus);
border-color: #d9d9d9;
}
.react-flow__node.selectable:focus:active {
box-shadow: var(--xy-node-boxshadow-default);
}
.react-flow__node.selectable:hover,
.react-flow__node.draggable:hover {
border-color: var(--xy-theme-hover);
}
.react-flow__node.selectable.selected {
border-color: var(--xy-theme-selected);
box-shadow: var(--xy-node-boxshadow-default);
}
.react-flow__node-group {
background-color: rgba(207, 182, 255, 0.4);
border-color: #9e86ed;
}
.react-flow__edge.selectable:hover .react-flow__edge-path,
.react-flow__edge.selectable.selected .react-flow__edge-path {
stroke: var(--xy-theme-edge-hover);
}
.react-flow__handle {
background-color: var(--xy-handle-background-color-default);
}
.react-flow__handle.connectionindicator:hover {
pointer-events: all;
border-color: var(--xy-theme-edge-hover);
background-color: white;
}
.react-flow__handle.connectionindicator:focus,
.react-flow__handle.connectingfrom,
.react-flow__handle.connectingto {
border-color: var(--xy-theme-edge-hover);
}
.react-flow__node-resizer {
border-radius: 0;
border: none;
}
.react-flow__resize-control.handle {
background-color: #ffffff;
border-color: #9e86ed;
border-radius: 0;
width: 5px;
height: 5px;
}
/*
Custom Example CSS - This CSS is to improve the example experience.
You can remove it if you want to use the default styles.
New Theme Classes:
.xy-theme__button - Styles for buttons.
.xy-theme__input - Styles for text inputs.
.xy-theme__checkbox - Styles for checkboxes.
.xy-theme__select - Styles for dropdown selects.
.xy-theme__label - Styles for labels.
Use these classes to apply consistent theming across your components.
*/
:root {
--color-primary: #ff0073;
--color-background: #fefefe;
--color-hover-bg: #f6f6f6;
--color-disabled: #76797e;
}
.xy-theme__button-group {
display: flex;
align-items: center;
.xy-theme__button:first-child {
border-radius: 100px 0 0 100px;
}
.xy-theme__button:last-child {
border-radius: 0 100px 100px 0;
margin: 0;
}
}
/* Custom Button Styling */
.xy-theme__button {
display: inline-flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 1rem;
border-radius: 100px;
border: 1px solid var(--color-primary);
background-color: var(--color-background);
color: var(--color-primary);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
box-shadow: var(--xy-node-boxshadow-default);
cursor: pointer;
}
.xy-theme__button.active {
background-color: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.xy-theme__button.active:hover,
.xy-theme__button.active:active {
background-color: var(--color-primary);
opacity: 0.9;
}
.xy-theme__button:hover {
background-color: var(--xy-controls-button-background-color-hover-default);
}
.xy-theme__button:active {
background-color: var(--color-hover-bg);
}
.xy-theme__button:disabled {
color: var(--color-disabled);
opacity: 0.8;
cursor: not-allowed;
border: 1px solid var(--color-disabled);
}
.xy-theme__button > span {
margin-right: 0.2rem;
}
/* Add gap between adjacent buttons */
.xy-theme__button + .xy-theme__button {
margin-left: 0.3rem;
}
/* Example Input Styling */
.xy-theme__input {
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-primary);
border-radius: 7px;
background-color: var(--color-background);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
font-size: 1rem;
color: inherit;
}
.xy-theme__input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(255, 0, 115, 0.3);
}
/* Specific Checkbox Styling */
.xy-theme__checkbox {
appearance: none;
-webkit-appearance: none;
width: 1.25rem;
height: 1.25rem;
border-radius: 7px;
border: 2px solid var(--color-primary);
background-color: var(--color-background);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
cursor: pointer;
display: inline-block;
vertical-align: middle;
margin-right: 0.5rem;
}
.xy-theme__checkbox:checked {
background-color: var(--color-primary);
border-color: var(--color-primary);
}
.xy-theme__checkbox:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(255, 0, 115, 0.3);
}
/* Dropdown Styling */
.xy-theme__select {
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-primary);
border-radius: 50px;
background-color: var(--color-background);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
font-size: 1rem;
color: inherit;
margin-right: 0.5rem;
box-shadow: var(--xy-node-boxshadow-default);
}
.xy-theme__select:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(255, 0, 115, 0.3);
}
.xy-theme__label {
margin-top: 10px;
margin-bottom: 3px;
display: inline-block;
}
```
##### index.css
```css
@import url('./xy-theme.css');
html,
body {
margin: 0;
font-family: sans-serif;
box-sizing: border-box;
}
#app {
width: 100vw;
height: 100vh;
}
.tool-overlay {
pointer-events: auto;
position: absolute;
top: 0;
left: 0;
z-index: 4;
height: 100%;
width: 100%;
transform-origin: top left;
cursor: crosshair;
touch-action: none;
}
```
##### utils.ts
```ts
// Utility functions for geometric intersection detection
// Type definitions for better type safety
type Point = [number, number];
type Rectangle = { x: number; y: number; width: number; height: number };
// Check if two line segments intersect
function lineSegmentsIntersect(p1: Point, p2: Point, p3: Point, p4: Point): boolean {
const [x1, y1] = p1;
const [x2, y2] = p2;
const [x3, y3] = p3;
const [x4, y4] = p4;
const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
if (Math.abs(denom) < 1e-10) return false; // Lines are parallel
const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom;
const u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom;
return t >= 0 && t <= 1 && u >= 0 && u <= 1;
}
// Check if a point is inside a rectangle
function pointInRectangle(point: Point, rect: Rectangle): boolean {
const [x, y] = point;
return (
x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height
);
}
// Get the four edges of a rectangle as line segments
function getRectangleEdges(rect: Rectangle): [Point, Point][] {
const { x, y, width, height } = rect;
return [
[
[x, y],
[x + width, y],
], // top edge
[
[x + width, y],
[x + width, y + height],
], // right edge
[
[x + width, y + height],
[x, y + height],
], // bottom edge
[
[x, y + height],
[x, y],
], // left edge
];
}
// Check if a polyline (series of connected line segments) intersects with a rectangle
export function polylineIntersectsRectangle(points: Point[], rect: Rectangle): boolean {
if (points.length < 2) return false;
// Early return if any point is inside the rectangle
for (const point of points) {
if (pointInRectangle(point, rect)) {
return true;
}
}
// Check if any line segment intersects with rectangle edges
const rectEdges = getRectangleEdges(rect);
for (let i = 0; i < points.length - 1; i++) {
const lineStart = points[i];
const lineEnd = points[i + 1];
for (const [edgeStart, edgeEnd] of rectEdges) {
if (lineSegmentsIntersect(lineStart, lineEnd, edgeStart, edgeEnd)) {
return true;
}
}
}
return false;
}
// Calculate distance between two points
function distanceBetweenPoints(p1: Point, p2: Point): number {
const [x1, y1] = p1;
const [x2, y2] = p2;
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}
// Calculate the closest point on a line segment to a given point
function closestPointOnSegment(
point: Point,
segmentStart: Point,
segmentEnd: Point,
): Point {
const [px, py] = point;
const [x1, y1] = segmentStart;
const [x2, y2] = segmentEnd;
const dx = x2 - x1;
const dy = y2 - y1;
const lengthSquared = dx * dx + dy * dy;
if (lengthSquared === 0) return segmentStart; // Segment is a point
const t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / lengthSquared));
return [x1 + t * dx, y1 + t * dy];
}
// Check if two paths intersect using a more efficient approach
export function pathsIntersect(
path1: Point[],
path2: Point[],
threshold: number = 1,
): boolean {
if (path1.length < 2 || path2.length < 2) return false;
// First, do the more precise line segment intersection check
for (let i = 0; i < path1.length - 1; i++) {
for (let j = 0; j < path2.length - 1; j++) {
if (lineSegmentsIntersect(path1[i], path1[i + 1], path2[j], path2[j + 1])) {
return true;
}
}
}
// If no exact intersection, check for proximity based on threshold
if (threshold > 0) {
for (let i = 0; i < path1.length - 1; i++) {
const segment1Start = path1[i];
const segment1End = path1[i + 1];
for (let j = 0; j < path2.length - 1; j++) {
const segment2Start = path2[j];
const segment2End = path2[j + 1];
// Check distance between segment endpoints and closest points
const distances = [
distanceBetweenPoints(
segment1Start,
closestPointOnSegment(segment1Start, segment2Start, segment2End),
),
distanceBetweenPoints(
segment1End,
closestPointOnSegment(segment1End, segment2Start, segment2End),
),
distanceBetweenPoints(
segment2Start,
closestPointOnSegment(segment2Start, segment1Start, segment1End),
),
distanceBetweenPoints(
segment2End,
closestPointOnSegment(segment2End, segment1Start, segment1End),
),
];
if (Math.min(...distances) <= threshold) {
return true;
}
}
}
}
return false;
}
// Simplified path sampling for cases where you need discrete points
export function samplePathPoints(points: Point[], maxDistance: number = 5): Point[] {
if (points.length < 2) return [...points];
const result: Point[] = [points[0]];
for (let i = 1; i < points.length; i++) {
const prev = result[result.length - 1];
const current = points[i];
const distance = distanceBetweenPoints(prev, current);
if (distance > maxDistance) {
// Add intermediate points
const numSegments = Math.ceil(distance / maxDistance);
for (let j = 1; j < numSegments; j++) {
const t = j / numSegments;
const interpolated: Point = [
prev[0] + (current[0] - prev[0]) * t,
prev[1] + (current[1] - prev[1]) * t,
];
result.push(interpolated);
}
}
result.push(current);
}
return result;
}
```
##### 📐 Rectangle draw
Create rectangular shapes by clicking and dragging. Good for highlighting areas or
creating backgrounds for node groups.
**Features:**
* Click-and-drag rectangle creation
* Customizable colors
**Common uses:**
* Creating background containers
* Grouping related nodes visually
* Highlighting sections of diagrams
Example: examples/whiteboard/rectangle
##### App.jsx
```jsx
import { useCallback, useState } from 'react';
import {
ReactFlow,
useNodesState,
useEdgesState,
addEdge,
Controls,
Background,
Panel,
} from '@xyflow/react';
import { RectangleNode } from './RectangleNode';
import { RectangleTool } from './RectangleTool';
import '@xyflow/react/dist/style.css';
const initialNodes = [
{
id: '1',
type: 'rectangle',
position: { x: 250, y: 5 },
data: { color: '#ff7000' },
width: 150,
height: 100,
},
];
const initialEdges = [];
const nodeTypes = {
rectangle: RectangleNode,
};
export default function RectangleFlow() {
const [nodes, _, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params) => setEdges((els) => addEdge(params, els)), []);
const [isRectangleActive, setIsRectangleActive] = useState(true);
return (
{isRectangleActive && }
setIsRectangleActive(true)}
>
Rectangle Mode
setIsRectangleActive(false)}
>
Selection Mode
);
}
```
##### RectangleNode.tsx
```tsx
import {
NodeResizer,
NodeToolbar,
type Node,
type NodeProps,
useReactFlow,
useOnSelectionChange,
} from '@xyflow/react';
import { useCallback, useState } from 'react';
export type RectangleNodeType = Node<{ color: string }, 'rectangle'>;
const colorOptions = [
'#f5efe9', // very light warm grey
'#ef4444', // red
'#f97316', // orange
'#eab308', // yellow
'#22c55e', // green
'#3b82f6', // blue
'#8b5cf6', // purple
'#ec4899', // pink
'#64748b', // gray
];
const styles = {
toolbar: {
display: 'flex',
gap: '0.25rem',
borderRadius: '0.5rem',
border: '1px solid #e5e5e5',
backgroundColor: 'white',
padding: '0.5rem',
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
},
colorButton: {
height: '1.5rem',
width: '1.5rem',
borderRadius: '9999px',
border: 'none',
cursor: 'pointer',
transition: 'transform 0.15s ease-in-out',
},
colorButtonHover: {
transform: 'scale(1.1)',
},
outerContainer: {
display: 'flex',
height: '100%',
width: '100%',
alignItems: 'center',
justifyContent: 'center',
},
innerContainer: {
position: 'relative' as const,
height: 'calc(100% - 5px)',
width: 'calc(100% - 5px)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'white',
borderRadius: '0.375rem',
boxShadow: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
border: '1px solid #e5e5e5',
},
innerContainerSelected: {
outline: '2px solid #3b82f6',
outlineOffset: '2px',
},
} as const;
export function RectangleNode({
id,
selected,
dragging,
data: { color },
}: NodeProps) {
const { updateNodeData } = useReactFlow();
const [multipleNodesSelected, setMultipleNodesSelected] = useState(false);
const onSelectionChange = useCallback(
({ nodes }: { nodes: Node[] }) => {
if (nodes.length > 1) {
setMultipleNodesSelected(true);
} else {
setMultipleNodesSelected(false);
}
},
[setMultipleNodesSelected],
);
useOnSelectionChange({ onChange: onSelectionChange });
const handleColorChange = (newColor: string) => {
updateNodeData(id, { color: newColor });
};
return (
<>
{colorOptions.map((colorOption) => (
handleColorChange(colorOption)}
style={{
...styles.colorButton,
backgroundColor: colorOption,
}}
title={`Set color to ${colorOption}`}
/>
))}
>
);
}
```
##### RectangleTool.tsx
```tsx
import { useState, type PointerEvent } from 'react';
import { useReactFlow, type XYPosition } from '@xyflow/react';
function getPosition(start: XYPosition, end: XYPosition) {
return {
x: Math.min(start.x, end.x),
y: Math.min(start.y, end.y),
};
}
function getDimensions(start: XYPosition, end: XYPosition, zoom: number = 1) {
return {
width: Math.abs(end.x - start.x) / zoom,
height: Math.abs(end.y - start.y) / zoom,
};
}
const colors = [
'#D14D41',
'#DA702C',
'#D0A215',
'#879A39',
'#3AA99F',
'#4385BE',
'#8B7EC8',
'#CE5D97',
];
function getRandomColor(): string {
return colors[Math.floor(Math.random() * colors.length)];
}
export function RectangleTool() {
const [start, setStart] = useState(null);
const [end, setEnd] = useState(null);
const { screenToFlowPosition, getViewport, setNodes } = useReactFlow();
function handlePointerDown(e: PointerEvent) {
(e.target as HTMLCanvasElement).setPointerCapture(e.pointerId);
setStart({ x: e.pageX, y: e.pageY });
}
function handlePointerMove(e: PointerEvent) {
if (e.buttons !== 1) return;
setEnd({ x: e.pageX, y: e.pageY });
}
function handlePointerUp() {
if (!start || !end) return;
const position = screenToFlowPosition(getPosition(start, end));
const dimension = getDimensions(start, end, getViewport().zoom);
setNodes((nodes) => [
...nodes,
{
id: crypto.randomUUID(),
type: 'rectangle',
position,
...dimension,
data: {
color: getRandomColor(),
},
},
]);
setStart(null);
setEnd(null);
}
const rect =
start && end
? {
position: getPosition(start, end),
dimension: getDimensions(start, end),
}
: null;
return (
);
}
```
##### xy-theme.css
```css
/* xyflow theme files. Delete these to start from our base */
.react-flow {
--xy-background-color: #f7f9fb;
/* Custom Variables */
--xy-theme-selected: #f57dbd;
--xy-theme-hover: #c5c5c5;
--xy-theme-edge-hover: black;
--xy-theme-color-focus: #e8e8e8;
/* Built-in Variables see https://reactflow.dev/learn/customization/theming */
--xy-node-border-default: 1px solid #ededed;
--xy-node-boxshadow-default:
0px 3.54px 4.55px 0px #00000005, 0px 3.54px 4.55px 0px #0000000d,
0px 0.51px 1.01px 0px #0000001a;
--xy-node-border-radius-default: 8px;
--xy-handle-background-color-default: #ffffff;
--xy-handle-border-color-default: #aaaaaa;
--xy-edge-label-color-default: #505050;
}
.react-flow.dark {
--xy-node-boxshadow-default:
0px 3.54px 4.55px 0px rgba(255, 255, 255, 0.05),
/* light shadow */ 0px 3.54px 4.55px 0px rgba(255, 255, 255, 0.13),
/* medium shadow */ 0px 0.51px 1.01px 0px rgba(255, 255, 255, 0.2); /* smallest shadow */
--xy-theme-color-focus: #535353;
}
/* Customizing Default Theming */
.react-flow__node {
box-shadow: var(--xy-node-boxshadow-default);
border-radius: var(--xy-node-border-radius-default);
background-color: var(--xy-node-background-color-default);
display: flex;
justify-content: center;
align-items: center;
text-align: center;
padding: 10px;
font-size: 12px;
flex-direction: column;
border: var(--xy-node-border-default);
color: var(--xy-node-color, var(--xy-node-color-default));
}
.react-flow__node.selectable:focus {
box-shadow: 0px 0px 0px 4px var(--xy-theme-color-focus);
border-color: #d9d9d9;
}
.react-flow__node.selectable:focus:active {
box-shadow: var(--xy-node-boxshadow-default);
}
.react-flow__node.selectable:hover,
.react-flow__node.draggable:hover {
border-color: var(--xy-theme-hover);
}
.react-flow__node.selectable.selected {
border-color: var(--xy-theme-selected);
box-shadow: var(--xy-node-boxshadow-default);
}
.react-flow__node-group {
background-color: rgba(207, 182, 255, 0.4);
border-color: #9e86ed;
}
.react-flow__edge.selectable:hover .react-flow__edge-path,
.react-flow__edge.selectable.selected .react-flow__edge-path {
stroke: var(--xy-theme-edge-hover);
}
.react-flow__handle {
background-color: var(--xy-handle-background-color-default);
}
.react-flow__handle.connectionindicator:hover {
pointer-events: all;
border-color: var(--xy-theme-edge-hover);
background-color: white;
}
.react-flow__handle.connectionindicator:focus,
.react-flow__handle.connectingfrom,
.react-flow__handle.connectingto {
border-color: var(--xy-theme-edge-hover);
}
.react-flow__node-resizer {
border-radius: 0;
border: none;
}
.react-flow__resize-control.handle {
background-color: #ffffff;
border-color: #9e86ed;
border-radius: 0;
width: 5px;
height: 5px;
}
/*
Custom Example CSS - This CSS is to improve the example experience.
You can remove it if you want to use the default styles.
New Theme Classes:
.xy-theme__button - Styles for buttons.
.xy-theme__input - Styles for text inputs.
.xy-theme__checkbox - Styles for checkboxes.
.xy-theme__select - Styles for dropdown selects.
.xy-theme__label - Styles for labels.
Use these classes to apply consistent theming across your components.
*/
:root {
--color-primary: #ff0073;
--color-background: #fefefe;
--color-hover-bg: #f6f6f6;
--color-disabled: #76797e;
}
.xy-theme__button-group {
display: flex;
align-items: center;
.xy-theme__button:first-child {
border-radius: 100px 0 0 100px;
}
.xy-theme__button:last-child {
border-radius: 0 100px 100px 0;
margin: 0;
}
}
/* Custom Button Styling */
.xy-theme__button {
display: inline-flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 1rem;
border-radius: 100px;
border: 1px solid var(--color-primary);
background-color: var(--color-background);
color: var(--color-primary);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
box-shadow: var(--xy-node-boxshadow-default);
cursor: pointer;
}
.xy-theme__button.active {
background-color: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.xy-theme__button.active:hover,
.xy-theme__button.active:active {
background-color: var(--color-primary);
opacity: 0.9;
}
.xy-theme__button:hover {
background-color: var(--xy-controls-button-background-color-hover-default);
}
.xy-theme__button:active {
background-color: var(--color-hover-bg);
}
.xy-theme__button:disabled {
color: var(--color-disabled);
opacity: 0.8;
cursor: not-allowed;
border: 1px solid var(--color-disabled);
}
.xy-theme__button > span {
margin-right: 0.2rem;
}
/* Add gap between adjacent buttons */
.xy-theme__button + .xy-theme__button {
margin-left: 0.3rem;
}
/* Example Input Styling */
.xy-theme__input {
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-primary);
border-radius: 7px;
background-color: var(--color-background);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
font-size: 1rem;
color: inherit;
}
.xy-theme__input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(255, 0, 115, 0.3);
}
/* Specific Checkbox Styling */
.xy-theme__checkbox {
appearance: none;
-webkit-appearance: none;
width: 1.25rem;
height: 1.25rem;
border-radius: 7px;
border: 2px solid var(--color-primary);
background-color: var(--color-background);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
cursor: pointer;
display: inline-block;
vertical-align: middle;
margin-right: 0.5rem;
}
.xy-theme__checkbox:checked {
background-color: var(--color-primary);
border-color: var(--color-primary);
}
.xy-theme__checkbox:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(255, 0, 115, 0.3);
}
/* Dropdown Styling */
.xy-theme__select {
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-primary);
border-radius: 50px;
background-color: var(--color-background);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
font-size: 1rem;
color: inherit;
margin-right: 0.5rem;
box-shadow: var(--xy-node-boxshadow-default);
}
.xy-theme__select:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(255, 0, 115, 0.3);
}
.xy-theme__label {
margin-top: 10px;
margin-bottom: 3px;
display: inline-block;
}
```
##### index.css
```css
@import url('./xy-theme.css');
html,
body {
margin: 0;
font-family: sans-serif;
box-sizing: border-box;
}
#app {
width: 100vw;
height: 100vh;
}
.react-flow__node {
padding: 0;
border: none;
}
.tool-overlay {
pointer-events: auto;
position: absolute;
top: 0;
left: 0;
z-index: 4;
height: 100%;
width: 100%;
transform-origin: top left;
cursor: copy;
touch-action: none;
}
.rectangle-preview {
position: absolute;
z-index: 10;
}
```
#### Whiteboard libraries
If you are looking for a more complete whiteboard solution, consider using libraries that
are specifically designed for whiteboard applications like [tldraw](https://tldraw.dev/)
or [Excalidraw](https://docs.excalidraw.com/). These libraries provide a full set of
features for collaborative drawing, shapes, text, and more.
### Adding Interactivity
Now that we've built our [first flow](/learn/concepts/building-a-flow), let's add
\*interactivity so you can select, drag, and remove nodes and edges.
#### Handling change events
By default React Flow doesn't manage any internal state updates besides handling the
viewport. As you would with an HTML ` ` element you need to pass
[event handlers](/api-reference/react-flow#event-handlers) to React Flow in order to apply
triggered changes to your nodes and edges.
### Add imports
To manage changes, we'll be using `useState` with two helper functions from React Flow:
[`applyNodeChanges`](/api-reference/utils/apply-node-changes) and
[`applyEdgeChanges`](/api-reference/utils/apply-edge-changes). So let's import these
functions:
```jsx
import { useState, useCallback } from 'react';
import { ReactFlow, applyEdgeChanges, applyNodeChanges } from '@xyflow/react';
```
##### Define nodes and edges
We need to define initial nodes and edges. These will be the starting point for our flow.
```jsx
const initialNodes = [
{
id: 'n1',
position: { x: 0, y: 0 },
data: { label: 'Node 1' },
type: 'input',
},
{
id: 'n2',
position: { x: 100, y: 100 },
data: { label: 'Node 2' },
},
];
const initialEdges = [
{
id: 'n1-n2',
source: 'n1',
target: 'n2',
},
];
```
##### Initialize state
In our component, we'll call the `useState` hook to manage the state of our nodes and
edges:
```jsx {2-3}
export default function App() {
const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState(initialEdges);
return (
);
}
```
##### Add event handlers
We need to create two event handlers:
[`onNodesChange`](/api-reference/react-flow#onnodeschange) and
[`onEdgesChange`](/api-reference/react-flow#onedgeschange). They will be used to update
the state of our nodes and edges when changes occur, such as dragging or deleting an
element. Go ahead and add these handlers to your component:
```jsx
const onNodesChange = useCallback(
(changes) => setNodes((nodesSnapshot) => applyNodeChanges(changes, nodesSnapshot)),
[],
);
const onEdgesChange = useCallback(
(changes) => setEdges((edgesSnapshot) => applyEdgeChanges(changes, edgesSnapshot)),
[],
);
```
##### Pass them to ReactFlow
Now we can pass our nodes, edges, and event handlers to the ` ` component:
```jsx {2-5}
```
##### Interactive flow
And that's it! You now have a basic interactive flow 🎉
When you drag or select a node, the `onNodesChange` handler is triggered. The
`applyNodeChanges` function then uses these change events to update the current state of
your nodes. Here's how it all comes together. Try clicking and dragging a node to move it
around and watch the UI update in real time.
Example: learn/make-it-interactive-1
##### App.jsx
```jsx
import { useState, useCallback } from 'react';
import {
ReactFlow,
Controls,
Background,
applyNodeChanges,
applyEdgeChanges,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
const initialNodes = [
{
id: 'n1',
data: { label: 'Node 1' },
position: { x: 0, y: 0 },
type: 'input',
},
{
id: 'n2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
},
];
const initialEdges = [
{ id: 'n1-n2', source: 'n1', target: 'n2', label: 'connects with', type: 'step' },
];
function Flow() {
const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState(initialEdges);
const onNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[],
);
const onEdgesChange = useCallback(
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[],
);
return (
);
}
export default Flow;
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
#### Handling connections
One last piece is missing: connecting nodes interactively. For this, we need to implement
an [`onConnect`](/api-reference/react-flow#onconnect) handler.
### Create `onConnect` handler
The `onConnect` handler is called whenever a new connection is made between two nodes. We
can use the [`addEdge`](/api-reference/utils/add-edge) utility function to create a new
edge and update the edge Array.
```jsx
const onConnect = useCallback(
(params) => setEdges((edgesSnapshot) => addEdge(params, edgesSnapshot)),
[],
);
```
##### Pass it to ReactFlow
Now we can pass the `onConnect` handler to the ` ` component:
```jsx {6}
```
##### Connectable flow
Try to connect the two nodes by dragging from on handle to another one. The `onConnect`
handler will be triggered, and the new edge will be added to the flow. 🥳
Example: learn/make-it-interactive-2
##### App.jsx
```jsx
import { useState, useCallback } from 'react';
import {
ReactFlow,
Controls,
Background,
applyNodeChanges,
applyEdgeChanges,
addEdge,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
const initialNodes = [
{
id: 'n1',
data: { label: 'Node 1' },
position: { x: 0, y: 0 },
type: 'input',
},
{
id: 'n2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
},
];
const initialEdges = [];
function Flow() {
const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState(initialEdges);
const onNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[],
);
const onEdgesChange = useCallback(
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[],
);
const onConnect = useCallback(
(params) => setEdges((eds) => addEdge(params, eds)),
[],
);
return (
);
}
export default Flow;
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
#### Full code example 🏁
Example: learn/make-it-interactive-2
##### App.jsx
```jsx
import { useState, useCallback } from 'react';
import {
ReactFlow,
Controls,
Background,
applyNodeChanges,
applyEdgeChanges,
addEdge,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
const initialNodes = [
{
id: 'n1',
data: { label: 'Node 1' },
position: { x: 0, y: 0 },
type: 'input',
},
{
id: 'n2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
},
];
const initialEdges = [];
function Flow() {
const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState(initialEdges);
const onNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[],
);
const onEdgesChange = useCallback(
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[],
);
const onConnect = useCallback(
(params) => setEdges((eds) => addEdge(params, eds)),
[],
);
return (
);
}
export default Flow;
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
What is happening here? Whenever React Flow triggers a change (node init, node drag, edge
select, etc.), the `onNodesChange` handler gets called. We export an `applyNodeChanges`
handler so that you don't need to handle the changes by yourself. The `applyNodeChanges`
handler returns an updated array of nodes that is your new nodes array. You now have an
interactive flow with the following capabilities:
* selectable nodes and edges
* draggable nodes
* connectable nodes by dragging from one node handle to another
* multi-selection area by pressing `shift` -- the default
[`selectionKeyCode`](/api-reference/react-flow#selectionkeycode)
* multi-selection by pressing `cmd` -- the default
[`multiSelectionKeyCode`](/api-reference/react-flow#multiselectionkeycode)
* removing selected elements by pressing `backspace` -- the default
[`deleteKeyCode`](/api-reference/react-flow#deletekeycode)
If you want to jump straight into creating your own application, we recommend checking out
the Customization section. Otherwise keep reading to learn more about React Flows
capabilities.
### Building a Flow
In the following pages we will introduce you to the core concepts of React Flow and
explain how to create a basic interactive flow. A flow consists of
[nodes](/api-reference/types/node), [edges](/api-reference/types/edge) and the viewport.
To follow along with this guide you will need to have a React project set up and install
the `@xyflow/react` package:
```bash copy npm2yarn
npm install @xyflow/react
```
#### Creating the flow
Let's start by creating an empty flow with viewport
[` `](/api-reference/components/controls) and a dotted
[` `](/api-reference/components/background).
### Add imports
First, we need to import some basic components from the `@xyflow/react` package
and the **css stylesheet**, which is **required** for React Flow to work:
```jsx "import '@xyflow/react/dist/style.css';"
import { ReactFlow, Background, Controls } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
```
##### Render ReactFlow
Now we create a React component, that renders our flow. The **width and height** on the
parent container are **required** because React Flow uses these dimensions.
```jsx "height: '100%', width: '100%'"
export default function App() {
return (
);
}
```
##### Empty flow
That's it! You have created your first empty flow 🎉
Example: learn/building-a-flow-1
##### App.jsx
```jsx
import { ReactFlow, Controls, Background } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
function Flow() {
return (
);
}
export default Flow;
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
#### Adding nodes
Now that the flow is set up, it's time to add nodes — each node represents an element in
your diagram with a specific position and content.
### Create node objects
Outside of your React component, create an array of [node](/api-reference/types/node)
objects. Each node object needs a unique `id` and a `position`. Let's also add a label to
them:
```jsx
const initialNodes = [
{
id: 'n1',
position: { x: 0, y: 0 },
data: { label: 'Node 1' },
type: 'input',
},
{
id: 'n2',
position: { x: 100, y: 100 },
data: { label: 'Node 2' },
},
];
```
##### Add nodes to the flow
Now we can pass our `initialNodes` array to the ` ` component using the
`nodes` prop:
```jsx "nodes={initialNodes}"
```
##### Flow with nodes
This gives us a flow with two labeled nodes 🎉
Example: learn/building-a-flow-2
##### App.jsx
```jsx
import { ReactFlow, Controls, Background } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
const initialNodes = [
{
id: 'n1',
data: { label: 'Node 1' },
position: { x: 0, y: 0 },
type: 'input',
},
{
id: 'n2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
},
];
function Flow() {
return (
);
}
export default Flow;
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
We have several built-in nodes that you can explore in the
[node](/api-reference/types/node) reference. However, once you start building your own
application, you will want to use [custom nodes](/learn/customization/custom-nodes).
#### Adding edges
Now that we have two nodes, let's connect them with an edge.
### Create an edge
To create an edge, we define an array of [edge](/api-reference/types/edge) objects. Each
edge object needs to have an `id`, a `source` (where the edge begins), and a `target`
(where it ends). In this example, we use the `id` values of the two nodes we created so
far (`n1` and `n2`) to define the edge:
```js
const initialEdges = [
{
id: 'n1-n2',
source: 'n1',
target: 'n2',
},
];
```
This edge connects the node with `id: 'n1'` (the source) to the node with `id: 'n2'` (the
target).
Example: learn/building-a-flow-3
##### App.jsx
```jsx
import { ReactFlow, Controls, Background } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
const initialNodes = [
{
id: 'n1',
data: { label: 'Node 1' },
position: { x: 0, y: 0 },
type: 'input',
},
{
id: 'n2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
},
];
const initialEdges = [{ id: 'n1-n2', source: 'n1', target: 'n2' }];
function Flow() {
return (
);
}
export default Flow;
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### Label the edge
Let's give this edge two properties that are built into React Flow, a `label` and a
`type: "step"`.
```jsx {6-7}
const initialEdges = [
{
id: 'n1-n2',
source: 'n1',
target: 'n2',
type: 'step',
label: 'connects with',
},
];
```
##### Add edges to the flow
Now we can pass our `initialEdges` array to the ` ` component using the
`edges` prop:
```jsx "edges={initialEdges}"
```
##### Basic flow
Congratulations! You have completed a basic flow with nodes and edges! 🎉
Example: learn/building-a-flow-4
##### App.jsx
```jsx
import { ReactFlow, Controls, Background } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
const initialNodes = [
{
id: 'n1',
data: { label: 'Node 1' },
position: { x: 0, y: 0 },
type: 'input',
},
{
id: 'n2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
},
];
const initialEdges = [
{ id: 'n1-n2', source: 'n1', target: 'n2', label: 'connects with', type: 'step' },
];
function Flow() {
return (
);
}
export default Flow;
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
#### Full code example 🏁
Example: learn/building-a-flow-4
##### App.jsx
```jsx
import { ReactFlow, Controls, Background } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
const initialNodes = [
{
id: 'n1',
data: { label: 'Node 1' },
position: { x: 0, y: 0 },
type: 'input',
},
{
id: 'n2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
},
];
const initialEdges = [
{ id: 'n1-n2', source: 'n1', target: 'n2', label: 'connects with', type: 'step' },
];
function Flow() {
return (
);
}
export default Flow;
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
You took your first steps in React Flow! You might have realized that you can't drag or
otherwise interact with nodes. On the next page you'll learn how to make the flow
interactive.
### Built-In Components
React Flow comes with several built-in components that can be passed as children to the [` `](/api-reference/react-flow) component.
#### MiniMap
The [`MiniMap`](/api-reference/components/minimap) provides a bird’s-eye view of your flowgraph, making navigation easier, especially for larger flows. You can customize the appearance of nodes in the minimap by providing a nodeColor function.
Example: learn/mini-map
##### App.jsx
```jsx
import { ReactFlow, MiniMap } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { defaultNodes } from './nodes';
import { defaultEdges } from './edges';
const nodeColor = (node) => {
switch (node.type) {
case 'input':
return '#6ede87';
case 'output':
return '#6865A5';
default:
return '#ff0072';
}
};
function Flow() {
return (
);
}
export default Flow;
```
##### edges.js
```js
export const defaultEdges = [
{ id: 'e1-2', source: '1', target: '2' },
{ id: 'e2-3', source: '2', target: '3', animated: true },
];
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### nodes.jsx
```jsx
export const defaultNodes = [
{
id: '1',
type: 'input',
data: { label: 'Input Node' },
position: { x: 250, y: 25 },
style: { backgroundColor: '#6ede87', color: 'white' },
},
{
id: '2',
// you can also pass a React component as a label
data: { label: Default Node
},
position: { x: 100, y: 125 },
style: { backgroundColor: '#ff0072', color: 'white' },
},
{
id: '3',
type: 'output',
data: { label: 'Output Node' },
position: { x: 250, y: 250 },
style: { backgroundColor: '#6865A5', color: 'white' },
},
];
```
#### Controls
React Flow comes with a set of customizable [`Controls`](/api-reference/components/controls) for the viewport. You can zoom in and out, fit the viewport and toggle if the user can move, select and edit the nodes.
Example: learn/controls
##### App.jsx
```jsx
import { ReactFlow, Controls } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { defaultNodes } from './nodes';
import { defaultEdges } from './edges';
function Flow() {
return (
);
}
export default Flow;
```
##### edges.js
```js
export const defaultEdges = [
{ id: 'e1-2', source: '1', target: '2' },
{ id: 'e2-3', source: '2', target: '3', animated: true },
];
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### nodes.jsx
```jsx
export const defaultNodes = [
{
id: '1',
type: 'input',
data: { label: 'Input Node' },
position: { x: 250, y: 25 },
},
{
id: '2',
// you can also pass a React component as a label
data: { label: Default Node
},
position: { x: 100, y: 125 },
},
{
id: '3',
type: 'output',
data: { label: 'Output Node' },
position: { x: 250, y: 250 },
},
];
```
#### Background
The [`Background`](/api-reference/components/background) component adds a visual grid pattern to your flowgraph, helping users maintain orientation. You can choose from different pattern variants, or if you need more advanced customization, you can explore the [source](https://github.com/xyflow/xyflow/blob/main/packages/react/src/additional-components/Background/Background.tsx) code to implement your own pattern.
Example: learn/background
##### App.jsx
```jsx
import { useState } from 'react';
import { ReactFlow, Background, Panel } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { defaultNodes } from './nodes';
import { defaultEdges } from './edges';
function Flow() {
const [variant, setVariant] = useState('cross');
return (
variant:
setVariant('dots')}>dots
setVariant('lines')}>lines
setVariant('cross')}>cross
);
}
export default Flow;
```
##### edges.jsx
```jsx
export const defaultEdges = [
{ id: 'e1-2', source: '1', target: '2' },
{ id: 'e2-3', source: '2', target: '3', animated: true },
];
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### nodes.jsx
```jsx
export const defaultNodes = [
{
id: '1',
type: 'input',
data: { label: 'Input Node' },
position: { x: 250, y: 25 },
},
{
id: '2',
// you can also pass a React component as a label
data: { label: Default Node
},
position: { x: 100, y: 125 },
},
{
id: '3',
type: 'output',
data: { label: 'Output Node' },
position: { x: 250, y: 250 },
},
];
```
#### Panel
The [`Panel`](/api-reference/components/panel) component allows you to add fixed overlays to your flowgraph, perfect for titles, controls, or any other UI elements that should remain stationary.
Example: learn/panel
##### App.jsx
```jsx
import { ReactFlow, Background, Panel } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
const nodes = [
{
id: '1',
data: { label: 'this is an example flow for the component' },
position: { x: 0, y: 0 },
},
];
function Flow() {
return (
top-left
top-center
top-right
bottom-left
bottom-center
bottom-right
center-left
center-right
);
}
export default Flow;
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
.react-flow__panel {
padding: 5px 10px;
background: white;
box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
#### Advanced
For more advanced use cases and customization, we have even more built-in components you can check out in the [API components overview](/api-reference/components)
### Overview
At its core, React Flow is about creating interactive flowgraphs -- a collection of nodes
connected by edges. To help you understand the terminology we use throughout the
documentation, let's take a look at the example flow below.
Example: learn/basic-terms
##### AnnotationNode.jsx
```jsx
import { memo } from 'react';
import { MoveDown } from 'lucide-react';
function AnnotationNode({ data }) {
return (
<>
{data.arrowStyle && (
)}
>
);
}
export { AnnotationNode };
export default memo(AnnotationNode);
```
##### App.jsx
```jsx
import {
ReactFlow,
ReactFlowProvider,
MarkerType,
Background,
Panel,
useViewport,
useConnection,
} from '@xyflow/react';
import { useCallback, useState } from 'react';
import { AnnotationNode } from './AnnotationNode';
import NodeWrapper from './NodeWrapper';
import '@xyflow/react/dist/style.css';
const nodeTypes = {
annotation: AnnotationNode,
};
const connectionAnnotation = {
id: 'connection-annotation',
type: 'annotation',
selectable: false,
data: {
label: 'this is a "connection"',
arrowStyle: 'arrow-top-left',
},
position: { x: 0, y: 0 },
};
const initialNodes = [
{
id: 'annotation-1',
type: 'annotation',
draggable: false,
selectable: false,
data: {
label: 'This is a "node"',
arrowStyle: 'arrow-bottom-right',
},
position: { x: -65, y: -50 },
},
{
id: '1-1',
type: 'default',
data: {
label: 'node label',
},
position: { x: 150, y: 0 },
},
{
id: 'annotation-2',
type: 'annotation',
draggable: false,
selectable: false,
data: {
label: 'This is a "handle"',
arrowStyle: 'arrow-top-left',
},
position: { x: 235, y: 35 },
},
{
id: 'annotation-3',
type: 'annotation',
draggable: false,
selectable: false,
data: {
level: 2,
label: 'This is an "edge"',
arrowStyle: 'arrow-top-right',
},
position: { x: 20, y: 120 },
},
{
id: '1-2',
type: 'default',
data: {
label: 'node label',
},
position: { x: 350, y: 200 },
},
{
id: 'annotation-4',
type: 'annotation',
draggable: false,
selectable: false,
data: {
label: 'Try dragging the handle',
arrowStyle: 'arrow-top-left',
},
position: { x: 430, y: 240 },
},
];
const initialEdges = [
{
id: 'e1-2',
source: '1-1',
target: '1-2',
label: 'edge label',
type: 'smoothstep',
},
{
id: 'e2-2',
source: '1-2',
target: '2-2',
type: 'smoothstep',
markerEnd: {
type: MarkerType.ArrowClosed,
},
},
];
function ViewportWithAnnotation() {
const viewport = useViewport();
return (
<>
x: {viewport.x.toFixed(2)}
y: {viewport.y.toFixed(2)}
zoom: {viewport.zoom.toFixed(2)}
>
);
}
function Flow() {
const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState(initialEdges);
const connection = useConnection();
const onMouseMove = useCallback(() => {
if (connection.inProgress) {
const { from, to } = connection;
const nodePosition = {
x: to.x,
y: to.y,
};
setNodes((prevNodes) => {
const nodeExists = prevNodes.some((node) => node.id === 'connection-annotation');
if (nodeExists) {
return prevNodes.map((node) =>
node.id === 'connection-annotation'
? {
...node,
position: nodePosition,
hidden: Math.abs(to.y - from.y) < 25 && Math.abs(to.x - from.x) < 25,
}
: node,
);
} else {
return [
...prevNodes,
{
...connectionAnnotation,
position: nodePosition,
hidden: Math.abs(to.y - from.y) < 25 && Math.abs(to.x - from.x) < 25,
},
];
}
});
}
}, [connection]);
const onConnectEnd = useCallback(() => {
setNodes((prevNodes) =>
prevNodes.filter((node) => node.id !== 'connection-annotation'),
);
}, []);
return (
);
}
function FlowWithProvider() {
return (
);
}
export default FlowWithProvider;
```
##### NodeWrapper.jsx
```jsx
const NodeWrapper = ({ children, bottom, top, left, right, width, height }) => {
const toPx = (value) => (value !== undefined ? `${value}px` : undefined);
return (
{children}
);
};
export default NodeWrapper;
```
##### xy-theme.css
```css
/* xyflow theme files. Delete these to start from our base */
.react-flow {
--xy-background-color: #f7f9fb;
/* Custom Variables */
--xy-theme-selected: #f57dbd;
--xy-theme-hover: #c5c5c5;
--xy-theme-edge-hover: black;
--xy-theme-color-focus: #e8e8e8;
/* Built-in Variables see https://reactflow.dev/learn/customization/theming */
--xy-node-border-default: 1px solid #ededed;
--xy-node-boxshadow-default:
0px 3.54px 4.55px 0px #00000005, 0px 3.54px 4.55px 0px #0000000d,
0px 0.51px 1.01px 0px #0000001a;
--xy-node-border-radius-default: 8px;
--xy-handle-background-color-default: #ffffff;
--xy-handle-border-color-default: #aaaaaa;
--xy-edge-label-color-default: #505050;
}
.react-flow.dark {
--xy-node-boxshadow-default:
0px 3.54px 4.55px 0px rgba(255, 255, 255, 0.05),
/* light shadow */ 0px 3.54px 4.55px 0px rgba(255, 255, 255, 0.13),
/* medium shadow */ 0px 0.51px 1.01px 0px rgba(255, 255, 255, 0.2); /* smallest shadow */
--xy-theme-color-focus: #535353;
}
/* Customizing Default Theming */
.react-flow__node {
box-shadow: var(--xy-node-boxshadow-default);
border-radius: var(--xy-node-border-radius-default);
background-color: var(--xy-node-background-color-default);
display: flex;
justify-content: center;
align-items: center;
text-align: center;
padding: 10px;
font-size: 12px;
flex-direction: column;
border: var(--xy-node-border-default);
color: var(--xy-node-color, var(--xy-node-color-default));
}
.react-flow__node.selectable:focus {
box-shadow: 0px 0px 0px 4px var(--xy-theme-color-focus);
border-color: #d9d9d9;
}
.react-flow__node.selectable:focus:active {
box-shadow: var(--xy-node-boxshadow-default);
}
.react-flow__node.selectable:hover,
.react-flow__node.draggable:hover {
border-color: var(--xy-theme-hover);
}
.react-flow__node.selectable.selected {
border-color: var(--xy-theme-selected);
box-shadow: var(--xy-node-boxshadow-default);
}
.react-flow__node-group {
background-color: rgba(207, 182, 255, 0.4);
border-color: #9e86ed;
}
.react-flow__edge.selectable:hover .react-flow__edge-path,
.react-flow__edge.selectable.selected .react-flow__edge-path {
stroke: var(--xy-theme-edge-hover);
}
.react-flow__handle {
background-color: var(--xy-handle-background-color-default);
}
.react-flow__handle.connectionindicator:hover {
pointer-events: all;
border-color: var(--xy-theme-edge-hover);
background-color: white;
}
.react-flow__handle.connectionindicator:focus,
.react-flow__handle.connectingfrom,
.react-flow__handle.connectingto {
border-color: var(--xy-theme-edge-hover);
}
.react-flow__node-resizer {
border-radius: 0;
border: none;
}
.react-flow__resize-control.handle {
background-color: #ffffff;
border-color: #9e86ed;
border-radius: 0;
width: 5px;
height: 5px;
}
/*
Custom Example CSS - This CSS is to improve the example experience.
You can remove it if you want to use the default styles.
New Theme Classes:
.xy-theme__button - Styles for buttons.
.xy-theme__input - Styles for text inputs.
.xy-theme__checkbox - Styles for checkboxes.
.xy-theme__select - Styles for dropdown selects.
.xy-theme__label - Styles for labels.
Use these classes to apply consistent theming across your components.
*/
:root {
--color-primary: #ff0073;
--color-background: #fefefe;
--color-hover-bg: #f6f6f6;
--color-disabled: #76797e;
}
.xy-theme__button-group {
display: flex;
align-items: center;
.xy-theme__button:first-child {
border-radius: 100px 0 0 100px;
}
.xy-theme__button:last-child {
border-radius: 0 100px 100px 0;
margin: 0;
}
}
/* Custom Button Styling */
.xy-theme__button {
display: inline-flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 1rem;
border-radius: 100px;
border: 1px solid var(--color-primary);
background-color: var(--color-background);
color: var(--color-primary);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
box-shadow: var(--xy-node-boxshadow-default);
cursor: pointer;
}
.xy-theme__button.active {
background-color: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.xy-theme__button.active:hover,
.xy-theme__button.active:active {
background-color: var(--color-primary);
opacity: 0.9;
}
.xy-theme__button:hover {
background-color: var(--xy-controls-button-background-color-hover-default);
}
.xy-theme__button:active {
background-color: var(--color-hover-bg);
}
.xy-theme__button:disabled {
color: var(--color-disabled);
opacity: 0.8;
cursor: not-allowed;
border: 1px solid var(--color-disabled);
}
.xy-theme__button > span {
margin-right: 0.2rem;
}
/* Add gap between adjacent buttons */
.xy-theme__button + .xy-theme__button {
margin-left: 0.3rem;
}
/* Example Input Styling */
.xy-theme__input {
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-primary);
border-radius: 7px;
background-color: var(--color-background);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
font-size: 1rem;
color: inherit;
}
.xy-theme__input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(255, 0, 115, 0.3);
}
/* Specific Checkbox Styling */
.xy-theme__checkbox {
appearance: none;
-webkit-appearance: none;
width: 1.25rem;
height: 1.25rem;
border-radius: 7px;
border: 2px solid var(--color-primary);
background-color: var(--color-background);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
cursor: pointer;
display: inline-block;
vertical-align: middle;
margin-right: 0.5rem;
}
.xy-theme__checkbox:checked {
background-color: var(--color-primary);
border-color: var(--color-primary);
}
.xy-theme__checkbox:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(255, 0, 115, 0.3);
}
/* Dropdown Styling */
.xy-theme__select {
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-primary);
border-radius: 50px;
background-color: var(--color-background);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
font-size: 1rem;
color: inherit;
margin-right: 0.5rem;
box-shadow: var(--xy-node-boxshadow-default);
}
.xy-theme__select:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(255, 0, 115, 0.3);
}
.xy-theme__label {
margin-top: 10px;
margin-bottom: 3px;
display: inline-block;
}
```
##### index.css
```css
@import url('./xy-theme.css');
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
/* Annotation Node */
.react-flow__node-annotation {
font-size: 16px;
max-width: 300px;
color: #683bfa;
position: absolute;
box-shadow: none;
font-family: monospace;
text-align: left;
background-color: transparent;
border: none;
pointer-events: none;
}
.react-flow__node-annotation .annotation-content {
padding: 10px;
display: flex;
}
.react-flow__node-annotation .annotation-level {
margin-right: 4px;
}
.react-flow__node-annotation .annotation-arrow {
position: absolute;
font-size: 24px;
}
.arrow-top-right {
right: 0;
bottom: 0;
transform: translate(10px, -25px) rotate(-140deg);
position: absolute;
}
.arrow-top-left {
left: 0;
bottom: 0;
transform: translate(-5px, -25px) rotate(145deg) scale(-1, 1);
position: absolute;
}
.arrow-bottom-left {
left: 0;
bottom: 0;
transform: translate(0px, -20px) rotate(90deg);
position: absolute;
}
.arrow-bottom-right {
right: 0;
bottom: 0;
transform: translate(10px, 5px) rotate(-60deg);
position: absolute;
}
.arrow-handle {
left: 0;
bottom: 0;
transform: translate(-15px, -25px) rotate(160deg) scale(-1, 1);
position: absolute;
}
.react-flow__edge-textbg {
fill: #f7f9fb;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### Nodes
React Flow has a few default node types out of the box, but customization is where the
magic of React Flow truly happens. You can design your nodes to work exactly the way you
need them to—whether that’s embedding interactive form elements, displaying dynamic data
visualizations, or even incorporating multiple connection handles. React Flow lays the
foundation, and your imagination does the rest.
We have a guide on creating your own [Custom Nodes](/learn/customization/custom-nodes) and
you can find all the options for customizing your nodes in the
[Node](/api-reference/types/node) reference.
##### Handles
A handle (also known as a “port” in other libraries) is the attachment point where an edge
connects to a node. By default, they appear as grey circles on the top, bottom, left, or
right sides of a node. But they are just `div` elements, and can be positioned and styled
any way you’d like. When creating a custom node, you can include as many handles as
needed. For more information, refer to the [Handle](/learn/customization/handles) page.
##### Edges
Edges connect nodes. Every edge needs a target and a source node. React Flow comes with
four built-in [edge types](/examples/edges/edge-types): `default` (bezier), `smoothstep`,
`step`, and `straight`. An edge is a SVG path that can be styled with CSS and is
completely customizable. If you are using multiple handles, you can reference them
individually to create multiple connections for a node.
Like custom nodes, you can also customize edges. Things that people do with custom edges
include:
* Adding buttons to remove edges
* Custom routing behavior
* Complex styling or interactions that cannot be solved with just one SVG path
For more information, refer to the [Edges](/learn/customization/custom-edges) page.
##### Selection
You can select an edge or a node by clicking on it. If you want to select multiple
nodes/edges via clicking, you can hold the `Meta/Control` key and click on multiple
elements to select them. If you want to change the keyboard key for multiselection to
something else, you can use the
[`multiSelectionKeyCode`](/api-reference/react-flow#multiselectionkeycode) prop.
You can also select multiple edges/nodes by holding down `Shift` and dragging the mouse to
make a selection box. When you release the mouse, any node or edge that falls within the
selection box is selected. If you want to change the keyboard key for this behavior, you
can use the [`selectionKeyCode`](/api-reference/react-flow#selectionkeycode) prop.
Selected nodes and edges are elevated (assigned a higher `zIndex` than other elements), so
that they are shown on top of all the other elements.
For default edges and nodes, selection is shown by a darker stroke/border than usual. If
you are using a custom node/edge, you can use the `selected` prop to customize selection
appearance inside your custom component.
##### Connection line
React Flow has built-in functionality that allows you to click and drag from one handle to
another to create a new edge. While dragging, the placeholder edge is referred to as a
connection line. The connection line comes with the same four built-in types as edges and
is customizable. You can find the props for configuring the connection line in the
[connection props](/api-reference/react-flow#connection-line-props) reference.
##### Viewport
All of React Flow is contained within the viewport. Each node has an x- and y-coordinate,
which indicates its position within the viewport. The viewport has x, y, and zoom values.
When you drag the pane, you change the x and y coordinates. When you zoom in or out, you
alter the zoom level.
### Panning and Zooming
The default pan and zoom behavior of React Flow is inspired by
[slippy maps](https://wiki.openstreetmap.org/wiki/Slippy_map). You pan by dragging your
pointer and zoom by scrolling. You can customize this behavior easily with the
[interaction](/api-reference/react-flow#interaction-props) and
[keyboard](/api-reference/react-flow#keyboard-props) props on the ` `
component.
#### Viewport configurations
Here we will list and explain some configurations that other tools use.
##### Default viewport controls
As mentioned above, the ReactFlow default controls are as follows:
* `pan:` pointer drag
* `zoom:` pinch or scroll
* `select:` shift + pointer drag
Example: learn/zoom-pan
##### App.jsx
```jsx
import { useCallback } from 'react';
import {
Background,
ReactFlow,
addEdge,
useEdgesState,
useNodesState,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { initialNodes } from './nodes';
import { initialEdges } from './edges';
function Flow() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(connection) => setEdges((eds) => addEdge(connection, eds)),
[setEdges],
);
return (
);
}
export default Flow;
```
##### edges.js
```js
export const initialEdges = [
{ id: 'e1-2', source: '1', target: '2' },
{ id: 'e1-3', source: '1', target: '3' },
];
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### nodes.js
```js
export const initialNodes = [
{
id: '1',
data: { label: 'Node 1' },
position: { x: 150, y: 0 },
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 0, y: 150 },
},
{
id: '3',
data: { label: 'Node 3' },
position: { x: 300, y: 150 },
},
];
```
##### Design tool viewport controls
If you prefer figma/sketch/design tool controls you can set
[`panOnScroll`](/api-reference/react-flow#panonscroll) and
[`selectionOnDrag`](/api-reference/react-flow#selectionondrag) to `true` and
[`panOnDrag`](/api-reference/react-flow#panondrag) to `false`:
* `pan:` scroll, middle / right mouse drag, space + pointer drag
* `zoom:` pinch or cmd + scroll
* `select:` pointer drag
Example: learn/zoom-pan-2
##### App.jsx
```jsx
import { useCallback } from 'react';
import {
ReactFlow,
addEdge,
SelectionMode,
useEdgesState,
useNodesState,
Background,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { initialNodes } from './nodes';
import { initialEdges } from './edges';
const panOnDrag = [1, 2];
function Flow() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(connection) => setEdges((eds) => addEdge(connection, eds)),
[setEdges],
);
return (
);
}
export default Flow;
```
##### edges.js
```js
export const initialEdges = [
{ id: 'e1-2', source: '1', target: '2' },
{ id: 'e1-3', source: '1', target: '3' },
];
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### nodes.js
```js
export const initialNodes = [
{
id: '1',
data: { label: 'Node 1' },
position: { x: 150, y: 0 },
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 0, y: 150 },
},
{
id: '3',
data: { label: 'Node 3' },
position: { x: 300, y: 150 },
},
];
```
In this example we also set `selectionMode="partial"` to be able to add nodes to a
selection that are only partially selected.
### Custom Edges
Like [custom nodes](/learn/customization/custom-nodes), parts of a custom edge
in React Flow are just React components. That means you can render anything you
want along an edge! This guide shows you how to implement a custom edge with
some additional controls. For a comprehensive reference of props available for
custom edges, see the [Edge](/api-reference/types/edge-props) reference.
#### A basic custom edge
An edge isn't much use to us if it doesn't render a path between two connected
nodes. These paths are always SVG-based and are typically rendered using the
[` `](/api-reference/components/base-edge) component. To calculate
the actual SVG path to render, React Flow comes with some handy utility functions:
* [`getBezierPath`](/api-reference/utils/get-bezier-path)
* [`getSimpleBezierPath`](/api-reference/utils/get-simple-bezier-path)
* [`getSmoothStepPath`](/api-reference/utils/get-smooth-step-path)
* [`getStraightPath`](/api-reference/utils/get-straight-path)
To kickstart our custom edge, we'll just render a straight path between the
source and target.
##### Create the component
We start by creating a new React component called `CustomEdge`. Then we render
the [` `](/api-reference/components/base-edge) component with the
calculated path. This gives us a straight edge that behaves the same as the
built-in default [edge version](/api-reference/types/edge#default-edge-types)
`"straight"`.
```jsx
import { BaseEdge, getStraightPath } from '@xyflow/react';
export function CustomEdge({ id, sourceX, sourceY, targetX, targetY }) {
const [edgePath] = getStraightPath({
sourceX,
sourceY,
targetX,
targetY,
});
return (
<>
>
);
}
```
Using TypeScript? You can head on over to our [TypeScript guide](/learn/advanced-use/typescript#custom-edges)
to learn how to set up your custom edges with the right types. This will make
sure you'll have typed access to your edge's props and `data`.
##### Create `edgeTypes`
Outside of our component, we define an `edgeTypes` object.
We name our new edge type `"custom-edge"` and assign the `CustomEdge` component
we just created to it.
```jsx
const edgeTypes = {
'custom-edge': CustomEdge,
};
```
##### Pass the `edgeTypes` prop
To use it, we also need to update the
[`edgeTypes`](/api-reference/react-flow#edge-types) prop on the
` ` component.
```jsx "edgeTypes={edgeTypes}"
export function Flow() {
return ;
}
```
##### Use the new edge type
After defining the `edgeTypes` object, we can use our new custom edge by setting
the `type` field of an edge to `"custom-edge"`.
```jsx {6}
const initialEdges = [
{
id: 'e1',
source: 'n1',
target: 'n2',
type: 'custom-edge',
},
];
```
##### Flow with a custom edge
Example: learn/custom-edge
##### App.jsx
```jsx
import { useCallback } from 'react';
import {
ReactFlow,
addEdge,
useNodesState,
useEdgesState,
} from '@xyflow/react';
import CustomEdge from './CustomEdge';
import '@xyflow/react/dist/style.css';
const initialNodes = [
{ id: 'a', position: { x: 0, y: 0 }, data: { label: 'Node A' } },
{ id: 'b', position: { x: 0, y: 100 }, data: { label: 'Node B' } },
];
const initialEdges = [
{ id: 'a->b', type: 'custom-edge', source: 'a', target: 'b' },
];
const edgeTypes = {
'custom-edge': CustomEdge,
};
function Flow() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(connection) => {
const edge = { ...connection, type: 'custom-edge' };
setEdges((eds) => addEdge(edge, eds));
},
[setEdges],
);
return (
);
}
export default Flow;
```
##### CustomEdge.jsx
```jsx
import { BaseEdge, getStraightPath } from '@xyflow/react';
export default function CustomEdge({ id, sourceX, sourceY, targetX, targetY }) {
const [edgePath] = getStraightPath({
sourceX,
sourceY,
targetX,
targetY,
});
return ;
}
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
#### Custom SVG edge paths
As discussed previously, if you want to make a custom edge in React Flow, you
have to use either of the four path creation functions discussed above
(e.g [`getBezierPath`](/api-reference/utils/get-bezier-path)). However if you
want to make some other path shape like a Sinusoidal edge or some other edge
type then you will have to make the edge path yourself.
The edge path we get from functions like
[`getBezierPath`](/api-reference/utils/get-bezier-path) is just a path string
which we pass into the `path` prop of the ` ` component. It contains
the necessary information needed in order to draw that path, like where it
should start from, where it should curve, where it should end, etc. A simple
straight path string between two points `(x1, y1)` to `(x2, y2)` would look like:
```jsx
M x1 y1 L x2 y2
```
An SVG path is a concatenated list of commands like `M`, `L`, `Q`, etc, along
with their values. Some of these commands are listed below, along with their
supported values.
* `M x1 y1` is the Move To command which moves the current point to the x1, y1
coordinate.
* `L x1 y1` is the Line To command which draws a line from the current point to
x1, y1 coordinate.
* `Q x1 y1 x2 y2` is the Quadratic Bezier Curve command which draws a bezier
curve from the current point to the x2, y2 coordinate. x1, y1 is the control
point of the curve which determines the curviness of the curve.
Whenever we want to start a path for our custom edge, we use the `M` command to
move our current point to `sourceX, sourceY` which we get as props in the custom
edge component. Then based on the shape we want, we will use other commands like
`L`(to make lines), `Q`(to make curves) and then finally end our path at
`targetX, targetY` which we get as props in the custom edge component.
If you want to learn more about SVG paths, you can check out
[SVG-Path-Editor](https://yqnn.github.io/svg-path-editor/). You can paste any
SVG path there and analyze individual path commands via an intuitive UI.
Here is an example with two types of custom edge paths, a Step edge and a
Sinusoidal edge. You should look at the Step edge first to get your hands dirty
with custom SVG paths since it's simple, and then look at how the Sinusoidal
edge is made. After going through this example, you will have the necessary
knowledge to make custom SVG paths for your custom edges.
Example: learn/custom-edge-path
##### App.jsx
```jsx
import { ReactFlow } from '@xyflow/react';
import StepEdge from './StepEdge';
import SineEdge from './SineEdge';
import '@xyflow/react/dist/style.css';
const initialNodes = [
{ id: 'a', position: { x: 0, y: 0 }, data: { label: 'Node A' } },
{ id: 'b', position: { x: 200, y: 100 }, data: { label: 'Node B' } },
{ id: 'c', position: { x: 0, y: 200 }, data: { label: 'Node C' } },
{ id: 'd', position: { x: 200, y: 300 }, data: { label: 'Node D' } },
];
const initialEdges = [
{ id: 'a->b', type: 'step', source: 'a', target: 'b' },
{ id: 'c->d', type: 'sine', source: 'c', target: 'd' },
];
const edgeTypes = {
step: StepEdge,
sine: SineEdge,
};
function Flow() {
return (
);
}
export default Flow;
```
##### SineEdge.jsx
```jsx
import { BaseEdge } from '@xyflow/react';
export default function SineEdge({ id, sourceX, sourceY, targetX, targetY }) {
const centerX = (targetX - sourceX) / 2 + sourceX;
const centerY = (targetY - sourceY) / 2 + sourceY;
const edgePath = `
M ${sourceX} ${sourceY}
Q ${(targetX - sourceX) * 0.2 + sourceX} ${targetY * 1.1} ${centerX} ${centerY}
Q ${(targetX - sourceX) * 0.8 + sourceX} ${sourceY * 0.9} ${targetX} ${targetY}
`;
return ;
}
```
##### StepEdge.jsx
```jsx
import { BaseEdge } from '@xyflow/react';
export default function StepEdge({ id, sourceX, sourceY, targetX, targetY }) {
const centerY = (targetY - sourceY) / 2 + sourceY;
const edgePath = `M ${sourceX} ${sourceY} L ${sourceX} ${centerY} L ${targetX} ${centerY} L ${targetX} ${targetY}`;
return ;
}
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
### Custom Nodes
A powerful feature of React Flow is the ability to create custom nodes. This gives you the
flexibility to render anything you want within your nodes. We generally recommend creating
your own custom nodes rather than relying on built-in ones. With custom nodes, you can add
as many source and target [handles](/learn/customization/handles) as you like—or even
embed form inputs, charts, and other interactive elements.
In this section, we'll walk through creating a custom node featuring an input field that
updates text elsewhere in your application. For further examples, we recommend checking
out our [Custom Node Example](/examples/nodes/custom-node).
#### Implementing a custom node
To create a custom node, all you need to do is create a React component. React Flow will
automatically wrap it in an interactive container that injects essential props like the
node's id, position, and data, and provides functionality for selection, dragging, and
connecting handles. For a full overview on all available node props, see the
[Node](/api-reference/types/node-props) reference.
### Create the component
Let's dive into an example by creating a custom node called `TextUpdaterNode`. For this,
we've added a simple input field with a change handler.
```jsx
export function TextUpdaterNode(props) {
const onChange = useCallback((evt) => {
console.log(evt.target.value);
}, []);
return (
);
}
```
Using TypeScript? You can head on over to our [TypeScript guide](/learn/advanced-use/typescript#custom-nodes)
to learn how to set up your custom nodes with the right types. This will make
sure you'll have typed access to your node's props and `data`.
##### Initialize nodeTypes
You can add a new node type to React Flow by adding it to the `nodeTypes` prop like below.
We define the `nodeTypes` outside of the component to prevent re-renderings.
```jsx
const nodeTypes = {
textUpdater: TextUpdaterNode,
};
```
##### Pass nodeTypes to React Flow
```jsx {4}
```
##### Update node definitions
After defining your new node type, you can use it by specifying the `type` property on
your node definition:
```jsx {4}
const nodes = [
{
id: 'node-1',
type: 'textUpdater',
position: { x: 0, y: 0 },
data: { value: 123 },
},
];
```
##### Flow with custom node
After putting all together and adding some basic styles we get a custom node that prints
text to the console:
Example: learn/custom-node
##### App.jsx
```jsx
import { useState } from 'react';
import { ReactFlow } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import TextUpdaterNode from './TextUpdaterNode';
const rfStyle = {
backgroundColor: '#B8CEFF',
};
const initialNodes = [
{
id: 'node-1',
type: 'textUpdater',
position: { x: 0, y: 0 },
data: { value: 123 },
},
];
// we define the nodeTypes outside of the component to prevent re-renderings
// you could also use useMemo inside the component
const nodeTypes = { textUpdater: TextUpdaterNode };
function Flow() {
const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState([]);
return (
);
}
export default Flow;
```
##### TextUpdaterNode.jsx
```jsx
import { useCallback } from 'react';
function TextUpdaterNode(props) {
const onChange = useCallback((evt) => {
console.log(evt.target.value);
}, []);
return (
);
}
export default TextUpdaterNode;
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
.text-updater-node {
height: 50px;
border: 1px solid #eee;
padding: 5px;
border-radius: 5px;
background: white;
}
.text-updater-node label {
display: block;
color: #777;
font-size: 12px;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
#### Full code example 🏁
Example: learn/custom-node
##### App.jsx
```jsx
import { useState } from 'react';
import { ReactFlow } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import TextUpdaterNode from './TextUpdaterNode';
const rfStyle = {
backgroundColor: '#B8CEFF',
};
const initialNodes = [
{
id: 'node-1',
type: 'textUpdater',
position: { x: 0, y: 0 },
data: { value: 123 },
},
];
// we define the nodeTypes outside of the component to prevent re-renderings
// you could also use useMemo inside the component
const nodeTypes = { textUpdater: TextUpdaterNode };
function Flow() {
const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState([]);
return (
);
}
export default Flow;
```
##### TextUpdaterNode.jsx
```jsx
import { useCallback } from 'react';
function TextUpdaterNode(props) {
const onChange = useCallback((evt) => {
console.log(evt.target.value);
}, []);
return (
);
}
export default TextUpdaterNode;
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
.text-updater-node {
height: 50px;
border: 1px solid #eee;
padding: 5px;
border-radius: 5px;
background: white;
}
.text-updater-node label {
display: block;
color: #777;
font-size: 12px;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
To enable your custom node to connect with other nodes, check out the
[Handles](/learn/customization/handles) page to learn how to add source and target
handles.
### Edge Labels
One of the more common uses for [custom edges](/learn/customization/custom-edges) is rendering some controls or info
along an edge's path. In React Flow we call that a *custom edge label* and unlike the
edge path, edge labels can be any React component!
#### Adding an edge label
To render a custom edge label we must wrap it in the
[` `](/api-reference/components/edge-label-renderer) component.
This allows us to render the labels outside of the SVG world where the edges life.
The edge label renderer is a portal to a single container that *all* edge labels are rendered into.
Let's add a button to our custom edge that can be used to delete the edge it's
attached to:
```jsx
import {
BaseEdge,
EdgeLabelRenderer,
getStraightPath,
useReactFlow,
} from '@xyflow/react';
export default function CustomEdge({ id, sourceX, sourceY, targetX, targetY }) {
const { deleteElements } = useReactFlow();
const [edgePath] = getStraightPath({
sourceX,
sourceY,
targetX,
targetY,
});
return (
<>
deleteElements({ edges: [{ id }] })}>delete
>
);
}
```
If we try to use this edge now, we'll see that the button is rendered in the
centre of the flow (it might be hidden behind "Node A"). Because of the edge
label portal, we'll need to do some extra work to position the button ourselves.
Fortunately, the path utils we've already seen can help us with this! Along with
the SVG path to render, these functions also return the `x` and `y` coordinates
of the path's midpoint. We can then use these coordinates to translate our custom
edge label's into the right position!
```jsx
export default function CustomEdge({ id, sourceX, sourceY, targetX, targetY }) {
const { deleteElements } = useReactFlow();
const [edgePath, labelX, labelY] = getStraightPath({ ... });
return (
...
deleteElements({ edges: [{ id }] })}
>
...
);
}
```
To make sure our edge labels are interactive and not just for presentation, it
is important to add `pointer-events: all` to the label's style. This will ensure
that the label is clickable.
And just like with interactive controls in custom nodes, we need to remember
to add the `nodrag` and `nopan` classes to the label to stop mouse events from
controlling the canvas.
Here's an interactive example with our updated custom edge. Clicking the delete
button will remove that edge from the flow. Creating a new edge will use the
custom node.
Example: learn/custom-edge-2
##### App.jsx
```jsx
import { useCallback } from 'react';
import {
ReactFlow,
addEdge,
useNodesState,
useEdgesState,
} from '@xyflow/react';
import CustomEdge from './CustomEdge';
import '@xyflow/react/dist/style.css';
const initialNodes = [
{ id: 'a', position: { x: 0, y: 0 }, data: { label: 'Node A' } },
{ id: 'b', position: { x: 0, y: 100 }, data: { label: 'Node B' } },
{ id: 'c', position: { x: 0, y: 200 }, data: { label: 'Node C' } },
];
const initialEdges = [
{ id: 'a->b', type: 'custom-edge', source: 'a', target: 'b' },
{ id: 'b->c', type: 'custom-edge', source: 'b', target: 'c' },
];
const edgeTypes = {
'custom-edge': CustomEdge,
};
function Flow() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(connection) => {
const edge = { ...connection, type: 'custom-edge' };
setEdges((eds) => addEdge(edge, eds));
},
[setEdges],
);
return (
);
}
export default Flow;
```
##### CustomEdge.jsx
```jsx
import {
BaseEdge,
EdgeLabelRenderer,
getStraightPath,
useReactFlow,
} from '@xyflow/react';
export default function CustomEdge({ id, sourceX, sourceY, targetX, targetY }) {
const { setEdges } = useReactFlow();
const [edgePath, labelX, labelY] = getStraightPath({
sourceX,
sourceY,
targetX,
targetY,
});
return (
<>
{
setEdges((es) => es.filter((e) => e.id !== id));
}}
>
delete
>
);
}
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
### Handles
Handles are the connection points on [nodes](/learn/concepts/terms-and-definitions#nodes)
in React Flow. Our built-in nodes include one source and one target handle, but you can
customize your nodes with as many different handles as you need.
#### Creating a node with handles
To create a [custom node](/learn/customization/custom-nodes) with handles, you can use the
[` `](/api-reference/components/handle) component provided by React Flow. This
component allows you to define source and target handles for your custom nodes. Here's an
example of how to implement a custom node with two handles:
```jsx {7-8}
import { Position, Handle } from '@xyflow/react';
export function CustomNode() {
return (
);
}
```
#### Using multiple handles
If you want to use multiple source or target handles in your custom node, you need to
specify each handle with a unique `id`. This allows React Flow to differentiate between
the handles when connecting edges.
```jsx /id="a"/ /id="b"/
```
To connect an edge to a specific handle of a node, use the properties `sourceHandle` (for
the edge's starting point) and `targetHandle` (for the edge's ending point). By defining
`sourceHandle` or `targetHandle` with the appropriate handle `id`, you instruct React Flow
to attach the edge to that specific handle, ensuring that connections are made where you
intend.
```js "sourceHandle: 'a'" "sourceHandle: 'b'"
const initialEdges = [
{ id: 'n1-n2', source: 'n1', sourceHandle: 'a', target: 'n2' },
{ id: 'n1-n3', source: 'n1', sourceHandle: 'b', target: 'n3' },
];
```
In this case, the source node is `n1` for both handles but the handle `id`s are different.
One comes from handle id `a` and the other one from `b`. Both edges also have different
target nodes:
Example: learn/custom-node-2
##### App.jsx
```jsx
import { useCallback, useState } from 'react';
import {
ReactFlow,
addEdge,
applyEdgeChanges,
applyNodeChanges,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import TextUpdaterNode from './TextUpdaterNode';
const rfStyle = {
backgroundColor: '#B8CEFF',
};
const initialNodes = [
{
id: 'node-1',
type: 'textUpdater',
position: { x: 100, y: 0 },
data: { value: 123 },
},
{
id: 'node-2',
type: 'output',
targetPosition: 'top',
position: { x: 0, y: 200 },
data: { label: 'node 2' },
},
{
id: 'node-3',
type: 'output',
targetPosition: 'top',
position: { x: 200, y: 200 },
data: { label: 'node 3' },
},
];
const initialEdges = [
{ id: 'edge-1', source: 'node-1', target: 'node-2', sourceHandle: 'a' },
{ id: 'edge-2', source: 'node-1', target: 'node-3', sourceHandle: 'b' },
];
// we define the nodeTypes outside of the component to prevent re-renderings
// you could also use useMemo inside the component
const nodeTypes = { textUpdater: TextUpdaterNode };
function Flow() {
const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState(initialEdges);
return (
);
}
export default Flow;
```
##### TextUpdaterNode.jsx
```jsx
import { Handle, Position } from '@xyflow/react';
const handleStyle = { left: 10 };
function TextUpdaterNode(props) {
return (
);
}
export default TextUpdaterNode;
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
.text-updater-node {
height: 50px;
border: 1px solid #eee;
padding: 5px;
border-radius: 5px;
background: white;
}
.text-updater-node label {
display: block;
color: #777;
font-size: 12px;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
By default React Flow positions a handle in the center of the specified side. If you want
to display multiple handles on a side, you can adjust the position via inline styles or
overwrite the default CSS and position it on your own.
#### Custom handles
You can use any custom component as a Handle by wrapping it with the
[` `](/api-reference/components/handle) component. Follow these steps to
integrate your custom component:
1. Wrap your custom component with [` `](/api-reference/components/handle)
component.
2. Hide the built-in [` `](/api-reference/components/handle) appearance by
setting `border` and `background` to `none`.
3. Set the `width`and `height` of [` `](/api-reference/components/handle) to
match your custom component.
4. Style the child component with `pointer-events: none`.
5. Then, reposition the child custom component using CSS position properties like
`top, left` if necessary to position it perfectly inside the
[` `](/api-reference/components/handle) component.
This example shows a Material UI icon `ArrowCircleRightIcon` used as a Handle.
```jsx
import { Handle, Position } from '@xyflow/react';
import ArrowCircleRightIcon from '@mui/icons-material/ArrowCircleRight';
export function CustomNode() {
return (
);
}
```
#### Typeless handles
If you want to create a handle that does not have a specific type (source or target), you
can set [connectionMode](/api-reference/react-flow#connectionmode) to `Loose` in the
` ` component. This allows the handle to be used for both incoming and
outgoing connections.
#### Dynamic handles
If you are programmatically changing the position or number of handles in your custom
node, you need to update the node internals with the
[`useUpdateNodeInternals`](/api-reference/hooks/use-update-node-internals) hook.
#### Custom handle styles
Since the handle is a div, you can use CSS to style it or pass a style prop to customize a
Handle. You can see this in the
[Add Node On Edge Drop](/examples/nodes/add-node-on-edge-drop) and
[Simple Floating Edges](/examples/edges/simple-floating-edges) examples.
##### Styling handles when connecting
The handle receives the additional class names `connecting` when the connection line is
above the handle and `valid` if the connection is valid. You can find an example which
uses these classes [here](/examples/interaction/validation).
##### Hiding handles
If you need to hide a handle for some reason, you must use `visibility: hidden` or
`opacity: 0` instead of `display: none`. This is important because React Flow needs to
calculate the dimensions of the handle to work properly and using `display: none` will
report a width and height of `0`!
### Theming
React Flow has been built with deep customization in mind. Many of our users
fully transform the look and feel of React Flow to match their own brand or design
system. This guide will introduce you to the different ways you can customize
React Flow's appearance.
#### Default styles
React Flow's default styles are enough to get going with the built-in nodes. They
provide some sensible defaults for styles like padding, border radius, and animated
edges. You can see what they look like below:
Example: examples/styling/default-style
##### App.tsx
```tsx
import React, { useCallback } from 'react';
import {
ReactFlow,
Background,
Controls,
MiniMap,
useNodesState,
useEdgesState,
addEdge,
Position,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
const nodeDefaults = {
sourcePosition: Position.Right,
targetPosition: Position.Left,
};
const initialNodes = [
{
id: '1',
position: { x: 0, y: 150 },
data: { label: 'default style 1' },
...nodeDefaults,
},
{
id: '2',
position: { x: 250, y: 0 },
data: { label: 'default style 2' },
...nodeDefaults,
},
{
id: '3',
position: { x: 250, y: 150 },
data: { label: 'default style 3' },
...nodeDefaults,
},
{
id: '4',
position: { x: 250, y: 300 },
data: { label: 'default style 4' },
...nodeDefaults,
},
];
const initialEdges = [
{
id: 'e1-2',
source: '1',
target: '2',
animated: true,
},
{
id: 'e1-3',
source: '1',
target: '3',
},
{
id: 'e1-4',
source: '1',
target: '4',
},
];
const Flow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(params) => setEdges((els) => addEdge(params, els)),
[],
);
return (
);
};
export default Flow;
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### index.tsx
```tsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
You'll typically load these default styles by importing them in you `App.jsx`
file or other entry point:
```js
import '@xyflow/react/dist/style.css';
```
Without dipping into [custom nodes](/examples/nodes/custom-node) and
[edges](/examples/edges/custom-edges), there are three ways you can style
React Flow's basic look:
* Passing inline styles through `style` props
* Overriding the built-in classes with custom CSS
* Overriding the CSS variables React Flow uses
##### Built in dark and light mode
You can choose one of the built-in color modes by using the `colorMode` prop ('dark', 'light' or 'system') as seen in the [dark mode example](/examples/styling/dark-mode).
```jsx
import ReactFlow from '@xyflow/react';
export default function Flow() {
return
}
```
When you use the `colorMode` prop, React Flow adds a class to the root element (`.react-flow`) that you can use to style your flow based on the color mode:
```css
.dark .react-flow__node {
background: #777;
color: white;
}
.light .react-flow__node {
background: white;
color: #111;
}
```
##### Customizing with `style` props
The easiest way to start customizing the look and feel of your flows is to use
the `style` prop found on many of React Flow's components to inline your own
CSS.
```jsx
import ReactFlow from '@xyflow/react'
const styles = {
background: 'red',
width: '100%',
height: 300,
};
export default function Flow() {
return
}
```
##### CSS variables
If you don't want to replace the default styles entirely but just want to tweak
the overall look and feel, you can override some of the CSS variables we use
throughout the library. For an example of how to use these CSS variables, check out our [Feature Overview](/examples/overview) example.
These variables are mostly self-explanatory. Below is a table of all the variables
you might want to tweak and their default values for reference:
| Variable name | Default |
| :---------------------------------------------------- | :---------------------------------- |
| `--xy-edge-stroke-default` | `#b1b1b7` |
| `--xy-edge-stroke-width-default` | `1` |
| `--xy-edge-stroke-selected-default` | `#555` |
| `--xy-connectionline-stroke-default` | `#b1b1b7` |
| `--xy-connectionline-stroke-width-default` | `1` |
| `--xy-attribution-background-color-default` | `rgba(255, 255, 255, 0.5)` |
| `--xy-minimap-background-color-default` | `#fff` |
| `--xy-background-pattern-dots-color-default` | `#91919a` |
| `--xy-background-pattern-line-color-default` | `#eee` |
| `--xy-background-pattern-cross-color-default` | `#e2e2e2` |
| `--xy-node-color-default` | `inherit` |
| `--xy-node-border-default` | `1px solid #1a192b` |
| `--xy-node-background-color-default` | `#fff` |
| `--xy-node-group-background-color-default` | `rgba(240, 240, 240, 0.25)` |
| `--xy-node-boxshadow-hover-default` | `0 1px 4px 1px rgba(0, 0, 0, 0.08)` |
| `--xy-node-boxshadow-selected-default` | `0 0 0 0.5px #1a192b` |
| `--xy-handle-background-color-default` | `#1a192b` |
| `--xy-handle-border-color-default` | `#fff` |
| `--xy-selection-background-color-default` | `rgba(0, 89, 220, 0.08)` |
| `--xy-selection-border-default` | `1px dotted rgba(0, 89, 220, 0.8)` |
| `--xy-controls-button-background-color-default` | `#fefefe` |
| `--xy-controls-button-background-color-hover-default` | `#f4f4f4` |
| `--xy-controls-button-color-default` | `inherit` |
| `--xy-controls-button-color-hover-default` | `inherit` |
| `--xy-controls-button-border-color-default` | `#eee` |
| `--xy-controls-box-shadow-default` | `0 0 2px 1px rgba(0, 0, 0, 0.08)` |
| `--xy-resize-background-color-default` | `#3367d9` |
These variables are used to define the *defaults* for the various elements of
React Flow. This means they can still be overridden by inline styles or custom
classes on a per-element basis. If you want to override these variables, you can do so
by adding:
```css
.react-flow {
--xy-node-background-color-default: #ff5050;
}
```
Be aware that these variables are defined under `.react-flow` and under
`:root`.
##### Overriding built-in classes
Some consider heavy use of inline styles to be an anti-pattern. In that case,
you can override the built-in classes that React Flow uses with your own CSS.
There are many classes attached to all sorts of elements in React Flow, but the
ones you'll likely want to override are listed below:
| Class name | Description |
| :--------------------------------- | :--------------------------------------------------------------------------------------- |
| `.react-flow` | The outermost container |
| `.react-flow__renderer` | The inner container |
| `.react-flow__zoompane` | Zoom & pan pane |
| `.react-flow__selectionpane` | Selection pane |
| `.react-flow__selection` | User selection |
| `.react-flow__edges` | The element containing all edges in the flow |
| `.react-flow__edge` | Applied to each [`Edge`](/api-reference/types/edge) in the flow |
| `.react-flow__edge.selected` | Added to an [`Edge`](/api-reference/types/edge) when selected |
| `.react-flow__edge.animated` | Added to an [`Edge`](/api-reference/types/edge) when its `animated` prop is `true` |
| `.react-flow__edge.updating` | Added to an [`Edge`](/api-reference/types/edge) while it gets updated via `onReconnect` |
| `.react-flow__edge-path` | The SVG ` ` element of an [`Edge`](/api-reference/types/edge) |
| `.react-flow__edge-text` | The SVG ` ` element of an [`Edge`](/api-reference/types/edge) label |
| `.react-flow__edge-textbg` | The SVG ` ` element behind an [`Edge`](/api-reference/types/edge) label |
| `.react-flow__connection` | Applied to the current connection line |
| `.react-flow__connection-path` | The SVG ` ` of a connection line |
| `.react-flow__nodes` | The element containing all nodes in the flow |
| `.react-flow__node` | Applied to each [`Node`](/api-reference/types/node) in the flow |
| `.react-flow__node.selected` | Added to a [`Node`](/api-reference/types/node) when selected. |
| `.react-flow__node-default` | Added when [`Node`](/api-reference/types/node) type is `"default"` |
| `.react-flow__node-input` | Added when [`Node`](/api-reference/types/node) type is `"input"` |
| `.react-flow__node-output` | Added when [`Node`](/api-reference/types/node) type is `"output"` |
| `.react-flow__nodesselection` | Nodes selection |
| `.react-flow__nodesselection-rect` | Nodes selection rect |
| `.react-flow__handle` | Applied to each [` `](/api-reference/components/handle) component |
| `.react-flow__handle-top` | Applied when a handle's [`Position`](/api-reference/types/position) is set to `"top"` |
| `.react-flow__handle-right` | Applied when a handle's [`Position`](/api-reference/types/position) is set to `"right"` |
| `.react-flow__handle-bottom` | Applied when a handle's [`Position`](/api-reference/types/position) is set to `"bottom"` |
| `.react-flow__handle-left` | Applied when a handle's [`Position`](/api-reference/types/position) is set to `"left"` |
| `.connectingfrom` | Added to a Handle when a connection line is above a handle. |
| `.connectingto` | Added to a Handle when a connection line is above a handle. |
| `.valid` | Added to a Handle when a connection line is above **and** the connection is valid |
| `.react-flow__background` | Applied to the [` `](/api-reference/components/background) component |
| `.react-flow__minimap` | Applied to the [` `](/api-reference/components/minimap) component |
| `.react-flow__controls` | Applied to the [` `](/api-reference/components/controls) component |
Be careful if you go poking around the source code looking for other classes
to override. Some classes are used internally and are required in order for
the library to be functional. If you replace them you may end up with
unexpected bugs or errors!
#### Third-party solutions
You can choose to opt-out of React Flow's default styling altogether and use a
third-party styling solution instead. If you want to do this, you must make sure
you still import the base styles.
```js
import '@xyflow/react/dist/base.css';
```
These base styles are **required** for React Flow to function correctly. If
you don't import them or you override them with your own styles, some things
might not work as expected!
Example: examples/styling/base-style
##### App.jsx
```jsx
import React, { useCallback } from 'react';
import {
ReactFlow,
Background,
Controls,
MiniMap,
useNodesState,
useEdgesState,
addEdge,
Position,
} from '@xyflow/react';
import '@xyflow/react/dist/base.css';
const nodeDefaults = {
sourcePosition: Position.Right,
targetPosition: Position.Left,
};
const initialNodes = [
{
id: '1',
position: { x: 0, y: 150 },
data: { label: 'base style 1' },
...nodeDefaults,
},
{
id: '2',
position: { x: 250, y: 0 },
data: { label: 'base style 2' },
...nodeDefaults,
},
{
id: '3',
position: { x: 250, y: 150 },
data: { label: 'base style 3' },
...nodeDefaults,
},
{
id: '4',
position: { x: 250, y: 300 },
data: { label: 'base style 4' },
...nodeDefaults,
},
];
const initialEdges = [
{
id: 'e1-2',
source: '1',
target: '2',
},
{
id: 'e1-3',
source: '1',
target: '3',
},
{
id: 'e1-4',
source: '1',
target: '4',
},
];
const Flow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(params) => setEdges((els) => addEdge(params, els)),
[],
);
return (
);
};
export default Flow;
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
box-sizing: border-box;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### TailwindCSS
Custom nodes and edges are just React components, and you can use any styling
solution you'd like to style them. For example, you might want to use
[Tailwind](https://tailwindcss.com/) to style your nodes:
```jsx
function CustomNode({ data }) {
return (
);
}
```
If you want to overwrite default styles, make sure to import Tailwinds entry
point after React Flows base styles.
```js
import '@xyflow/react/dist/style.css';
import 'tailwind.css';
```
For a complete example of using Tailwind with React Flow, check out
[the example](/examples/styling/tailwind)!
### Utility Classes
React Flow provides several built-in utility CSS classes to help you fine-tune how
interactions work within your custom elements.
#### `nodrag`
Adding the class `nodrag` to an element ensures that interacting with it doesn't trigger a
drag. This is particularly useful for elements like buttons or inputs that should not
initiate a drag operation when clicked.
Nodes have a `drag` class name in place by default. However, this class name can affect
the behaviour of the event listeners inside your custom nodes. To prevent unexpected
behaviours, add a `nodrag` class name to elements with an event listener. This prevents
the default drag behavior as well as the default node selection behavior when elements
with this class are clicked.
```tsx
export default function CustomNode(props: NodeProps) {
return (
);
}
```
#### `nopan`
If an element in the canvas does not stop mouse events from propagating, clicking and
dragging that element will pan the viewport. Adding the "nopan" class prevents this
behavior and this prop allows you to change the name of that class.
```tsx
export default function CustomNode(props: NodeProps) {
return (
);
}
```
#### `nowheel`
If your custom element contains scrollable content, you can apply the `nowheel` class.
This disables the canvas' default pan behavior when you scroll inside your custom node,
ensuring that only the content scrolls instead of moving the entire canvas.
```tsx
export default function CustomNode(props: NodeProps) {
return (
);
}
```
Applying these utility classes helps you control interaction on a granular level. You can
customize these class names inside React Flow's
[style props](/api-reference/react-flow/#style-props).
When creating your own custom nodes, you will also need to remember to style them!
Unlike the built-in nodes, custom nodes have no default styles, so feel free to use any
styling method you prefer, such as [Tailwind CSS](/examples/styling/tailwind).
### Overview
We regularly get asked how to handle layouting in React Flow. We have not implemented our
own layouting solution yet, but will present some viable external libraries on this page.
We'll split things up into resources for layouting nodes and resources for routing edges.
You can test out some of the layouting options in our
[playground](https://play.reactflow.dev/) or have a look at the
[examples](/examples#layout) we've put together.
To start let's put together a simple example flow that we can use as a base for testing
out the different layouting options.
Example: learn/layouting-flow-1-empty
##### App.jsx
```jsx
import React, { useCallback } from 'react';
import {
ReactFlow,
ReactFlowProvider,
useNodesState,
useEdgesState,
useReactFlow,
} from '@xyflow/react';
import { initialNodes, initialEdges } from './nodes-edges.js';
import '@xyflow/react/dist/style.css';
const getLayoutedElements = (nodes, edges) => {
return { nodes, edges };
};
const LayoutFlow = () => {
const { fitView } = useReactFlow();
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onLayout = useCallback(() => {
const layouted = getLayoutedElements(nodes, edges);
setNodes([...layouted.nodes]);
setEdges([...layouted.edges]);
fitView();
}, [nodes, edges]);
return (
);
};
export default function () {
return (
);
}
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### nodes-edges.js
```js
export const initialNodes = [
{
id: '1',
type: 'input',
data: { label: 'input' },
position: { x: 0, y: 0 },
},
{
id: '2',
data: { label: 'node 2' },
position: { x: 0, y: 100 },
},
{
id: '2a',
data: { label: 'node 2a' },
position: { x: 0, y: 200 },
},
{
id: '2b',
data: { label: 'node 2b' },
position: { x: 0, y: 300 },
},
{
id: '2c',
data: { label: 'node 2c' },
position: { x: 0, y: 400 },
},
{
id: '2d',
data: { label: 'node 2d' },
position: { x: 0, y: 500 },
},
{
id: '3',
data: { label: 'node 3' },
position: { x: 200, y: 100 },
},
];
export const initialEdges = [
{ id: 'e12', source: '1', target: '2', animated: true },
{ id: 'e13', source: '1', target: '3', animated: true },
{ id: 'e22a', source: '2', target: '2a', animated: true },
{ id: 'e22b', source: '2', target: '2b', animated: true },
{ id: 'e22c', source: '2', target: '2c', animated: true },
{ id: 'e2c2d', source: '2c', target: '2d', animated: true },
];
```
Each of the examples that follow will be built on this empty flow. Where possible we've
tried to keep the examples confined to just one `index.js` file so it's easy for you to
compare how they're set up.
#### Layouting nodes
For layouting nodes, there are a few third-party libraries that we think are worth
checking out:
| Library | Dynamic node sizes | Sub-flow layouting | Edge routing | Bundle size |
| -------------------------------------------------- | ------------------ | ------------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| [Dagre](https://github.com/dagrejs/dagre) | Yes | Yes¹ | No | |
| [D3-Hierarchy](https://github.com/d3/d3-hierarchy) | No | No | No | |
| [D3-Force](https://github.com/d3/d3-force) | Yes | No | No | |
| [ELK](https://github.com/kieler/elkjs) | Yes | Yes | Yes | |
¹ Dagre currently has an [open issue](https://github.com/dagrejs/dagre/issues/238) that
prevents it from laying out sub-flows correctly if any nodes in the sub-flow are connected
to nodes outside the sub-flow.
We've loosely ordered these options from simplest to most complex, where dagre is largely
a drop-in solution and elkjs is a full-blown highly configurable layouting engine. Below,
we'll take a look at a brief example of how each of these libraries can be used with React
Flow. For dagre and elkjs specifically, we have some separate examples you can refer back
to [here](/examples/layout/dagre) and [here](/examples/layout/elkjs).
##### Dagre
* Repo:
* Docs:
Dagre is a simple library for layouting directed graphs. It has minimal configuration
options and a focus on speed over choosing the most optimal layout. If you need to
organize your flows into a tree, *we highly recommend dagre*.
Example: learn/layouting-flow-2-dagre
##### App.jsx
```jsx
import Dagre from '@dagrejs/dagre';
import React, { useCallback } from 'react';
import {
ReactFlow,
ReactFlowProvider,
Panel,
useNodesState,
useEdgesState,
useReactFlow,
} from '@xyflow/react';
import { initialNodes, initialEdges } from './nodes-edges.js';
import '@xyflow/react/dist/style.css';
const getLayoutedElements = (nodes, edges, options) => {
const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
g.setGraph({ rankdir: options.direction });
edges.forEach((edge) => g.setEdge(edge.source, edge.target));
nodes.forEach((node) =>
g.setNode(node.id, {
...node,
width: node.measured?.width ?? 0,
height: node.measured?.height ?? 0,
}),
);
Dagre.layout(g);
return {
nodes: nodes.map((node) => {
const position = g.node(node.id);
// We are shifting the dagre node position (anchor=center center) to the top left
// so it matches the React Flow node anchor point (top left).
const x = position.x - (node.measured?.width ?? 0) / 2;
const y = position.y - (node.measured?.height ?? 0) / 2;
return { ...node, position: { x, y } };
}),
edges,
};
};
const LayoutFlow = () => {
const { fitView } = useReactFlow();
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onLayout = useCallback(
(direction) => {
console.log(nodes);
const layouted = getLayoutedElements(nodes, edges, { direction });
setNodes([...layouted.nodes]);
setEdges([...layouted.edges]);
fitView();
},
[nodes, edges],
);
return (
onLayout('TB')}>vertical layout
onLayout('LR')}>horizontal layout
);
};
export default function () {
return (
);
}
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### nodes-edges.js
```js
export const initialNodes = [
{
id: '1',
type: 'input',
data: { label: 'input' },
position: { x: 0, y: 0 },
},
{
id: '2',
data: { label: 'node 2' },
position: { x: 0, y: 100 },
},
{
id: '2a',
data: { label: 'node 2a' },
position: { x: 0, y: 200 },
},
{
id: '2b',
data: { label: 'node 2b' },
position: { x: 0, y: 300 },
},
{
id: '2c',
data: { label: 'node 2c' },
position: { x: 0, y: 400 },
},
{
id: '2d',
data: { label: 'node 2d' },
position: { x: 0, y: 500 },
},
{
id: '3',
data: { label: 'node 3' },
position: { x: 200, y: 100 },
},
];
export const initialEdges = [
{ id: 'e12', source: '1', target: '2', animated: true },
{ id: 'e13', source: '1', target: '3', animated: true },
{ id: 'e22a', source: '2', target: '2a', animated: true },
{ id: 'e22b', source: '2', target: '2b', animated: true },
{ id: 'e22c', source: '2', target: '2c', animated: true },
{ id: 'e2c2d', source: '2c', target: '2d', animated: true },
];
```
With no effort at all we get a well-organized tree layout! Whenever `getLayoutedElements`
is called, we'll reset the dagre graph and set the graph's direction (either left-to-right
or top-to-bottom) based on the `direction` prop. Dagre needs to know the dimensions of
each node in order to lay them out, so we iterate over our list of nodes and add them to
dagre's internal graph.
After laying out the graph, we'll return an object with the layouted nodes and edges. We
do this by mapping over the original list of nodes and updating each node's position
according to node stored in the dagre graph.
Documentation for dagre's configuration options can be found
[here](https://github.com/dagrejs/dagre/wiki#configuring-the-layout), including properties
to set for spacing and alignment.
##### D3-Hierarchy
* Repo:
* Docs:
When you know your graph is a tree with a single root node, d3-hierarchy can provide a
handful of interesting layouting options. While the library can layout a simple tree just
fine, it also has layouting algorithms for tree maps, partition layouts, and enclosure
diagrams.
Example: learn/layouting-flow-3-d3-hierarchy
##### App.jsx
```jsx
import React, { useCallback } from 'react';
import { stratify, tree } from 'd3-hierarchy';
import {
ReactFlow,
ReactFlowProvider,
Panel,
useNodesState,
useEdgesState,
useReactFlow,
} from '@xyflow/react';
import { initialNodes, initialEdges } from './nodes-edges';
import '@xyflow/react/dist/style.css';
const g = tree();
const getLayoutedElements = (nodes, edges, options) => {
if (nodes.length === 0) return { nodes, edges };
const { width, height } = document
.querySelector(`[data-id="${nodes[0].id}"]`)
.getBoundingClientRect();
const hierarchy = stratify()
.id((node) => node.id)
.parentId((node) => edges.find((edge) => edge.target === node.id)?.source);
const root = hierarchy(nodes);
const layout = g.nodeSize([width * 2, height * 2])(root);
return {
nodes: layout
.descendants()
.map((node) => ({ ...node.data, position: { x: node.x, y: node.y } })),
edges,
};
};
const LayoutFlow = () => {
const { fitView } = useReactFlow();
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onLayout = useCallback(
(direction) => {
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
nodes,
edges,
{
direction,
},
);
setNodes([...layoutedNodes]);
setEdges([...layoutedEdges]);
fitView();
},
[nodes, edges],
);
return (
layout
);
};
export default function () {
return (
);
}
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### nodes-edges.js
```js
export const initialNodes = [
{
id: '1',
type: 'input',
data: { label: 'input' },
position: { x: 0, y: 0 },
},
{
id: '2',
data: { label: 'node 2' },
position: { x: 0, y: 100 },
},
{
id: '2a',
data: { label: 'node 2a' },
position: { x: 0, y: 200 },
},
{
id: '2b',
data: { label: 'node 2b' },
position: { x: 0, y: 300 },
},
{
id: '2c',
data: { label: 'node 2c' },
position: { x: 0, y: 400 },
},
{
id: '2d',
data: { label: 'node 2d' },
position: { x: 0, y: 500 },
},
{
id: '3',
data: { label: 'node 3' },
position: { x: 200, y: 100 },
},
];
export const initialEdges = [
{ id: 'e12', source: '1', target: '2', animated: true },
{ id: 'e13', source: '1', target: '3', animated: true },
{ id: 'e22a', source: '2', target: '2a', animated: true },
{ id: 'e22b', source: '2', target: '2b', animated: true },
{ id: 'e22c', source: '2', target: '2c', animated: true },
{ id: 'e2c2d', source: '2c', target: '2d', animated: true },
];
```
D3-hierarchy expects your graphs to have a single root node, so it won't work in all
cases. It's also important to note that d3-hierarchy assigns the same width and height
to _all_ nodes when calculating the layout, so it's not the best choice if you're
displaying lots of different node types.
##### D3-Force
* Repo:
* Docs:
For something more interesting than a tree, a force-directed layout might be the way to
go. D3-Force is a physics-based layouting library that can be used to position nodes by
applying different forces to them.
As a consequence, it's a little more complicated to configure and use compared to dagre
and d3-hierarchy. Importantly, d3-force's layouting algorithm is iterative, so we need a
way to keep computing the layout across multiple renders.
First, let's see what it does:
Example: learn/layouting-flow-4-d3-force
##### App.jsx
```jsx
import {
forceSimulation,
forceLink,
forceManyBody,
forceX,
forceY,
} from 'd3-force';
import React, { useCallback, useMemo, useRef } from 'react';
import {
ReactFlow,
ReactFlowProvider,
Panel,
useNodesState,
useEdgesState,
useReactFlow,
useNodesInitialized,
} from '@xyflow/react';
import { initialNodes, initialEdges } from './nodes-edges.js';
import { collide } from './collide.js';
import '@xyflow/react/dist/style.css';
const simulation = forceSimulation()
.force('charge', forceManyBody().strength(-1000))
.force('x', forceX().x(0).strength(0.05))
.force('y', forceY().y(0).strength(0.05))
.force('collide', collide())
.alphaTarget(0.05)
.stop();
const useLayoutedElements = () => {
const { getNodes, setNodes, getEdges, fitView } = useReactFlow();
const initialized = useNodesInitialized();
// You can use these events if you want the flow to remain interactive while
// the simulation is running. The simulation is typically responsible for setting
// the position of nodes, but if we have a reference to the node being dragged,
// we use that position instead.
const draggingNodeRef = useRef(null);
const dragEvents = useMemo(
() => ({
start: (_event, node) => (draggingNodeRef.current = node),
drag: (_event, node) => (draggingNodeRef.current = node),
stop: () => (draggingNodeRef.current = null),
}),
[],
);
return useMemo(() => {
let nodes = getNodes().map((node) => ({
...node,
x: node.position.x,
y: node.position.y,
}));
let edges = getEdges().map((edge) => edge);
let running = false;
// If React Flow hasn't initialized our nodes with a width and height yet, or
// if there are no nodes in the flow, then we can't run the simulation!
if (!initialized || nodes.length === 0) return [false, {}, dragEvents];
simulation.nodes(nodes).force(
'link',
forceLink(edges)
.id((d) => d.id)
.strength(0.05)
.distance(100),
);
// The tick function is called every animation frame while the simulation is
// running and progresses the simulation one step forward each time.
const tick = () => {
getNodes().forEach((node, i) => {
const dragging = draggingNodeRef.current?.id === node.id;
// Setting the fx/fy properties of a node tells the simulation to "fix"
// the node at that position and ignore any forces that would normally
// cause it to move.
if (dragging) {
nodes[i].fx = draggingNodeRef.current.position.x;
nodes[i].fy = draggingNodeRef.current.position.y;
} else {
delete nodes[i].fx;
delete nodes[i].fy;
}
});
simulation.tick();
setNodes(
nodes.map((node) => ({
...node,
position: { x: node.fx ?? node.x, y: node.fy ?? node.y },
})),
);
window.requestAnimationFrame(() => {
// Give React and React Flow a chance to update and render the new node
// positions before we fit the viewport to the new layout.
fitView();
// If the simulation hasn't been stopped, schedule another tick.
if (running) tick();
});
};
const toggle = () => {
if (!running) {
getNodes().forEach((node, index) => {
let simNode = nodes[index];
Object.assign(simNode, node);
simNode.x = node.position.x;
simNode.y = node.position.y;
});
}
running = !running;
running && window.requestAnimationFrame(tick);
};
const isRunning = () => running;
return [true, { toggle, isRunning }, dragEvents];
}, [initialized, dragEvents, getNodes, getEdges, setNodes, fitView]);
};
const LayoutFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, , onEdgesChange] = useEdgesState(initialEdges);
const [initialized, { toggle, isRunning }, dragEvents] =
useLayoutedElements();
return (
{initialized && (
{isRunning() ? 'Stop' : 'Start'} force simulation
)}
);
};
export default function () {
return (
);
}
```
##### collide.js
```js
import { quadtree } from 'd3-quadtree';
export function collide() {
let nodes = [];
let force = (alpha) => {
const tree = quadtree(
nodes,
(d) => d.x,
(d) => d.y,
);
for (const node of nodes) {
const r = node.measured.width / 2;
const nx1 = node.x - r;
const nx2 = node.x + r;
const ny1 = node.y - r;
const ny2 = node.y + r;
tree.visit((quad, x1, y1, x2, y2) => {
if (!quad.length) {
do {
if (quad.data !== node) {
const r = node.measured.width / 2 + quad.data.width / 2;
let x = node.x - quad.data.x;
let y = node.y - quad.data.y;
let l = Math.hypot(x, y);
if (l < r) {
l = ((l - r) / l) * alpha;
node.x -= x *= l;
node.y -= y *= l;
quad.data.x += x;
quad.data.y += y;
}
}
} while ((quad = quad.next));
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
}
};
force.initialize = (newNodes) => (nodes = newNodes);
return force;
}
export default collide;
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### nodes-edges.js
```js
export const initialNodes = [
{
id: '1',
type: 'input',
data: { label: 'input' },
position: { x: 0, y: 0 },
},
{
id: '2',
data: { label: 'node 2' },
position: { x: 0, y: 100 },
},
{
id: '2a',
data: { label: 'node 2a' },
position: { x: 0, y: 200 },
},
{
id: '2b',
data: { label: 'node 2b' },
position: { x: 0, y: 300 },
},
{
id: '2c',
data: { label: 'node 2c' },
position: { x: 0, y: 400 },
},
{
id: '2d',
data: { label: 'node 2d' },
position: { x: 0, y: 500 },
},
{
id: '3',
data: { label: 'node 3' },
position: { x: 200, y: 100 },
},
];
export const initialEdges = [
{ id: 'e12', source: '1', target: '2', animated: true },
{ id: 'e13', source: '1', target: '3', animated: true },
{ id: 'e22a', source: '2', target: '2a', animated: true },
{ id: 'e22b', source: '2', target: '2b', animated: true },
{ id: 'e22c', source: '2', target: '2c', animated: true },
{ id: 'e2c2d', source: '2c', target: '2d', animated: true },
];
```
We've changed our `getLayoutedElements` to a hook called `useLayoutedElements` instead.
Additionally, instead of passing in the nodes and edges explicitly, we'll use get
`getNodes` and `getEdges` functions from the `useReactFlow` hook. This is important when
combined with the store selector in `initialized` because it will prevent us from
reconfiguring the simulation any time the nodes update.
The simulation is configured with a number of different forces applied so you can see how
they interact: play around in your own code to see how you want to configure those forces.
You can find the documentation and some different examples of d3-force
[here](https://d3js.org/d3-force).
Rectangular collisions
D3-Force has a built-in collision force, but it assumes nodes are circles. We've thrown
together a custom force in `collision.js` that uses a similar algorithm but accounts for
our rectangular nodes instead. Feel free to steal it or let us know if you have any
suggestions for improvements!
The tick function progresses the simulation by one step and then updates React Flow with
the new node positions. We've also included a demonstration on how to handle node dragging
while the simulation is running: if your flow isn't interactive you can ignore that part!
For larger graphs, computing the force layout every render forever is going to incur a
big performance hit. In this example we have a simple toggle to turn the layouting on
and off, but you might want to come up with some other approach to only compute the
layout when necessary.
##### Elkjs
* Repo:
* Docs: (good luck!)
Elkjs is certainly the most configurable option available, but it's also the most
complicated. Elkjs is a Java library that's been ported to JavaScript, and it provides a
huge number of options for configuring the layout of your graph.
Example: learn/layouting-flow-6-elkjs
##### App.jsx
```jsx
import ELK from 'elkjs/lib/elk.bundled.js';
import React, { useCallback } from 'react';
import {
ReactFlow,
ReactFlowProvider,
Panel,
useNodesState,
useEdgesState,
useReactFlow,
} from '@xyflow/react';
import { initialNodes, initialEdges } from './nodes-edges.js';
import '@xyflow/react/dist/style.css';
const elk = new ELK();
const useLayoutedElements = () => {
const { getNodes, setNodes, getEdges, fitView } = useReactFlow();
const defaultOptions = {
'elk.algorithm': 'layered',
'elk.layered.spacing.nodeNodeBetweenLayers': 100,
'elk.spacing.nodeNode': 80,
};
const getLayoutedElements = useCallback((options) => {
const layoutOptions = { ...defaultOptions, ...options };
const graph = {
id: 'root',
layoutOptions: layoutOptions,
children: getNodes().map((node) => ({
...node,
width: node.measured.width,
height: node.measured.height,
})),
edges: getEdges(),
};
elk.layout(graph).then(({ children }) => {
// By mutating the children in-place we saves ourselves from creating a
// needless copy of the nodes array.
children.forEach((node) => {
node.position = { x: node.x, y: node.y };
});
setNodes(children);
fitView();
});
}, []);
return { getLayoutedElements };
};
const LayoutFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, , onEdgesChange] = useEdgesState(initialEdges);
const { getLayoutedElements } = useLayoutedElements();
return (
getLayoutedElements({
'elk.algorithm': 'layered',
'elk.direction': 'DOWN',
})
}
>
vertical layout
getLayoutedElements({
'elk.algorithm': 'layered',
'elk.direction': 'RIGHT',
})
}
>
horizontal layout
getLayoutedElements({
'elk.algorithm': 'org.eclipse.elk.radial',
})
}
>
radial layout
getLayoutedElements({
'elk.algorithm': 'org.eclipse.elk.force',
})
}
>
force layout
);
};
export default function () {
return (
);
}
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### nodes-edges.js
```js
export const initialNodes = [
{
id: '1',
type: 'input',
data: { label: 'input' },
position: { x: 0, y: 0 },
},
{
id: '2',
data: { label: 'node 2' },
position: { x: 0, y: 100 },
},
{
id: '2a',
data: { label: 'node 2a' },
position: { x: 0, y: 200 },
},
{
id: '2b',
data: { label: 'node 2b' },
position: { x: 0, y: 300 },
},
{
id: '2c',
data: { label: 'node 2c' },
position: { x: 0, y: 400 },
},
{
id: '2d',
data: { label: 'node 2d' },
position: { x: 0, y: 500 },
},
{
id: '3',
data: { label: 'node 3' },
position: { x: 200, y: 100 },
},
];
export const initialEdges = [
{ id: 'e12', source: '1', target: '2', animated: true },
{ id: 'e13', source: '1', target: '3', animated: true },
{ id: 'e22a', source: '2', target: '2a', animated: true },
{ id: 'e22b', source: '2', target: '2b', animated: true },
{ id: 'e22c', source: '2', target: '2c', animated: true },
{ id: 'e2c2d', source: '2c', target: '2d', animated: true },
];
```
At it's most basic we can compute layouts similar to dagre, but because the layouting
algorithm runs asynchronously we need to create a `useLayoutedElements` hook similar to
the one we created for d3-force.
The ELK reference is your new best friend
We don't often recommend elkjs because it's complexity makes it difficult for us to
support folks when they need it. If you do decide to use it, you'll want to keep the
original [Java API reference](https://eclipse.dev/elk/reference.html) handy.
We've also included a few examples of some of the other layouting algorithms available,
including a non-interactive force layout.
##### Honourable Mentions
Of course, we can't go through every layouting library out there: we'd never work on
anything else! Here are some other libraries we've come across that might be worth taking
a look at:
* If you want to use dagre or d3-hierarchy but need to support nodes with different
dimensions, both [d3-flextree](https://github.com/klortho/d3-flextree) and
[entitree-flex](https://github.com/codeledge/entitree-flex) look promising.
You can find an example of how to use entitree-flex with React Flow
[here](/examples/layout/entitree-flex).
* [Cola.js](https://github.com/tgdwyer/WebCola) looks like a promising option for
so-called "constraint-based" layouts. We haven't had time to properly investigate it
yet, but it looks like you can achieve results similar to d3-force but with a lot more
control.
#### Routing Edges
If you don't have any requirements for edge routing, you can use one of the layouting
libraries above to position nodes and let the edges fall wherever they may. Otherwise,
you'll want to look into some libraries and techniques for edge routing.
Your options here are more limited than for node layouting, but here are some resources we
thought looked promising:
* [react-flow-smart-edge](https://github.com/tisoap/react-flow-smart-edge)
* [Routing Orthogonal Diagram Connectors in JavaScript](https://medium.com/swlh/routing-orthogonal-diagram-connectors-in-javascript-191dc2c5ff70)
If you do explore some custom edge routing options, consider contributing back to the
community by writing a blog post or creating a library!
Our [editable edge Pro Example](/examples/edges/editable-edge) could also be used as a
starting point for implementing a custom edge that can be routed along a specific path.
### Sub Flows
**Deprecation of `parentNode` property!** We have renamed the `parentNode` option to
`parentId` in version 11.11.0. The old property is still supported but will be removed
in version 12.
A sub flow is a flow inside a node. It can be a separate flow or a flow that is connected
with other nodes outside of its parent. This feature can also be used for grouping nodes.
In this part of the docs we are going to build a flow with sub flows and show you the
child node specific options.
Order of Nodes
It's important that your parent nodes appear before their children in the `nodes`/
`defaultNodes` array to get processed correctly.
##### Adding child nodes
If you want to add a node as a child of another node you need to use the `parentId` (this
was called `parentNode` in previous versions) option (you can find a list of all options
in the [node options section](/api-reference/types/node)). Once we do that, the child node
is positioned relative to its parent. A position of `{ x: 0, y: 0 }` is the top left
corner of the parent.
In this example we are setting a fixed width and height of the parent node by passing the
style option. Additionally, we set the child extent to `'parent'` so that we can't move
the child nodes out of the parent node.
Example: learn/sub-flows
##### App.jsx
```jsx
import { useCallback, useState } from 'react';
import {
ReactFlow,
addEdge,
applyEdgeChanges,
applyNodeChanges,
Background,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { initialNodes } from './nodes';
import { initialEdges } from './edges';
const rfStyle = {
backgroundColor: '#D0C0F7',
};
function Flow() {
const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState(initialEdges);
const onNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[setNodes],
);
const onEdgesChange = useCallback(
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[setEdges],
);
const onConnect = useCallback(
(connection) => setEdges((eds) => addEdge(connection, eds)),
[setEdges],
);
return (
);
}
export default Flow;
```
##### edges.js
```js
export const initialEdges = [{ id: 'b-c', source: 'B', target: 'C' }];
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### nodes.js
```js
export const initialNodes = [
{
id: 'A',
type: 'group',
data: { label: null },
position: { x: 0, y: 0 },
style: {
width: 170,
height: 140,
},
},
{
id: 'B',
type: 'input',
data: { label: 'child node 1' },
position: { x: 10, y: 10 },
parentId: 'A',
extent: 'parent',
},
{
id: 'C',
data: { label: 'child node 2' },
position: { x: 10, y: 90 },
parentId: 'A',
extent: 'parent',
},
];
```
##### Using child specific options
When you move the parent node you can see that the child nodes move, too. Adding a node to
another node with the `parentId` option, just does one thing: It positions it relatively
to its parent. The child node is not really a child markup-wise. You can drag or position
the child outside of its parent (when the `extent: 'parent'` option is not set) but when
you move the parent, the child moves with it.
In the example above we are using the `group` type for the parent node but you can use any
other type as well. The `group` type is just a convenience node type that has no handles
attached.
Now we are going to add some more nodes and edges. As you can see, we can connect nodes
within a group and create connections that go from a sub flow to an outer node:
Example: learn/sub-flows-2
##### App.jsx
```jsx
import { useCallback, useState } from 'react';
import {
ReactFlow,
addEdge,
applyEdgeChanges,
applyNodeChanges,
Background,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { initialNodes } from './nodes';
import { initialEdges } from './edges';
const rfStyle = {
backgroundColor: '#D0C0F7',
};
function Flow() {
const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState(initialEdges);
const onNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[setNodes],
);
const onEdgesChange = useCallback(
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[setEdges],
);
const onConnect = useCallback(
(connection) => setEdges((eds) => addEdge(connection, eds)),
[setEdges],
);
return (
);
}
export default Flow;
```
##### edges.js
```js
export const initialEdges = [
{ id: 'a1-a2', source: 'A-1', target: 'A-2' },
{ id: 'a2-b', source: 'A-2', target: 'B' },
{ id: 'a2-c', source: 'A-2', target: 'C' },
];
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### nodes.js
```js
export const initialNodes = [
{
id: 'A',
type: 'group',
position: { x: 0, y: 0 },
style: {
width: 170,
height: 140,
},
},
{
id: 'A-1',
type: 'input',
data: { label: 'Child Node 1' },
position: { x: 10, y: 10 },
parentId: 'A',
extent: 'parent',
},
{
id: 'A-2',
data: { label: 'Child Node 2' },
position: { x: 10, y: 90 },
parentId: 'A',
extent: 'parent',
},
{
id: 'B',
type: 'output',
position: { x: -100, y: 200 },
data: { label: 'Node B' },
},
{
id: 'C',
type: 'output',
position: { x: 100, y: 200 },
data: { label: 'Node C' },
},
];
```
##### Edge rendering behavior
Edges are rendered below nodes by default, and this behavior applies to both normal nodes
and group nodes. However, edges connected to a node with a parent are rendered above
nodes.
If you want to customize the z-index of edges, you can use the `zIndex` option. For
example:
```tsx
const defaultEdgeOptions = { zIndex: 1 };
;
```
This allows you to render edges above nodes or adjust their stacking order as needed.
##### Using a default node type as a parent
Let's remove the label of node B and add some child nodes. In this example you can see
that you can use one of the default node types as parents, too. We also set the child
nodes to `draggable: false` so that they are not draggable anymore.
Example: learn/sub-flows-3
##### App.jsx
```jsx
import { useCallback, useState } from 'react';
import {
ReactFlow,
addEdge,
applyEdgeChanges,
applyNodeChanges,
Background,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { initialNodes } from './nodes';
import { initialEdges } from './edges';
const rfStyle = {
backgroundColor: '#D0C0F7',
};
function Flow() {
const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState(initialEdges);
const onNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[setNodes],
);
const onEdgesChange = useCallback(
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[setEdges],
);
const onConnect = useCallback(
(connection) => setEdges((eds) => addEdge(connection, eds)),
[setEdges],
);
return (
);
}
export default Flow;
```
##### edges.js
```js
export const initialEdges = [
{ id: 'a1-a2', source: 'A-1', target: 'A-2' },
{ id: 'a2-b', source: 'A-2', target: 'B' },
{ id: 'a2-c', source: 'A-2', target: 'C' },
{ id: 'b1-b2', source: 'B-1', target: 'B-2' },
{ id: 'b1-b3', source: 'B-1', target: 'B-3' },
];
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### nodes.js
```js
export const initialNodes = [
{
id: 'A',
type: 'group',
position: { x: 0, y: 0 },
style: {
width: 170,
height: 140,
},
},
{
id: 'A-1',
type: 'input',
data: { label: 'Child Node 1' },
position: { x: 10, y: 10 },
parentId: 'A',
extent: 'parent',
},
{
id: 'A-2',
data: { label: 'Child Node 2' },
position: { x: 10, y: 90 },
parentId: 'A',
extent: 'parent',
},
{
id: 'B',
type: 'output',
position: { x: -100, y: 200 },
data: null,
style: {
width: 170,
height: 140,
backgroundColor: 'rgba(240,240,240,0.25)',
},
},
{
id: 'B-1',
data: { label: 'Child 1' },
position: { x: 50, y: 10 },
parentId: 'B',
extent: 'parent',
draggable: false,
style: {
width: 60,
},
},
{
id: 'B-2',
data: { label: 'Child 2' },
position: { x: 10, y: 90 },
parentId: 'B',
extent: 'parent',
draggable: false,
style: {
width: 60,
},
},
{
id: 'B-3',
data: { label: 'Child 3' },
position: { x: 100, y: 90 },
parentId: 'B',
extent: 'parent',
draggable: false,
style: {
width: 60,
},
},
{
id: 'C',
type: 'output',
position: { x: 100, y: 200 },
data: { label: 'Node C' },
},
];
```
### Common Errors
This guide contains warnings and errors that can occur when using React Flow. We are also
adding common questions and pitfalls that we collect from our
[Discord Server](https://discord.gg/RVmnytFmGW),
[Github Issues](https://github.com/xyflow/xyflow/issues) and
[Github Discussions](https://github.com/xyflow/xyflow/discussions).
### Warning: Seems like you have not used zustand provider as an ancestor.
This usually happens when:
**A:** You have two different versions of @reactflow/core installed. **B:** You are
trying to access the internal React Flow state outside of the React Flow context.
###### Solution for A
Update reactflow and @reactflow/node-resizer (in case you are using it), remove
node\_modules and package-lock.json and reinstall the dependencies.
###### Solution for B
A possible solution is to wrap your component with a
[` `](/api-reference/react-flow-provider) or move the code that is
accessing the state inside a child of your React Flow instance.
This will cause an error:
```jsx
import { ReactFlow } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
function FlowWithoutProvider(props) {
// cannot access the state here
const reactFlowInstance = useReactFlow();
return ;
}
export default FlowWithoutProvider;
```
This will cause an error, too:
```jsx
import { ReactFlow, ReactFlowProvider } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
function Flow(props) {
// still cannot access the state here
// only child components of this component can access the state
const reactFlowInstance = useReactFlow();
return (
);
}
export default FlowWithProvider;
```
This works:
As soon as you want to access the internal state of React Flow (for example by using the
`useReactFlow` hook), you need to wrap your component with a ` `. Here
the wrapping is done outside of the component:
```jsx
import { ReactFlow, ReactFlowProvider } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
function Flow(props) {
// you can access the internal state here
const reactFlowInstance = useReactFlow();
return ;
}
// wrapping with ReactFlowProvider is done outside of the component
function FlowWithProvider(props) {
return (
);
}
export default FlowWithProvider;
```
### It looks like you have created a new nodeTypes or edgeTypes object.
If this wasn't on purpose please define the nodeTypes/edgeTypes outside of the component
or memoize them.
This warning appears when the `nodeTypes` or `edgeTypes` properties change after the
initial render. The `nodeTypes` or `edgeTypes` should only be changed dynamically in very
rare cases. Usually, they are defined once, along with all the types you use in your
application. It can happen easily that you are defining the nodeTypes or edgeTypes object
inside of your component render function, which will cause React Flow to re-render every
time your component re-renders.
Causes a warning:
```jsx
import { ReactFlow } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import MyCustomNode from './MyCustomNode';
function Flow(props) {
// new object being created on every render
// causing unnecessary re-renders
const nodeTypes = {
myCustomNode: MyCustomNode,
};
return ;
}
export default Flow;
```
Recommended implementation:
```jsx
import { ReactFlow } from '@xyflow/react';
import MyCustomNode from './MyCustomNode';
// defined outside of the component
const nodeTypes = {
myCustomNode: MyCustomNode,
};
function Flow(props) {
return ;
}
export default Flow;
```
Alternative implementation:
You can use this if you want to change your nodeTypes dynamically without causing
unnecessary re-renders.
```jsx
import { useMemo } from 'react';
import { ReactFlow } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import MyCustomNode from './MyCustomNode';
function Flow(props) {
const nodeTypes = useMemo(
() => ({
myCustomNode: MyCustomNode,
}),
[],
);
return ;
}
export default Flow;
```
### Node type not found. Using fallback type "default".
This usually happens when you specify a custom node type for one of your nodes but do not
pass the correct nodeTypes property to React Flow. The string for the type option of your
custom node needs to be exactly the same as the key of the nodeTypes object.
Doesn't work:
```jsx
import { ReactFlow } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import MyCustomNode from './MyCustomNode';
const nodes = [
{
id: 'mycustomnode',
type: 'custom',
// ...
},
];
function Flow(props) {
// nodeTypes property is missing, so React Flow cannot find the custom node component to render
return ;
}
```
Doesn't work either:
```jsx
import { ReactFlow } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import MyCustomNode from './MyCustomNode';
const nodes = [
{
id: 'mycustomnode',
type: 'custom',
// ...
},
];
const nodeTypes = {
Custom: MyCustomNode,
};
function Flow(props) {
// node.type and key in nodeTypes object are not exactly the same (capitalized)
return ;
}
```
This does work:
```jsx
import { ReactFlow } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import MyCustomNode from './MyCustomNode';
const nodes = [
{
id: 'mycustomnode',
type: 'custom',
// ...
},
];
const nodeTypes = {
custom: MyCustomNode,
};
function Flow(props) {
return ;
}
```
### The React Flow parent container needs a width and a height to render the graph.
Under the hood, React Flow measures the parent DOM element to adjust the renderer. If you
try to render React Flow in a regular div without a height, we cannot display the graph.
If you encounter this warning, you need to make sure that your wrapper component has some
CSS attached to it so that it gets a fixed height or inherits the height of its parent.
This will cause the warning:
```jsx
import { ReactFlow } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
function Flow(props) {
return (
);
}
```
Working example:
```jsx
import { ReactFlow } from '@xyflow/react';
function Flow(props) {
return (
);
}
```
### Only child nodes can use a parent extent.
This warning appears when you are trying to add the `extent` option to a node that does
not have a parent node. Depending on what you are trying to do, you can remove the
`extent` option or specify a `parentNode`.
Does show a warning:
```jsx
import { ReactFlow } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
const nodes = [
{
id: 'mycustomnode',
extent: 'parent',
// ...
},
];
function Flow(props) {
return ;
}
```
Warning resolved:
```jsx import { ReactFlow } from '@xyflow/react';
const nodes = [
{
id: 'mycustomnode',
parentNode: 'someothernode',
extent: 'parent',
// ...
},
];
function Flow(props) {
return ;
}
```
### Can't create edge. An edge needs a source and a target.
This happens when you do not pass a `source` and a `target` option to the edge object.
Without the source and target, the edge cannot be rendered.
Will show a warning:
```jsx
import { ReactFlow } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
const nodes = [
/* ... */
];
const edges = [
{
nosource: '1',
notarget: '2',
},
];
function Flow(props) {
return ;
}
```
This works:
```jsx
import { ReactFlow } from '@xyflow/react';
const nodes = [
/* ... */
];
const edges = [
{
source: '1',
target: '2',
},
];
function Flow(props) {
return ;
}
```
### The old edge with id="some-id" does not exist.
This can happen when you are trying to [reconnect an edge](/examples/edges/reconnect-edge)
but the edge you want to update is already removed from the state. This is a very rare
case. Please see the [Reconnect Edge example](/examples/edges/reconnect-edge) for
implementation details.
### Couldn't create edge for source/target handle id: "some-id"; edge id: "some-id".
This can happen if you are working with multiple handles and a handle is not found by its
`id` property or if you haven't
[updated the node internals after adding or removing handles](/api-reference/hooks/use-update-node-internals)
programmatically. Please see the [Custom Node Example](/examples/nodes/custom-node) for an
example of working with multiple handles.
### Marker type doesn't exist.
This warning occurs when you are trying to specify a marker type that is not built into
React Flow. The existing marker types are documented
[here](/api-reference/types/edge#edgemarker).
### Handle: No node id found.
This warning occurs when you try to use a ` ` component outside of a custom node
component.
##### I get an error when building my app with webpack 4.
If you're using webpack 4, you'll likely run into an error like this:
```
ERROR in /node_modules/@reactflow/core/dist/esm/index.js 16:19
Module parse failed: Unexpected token (16:19)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
```
React Flow is a modern JavaScript code base and makes use of lots of newer JavaScript
features. By default, webpack 4 does not transpile your code and it doesn't know how to
handle React Flow.
You need to add a number of babel plugins to your webpack config to make it work:
```sh npm2yarn copy
npm i --save-dev babel-loader@8.2.5 @babel/preset-env @babel/preset-react @babel/plugin-proposal-optional-chaining @babel/plugin-proposal-nullish-coalescing-operator
```
and configure the loader like this:
```js
{
test: /node_modules[\/\\]@?reactflow[\/\\].*.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', "@babel/preset-react"],
plugins: [
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-nullish-coalescing-operator",
]
}
}
}
```
If you're using webpack 5, you don't need to do anything! React Flow will work out of
the box.
##### Mouse events aren't working consistently when my nodes contain a ` ` element.
If you’re using a ` ` element inside your custom node, you might run into
problems with seemingly incorrect coordinates in mouse events from the canvas.
React Flow uses CSS transforms to scale nodes as you zoom in and out. However, from the
DOM’s perspective, the element is still the same size. This can cause problems if you have
event listeners that want to calculate the mouse position relative to the canvas element.
To remedy this in event handlers you control, you can scale your computed relative
position by `1 / zoom` where `zoom` is the current zoom level of the flow. To get the
current zoom level, you can use the `getZoom` method from the
[`useReactFlow`](/api-reference/hooks/use-react-flow) hook.
##### Edges are not displaying.
If your edges are not displaying in React Flow, this might be due to one of the following
reasons:
* You have not imported the React Flow stylesheet. If you haven't imported it, you can
* If you have replaced your default nodes with a custom node, check if that custom node
has appropriate `source/target` handles in the custom node component. An edge cannot be
made without a handle.
* If you use an external styling library like Tailwind or Bulma, ensure it doesn't
override the edge styles. For example, sometimes styling libraries override the
`.react-flow__edges` SVG selector with `overflow: hidden`, which hides the edges.
* If you are using an async operation like a request to the backend, make sure to call the
`updateNodeInternals` function returned by the
[`useUpdateNodeInternal`](/api-reference/hooks/use-update-node-internals) hook after the
async operation so React Flow updates the handle position internally.
##### Edges are not displaying correctly.
If your edges are not rendering as they should, this could be due to one of the following
reasons:
* If you want to hide your handles, do not use `display: none` to hide them. Use either
`opacity: 0` or `visibility: hidden`.
* If edges are not connected to the correct handle, check if you have added more than one
handle of the same type(`source` or `target`) in your custom node component. If that is
the case, assign IDs to them. Multiple handles of the same kind on a node need to have
distinguishable IDs so that React Flow knows which handle an edge corresponds to.
* If you are changing the position of the handles (via reordering, etc.), make sure to
call the `updateNodeInternals` function returned by the
[`useUpdateNodeInternals`](/api-reference/hooks/use-update-node-internals) hook after so
React Flow knows to update the handle position internally.
* If you are using a custom edge and want your edge to go from the source handle to a
target handle, make sure to correctly pass the `sourceX, sourceY, targetX, and targetY`
props you get from the custom edge component in the edge path creation function(e.g.,
[`getBezierPath`](/api-reference/utils/get-bezier-path), etc.). `sourceX, sourceY`, and
`targetX, targetY` represent the `x,y` coordinates for the source and target handle,
respectively.
* If the custom edge from the source or target side is not going towards the handle as
expected (entering or exiting from a handle at a weird angle), make sure to pass the
`sourcePosition` and `targetPosition` props you get from the custom edge component in
the edge path creation function(e.g.,
[`getBezierPath`](/api-reference/utils/get-bezier-path)). Passing the source/target
handle position in the path creation function is necessary for the edge to start or end
properly at a handle.
### Migrate to React Flow v10
You can find the docs for old versions of React Flow here:
[v11](https://v11.reactflow.dev), [v10](https://v10.reactflow.dev),
[v9](https://v9.reactflow.dev)
Welcome to React Flow v10! With the major version update, there are coming many new features but also some breaking changes.
#### New Features
* [**Sub Flows**](/learn/layouting/sub-flows): You can now add nodes to a parent node and create groups and nested flows
* **Node Type 'group'**: A new node type without handles that can be used as a group node
* **Touch Device Support**: It is now possible to connect nodes on touch devices
* **Fit View on Init**: You can use the new `fitView` prop to fit the initial view
* **Key Handling**: Not only single keys but also multiple keys and key combinations are possible now
* [**useKeyPress hook**](/api-reference/hooks/use-key-press): A util hook for handling keyboard events
* [**useReactFlow hook**](/api-reference/hooks/use-react-flow): Returns a React Flow instance that exposes functions to manipulate the flow
* **[useNodes](/api-reference/hooks/use-nodes), [useEdges](/api-reference/hooks/use-edges) and [useViewport](/api-reference/hooks/use-viewport) hooks**: Hooks for receiving nodes, edges and the viewport
* **Edge Marker**: More options to configure the start and end markers of an edge
#### Breaking Changes
TLDR:
* Split the `elements` array into `nodes` and `edges` arrays and implement `onNodesChange` and `onEdgesChange` handlers (detailed guide in the [core concepts section](/learn/concepts/core-concepts))
* Memoize your custom `nodeTypes` and `edgeTypes`
* Rename `onLoad` to `onInit`
* Rename `paneMoveable` to `panOnDrag`
* Rename `useZoomPanHelper` to `useReactFlow` (and `setTransform` to `setViewport`)
* Rename node and edge option `isHidden` to `hidden`
Detailed explanation of breaking changes:
##### 1. ~~Elements~~ - Nodes and Edges
We saw that a lot of people struggle with the semi controlled `elements` prop. It was always a bit messy to sync the local user state with the internal state of React Flow. Some of you used the internal store that was never documented and always a kind of hacky solution. For the new version we offer two ways to use React Flow - uncontrolled and controlled.
##### 1.1. Controlled `nodes` and `edges`
If you want to have the full control and use nodes and edges from your local state or your store, you can use the `nodes`, `edges` props in combination with the `onNodesChange` and `onEdgesChange` handlers. You need to implement these handlers for an interactive flow (if you are fine with just pan and zoom you don't need them). You'll receive a change when a node(s) gets initialized, dragged, selected or removed. This means that you always know the exact position and dimensions of a node or if it's selected for example. We export the helper functions `applyNodeChanges` and `applyEdgeChanges` that you should use to apply the changes.
###### Old API
```jsx
import { useState, useCallback } from 'react';
import { ReactFlow, removeElements, addEdge } from 'react-flow-renderer';
const initialElements = [
{ id: '1', data: { label: 'Node 1' }, position: { x: 250, y: 0 } },
{ id: '2', data: { label: 'Node 2' }, position: { x: 150, y: 100 } },
{ id: 'e1-2', source: '1', target: '2' },
];
const BasicFlow = () => {
const [elements, setElements] = useState(initialElements);
const onElementsRemove = useCallback(
(elementsToRemove) =>
setElements((els) => removeElements(elementsToRemove, els)),
[],
);
const onConnect = useCallback((connection) =>
setElements((es) => addEdge(connection, es)),
);
return (
);
};
export default BasicFlow;
```
###### New API
```jsx
import { useState, useCallback } from 'react';
import {
ReactFlow,
applyNodeChanges,
applyEdgeChanges,
addEdge,
} from 'react-flow-renderer';
const initialNodes = [
{ id: '1', data: { label: 'Node 1' }, position: { x: 250, y: 0 } },
{ id: '2', data: { label: 'Node 2' }, position: { x: 150, y: 100 } },
];
const initialEdges = [{ id: 'e1-2', source: '1', target: '2' }];
const BasicFlow = () => {
const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState(initialEdges);
const onNodesChange = useCallback(
(changes) => setNodes((ns) => applyNodeChanges(changes, ns)),
[],
);
const onEdgesChange = useCallback(
(changes) => setEdges((es) => applyEdgeChanges(changes, es)),
[],
);
const onConnect = useCallback((connection) =>
setEdges((eds) => addEdge(connection, eds)),
);
return (
);
};
export default BasicFlow;
```
You can also use the new hooks `useNodesState` and `useEdgesState` for a quick start:
```js
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
```
related changes:
* `onElementsClick` ->`onNodeClick` and `onEdgeClick`
* `onElementsRemove` -> replaced by the `onNodesChange` and `onEdgesChange` handler
##### 1.2 Uncontrolled `defaultNodes` and `defaultEdges`
The easiest way to get started is to use the `defaultNodes` and `defaultEdges` props. When you set these props, all actions are handled internally. You don't need to add any other handlers to get a fully interactive flow with the ability to drag nodes, connect nodes and remove nodes and edges:
###### New API
```jsx
import ReactFlow from 'react-flow-renderer';
const defaultNodes = [
{ id: '1', data: { label: 'Node 1' }, position: { x: 250, y: 0 } },
{ id: '2', data: { label: 'Node 2' }, position: { x: 150, y: 100 } },
];
const defaultEdges = [{ id: 'e1-2', source: '1', target: '2' }];
const BasicFlow = () => {
return ;
};
export default BasicFlow;
```
If you want to add, remove or update a node or edge you can only do this by using the [ReactFlow instance](/api-reference/types/react-flow-instance) that you can receive either with the new `useReactFlow` hook or by using the `onInit` handler that gets the instance as a function param.
##### 2. Memoize your custom `nodeTypes` and `edgeTypes`
Whenever you pass new node or edge types, we create wrapped node or edge component types in the background. This means that you should not create a new `nodeType` or `edgeType` object on every render. **Memoize your nodeTypes and edgeTypes or define them outside of the component when they don't change**.
**Don't do this:**
This creates a new object on every render and leads to bugs and performance issues:
```jsx
// this is bad! Don't do it.
```
**Do this:**
```jsx
function Flow() {
const nodeTypes = useMemo(() => ({ specialType: SpecialNode }), []);
return ;
}
```
or create the types outside of the component when they don't change:
```jsx
const nodeTypes = { specialType: SpecialNode };
function Flow() {
return ;
}
```
##### 3. ~~Redux~~ - Zustand
We switched our state management library from Redux to [Zustand](https://github.com/pmndrs/zustand). With this change we could remove about 300LOC from our state related code. If you need to access the internal store, you can use the [`useStore` hook](/api-reference/hooks/use-store):
###### Old API
```jsx
import { useStoreState, useStoreActions } from 'react-flow-renderer';
...
const transform = useStoreState((store) => store.transform);
```
###### New API
```jsx
import { useStore } from 'react-flow-renderer';
...
const transform = useStore((store) => store.transform);
```
You still need to wrap your component with the ` ` if you want to access the internal store.
We are also exporting `useStoreApi` if you need to get the store in an event handler for example without triggering re-renders.
```js
import { useStoreApi } from 'react-flow-renderer';
...
const store = useStoreApi();
...
// in an event handler
const [x, y, zoom] = store.getState().transform;
```
##### 4. ~~onLoad~~ - onInit
The `onLoad` callback has been renamed to `onInit` and now fires when the nodes are initialized.
###### Old API
```jsx
const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.zoomTo(2);
...
```
###### New API
```jsx
const onInit = (reactFlowInstance: ReactFlowInstance) => reactFlowInstance.zoomTo(2);
...
```
##### 5. ~~paneMoveable~~ - panOnDrag
This is more consistent with the rest of the API (`panOnScroll`, `zoomOnScroll`, etc.)
###### Old API
```jsx
```
###### New API
```jsx
```
##### 6. ~~useZoomPanHelper transform~~ - unified in `useReactFlow`
As "transform" is also the variable name of the transform in the store and it's not clear that `transform` is a setter we renamed it to `setViewport`. This is also more consistent with the other functions. Also, all `useZoomPanHelper` functions have been moved to the [React Flow instance](/api-reference/types/react-flow-instance) that you get from the [`useReactFlow` hook](/api-reference/hooks/use-react-flow) or the `onInit` handler.
###### Old API
```js
const { transform, setCenter, setZoom } = useZoomPanHelper();
...
transform({ x: 100, y: 100, zoom: 2 });
```
###### New API
```js
const { setViewport, setCenter, setZoom } = useReactFlow();
...
setViewport({ x: 100, y: 100, zoom: 2 });
```
New viewport functions:
* `getZoom`
* `getViewport`
##### 7. ~~isHidden~~ - hidden
We mixed prefixed (`is...`) and non-prefixed boolean option names. All node and edge options are not prefixed anymore. So it's `hidden`, `animated`, `selected`, `draggable`, `selectable` and `connectable`.
###### Old API
```js
const hiddenNode = { id: '1', isHidden: true, position: { x: 50, y: 50 } };
```
###### New API
```js
const hiddenNode = { id: '1', hidden: true, position: { x: 50, y: 50 } };
```
##### 8. ~~arrowHeadType~~ ~~markerEndId~~ - markerStart / markerEnd
We improved the API for customizing the markers for an edge. With the new API you are able to set individual markers at the start and the end of an edge as well as customizing them with colors, strokeWidth etc. You still have the ability to set a markerEndId but instead of using different properties, the `markerStart` and `markerEnd` property accepts either a string (id for the svg marker that you need to define yourself) or a configuration object for using the built in arrowClosed or arrow markers.
###### Old API
```js
const markerEdge = { source: '1', target: '2', arrowHeadType: 'arrow' };
```
###### New API
```js
const markerEdge = {
source: '1',
target: '2',
markerStart: 'myCustomSvgMarker',
markerEnd: { type: 'arrow', color: '#f00' },
};
```
##### 9. ~~ArrowHeadType~~ - MarkerType
This is just a wording change for making the marker API more consistent. As we are now able to set markers for the start of the edge, the name type ArrowHeadType has been renamed to MarkerType. In the future, this can also not only contain arrow shapes but others like circles, diamonds etc.
##### 10. Attribution
This is not really a breaking change to the API but a little change in the general appearance of React Flow. We added a tiny "React Flow" attribution to the bottom right (the position is configurable via the `attributionPosition` prop). This change comes with the new "React Flow Pro" subscription model. If you want to remove the attribution in a commercial application, please subscribe to ["React Flow Pro"](/pro).
### Migrate to React Flow v11
You can find the docs for old versions of React Flow here:
[v11](https://v11.reactflow.dev), [v10](https://v10.reactflow.dev),
[v9](https://v9.reactflow.dev)
A lot changed in v11 but this time we've tried to keep the breaking changes small. The biggest change is the new package name `reactflow` and the new repo structure. React Flow is now managed as a monorepo and separated into multiple packages that can be installed separately. In addition to that, there are some API changes and new APIs introduced in v11. This guide explains the changes in detail and helps you to migrate from v10 to v11.
React Flow 11 only works with **React 17** or greater
#### New Features
* **Better [Accessibility](/learn/advanced-use/accessibility)**
* Nodes and edges are focusable, selectable, moveable and deletable with the keyboard.
* `aria-` default attributes for all elements and controllable via `ariaLabel` options
* Keyboard controls can be disabled with the new `disableKeyboardA11y` prop
* **Better selectable edges** via new edge option: `interactionWidth` - renders invisible edge that makes it easier to interact
* **Better routing for smoothstep and step edges**:
* **Nicer edge updating behavior**:
* **Node origin**: The new `nodeOrigin` prop lets you control the origin of a node. Useful for layouting.
* **New background pattern**: `BackgroundVariant.Cross` variant
* **[`useOnViewportChange`](/api-reference/hooks/use-on-viewport-change) hook** - handle viewport changes within a component
* **[`use-on-selection-change`](/api-reference/hooks/use-on-selection-change) hook** - handle selection changes within a component
* **[`useNodesInitialized`](/api-reference/hooks/use-nodes-initialized) hook** - returns true if all nodes are initialized and if there is more than one node
* **Deletable option** for Nodes and edges
* **New Event handlers**: `onPaneMouseEnter`, `onPaneMouseMove` and `onPaneMouseLeave`
* **Edge `pathOptions`** for `smoothstep` and `default` edges
* **Nicer cursor defaults**: Cursor is grabbing, while dragging a node or panning
* **Pane moveable** with middle mouse button
* **Pan over nodes** when they are not draggable (`draggable=false` or `nodesDraggable` false)
* you can disable this behavior by adding the class name `nopan` to a wrapper of a custom node
* **[` `](/api-reference/components/base-edge) component** that makes it easier to build custom edges
* **[Separately installable packages](/learn/concepts/built-in-components)**
* @reactflow/core
* @reactflow/background
* @reactflow/controls
* @reactflow/minimap
#### Breaking Changes
##### 1. New npm package name
The package `react-flow-renderer` has been renamed to `reactflow`.
###### Old API
```js
// npm install react-flow-renderer
import ReactFlow from 'react-flow-renderer';
```
###### New API
```js
// npm install reactflow
import { ReactFlow } from '@xyflow/react';
```
##### 2. Importing CSS is mandatory
We are not injecting CSS anymore. **React Flow won't work if you are not loading the styles!**
```js
// default styling
import '@xyflow/react/dist/style.css';
// or if you just want basic styles
import '@xyflow/react/dist/base.css';
```
###### 2.1. Removal of the nocss entrypoint
This change also means that there is no `react-flow-renderer/nocss` entry point anymore. If you used that, you need to adjust the CSS entry points as mentioned above.
##### 3. `defaultPosition` and `defaultZoom` have been merged to `defaultViewport`
###### Old API
```jsx
import ReactFlow from 'react-flow-renderer';
const Flow = () => {
return ;
};
export default Flow;
```
###### New API
```jsx
import { ReactFlow } from '@xyflow/react';
const defaultViewport: Viewport = { x: 10, y: 15, zoom: 5 };
const Flow = () => {
return ;
};
export default Flow;
```
##### 4. Removal of `getBezierEdgeCenter`, `getSimpleBezierEdgeCenter` and `getEdgeCenter`
In v10 we had `getBezierEdgeCenter`, `getSimpleBezierEdgeCenter` and `getEdgeCenter` for getting the center of a certain edge type.
In v11 we changed the helper function for creating the path, so that it also returns the center / label position of an edge.
Let's say you want to get the path and the center / label position of a bezier edge:
###### Old API
```jsx
import { getBezierEdgeCenter, getBezierPath } from 'react-flow-renderer';
const path = getBezierPath(edgeParams);
const [centerX, centerY] = getBezierEdgeCenter(params);
```
###### New API
```jsx
import { getBezierPath } from '@xyflow/react';
const [path, labelX, labelY] = getBezierPath(edgeParams);
```
We avoid to call it `centerX` and `centerY` anymore, because it's actually the label position and not always the center for every edge type.
##### 5. Removal of `onClickConnectStop` and `onConnectStop`
###### Old API
```jsx
import ReactFlow from 'react-flow-renderer';
const Flow = () => {
const onConnectStop = () => console.log('on connect stop');
return (
);
};
export default Flow;
```
###### New API
```jsx
import { ReactFlow } from '@xyflow/react';
const Flow = () => {
const onConnectEnd = () => console.log('on connect stop');
return (
);
};
export default Flow;
```
##### 6. Pan over nodes
In the previous versions you couldn't pan over nodes even if they were not draggable. In v11, you can pan over nodes when `nodesDraggable=false` or node option `draggable=false`. If you want the old behavior back, you can add the class name `nopan` to the wrappers of your custom nodes.
### Migrate to React Flow 12
You can find the docs for old versions of React Flow here:
[v11](https://v11.reactflow.dev), [v10](https://v10.reactflow.dev),
[v9](https://v9.reactflow.dev)
Before you can use the **[new features](#new-features)** that come with React Flow 12 like
server side rendering, computing flows, and dark mode, here are the breaking changes
you'll have to address first. We tried to keep the breaking changes to a minimum, but some
of them were necessary to implement the new features.
#### Migration guide
Before you start to migrate, you need to install the new package.
```bash npm2yarn
npm install @xyflow/react
```
##### 1. A new npm package name
The package `reactflow` has been renamed to `@xyflow/react` and it's not a default import
anymore. You also need to adjust the style import. Before v12, React Flow was divided into
multiple packages. That's not the case anymore. If you just used the core, you now need to
install the `@xyflow/react` package.
**Old API**
```js
// npm install reactflow
import ReactFlow from 'reactflow';
```
**New API**
```js
// npm install @xyflow/react
import { ReactFlow } from '@xyflow/react';
// you also need to adjust the style import
import '@xyflow/react/dist/style.css';
// or if you just want basic styles
import '@xyflow/react/dist/base.css';
```
##### 2. Node measured attribute for measured width and height
All measured node values are now stored in `node.measured`. Besides the new package name,
this is the biggest change. After React Flow measures your nodes, it writes the dimensions
to `node.measured.width` and `node.measured.height`. If you are using any layouting
library like dagre or elk, you now need to take the dimensions from `node.measured`
instead of `node`. If you are using `width` and `height`, those values will now be used as
inline styles to specify the node dimensions.
**Old API**
```js
// getting the measured width and height
const nodeWidth = node.width;
const nodeHeight = node.height;
```
**New API**
```js
// getting the measured width and height
const nodeWidth = node.measured?.width;
const nodeHeight = node.measured?.height;
```
##### 3. New dimension handling node.width / node.height vs node.measured.width / node.measured.height
In order to support server side rendering we had to restructure the API a bit, so that
users can pass node dimensions more easily. For this we changed the behavior of the
`node.width` and `node.height` attributes. In React Flow 11, those attributes were
measured values and only used as a reference. In React Flow 12 those attributes are used
as inline styles to specify the node dimensions. If you load nodes from a database, you
probably want to remove the `width` and `height` attributes from your nodes, because the
behavior is slightly different now. Using `width` and `height` now means that the
dimensions are not dynamic based on the content but fixed.
**Old API**
```js
// in React Flow 11 you might have used node.style to set the dimensions
const nodes = [
{
id: '1',
type: 'input',
data: { label: 'input node' },
position: { x: 250, y: 5 },
style: { width: 180, height: 40 },
},
];
```
**New API**
```js
// in React Flow 12 you can used node.width and node.height to set the dimensions
const nodes = [
{
id: '1',
type: 'input',
data: { label: 'input node' },
position: { x: 250, y: 5 },
width: 180,
height: 40,
},
];
```
If you want to read more about how to configure React Flow for server side rendering, you
can read about it in the
[server side rendering guide](/learn/advanced-use/ssr-ssg-configuration).
##### 4. Updating nodes and edges
We are not supporting node and edge updates with object mutations anymore. If you want to
update a certain attribute, you need to create a new node / edge.
**Old API**
```js
setNodes((currentNodes) =>
currentNodes.map((node) => {
node.hidden = true;
return node;
}),
);
```
**New API**
```js
setNodes((currentNodes) =>
currentNodes.map((node) => ({
...node,
hidden: true,
})),
);
```
##### 5. Rename onEdgeUpdate (and related APIs) to onReconnect
We renamed the `onEdgeUpdate` function to `onReconnect` and all related APIs (mentioned
below). The new name is more descriptive and makes it clear that the function is used to
reconnect edges.
* `updateEdge` renamed to `reconnectEdge`
* `onEdgeUpdateStart` renamed to `onReconnectStart`
* `onEdgeUpdate` renamed to `onReconnect`
* `onEdgeUpdateEnd` renamed to `onReconnectEnd`
* `edgeUpdaterRadius` renamed to `reconnectRadius`
* `edge.updatable` renamed to `edge.reconnectable`
* `edgesUpdatable` renamed to `edgesReconnectable`
**Old API**
```js
```
**New API**
```js
```
##### 6. Rename parentNode to parentId
If you are working with subflows, you need to rename `node.parentNode` to `node.parentId`.
The `parentNode` attribute was a bit misleading, because it was not a reference to the
parent node, but the `id` of the parent node.
**Old API**
```js
const nodes = [
// some nodes ...
{
id: 'xyz-id',
position: { x: 0, y: 0 },
type: 'default',
data: {},
parentNode: 'abc-id',
},
];
```
**New API**
```js
const nodes = [
// some nodes ...
{
id: 'xyz-id',
position: { x: 0, y: 0 },
type: 'default',
data: {},
parentId: 'abc-id',
},
];
```
##### 7. Custom node props
We renamed the `xPos` and `yPos` props to `positionAbsoluteX` and `positionAbsoluteY`
**Old API**
```js
function CustomNode({ xPos, yPos }) {
...
}
```
**New API**
```js
function CustomNode({ positionAbsoluteX, positionAbsoluteY }) {
...
}
```
##### 8. Handle component class names
We renamed some of the classes used to define the current state of a handle.
* `react-flow__handle-connecting` renamed to `connectingto` / `connectingfrom`
* `react-flow__handle-valid` renamed to `valid`
##### 9. getNodesBounds options
The type of the second param changed from `nodeOrigin` to `options.nodeOrigin`
**Old API**
```js
const bounds = getNodesBounds(nodes: Node[], nodeOrigin)
```
**New API**
```js
const bounds = getNodesBounds(nodes: Node[], { nodeOrigin })
```
##### 10. Typescript changes for defining nodes and edges
We simplified types and fixed issues about functions where users could pass a NodeData
generic. The new way is to define your own node type with a union of all your nodes. With
this change, you can now have multiple node types with different data structures and
always be able to distinguish by checking the `node.type` attribute.
**New API**
```js
type NumberNode = Node<{ value: number }, 'number'>;
type TextNode = Node<{ text: string }, 'text'>;
type AppNode = NumberNode | TextNode;
```
You can then use the `AppNode` type as the following:
```js
const nodes: AppNode[] = [
{ id: '1', type: 'number', data: { value: 1 }, position: { x: 100, y: 100 } },
{ id: '2', type: 'text', data: { text: 'Hello' }, position: { x: 200, y: 200 } },
];
```
```js
const onNodesChange: onNodesChange = useCallback((changes) => setNodes(nds => applyChanges(changes, nds)), []);
```
You can read more about this in the [Typescript guide](/learn/advanced-use/typescript).
##### 11. Rename nodeInternals
If you are using `nodeInternals` you need to rename it to `nodeLookup`.
**Old API**
```js
const node = useStore((s) => s.nodeInternals.get(id));
```
**New API**
```js
const node = useStore((s) => s.nodeLookup.get(id));
```
##### 12. Removal of deprecated functions
We removed the following deprecated functions:
* `getTransformForBounds` (replaced by `getViewportForBounds`)
* `getRectOfNodes` (replaced by `getNodesBounds`)
* `project` (replaced by `screenToFlowPosition`)
* `getMarkerEndId`
* `updateEdge` (replaced by `reconnectEdge`)
##### 13. Custom applyNodeChanges and applyEdgeChanges
If you wrote your own function for applying changes, you need to handle the new "replace"
event. We removed the "reset" event and added a "replace" event that replaces specific
nodes or edges.
#### New features
Now that you successfully migrated to v12, you can use all the fancy features. As
mentioned above, the biggest updates for v12 are:
##### 1. Server side rendering
You can define `width`, `height` and `handles` for the nodes. This makes it possible to
render a flow on the server and hydrate on the client:
[server side rendering guide](/learn/advanced-use/ssr-ssg-configuration).
* **Details:** In v11, `width` and `height` were set by the library as soon as the nodes
got measured. This still happens, but we are now using `measured.width` and
`measured.height` to store this information. In the previous versions there was always a
lot of confusion about `width` and `height`. It’s hard to understand, that you can’t use
it for passing an actual width or height. It’s also not obvious that those attributes
get added by the library. We think that the new implementation solves both of the
problems: `width` and `height` are optional attributes that can be used to define
dimensions and everything that is set by the library, is stored in `measured`.
##### 2. Computing flows
The new hooks [`useHandleConnections`](/api-reference/hooks/use-handle-connections) and
[`useNodesData`](/api-reference/hooks/use-nodes-data) and the new
[`updateNode`](/api-reference/hooks/use-react-flow#update-node) and
[`updateNodeData`](/api-reference/hooks/use-react-flow#update-node-data) functions (both
are part of `useReactFlow`) can be used to manage the data flow between your nodes:
[computing flows guide](/learn/advanced-use/computing-flows). We also added those helpers
for edges (`updateEdge` and `updateEdgeData`)!
* **Details:** Working with flows where one node data relies on another node is super
common. You update node A and want to react on those changes in the connected node B.
Until now everyone had to come up with a custom solution. With this version we want to
change this and give you performant helpers to handle use cases like this.
##### 3. Dark mode and CSS variables
React Flow now comes with a built-in dark mode, that can be toggled by using the new
[`colorMode`](/api-reference/react-flow#color-mode) prop (”light”, “dark” or “system”):
[dark mode example](/examples/styling/dark-mode)
* **Details:** With this version we want to make it easier to switch between dark and
light modes and give you a better starting point for dark flows. If you pass
`colorMode="dark"`, we add the class name "dark" to the wrapper and use it to adjust the
styling. To make the implementation for this new feature easier on our ends, we switched
to CSS variables for most of the styles. These variables can also be used in user land
to customize a flow.
##### 4. A better DX with TSDoc
We started to use TSDoc for a better DX. While developing your IDE will now show you the
documentation for the props and hooks. This is a big step for us to make the library more
accessible and easier to use. We will also use TSDoc in the near future to generate the
documentation.
##### More features and updates
There is more! Besides the new main features, we added some minor things that were on our
list for a long time:
* **[`useConnection` hook](/api-reference/hooks/use-connection):** With this hook you can
access the ongoing connection. For example, you can use it for colorizing handles
styling a custom connection line based on the current start / end handles.
* **Controlled `viewport`:** This is an advanced feature. Possible use cases are to
animate the viewport or round the transform for lower res screens for example. This
features brings two new props: [`viewport`](/api-reference/react-flow#viewport) and
[`onViewportChange`](/api-reference/react-flow#on-viewport-change).
* **[`ViewportPortal`](/api-reference/components/viewport-portal) component:** This makes
it possible to render elements in the viewport without the need to implement a custom
node.
* **[`onDelete`](/api-reference/react-flow#on-delete) handler**: We added a combined
handler for `onDeleteNodes` and `onDeleteEdges` to make it easier to react to deletions.
* **[`onBeforeDelete`](/api-reference/react-flow#on-before-delete) handler**: With this
handler you can prevent/ manage deletions.
* **[`isValidConnection`](/api-reference/react-flow#isvalidconnection) prop:** This makes
it possible to implement one validation function for all connections. It also gets
called for programmatically added edges.
* **[`autoPanSpeed`](/api-reference/react-flow#autoPanSpeed) prop:** For controlling the
speed while auto panning.
* **[`paneClickDistance`](/api-reference/react-flow#paneClickDistance) prop:** max
distance between mousedown/up that will trigger a click.
* **Background component**: add
[`patternClassName`](/api-reference/components/background#pattern-class-name) prop to be
able to style the background pattern by using a class name. This is useful if you want
to style the background pattern with Tailwind for example.
* **`onMove` callback** gets triggered for library-invoked viewport updates (like fitView
or zoom-in)
* **`deleteElements`** now returns deleted nodes and deleted edges
* add **`origin` attribute** for nodes
* add **`selectable` attribute** for edges
* **Node Resizer updates**: child nodes don't move when the group is resized, extent and
expand is recognized correctly
* Correct types for `BezierEdge`, `StepEdge`, `SmoothStepEdge` and `StraightEdge`
components
* New edges created by the library only have `sourceHandle` and `targetHandle` attributes
when those attributes are set. (We used to pass `sourceHandle: null` and
`targetHandle: null`)
* Edges do not mount/unmount when their z-index change
* connection line knows about the target handle position so that the path is drawn
correctly
* `nodeDragThreshold` is 1 by default instead of 0
* a better selection box usability (capture while dragging out of the flow)
* add `selectable`, `deletable`, `draggable` and `parentId` to `NodeProps`
* add a warning when styles not loaded
##### Internal changes
These changes are not really user-facing, but it could be important for folks who are
working with the internal React Flow store:
* The biggest internal change is that we created a new package **@xyflow/system with
framework agnostic helpers** that can be used by React Flow and Svelte Flow
* **XYDrag** for handling dragging node(s) and selection
* **XYPanZoom** for controlling the viewport panning and zooming
* **XYHandle** for managing new connections
* We renamed `nodeInternals` to `nodeLookup`. That map serves as a lookup, but we are not
creating a new map object on any change so it’s really only useful as a lookup.
* We removed the internal "reset" event and added a "replace" event to be able to update
specific nodes.
* We removed `connectionNodeId`, `connectionHandleId`, `connectionHandleType` from the
store and added `connection.fromHandle.nodeId`, `connection.fromHandle.id`, …
* add `data-id` to edges
* `onNodeDragStart`, `onNodeDrag` and `onNodeDragStop` also get called when user drags a
selection (in addition to `onSelectionDragStart`, `onSelectionDrag`,
`onSelectionDragStop`)
### Remove Attribution
This example demonstrates how you can remove the React Flow attribution from the renderer.
If you’re considering removing the attribution, we’d first like to mention:
**If you’re using React Flow at your organization and making money from it**, we rely on your support to keep React Flow developed and maintained under an MIT License. Before you remove the attribution, [see the ways you can support React Flow to keep it running](/pro).
**Are you using React Flow for a personal project?** Great! Go ahead and remove the attribution. You can support us by reporting any bugs you find, sending us screenshots of your projects, and starring us on [Github](https://github.com/xyflow/xyflow). If you start making money using React Flow or use it in an organization in the future, we would ask that you re-add the attribution or sign up for one of our subscriptions.
Thank you for supporting us ✌🏻
* [the xyflow team](https://xyflow.com/about)
Example: learn/remove-attribution
##### App.jsx
```jsx
import React from 'react';
import { ReactFlow, Background } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { nodes, edges } from './initialElements';
/**
* This example demonstrates how you can remove the attribution from the React Flow renderer.
* Please only hide the attribution if you are subscribed to React Flow Pro: https://reactflow.dev/pro
*/
const proOptions = { hideAttribution: true };
function RemoveAttributionExample() {
return (
);
}
export default RemoveAttributionExample;
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### initialElements.js
```js
const nodeStyle = {
color: '#0041d0',
borderColor: '#0041d0',
};
export const nodes = [
{
type: 'input',
id: '1',
data: { label: 'Thanks' },
position: { x: 100, y: 0 },
style: nodeStyle,
},
{
id: '2',
data: { label: 'for' },
position: { x: 0, y: 100 },
style: nodeStyle,
},
{
id: '3',
data: { label: 'using' },
position: { x: 200, y: 100 },
style: nodeStyle,
},
{
id: '4',
data: { label: 'React Flow Pro!' },
position: { x: 100, y: 200 },
style: nodeStyle,
},
];
export const edges = [
{
id: '1->2',
source: '1',
target: '2',
animated: true,
},
{
id: '1->3',
source: '1',
target: '3',
animated: true,
},
{
id: '2->4',
source: '2',
target: '4',
animated: true,
},
{
id: '3->4',
source: '3',
target: '4',
animated: true,
},
];
```
### Getting started with React Flow UI
***Update November 2025**: We have updated the tutorial to use the latest version of shadcn/ui, on React 19 and Tailwind 4!*
***Update July 2025**: "React Flow UI" was formerly known as "React Flow Components". We
renamed it because it now includes both components and templates. Additionally, since it's
built on shadcn/ui, the "UI" naming makes it easier for developers to recognize the
connection and understand what we offer.*
Recently, we launched an exciting new addition to our open-source roster: React Flow
UI (Previously known as React Flow Components). These are pre-built nodes, edges, and other ui elements that you can quickly
add to your React Flow applications to get up and running. The catch is these components
are built on top of [shadcn/ui](https://ui.shadcn.com) and the shadcn CLI.
We've previously written about our experience and what led us to choosing shadcn over on
the [xyflow blog](https://xyflow.com/blog/react-flow-components), but in this tutorial
we're going to focus on how to get started from scratch with shadcn, Tailwind CSS, and
React Flow Components.
**Wait, what's shadcn?**
No what, **who**! Shadcn is the author of a collection of pre-designed components known as
`shadcn/ui`. Notice how we didn't say *library* there? Shadcn takes a different approach
where components are added to your project's source code and are "owned" by you: once you
add a component you're free to modify it to suit your needs!
#### Getting started
To begin with, we will:
* Set up a new [`vite`](https://vite.dev) project.
* Set up [shadcn/ui](https://ui.shadcn.com/) along with [Tailwind CSS](https://tailwindcss.com/).
* Add and configure React Flow.
* Create our custom React Flow components using the building blocks in our [UI components registry](/ui).
##### Setting up a new vite project
```bash copy npm2yarn
npm create vite@latest
```
Vite is able to scaffold projects for many popular frameworks, but we only care about
React! Additionally, make sure to set up a **TypeScript** project. React Flow's
documentation is a mix of JavaScript and TypeScript, but for shadcn components TypeScript
is *required*!
During the interactive setup, select `React` and `TypeScript`:
```
◇ Project name:
│ my-react-flow-app
│
◇ Select a framework:
│ React
│
◇ Select a variant:
│ TypeScript
│
◇ Use rolldown-vite (Experimental)?:
│ No
│
◇ Install with pnpm and start now?
│ Yes
│
◇ Scaffolding project in /Users/alessandro/src/xyflow/wip/component-style-test-2...
│
◇ Installing dependencies with pnpm...
```
##### Setting up Tailwind CSS
All shadcn and React Flow components are styled with
[Tailwind CSS](https://tailwindcss.com/), so we'll need to install that
and a few other dependencies next.
We can follow the instructions in the [shadcn installation guide](https://ui.shadcn.com/docs/installation)
to install shadcn and Tailwind CSS inside of a freshly scaffolded vite project.
```bash copy npm2yarn
npm install tailwindcss @tailwindcss/vite
```
It is now a lot simpler to set up Tailwind CSS in a vite project, and Tailwind 4 is configured completely in CSS.
You can just replace the generated `src/index.css` file with this one line:
```css filename="src/index.css"
@import "tailwindcss";
```
##### Importing Tailwind CSS as a Vite plugin
Starting with [Tailwind CSS v4](https://tailwindcss.com/blog/tailwindcss-v4), you can use the dedicated Vite plugin `@tailwindcss/vite`
rather than the traditional PostCSS plugin. This plugin is configured in our `vite.config.ts` file, and makes
things a lot simpler, both for us developers, and for the compilers.
We simply need to import the plugin and add it to the `plugins` array in our `vite.config.ts` file.
We also need to add the `alias` property to the `resolve` object to tell Vite where to find our source files,
as shadcn components use the `@` alias to refer to the `src` directory.
```ts filename="vite.config.ts" {1-2} {8-13}
import path from "path"
import tailwindcss from "@tailwindcss/vite"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
})
```
##### Importing the Tailwind CSS file
We now need to make sure that the only CSS file in our project is the Tailwind CSS file.
In the generated `App.tsx`, you can safely remove the import of the `App.css` file, and
remove everything else that is in the scaffolded `App.tsx` file.
To verify that Tailwind CSS is working, we can add a simple `div` and `h1` elements with Tailwind classes.
The updated `App.tsx` file should look like this:
```tsx filename="src/App.tsx"
export function App() {
return (
Hello World
);
}
export default App;
```
And, the `main.tsx` file should look like this:
```tsx filename="src/main.tsx"
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
,
)
```
If you updated your `index.css` file and configured Vite to use the Tailwind CSS
plugin, you should be able to run the project and see the "Hello World" message
in your browser, in a nice, large, bold font.
The classes `w-screen` and `h-screen` are two examples of Tailwind's utility classes. If
you're used to styling React apps using a different approach, you might find this a bit
strange at first. You can think of Tailwind classes as supercharged inline styles:
they're constrained to a set design system and you have access to responsive media
queries or pseudo-classes like `hover` and `focus`.
##### Setting up shadcn/ui
Vite scaffolds some `tsconfig` files for us when generating a TypeScript project and we'll
need to make some changes to these so the shadcn components can work correctly. The shadcn
CLI is pretty clever (we'll get to that in a second) but it can't account for every
project structure so instead shadcn components that depend on one another make use of
TypeScript's import paths.
The current version of Vite splits TypeScript configuration into three files,
two of which need to be edited. Add the `baseUrl` and `paths` properties to the `compilerOptions` section of the
`tsconfig.json` and `tsconfig.app.json` files:
```json filename="tsconfig.json" {3-8}
{
// ...
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
// ...
}
```
```json filename="tsconfig.app.json" {4-7}
{
"compilerOptions": {
// ...
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
// ...
}
}
```
Nice! Now we're ready to set up the `shadcn/ui` CLI and add our first
components. Once the CLI is set up, we'll be able to add new components to our
project with a single command - even if they have dependencies or need to modify
existing files!
We can now run the following command to set up shadcn/ui in our project:
```bash copy npm2yarn
npx shadcn@latest init
```
The CLI will perform a few tasks, first it will identify your project's
framework, tailwind version, and then ask you what color you would like to use
as the base color for your project. It will then update your `index.css` file and generate a `components.json` file in the
root of your project, which will be shadcn's main configuration points.
We can take all the default options for now
```
✔ Preflight checks.
✔ Verifying framework. Found Vite.
✔ Validating Tailwind CSS config. Found v4.
✔ Validating import alias.
✔ Which color would you like to use as the base color? › Neutral
✔ Writing components.json.
✔ Checking registry.
✔ Updating CSS variables in src/index.css
✔ Installing dependencies.
✔ Created 1 file:
- src/lib/utils.ts
Success! Project initialization completed.
You may now add components.
```
#### Installing React Flow and importing its CSS.
Now we can install React Flow and import its CSS.
```bash copy npm2yarn
npm install @xyflow/react
```
And then import its CSS in our `App.tsx` file:
```tsx filename="src/App.tsx"
import '@xyflow/react/dist/style.css';
export function App() {
return (
Hello World
);
}
export default App;
```
#### Adding your first components
To demonstrate how powerful shadcn can be, let's dive right into making a new **React
Flow** app! Now everything is set up, we can add the
[` `](/ui/components/base-node) component with a single command:
```bash copy npm2yarn
npx shadcn@latest add https://ui.reactflow.dev/base-node
```
This command will generate a new file `src/components/base-node.tsx`, and install the necessary dependencies.
That ` ` component is not a React Flow node directly. Instead, as the name
implies, it's a base that many of our other nodes build upon. It also comes with
additional components that you can use to provide a header and content for your nodes.
These components are:
* ` `
* ` `
* ` `
* ` `
You can use it to have a unified style for all of your nodes as well. Let's see what it
looks like by updating our `App.tsx` file:
```tsx filename="src/App.tsx"
import '@xyflow/react/dist/style.css';
import {
BaseNode,
BaseNodeContent,
BaseNodeHeader,
BaseNodeHeaderTitle,
} from "@/components/base-node";
function App() {
return (
Base Node
This is a base node component that can be used to build other nodes.
);
}
export default App;
```
Ok, not super exciting...
The ` ` component is one of the most used components in our [UI components registry](/ui).
Some components may use it internally, to create custom nodes with a consistent style,
while some other components can be used in combination with it to create more complex nodes.
For example, let's add the ` ` component to our project, to display a tooltip when hovering over a node.
```bash copy npm2yarn
npx shadcn@latest add https://ui.reactflow.dev/node-tooltip
```
And we'll update our `App.tsx` file to render a proper flow. We'll use the same basic
setup as most of our examples so we won't break down the individual pieces here. If you're
still new to React Flow and want to learn a bit more about how to set up a basic flow from
scratch, check out our [quickstart guide](/learn).
{/\* TODO this could be linked to example app with RemoteCodeViewer editor \*/}
```tsx filename="src/App.tsx"
import { Position, ReactFlow, useNodesState, type Node } from "@xyflow/react";
import "@xyflow/react/dist/style.css";
import { BaseNode, BaseNodeContent } from "@/components/base-node";
import {
NodeTooltip,
NodeTooltipContent,
NodeTooltipTrigger,
} from "@/components/node-tooltip";
function Tooltip() {
return (
Hidden Content
Hover
);
}
const nodeTypes = {
tooltip: Tooltip,
};
const initialNodes: Node[] = [
{
id: "1",
position: { x: 0, y: 0 },
data: {},
type: "tooltip",
},
];
function Flow() {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
return (
);
}
export default function App() {
return ;
}
```
And would you look at that, the tooltip node we added automatically uses the
` ` component we customized!
Example: tutorials/components/tooltip
##### App.tsx
```tsx
import { Position, ReactFlow, useNodesState, type Node } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { BaseNode, BaseNodeContent } from './components/base-node';
import {
NodeTooltip,
NodeTooltipContent,
NodeTooltipTrigger,
} from './components/node-tooltip';
function Tooltip() {
return (
Hidden Content
Hover
);
}
const nodeTypes = {
tooltip: Tooltip,
};
const initialNodes: Node[] = [
{
id: '1',
position: { x: 0, y: 0 },
data: {},
type: 'tooltip',
},
];
function Flow() {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
return (
);
}
export default function App() {
return ;
}
```
##### index.css
```css
@import 'tailwindcss';
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
```
##### index.html
```html
React Flow Example
```
##### index.tsx
```tsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### components/base-node.tsx
```tsx
import type { ComponentProps } from 'react';
import { cn } from '../lib/utils';
export function BaseNode({ className, ...props }: ComponentProps<'div'>) {
return (
);
}
/**
* A container for a consistent header layout intended to be used inside the
* ` ` component.
*/
export function BaseNodeHeader({ className, ...props }: ComponentProps<'header'>) {
return (
` component.
className,
)}
/>
);
}
/**
* The title text for the node. To maintain a native application feel, the title
* text is not selectable.
*/
export function BaseNodeHeaderTitle({ className, ...props }: ComponentProps<'h3'>) {
return (
);
}
export function BaseNodeContent({ className, ...props }: ComponentProps<'div'>) {
return (
);
}
export function BaseNodeFooter({ className, ...props }: ComponentProps<'div'>) {
return (
);
}
```
##### components/node-tooltip.tsx
```tsx
'use client';
import React, {
createContext,
useCallback,
useContext,
useState,
type ComponentProps,
} from 'react';
import { NodeToolbar, type NodeToolbarProps } from '@xyflow/react';
import { cn } from '../lib/utils';
/* TOOLTIP CONTEXT ---------------------------------------------------------- */
type TooltipContextType = {
isVisible: boolean;
showTooltip: () => void;
hideTooltip: () => void;
};
const TooltipContext = createContext(null);
/* TOOLTIP NODE ------------------------------------------------------------- */
export function NodeTooltip({ children }: ComponentProps<'div'>) {
const [isVisible, setIsVisible] = useState(false);
const showTooltip = useCallback(() => setIsVisible(true), []);
const hideTooltip = useCallback(() => setIsVisible(false), []);
return (
{children}
);
}
/* TOOLTIP TRIGGER ---------------------------------------------------------- */
export function NodeTooltipTrigger(props: ComponentProps<'div'>) {
const tooltipContext = useContext(TooltipContext);
if (!tooltipContext) {
throw new Error('NodeTooltipTrigger must be used within NodeTooltip');
}
const { showTooltip, hideTooltip } = tooltipContext;
const onMouseEnter = useCallback(
(e: React.MouseEvent) => {
props.onMouseEnter?.(e);
showTooltip();
},
[props, showTooltip],
);
const onMouseLeave = useCallback(
(e: React.MouseEvent) => {
props.onMouseLeave?.(e);
hideTooltip();
},
[props, hideTooltip],
);
return
;
}
/* TOOLTIP CONTENT ---------------------------------------------------------- */
// /**
// * A component that displays the tooltip content based on visibility context.
// */
export function NodeTooltipContent({
children,
position,
className,
...props
}: NodeToolbarProps) {
const tooltipContext = useContext(TooltipContext);
if (!tooltipContext) {
throw new Error('NodeTooltipContent must be used within NodeTooltip');
}
const { isVisible } = tooltipContext;
return (
{children}
);
}
```
##### lib/utils.ts
```ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
```
#### Moving fast and making things
Now we've got a basic understanding of how shadcn/ui and the CLI works, we can begin to
see how easy it is to add new components and build out a flow. To see everything React
Flow Components has to offer let's build out a simple calculator flow.
First let's remove the ` ` and undo our changes to ` `. In
addition to pre-made nodes, React Flow UI also contains building blocks for creating your
own custom nodes. To see them, we'll add the `labeled-handle` component:
```bash copy npm2yarn
npx shadcn@latest add https://ui.reactflow.dev/labeled-handle
```
##### The Number Node
The first node we'll create is a simple number node with some buttons to increment and
decrement the value and a handle to connect it to other nodes. Create a folder
`src/components/nodes` and then add a new file `src/components/nodes/num-node.tsx`.
We need to install the following `shadcn/ui` components:
```bash copy npm2yarn
npx shadcn@latest add dropdown-menu button
```
Now we can start building the node. We will need to access the `updateNodeData`
function to update the node's data and the `setNodes` function to delete the
node, from the `useReactFlow` hook. The hook helps us make self-contained
components that can be used in other parts of our application, while still
giving us quick access to React Flow's state and functions.
We will need to make four callbacks, to handle the different actions that can be performed on the node.
* Reset the node's value to 0
* Delete the node
* Increment the node's value by 1
* Decrement the node's value by 1
We will also need to access the node's data to get the current value and update it.
```tsx filename="src/components/nodes/num-node.tsx"
import { type Node, type NodeProps, Position, useReactFlow } from '@xyflow/react';
import { useCallback } from 'react';
import {
BaseNode,
BaseNodeContent,
BaseNodeFooter,
BaseNodeHeader,
BaseNodeHeaderTitle,
} from '../base-node';
import { LabeledHandle } from '../labeled-handle';
import { EllipsisVertical } from 'lucide-react';
import { Button } from '../ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from '../ui/dropdown-menu';
export type NumNode = Node<{
value: number;
}>;
export function NumNode({ id, data }: NodeProps) {
const { updateNodeData, setNodes } = useReactFlow();
const handleReset = useCallback(() => {
updateNodeData(id, { value: 0 });
}, [id, updateNodeData]);
const handleDelete = useCallback(() => {
setNodes((nodes) => nodes.filter((node) => node.id !== id));
}, [id, setNodes]);
const handleIncr = useCallback(() => {
updateNodeData(id, { value: data.value + 1 });
}, [id, data.value, updateNodeData]);
const handleDecr = useCallback(() => {
updateNodeData(id, { value: data.value - 1 });
}, [id, data.value, updateNodeData]);
return (
Num
Node Actions
Reset
Delete
-
{String(data.value).padStart(3, ' ')}
+
);
}
```
##### The Sum Node
The second node we can create is a simple sum node that adds the values of the two input nodes.
Create a new file `src/components/nodes/sum-node.tsx` and paste the following into it:
Particularly, we will need to access the `getNodeConnections` function to get
the values of the two connected input nodes and the `updateNodeData` function to
update the node's data with the sum of the two input nodes inside of a
`useEffect` hook, whenever one of the values of the input nodes changes.
```tsx filename="src/components/nodes/sum-node.tsx"
import {
type Node,
type NodeProps,
Position,
useReactFlow,
useStore,
} from '@xyflow/react';
import { useCallback, useEffect } from 'react';
import {
BaseNode,
BaseNodeContent,
BaseNodeFooter,
BaseNodeHeader,
BaseNodeHeaderTitle,
} from '../base-node';
import { LabeledHandle } from '../labeled-handle';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from '../ui/dropdown-menu';
import { EllipsisVertical } from 'lucide-react';
import { Button } from '../ui/button';
export type SumNode = Node<{
value: number;
}>;
export function SumNode({ id }: NodeProps) {
const { updateNodeData, getNodeConnections, setNodes, setEdges } = useReactFlow();
const { x, y } = useStore((state) => ({
x: getHandleValue(
getNodeConnections({ nodeId: id, handleId: 'x', type: 'target' }),
state.nodeLookup,
),
y: getHandleValue(
getNodeConnections({ nodeId: id, handleId: 'y', type: 'target' }),
state.nodeLookup,
),
}));
const handleDelete = useCallback(() => {
setNodes((nodes) => nodes.filter((node) => node.id !== id));
setEdges((edges) => edges.filter((edge) => edge.source !== id));
}, [id, setNodes, setEdges]);
useEffect(() => {
updateNodeData(id, { value: x + y });
}, [x, y]);
return (
Sum
Node Actions
Delete
);
}
function getHandleValue(
connections: Array<{ source: string }>,
lookup: Map>,
) {
return connections.reduce((acc, { source }) => {
const node = lookup.get(source)!;
const value = node.data.value;
return typeof value === 'number' ? acc + value : acc;
}, 0);
}
```
##### The Data Edge
React Flow UI doesn't just provide components for building nodes. We also provide
pre-built edges and other UI elements you can drop into your flows for quick building.
To better visualize data in our calculator flow, let's pull in the `data-edge` component.
This edge renders a field from the source node's data object as a label on the edge
itself. Add the `data-edge` component to your project:
```bash copy npm2yarn
npx shadcn@latest add https://ui.reactflow.dev/data-edge
```
The ` ` component works by looking up a field from its source node's `data`
object. We've been storing the value of each node in our calculator field in a `"value"`
property so we'll update our `edgeType` object to include the new `data-edge` and we'll
update the `onConnect` handler to create a new edge of this type, making sure to set the
edge's `data` object correctly:
##### The Flow
Now we can put everything together and create our flow.
We will start by defining the custom node and edge types, and the initial nodes and edges that will be
displayed in our app.
```tsx filename="src/App.tsx"
import React, { useCallback } from 'react';
import {
ReactFlow,
type Node,
type Edge,
type OnConnect,
addEdge,
useNodesState,
useEdgesState,
} from '@xyflow/react';
import { NumNode } from './components/nodes/num-node';
import { SumNode } from './components/nodes/sum-node';
import { DataEdge } from './components/data-edge';
import '@xyflow/react/dist/style.css';
const nodeTypes = {
num: NumNode,
sum: SumNode,
};
const initialNodes: Node[] = [
{ id: 'a', type: 'num', data: { value: 0 }, position: { x: 0, y: 0 } },
{ id: 'b', type: 'num', data: { value: 0 }, position: { x: 0, y: 200 } },
{ id: 'c', type: 'sum', data: { value: 0 }, position: { x: 300, y: 100 } },
{ id: 'd', type: 'num', data: { value: 0 }, position: { x: 0, y: 400 } },
{ id: 'e', type: 'sum', data: { value: 0 }, position: { x: 600, y: 400 } },
];
const edgeTypes = {
data: DataEdge,
};
const initialEdges: Edge[] = [
{
id: 'a->c',
type: 'data',
data: { key: 'value' },
source: 'a',
target: 'c',
targetHandle: 'x',
},
{
id: 'b->c',
type: 'data',
data: { key: 'value' },
source: 'b',
target: 'c',
targetHandle: 'y',
},
{
id: 'c->e',
type: 'data',
data: { key: 'value' },
source: 'c',
target: 'e',
targetHandle: 'x',
},
{
id: 'd->e',
type: 'data',
data: { key: 'value' },
source: 'd',
target: 'e',
targetHandle: 'y',
},
];
function Flow() {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect: OnConnect = useCallback(
(params) => {
setEdges((edges) =>
addEdge({ type: 'data', data: { key: 'value' }, ...params }, edges),
);
},
[setEdges],
);
return (
);
}
export function App() {
return ;
}
```
Putting everything together we end up with quite a capable little calculator!
Example: tutorials/components/complete
##### App.tsx
```tsx
import React, { useCallback } from 'react';
import {
ReactFlow,
type Node,
type Edge,
type OnConnect,
addEdge,
useNodesState,
useEdgesState,
} from '@xyflow/react';
import { NumNode } from './components/nodes/num-node';
import { SumNode } from './components/nodes/sum-node';
import { DataEdge } from './components/data-edge';
import '@xyflow/react/dist/style.css';
const nodeTypes = {
num: NumNode,
sum: SumNode,
};
const initialNodes: Node[] = [
{ id: 'a', type: 'num', data: { value: 0 }, position: { x: 0, y: 0 } },
{ id: 'b', type: 'num', data: { value: 0 }, position: { x: 0, y: 200 } },
{ id: 'c', type: 'sum', data: { value: 0 }, position: { x: 300, y: 100 } },
{ id: 'd', type: 'num', data: { value: 0 }, position: { x: 0, y: 400 } },
{ id: 'e', type: 'sum', data: { value: 0 }, position: { x: 600, y: 400 } },
];
const edgeTypes = {
data: DataEdge,
};
const initialEdges: Edge[] = [
{
id: 'a->c',
type: 'data',
data: { key: 'value' },
source: 'a',
target: 'c',
targetHandle: 'x',
},
{
id: 'b->c',
type: 'data',
data: { key: 'value' },
source: 'b',
target: 'c',
targetHandle: 'y',
},
{
id: 'c->e',
type: 'data',
data: { key: 'value' },
source: 'c',
target: 'e',
targetHandle: 'x',
},
{
id: 'd->e',
type: 'data',
data: { key: 'value' },
source: 'd',
target: 'e',
targetHandle: 'y',
},
];
function Flow() {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect: OnConnect = useCallback(
(params) => {
setEdges((edges) =>
addEdge({ type: 'data', data: { key: 'value' }, ...params }, edges),
);
},
[setEdges],
);
return (
);
}
export function App() {
return ;
}
```
##### index.css
```css
@import 'tailwindcss';
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
```
##### index.html
```html
React Flow Example
```
##### index.tsx
```tsx
import { createRoot } from 'react-dom/client';
import { App } from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### components/base-handle.tsx
```tsx
import type { ComponentProps } from 'react';
import { Handle, type HandleProps } from '@xyflow/react';
import { cn } from '../lib/utils';
export type BaseHandleProps = HandleProps;
export function BaseHandle({
className,
children,
...props
}: ComponentProps) {
return (
{children}
);
}
```
##### components/base-node.tsx
```tsx
import type { ComponentProps } from 'react';
import { cn } from '../lib/utils';
export function BaseNode({ className, ...props }: ComponentProps<'div'>) {
return (
);
}
/**
* A container for a consistent header layout intended to be used inside the
* ` ` component.
*/
export function BaseNodeHeader({ className, ...props }: ComponentProps<'header'>) {
return (
` component.
className,
)}
/>
);
}
/**
* The title text for the node. To maintain a native application feel, the title
* text is not selectable.
*/
export function BaseNodeHeaderTitle({ className, ...props }: ComponentProps<'h3'>) {
return (
);
}
export function BaseNodeContent({ className, ...props }: ComponentProps<'div'>) {
return (
);
}
export function BaseNodeFooter({ className, ...props }: ComponentProps<'div'>) {
return (
);
}
```
##### components/data-edge.tsx
```tsx
'use client';
import {
type Edge,
type EdgeProps,
type Node,
BaseEdge,
EdgeLabelRenderer,
getBezierPath,
getSmoothStepPath,
getStraightPath,
Position,
useStore,
} from '@xyflow/react';
import React, { useMemo } from 'react';
export type DataEdge = Edge<{
/**
* The key to lookup in the source node's `data` object. For additional safety,
* you can parameterize the `DataEdge` over the type of one of your nodes to
* constrain the possible values of this key.
*
* If no key is provided this edge behaves identically to React Flow's default
* edge component.
*/
key?: keyof T['data'];
/**
* Which of React Flow's path algorithms to use. Each value corresponds to one
* of React Flow's built-in edge types.
*
* If not provided, this defaults to `"bezier"`.
*/
path?: 'bezier' | 'smoothstep' | 'step' | 'straight';
}>;
export function DataEdge({
data = { path: 'bezier' },
id,
markerEnd,
source,
sourcePosition,
sourceX,
sourceY,
style,
targetPosition,
targetX,
targetY,
}: EdgeProps) {
const nodeData = useStore((state) => state.nodeLookup.get(source)?.data);
const [edgePath, labelX, labelY] = getPath({
type: data.path ?? 'bezier',
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
const label = useMemo(() => {
if (data.key && nodeData) {
const value = nodeData[data.key];
switch (typeof value) {
case 'string':
case 'number':
return value;
case 'object':
return JSON.stringify(value);
default:
return '';
}
}
}, [data, nodeData]);
const transform = `translate(${labelX}px,${labelY}px) translate(-50%, -50%)`;
return (
<>
{data.key && (
)}
>
);
}
/**
* Chooses which of React Flow's edge path algorithms to use based on the provided
* `type`.
*/
function getPath({
type,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
}: {
type: 'bezier' | 'smoothstep' | 'step' | 'straight';
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
sourcePosition: Position;
targetPosition: Position;
}) {
switch (type) {
case 'bezier':
return getBezierPath({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
});
case 'smoothstep':
return getSmoothStepPath({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
});
case 'step':
return getSmoothStepPath({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
borderRadius: 0,
});
case 'straight':
return getStraightPath({
sourceX,
sourceY,
targetX,
targetY,
});
}
}
```
##### components/labeled-handle.tsx
```tsx
import React, { type ComponentProps } from 'react';
import { type HandleProps } from '@xyflow/react';
import { cn } from '../lib/utils';
import { BaseHandle } from './base-handle';
const flexDirections = {
top: 'flex-col',
right: 'flex-row-reverse justify-end',
bottom: 'flex-col-reverse justify-end',
left: 'flex-row',
};
export function LabeledHandle({
className,
labelClassName,
handleClassName,
title,
position,
...props
}: HandleProps &
ComponentProps<'div'> & {
title: string;
handleClassName?: string;
labelClassName?: string;
}) {
const { ref, ...handleProps } = props;
return (
{title}
);
}
```
##### lib/utils.ts
```ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
```
##### components/nodes/num-node.tsx
```tsx
import { type Node, type NodeProps, Position, useReactFlow } from '@xyflow/react';
import { useCallback } from 'react';
import {
BaseNode,
BaseNodeContent,
BaseNodeFooter,
BaseNodeHeader,
BaseNodeHeaderTitle,
} from '../base-node';
import { LabeledHandle } from '../labeled-handle';
import { EllipsisVertical } from 'lucide-react';
import { Button } from '../ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from '../ui/dropdown-menu';
export type NumNode = Node<{
value: number;
}>;
export function NumNode({ id, data }: NodeProps) {
const { updateNodeData, setNodes, setEdges } = useReactFlow();
const handleReset = useCallback(() => {
updateNodeData(id, { value: 0 });
}, [id, updateNodeData]);
const handleDelete = useCallback(() => {
setNodes((nodes) => nodes.filter((node) => node.id !== id));
setEdges((edges) => edges.filter((edge) => edge.source !== id));
}, [id, setNodes, setEdges]);
const handleIncr = useCallback(() => {
updateNodeData(id, { value: data.value + 1 });
}, [id, data.value, updateNodeData]);
const handleDecr = useCallback(() => {
updateNodeData(id, { value: data.value - 1 });
}, [id, data.value, updateNodeData]);
return (
Num
Node Actions
Reset
Delete
-
{String(data.value).padStart(3, ' ')}
+
);
}
```
##### components/nodes/sum-node.tsx
```tsx
import {
type Node,
type NodeProps,
Position,
useReactFlow,
useStore,
} from '@xyflow/react';
import { useCallback, useEffect } from 'react';
import {
BaseNode,
BaseNodeContent,
BaseNodeFooter,
BaseNodeHeader,
BaseNodeHeaderTitle,
} from '../base-node';
import { LabeledHandle } from '../labeled-handle';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from '../ui/dropdown-menu';
import { EllipsisVertical } from 'lucide-react';
import { Button } from '../ui/button';
export type SumNode = Node<{
value: number;
}>;
export function SumNode({ id }: NodeProps) {
const { updateNodeData, getNodeConnections, setNodes, setEdges } = useReactFlow();
const { x, y } = useStore((state) => ({
x: getHandleValue(
getNodeConnections({ nodeId: id, handleId: 'x', type: 'target' }),
state.nodeLookup,
),
y: getHandleValue(
getNodeConnections({ nodeId: id, handleId: 'y', type: 'target' }),
state.nodeLookup,
),
}));
const handleDelete = useCallback(() => {
setNodes((nodes) => nodes.filter((node) => node.id !== id));
setEdges((edges) => edges.filter((edge) => edge.source !== id));
}, [id, setNodes, setEdges]);
useEffect(() => {
updateNodeData(id, { value: x + y });
}, [x, y]);
return (
Sum
Node Actions
Delete
);
}
function getHandleValue(
connections: Array<{ source: string }>,
lookup: Map>,
) {
return connections.reduce((acc, { source }) => {
const node = lookup.get(source)!;
const value = node.data.value;
return typeof value === 'number' ? acc + value : acc;
}, 0);
}
```
##### components/ui/button.tsx
```tsx
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../../lib/utils';
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive:
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9',
'icon-sm': 'size-8',
'icon-lg': 'size-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<'button'> &
VariantProps & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : 'button';
return (
);
}
export { Button, buttonVariants };
```
##### components/ui/dropdown-menu.tsx
```tsx
'use client';
import * as React from 'react';
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
import { cn } from '../../lib/utils';
function DropdownMenu({
...props
}: React.ComponentProps) {
return ;
}
function DropdownMenuPortal({
...props
}: React.ComponentProps) {
return ;
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps) {
return ;
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps) {
return (
);
}
function DropdownMenuGroup({
...props
}: React.ComponentProps) {
return ;
}
function DropdownMenuItem({
className,
inset,
variant = 'default',
...props
}: React.ComponentProps & {
inset?: boolean;
variant?: 'default' | 'destructive';
}) {
return (
);
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps) {
return (
{children}
);
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps) {
return (
);
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps) {
return (
{children}
);
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps & {
inset?: boolean;
}) {
return (
);
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps) {
return (
);
}
function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) {
return (
);
}
function DropdownMenuSub({
...props
}: React.ComponentProps) {
return ;
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps & {
inset?: boolean;
}) {
return (
{children}
);
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps) {
return (
);
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
};
```
You could continue to improve this flow by adding nodes to perform other operations or to
take user input using additional components from the
[shadcn/ui registry](https://ui.shadcn.com/docs/components/slider). In fact, keep your
eyes peeled soon for a follow-up to this guide where we'll show a complete application
built using React Flow Components .
#### Wrapping up
In just a short amount of time we've managed to build out a fairly complete flow using the
components and building blocks provided by shadcn React Flow Components. We've learned:
* How to use building blocks like the [` `](/ui/components/base-node) and
[` `](/ui/components/labeled-handle) components to build our own custom
nodes without starting from scratch.
* That React Flow UI also provides custom edges like the
[` `](/ui/components/data-edge) to drop into our applications.
And thanks to the power of Tailwind, tweaking the visual style of these components is as
simple as editing the variables in your CSS file.
That's all for now! You can see all the components we currently have available
over on the [UI docs page](/ui). If you have any suggestions or requests for new
components we'd love to hear about them. Or perhaps you're already starting to
build something with shadcn and React Flow UI. Either way make sure you let us
know on our [Discord server](https://discord.com/invite/RVmnytFmGW) or on
[Twitter](https://twitter.com/xyflowdev)!
### Build a Mind Map App with React Flow
In this tutorial, you will learn to create a simple mind map tool with React Flow that can be used for brainstorming, organizing an idea, or mapping your thoughts in a visual way. To build this app, we'll be using state management, custom nodes and edges, and more.
#### It's Demo Time!
Before we get our hands dirty, I want to show you the mind-mapping tool we'll have by the end of this tutorial:
Example: tutorials/mindmap/app
##### App.tsx
```tsx
import React, { useCallback, useRef } from 'react';
import {
ReactFlow,
Controls,
Panel,
useStoreApi,
useReactFlow,
ReactFlowProvider,
ConnectionLineType,
type NodeOrigin,
type InternalNode,
type OnConnectStart,
type OnConnectEnd,
} from '@xyflow/react';
import { useShallow } from 'zustand/shallow';
import useStore, { type RFState } from './store';
import MindMapNode from './MindMapNode';
import MindMapEdge from './MindMapEdge';
// we need to import the React Flow styles to make it work
import '@xyflow/react/dist/style.css';
const selector = (state: RFState) => ({
nodes: state.nodes,
edges: state.edges,
onNodesChange: state.onNodesChange,
onEdgesChange: state.onEdgesChange,
addChildNode: state.addChildNode,
});
const nodeTypes = {
mindmap: MindMapNode,
};
const edgeTypes = {
mindmap: MindMapEdge,
};
const nodeOrigin: NodeOrigin = [0.5, 0.5];
const connectionLineStyle = { stroke: '#F6AD55', strokeWidth: 3 };
const defaultEdgeOptions = { style: connectionLineStyle, type: 'mindmap' };
function Flow() {
// whenever you use multiple values, you should use shallow for making sure that the component only re-renders when one of the values change
const { nodes, edges, onNodesChange, onEdgesChange, addChildNode } = useStore(
useShallow(selector),
);
const connectingNodeId = useRef(null);
const store = useStoreApi();
const { screenToFlowPosition } = useReactFlow();
const getChildNodePosition = (
event: MouseEvent | TouchEvent,
parentNode?: InternalNode,
) => {
const { domNode } = store.getState();
if (
!domNode ||
// we need to check if these properties exist, because when a node is not initialized yet,
// it doesn't have a positionAbsolute nor a width or height
!parentNode?.internals.positionAbsolute ||
!parentNode?.measured.width ||
!parentNode?.measured.height
) {
return;
}
const isTouchEvent = 'touches' in event;
const x = isTouchEvent ? event.touches[0].clientX : event.clientX;
const y = isTouchEvent ? event.touches[0].clientY : event.clientY;
// we need to remove the wrapper bounds, in order to get the correct mouse position
const panePosition = screenToFlowPosition({
x,
y,
});
// we are calculating with positionAbsolute here because child nodes are positioned relative to their parent
return {
x:
panePosition.x -
parentNode.internals.positionAbsolute.x +
parentNode.measured.width / 2,
y:
panePosition.y -
parentNode.internals.positionAbsolute.y +
parentNode.measured.height / 2,
};
};
const onConnectStart: OnConnectStart = useCallback((_, { nodeId }) => {
connectingNodeId.current = nodeId;
}, []);
const onConnectEnd: OnConnectEnd = useCallback(
(event) => {
const { nodeLookup } = store.getState();
const targetIsPane = (event.target as Element).classList.contains(
'react-flow__pane',
);
const node = (event.target as Element).closest('.react-flow__node');
if (node) {
node.querySelector('input')?.focus({ preventScroll: true });
} else if (targetIsPane && connectingNodeId.current) {
const parentNode = nodeLookup.get(connectingNodeId.current);
const childNodePosition = getChildNodePosition(event, parentNode);
if (parentNode && childNodePosition) {
addChildNode(parentNode, childNodePosition);
}
}
},
[getChildNodePosition],
);
return (
React Flow Mind Map
);
}
export default () => (
);
```
##### MindMapEdge.tsx
```tsx
import React from 'react';
import { BaseEdge, getStraightPath, type EdgeProps } from '@xyflow/react';
function MindMapEdge(props: EdgeProps) {
const { sourceX, sourceY, targetX, targetY } = props;
const [edgePath] = getStraightPath({
sourceX,
sourceY: sourceY + 20,
targetX,
targetY,
});
return ;
}
export default MindMapEdge;
```
##### MindMapNode.tsx
```tsx
import React, { useRef, useEffect, useLayoutEffect } from 'react';
import { Handle, Position, type Node, type NodeProps } from '@xyflow/react';
import useStore from './store';
export type NodeData = {
label: string;
};
function MindMapNode({ id, data }: NodeProps>) {
const inputRef = useRef();
const updateNodeLabel = useStore((state) => state.updateNodeLabel);
useLayoutEffect(() => {
if (inputRef.current) {
inputRef.current.style.width = `${data.label.length * 8}px`;
}
}, [data.label.length]);
useEffect(() => {
setTimeout(() => {
if (inputRef.current) {
inputRef.current.focus({ preventScroll: true });
}
}, 1);
}, []);
return (
<>
>
);
}
export default MindMapNode;
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
body {
margin: 0;
background-color: #f8f8f8;
}
html,
body,
#root {
height: 100%;
}
.header {
color: #cdcdcd;
}
.react-flow__node-mindmap {
background: #f6ad55;
border-radius: 2px;
border: 1px solid transparent;
padding: 2px 5px;
font-weight: 700;
}
.react-flow__handle.target {
top: 50%;
pointer-events: none;
opacity: 0;
}
.react-flow__handle.source {
top: 0;
left: 0;
transform: none;
background: #f6ad55;
height: 100%;
width: 100%;
border-radius: 2px;
border: none;
}
.react-flow .react-flow__connectionline {
z-index: 0;
}
.inputWrapper {
display: flex;
height: 20px;
z-index: 1;
position: relative;
pointer-events: none;
}
.dragHandle {
background: transparent;
width: 14px;
height: 100%;
margin-right: 4px;
display: flex;
align-items: center;
pointer-events: all;
}
.input {
border: none;
padding: 0 2px;
border-radius: 1px;
font-weight: 700;
background: transparent;
height: 100%;
color: #222;
pointer-events: none;
}
.input:focus {
border: none;
outline: none;
background: rgba(255, 255, 255, 0.25);
pointer-events: all;
}
```
##### index.html
```html
React Flow Example
```
##### index.tsx
```tsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### store.ts
```ts
import {
applyNodeChanges,
applyEdgeChanges,
type Edge,
type EdgeChange,
type Node,
type NodeChange,
type OnNodesChange,
type OnEdgesChange,
type XYPosition,
} from '@xyflow/react';
import { create } from 'zustand';
import { nanoid } from 'nanoid/non-secure';
export type RFState = {
nodes: Node[];
edges: Edge[];
onNodesChange: OnNodesChange;
onEdgesChange: OnEdgesChange;
addChildNode: (parentNode: Node, position: XYPosition) => void;
updateNodeLabel: (nodeId: string, label: string) => void;
};
const useStore = create((set, get) => ({
nodes: [
{
id: 'root',
type: 'mindmap',
data: { label: 'React Flow' },
position: { x: 0, y: 0 },
},
{
id: '1',
type: 'mindmap',
data: { label: 'Website' },
position: { x: -20, y: -110 },
parentId: 'root',
},
{
id: '1-1',
type: 'mindmap',
data: { label: 'Docs' },
position: { x: -40, y: -50 },
parentId: '1',
},
{
id: '1-2',
type: 'mindmap',
data: { label: 'Examples' },
position: { x: 60, y: -60 },
parentId: '1',
},
{
id: '2',
type: 'mindmap',
data: { label: 'Github' },
position: { x: -120, y: 80 },
parentId: 'root',
},
{
id: '2-1',
type: 'mindmap',
data: { label: 'Issues' },
position: { x: -70, y: 10 },
parentId: '2',
},
{
id: '2-2',
type: 'mindmap',
data: { label: 'PRs' },
position: { x: -20, y: 50 },
parentId: '2',
},
{
id: '3',
type: 'mindmap',
data: { label: 'React Flow Pro' },
position: { x: 200, y: 70 },
parentId: 'root',
},
{
id: '3-1',
type: 'mindmap',
data: { label: 'Pro Examples' },
position: { x: 80, y: 60 },
parentId: '3',
},
],
edges: [
{
id: 'r-1',
source: 'root',
target: '1',
},
{
id: '1-1',
source: '1',
target: '1-1',
},
{
id: '1-2',
source: '1',
target: '1-2',
},
{
id: 'r-2',
source: 'root',
target: '2',
},
{
id: '2-1',
source: '2',
target: '2-1',
},
{
id: '2-2',
source: '2',
target: '2-2',
},
{
id: 'r-3',
source: 'root',
target: '3',
},
{
id: '3-1',
source: '3',
target: '3-1',
},
],
onNodesChange: (changes: NodeChange[]) => {
set({
nodes: applyNodeChanges(changes, get().nodes),
});
},
onEdgesChange: (changes: EdgeChange[]) => {
set({
edges: applyEdgeChanges(changes, get().edges),
});
},
addChildNode: (parentNode: Node, position: XYPosition) => {
const newNode = {
id: nanoid(),
type: 'mindmap',
data: { label: 'New Node' },
position,
parentNode: parentNode.id,
};
const newEdge = {
id: nanoid(),
source: parentNode.id,
target: newNode.id,
};
set({
nodes: [...get().nodes, newNode],
edges: [...get().edges, newEdge],
});
},
updateNodeLabel: (nodeId: string, label: string) => {
set({
nodes: get().nodes.map((node) => {
if (node.id === nodeId) {
// Create a completely new node object to ensure React Flow detects the change
return {
...node,
data: {
...node.data,
label,
},
};
}
return node;
}),
});
},
}));
export default useStore;
```
If you'd like to live dangerously and dive right into the code, you can find the source code on [Github](https://github.com/xyflow/react-flow-mindmap-app).
#### Getting started
To do this tutorial you will need some knowledge of [React](https://reactjs.org/docs/getting-started.html) and [React Flow](/learn/concepts/terms-and-definitions) (hi, that's us! it's an open source library for building node-based UIs like workflow tools, ETL pipelines, and [more](/showcase/).)
We'll be using [Vite](https://vitejs.dev/) to develop our app, but you can also use [Create React App](https://create-react-app.dev/) or any other tool you like. To scaffold a new React app with Vite you need to do:
```bash npm2yarn
npm create vite@latest reactflow-mind-map -- --template react
```
if you would like to use Typescript:
```bash npm2yarn
npm create vite@latest reactflow-mind-map -- --template react-ts
```
After the initial setup, you need to install some packages:
```bash npm2yarn
npm install reactflow zustand classcat nanoid
```
We are using [Zustand](https://github.com/pmndrs/zustand) for managing the state of our application. It's a bit like Redux but way smaller and there's less boilerplate code to write. React Flow also uses Zustand, so the installation comes with no additional cost. (For this tutorial we are using Typescript but you can also use plain Javascript.)
To keep it simple we are putting all of our code in the `src/App` folder. For this you need to create the `src/App` folder and add an index file with the following content:
###### src/App/index.tsx
```tsx
import { ReactFlow, Controls, Panel } from '@xyflow/react';
// we have to import the React Flow styles for it to work
import '@xyflow/react/dist/style.css';
function Flow() {
return (
React Flow Mind Map
);
}
export default Flow;
```
This will be our main component for rendering the mind map. There are no nodes or edges yet, but we added the React Flow [`Controls`](/api-reference/components/controls) component and a [`Panel`](/api-reference/components/panel) to display the title of our app.
To be able to use React Flow hooks, we need to wrap the application with the [`ReactFlowProvider`](/api-reference/react-flow-provider) component in our main.tsx (entry file for vite). We are also importing the newly created `App/index.tsx` and render it inside the `ReactFlowProvider.` Your main file should look like this:
###### src/main.tsx
```tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ReactFlowProvider } from '@xyflow/react';
import App from './App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
,
);
```
The parent container of the React Flow component needs a width and a height to work properly. Our app is a fullscreen app, so we add these rules to the `index.css` file:
###### src/index.css
```css
body {
margin: 0;
}
html,
body,
#root {
height: 100%;
}
```
We are adding all styles of our app to the `index.css` file (you could also use [Tailwind](/examples/styling/tailwind)). Now you can start the development server with `npm run dev` and you should see the following:
Example: tutorials/mindmap/getting-started
##### App.tsx
```tsx
import React from 'react';
import { ReactFlow, Controls, Panel } from '@xyflow/react';
// we need to import the React Flow styles to make it work
import '@xyflow/react/dist/style.css';
function Flow() {
return (
React Flow Mind Map
);
}
export default Flow;
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
body {
margin: 0;
}
html,
body,
#root {
height: 100%;
}
```
##### index.html
```html
React Flow Example
```
##### index.tsx
```tsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
#### A store for nodes and edges
As mentioned above, we are using Zustand for state management. For this, we create a new file in our `src/App` folder called `store.ts`:
###### src/App/store.ts
```ts
import {
Edge,
EdgeChange,
Node,
NodeChange,
OnNodesChange,
OnEdgesChange,
applyNodeChanges,
applyEdgeChanges,
} from '@xyflow/react';
import { createWithEqualityFn } from 'zustand/traditional';
export type RFState = {
nodes: Node[];
edges: Edge[];
onNodesChange: OnNodesChange;
onEdgesChange: OnEdgesChange;
};
const useStore = createWithEqualityFn((set, get) => ({
nodes: [
{
id: 'root',
type: 'mindmap',
data: { label: 'React Flow Mind Map' },
position: { x: 0, y: 0 },
},
],
edges: [],
onNodesChange: (changes: NodeChange[]) => {
set({
nodes: applyNodeChanges(changes, get().nodes),
});
},
onEdgesChange: (changes: EdgeChange[]) => {
set({
edges: applyEdgeChanges(changes, get().edges),
});
},
}));
export default useStore;
```
It seems like a lot of code, but it's mostly types The store keeps track of the nodes and edges and handles the change events. When a user drags a node, React Flow fires a change event, the store then applies the changes and the updated nodes get rendered. (You can read more about this in our [state management library guide](/api-reference/hooks/use-store).)
As you can see we start with one initial node placed at `{ x: 0, y: 0 }` of type 'mindmap'. To connect the store with our app, we use the `useStore` hook:
###### src/App/index.tsx
```tsx
import { ReactFlow, Controls, Panel, NodeOrigin } from '@xyflow/react';
import { shallow } from 'zustand/shallow';
import useStore, { RFState } from './store';
// we have to import the React Flow styles for it to work
import '@xyflow/react/dist/style.css';
const selector = (state: RFState) => ({
nodes: state.nodes,
edges: state.edges,
onNodesChange: state.onNodesChange,
onEdgesChange: state.onEdgesChange,
});
// this places the node origin in the center of a node
const nodeOrigin: NodeOrigin = [0.5, 0.5];
function Flow() {
// whenever you use multiple values, you should use shallow to make sure the component only re-renders when one of the values changes
const { nodes, edges, onNodesChange, onEdgesChange } = useStore(selector, shallow);
return (
React Flow Mind Map
);
}
export default Flow;
```
We access the nodes, edges and change handlers from the store and pass them to the React Flow component. We also use the `fitView` prop to make sure that the initial node is centered in the view and set the node origin to `[0.5, 0.5]` to set the origin to the center of a node. After this, your app should look like this:
Example: tutorials/mindmap/store-nodes-edges
##### App.tsx
```tsx
import React from 'react';
import {
ReactFlow,
Controls,
Panel,
type NodeOrigin,
ConnectionLineType,
} from '@xyflow/react';
import { useShallow } from 'zustand/shallow';
import useStore, { type RFState } from './store';
// we need to import the React Flow styles to make it work
import '@xyflow/react/dist/style.css';
const selector = (state: RFState) => ({
nodes: state.nodes,
edges: state.edges,
onNodesChange: state.onNodesChange,
onEdgesChange: state.onEdgesChange,
});
// this makes the node origin to be in the center of a node
const nodeOrigin: NodeOrigin = [0.5, 0.5];
const connectionLineStyle = { stroke: '#F6AD55', strokeWidth: 3 };
const defaultEdgeOptions = { style: connectionLineStyle, type: 'mindmap' };
function Flow() {
// whenever you use multiple values, you should use shallow for making sure that the component only re-renders when one of the values change
const { nodes, edges, onNodesChange, onEdgesChange } = useStore(
useShallow(selector),
);
return (
React Flow Mind Map
);
}
export default Flow;
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
body {
margin: 0;
}
html,
body,
#root {
height: 100%;
}
```
##### index.html
```html
React Flow Example
```
##### index.tsx
```tsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### store.ts
```ts
import {
applyNodeChanges,
applyEdgeChanges,
type Edge,
type EdgeChange,
type Node,
type NodeChange,
type OnNodesChange,
type OnEdgesChange,
} from '@xyflow/react';
import { create } from 'zustand';
export type RFState = {
nodes: Node[];
edges: Edge[];
onNodesChange: OnNodesChange;
onEdgesChange: OnEdgesChange;
};
const useStore = create((set, get) => ({
nodes: [
{
id: 'root',
type: 'mindmap',
data: { label: 'React Flow Mind Map' },
position: { x: 0, y: 0 },
},
],
edges: [],
onNodesChange: (changes: NodeChange[]) => {
set({
nodes: applyNodeChanges(changes, get().nodes),
});
},
onEdgesChange: (changes: EdgeChange[]) => {
set({
edges: applyEdgeChanges(changes, get().edges),
});
},
}));
export default useStore;
```
You can move the node around and zoom in and out, we are getting somewhere Now let's add some more functionality.
#### Custom nodes and edges
We want to use a custom type called 'mindmap' for our nodes. We need to add a new component for this. Let's create a new folder called `MindMapNode` with an index file under `src/App` with the following content:
###### src/App/MindMapNode/index.tsx
```tsx
import { Handle, NodeProps, Position } from '@xyflow/react';
export type NodeData = {
label: string;
};
function MindMapNode({ id, data }: NodeProps) {
return (
<>
>
);
}
export default MindMapNode;
```
We are using an input for displaying and editing the labels of our mind map nodes, and two handles for connecting them. This is necessary for React Flow to work; the handles are used as the start and end position of the edges.
We also add some CSS to the `index.css` file to make the nodes look a bit prettier:
###### src/index.css
```css
.react-flow__node-mindmap {
background: white;
border-radius: 2px;
border: 1px solid transparent;
padding: 2px 5px;
font-weight: 700;
}
```
(For more on this, you can read the [guide to custom nodes](/learn/customization/custom-nodes) in our docs.)
Let's do the same for the custom edge. Create a new folder called `MindMapEdge` with an index file under `src/App`:
###### src/App/MindMapEdge/index.tsx
```tsx
import { BaseEdge, EdgeProps, getStraightPath } from '@xyflow/react';
function MindMapEdge(props: EdgeProps) {
const { sourceX, sourceY, targetX, targetY } = props;
const [edgePath] = getStraightPath({
sourceX,
sourceY,
targetX,
targetY,
});
return ;
}
export default MindMapEdge;
```
I will get into more detail about the custom nodes and edges in the next section. For now it's important that we can use the new types in our app, by adding the following to our `Flow` component:
```tsx
import MindMapNode from './MindMapNode';
import MindMapEdge from './MindMapEdge';
const nodeTypes = {
mindmap: MindMapNode,
};
const edgeTypes = {
mindmap: MindMapEdge,
};
```
and then pass the newly created types to the React Flow component.
Example: tutorials/mindmap/custom-nodes-edges
##### App.tsx
```tsx
import React from 'react';
import {
ReactFlow,
Controls,
Panel,
ConnectionLineType,
type NodeOrigin,
} from '@xyflow/react';
import { useShallow } from 'zustand/shallow';
import useStore, { type RFState } from './store';
import MindMapNode from './MindMapNode';
import MindMapEdge from './MindMapEdge';
import './index.css';
// we need to import the React Flow styles to make it work
import '@xyflow/react/dist/style.css';
const selector = (state: RFState) => ({
nodes: state.nodes,
edges: state.edges,
onNodesChange: state.onNodesChange,
onEdgesChange: state.onEdgesChange,
});
const nodeTypes = {
mindmap: MindMapNode,
};
const edgeTypes = {
mindmap: MindMapEdge,
};
// this makes the node origin to be in the center of a node
const nodeOrigin: NodeOrigin = [0.5, 0.5];
const connectionLineStyle = { stroke: '#F6AD55', strokeWidth: 3 };
const defaultEdgeOptions = { style: connectionLineStyle, type: 'mindmap' };
function Flow() {
// whenever you use multiple values, you should use shallow for making sure that the component only re-renders when one of the values change
const { nodes, edges, onNodesChange, onEdgesChange } = useStore(useShallow(selector));
return (
React Flow Mind Map
);
}
export default Flow;
```
##### MindMapEdge.tsx
```tsx
import React from 'react';
import { BaseEdge, getStraightPath, type EdgeProps } from '@xyflow/react';
function MindMapEdge(props: EdgeProps) {
const { sourceX, sourceY, targetX, targetY } = props;
const [edgePath] = getStraightPath({
sourceX,
sourceY,
targetX,
targetY,
});
return ;
}
export default MindMapEdge;
```
##### MindMapNode.tsx
```tsx
import React from 'react';
import { Handle, Position, type Node, type NodeProps } from '@xyflow/react';
export type NodeData = {
label: string;
};
function MindMapNode({ id, data }: NodeProps>) {
return (
<>
>
);
}
export default MindMapNode;
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
body {
margin: 0;
}
html,
body,
#root {
height: 100%;
}
.react-flow__node-mindmap {
background: #f6ad55;
border-radius: 2px;
border: 1px solid transparent;
padding: 2px 5px;
font-weight: 700;
}
```
##### index.html
```html
React Flow Example
```
##### index.tsx
```tsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### store.ts
```ts
import {
applyNodeChanges,
applyEdgeChanges,
type Edge,
type EdgeChange,
type Node,
type NodeChange,
type OnNodesChange,
type OnEdgesChange,
} from '@xyflow/react';
import { create } from 'zustand';
export type RFState = {
nodes: Node[];
edges: Edge[];
onNodesChange: OnNodesChange;
onEdgesChange: OnEdgesChange;
};
const useStore = create((set, get) => ({
nodes: [
{
id: 'root',
type: 'mindmap',
data: { label: 'React Flow Mind Map' },
position: { x: 0, y: 0 },
},
],
edges: [],
onNodesChange: (changes: NodeChange[]) => {
set({
nodes: applyNodeChanges(changes, get().nodes),
});
},
onEdgesChange: (changes: EdgeChange[]) => {
set({
edges: applyEdgeChanges(changes, get().edges),
});
},
}));
export default useStore;
```
Nice! We can already change the labels of our nodes by clicking in the input field and typing something.
#### New nodes
We want to make it super quick for a user to create a new node. The user should be able to add a new node by clicking on a node and drag to the position where a new node should be placed. This functionality is not built into React Flow, but we can implement it by using the [`onConnectStart` and `onConnectEnd`](/api-reference/react-flow#onconnectstart) handlers.
We are using the start handler to remember the node that was clicked and the end handler to create the new node:
###### Add to src/App/index.tsx
```tsx
const connectingNodeId = useRef(null);
const onConnectStart: OnConnectStart = useCallback((_, { nodeId }) => {
connectingNodeId.current = nodeId;
}, []);
const onConnectEnd: OnConnectEnd = useCallback((event) => {
// we only want to create a new node if the connection ends on the pane
const targetIsPane = (event.target as Element).classList.contains('react-flow__pane');
if (targetIsPane && connectingNodeId.current) {
console.log(`add new node with parent node ${connectingNodeId.current}`);
}
}, []);
```
Since our nodes are managed by the store, we create an action to add a new node and its edge. This is how our `addChildNode` action looks:
###### New action in src/store.ts
```ts
addChildNode: (parentNode: Node, position: XYPosition) => {
const newNode = {
id: nanoid(),
type: 'mindmap',
data: { label: 'New Node' },
position,
parentNode: parentNode.id,
};
const newEdge = {
id: nanoid(),
source: parentNode.id,
target: newNode.id,
};
set({
nodes: [...get().nodes, newNode],
edges: [...get().edges, newEdge],
});
};
```
We are using the passed node as a parent. Normally this feature is used to implement [grouping](/examples/nodes/dynamic-grouping) or [sub flows](/examples/grouping/sub-flows). Here we are using it to move all child nodes when their parent is moved. It enables us to clean up and re-order the mind map so that we don't have to move all child nodes manually. Let's use the new action in our `onConnectEnd` handler:
###### Adjustments in src/App/index.tsx
```tsx
const store = useStoreApi();
const onConnectEnd: OnConnectEnd = useCallback(
(event) => {
const { nodeLookup } = store.getState();
const targetIsPane = (event.target as Element).classList.contains('react-flow__pane');
if (targetIsPane && connectingNodeId.current) {
const parentNode = nodeLookup.get(connectingNodeId.current);
const childNodePosition = getChildNodePosition(event, parentNode);
if (parentNode && childNodePosition) {
addChildNode(parentNode, childNodePosition);
}
}
},
[getChildNodePosition],
);
```
First we are getting the `nodeLookup` from the React Flow store via `store.getState()`. `nodeLookup` is a map that contains all nodes and their current state. We need it to get the position and dimensions of the clicked node. Then we check if the target of the onConnectEnd event is the React Flow pane. If it is, we want to add a new node. For this we are using our `addChildNode` and the newly created `getChildNodePosition` helper function.
###### Helper function in src/App/index.tsx
```tsx
const getChildNodePosition = (event: MouseEvent, parentNode?: Node) => {
const { domNode } = store.getState();
if (
!domNode ||
// we need to check if these properties exist, because when a node is not initialized yet,
// it doesn't have a positionAbsolute nor a width or height
!parentNode?.computed?.positionAbsolute ||
!parentNode?.computed?.width ||
!parentNode?.computed?.height
) {
return;
}
const panePosition = screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
// we are calculating with positionAbsolute here because child nodes are positioned relative to their parent
return {
x:
panePosition.x -
parentNode.computed?.positionAbsolute.x +
parentNode.computed?.width / 2,
y:
panePosition.y -
parentNode.computed?.positionAbsolute.y +
parentNode.computed?.height / 2,
};
};
```
This function returns the position of the new node we want to add to our store. We are using the [`project` function](/api-reference/types/react-flow-instance#project) to convert screen coordinates into React Flow coordinates. As mentioned earlier, child nodes are positioned relative to their parents. That's why we need to subtract the parent position from the child node position. That was a lot to take in, let's see it in action:
Example: tutorials/mindmap/create-nodes
##### App.tsx
```tsx
import React, { useCallback, useRef } from 'react';
import {
ReactFlow,
Controls,
Panel,
useStoreApi,
useReactFlow,
ReactFlowProvider,
ConnectionLineType,
type NodeOrigin,
type InternalNode,
type OnConnectEnd,
type OnConnectStart,
} from '@xyflow/react';
import { useShallow } from 'zustand/shallow';
import useStore, { type RFState } from './store';
import MindMapNode from './MindMapNode';
import MindMapEdge from './MindMapEdge';
// we need to import the React Flow styles to make it work
import '@xyflow/react/dist/style.css';
const selector = (state: RFState) => ({
nodes: state.nodes,
edges: state.edges,
onNodesChange: state.onNodesChange,
onEdgesChange: state.onEdgesChange,
addChildNode: state.addChildNode,
});
const nodeTypes = {
mindmap: MindMapNode,
};
const edgeTypes = {
mindmap: MindMapEdge,
};
const nodeOrigin: NodeOrigin = [0.5, 0.5];
const connectionLineStyle = { stroke: '#F6AD55', strokeWidth: 3 };
const defaultEdgeOptions = { style: connectionLineStyle, type: 'mindmap' };
function Flow() {
// whenever you use multiple values, you should use shallow for making sure that the component only re-renders when one of the values change
const { nodes, edges, onNodesChange, onEdgesChange, addChildNode } = useStore(
useShallow(selector),
);
const connectingNodeId = useRef(null);
const store = useStoreApi();
const { screenToFlowPosition } = useReactFlow();
const getChildNodePosition = (
event: MouseEvent | TouchEvent,
parentNode?: InternalNode,
) => {
const { domNode } = store.getState();
if (
!domNode ||
// we need to check if these properties exist, because when a node is not initialized yet,
// it doesn't have a positionAbsolute nor a width or height
!parentNode?.internals.positionAbsolute ||
!parentNode?.measured.width ||
!parentNode?.measured.height
) {
return;
}
const isTouchEvent = 'touches' in event;
const x = isTouchEvent ? event.touches[0].clientX : event.clientX;
const y = isTouchEvent ? event.touches[0].clientY : event.clientY;
// we need to remove the wrapper bounds, in order to get the correct mouse position
const panePosition = screenToFlowPosition({
x,
y,
});
// we are calculating with positionAbsolute here because child nodes are positioned relative to their parent
return {
x:
panePosition.x -
parentNode.internals.positionAbsolute.x +
parentNode.measured.width / 2,
y:
panePosition.y -
parentNode.internals.positionAbsolute.y +
parentNode.measured.height / 2,
};
};
const onConnectStart: OnConnectStart = useCallback((_, { nodeId }) => {
connectingNodeId.current = nodeId;
}, []);
const onConnectEnd: OnConnectEnd = useCallback(
(event) => {
const { nodeLookup } = store.getState();
const targetIsPane = (event.target as Element).classList.contains(
'react-flow__pane',
);
if (targetIsPane && connectingNodeId.current) {
const parentNode = nodeLookup.get(connectingNodeId.current);
const childNodePosition = getChildNodePosition(event, parentNode);
if (parentNode && childNodePosition) {
addChildNode(parentNode, childNodePosition);
}
}
},
[getChildNodePosition],
);
return (
React Flow Mind Map
);
}
export default () => (
);
```
##### MindMapEdge.tsx
```tsx
import React from 'react';
import { BaseEdge, getStraightPath, type EdgeProps } from '@xyflow/react';
function MindMapEdge(props: EdgeProps) {
const { sourceX, sourceY, targetX, targetY } = props;
const [edgePath] = getStraightPath({
sourceX,
sourceY,
targetX,
targetY,
});
return ;
}
export default MindMapEdge;
```
##### MindMapNode.tsx
```tsx
import React from 'react';
import { Handle, Position, type NodeProps, type Node } from '@xyflow/react';
export type NodeData = {
label: string;
};
function MindMapNode({ data }: NodeProps>) {
return (
<>
>
);
}
export default MindMapNode;
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
body {
margin: 0;
}
html,
body,
#root {
height: 100%;
}
.react-flow__node-mindmap {
background: #f6ad55;
border-radius: 2px;
border: 1px solid transparent;
padding: 2px 5px;
font-weight: 700;
}
```
##### index.html
```html
React Flow Example
```
##### index.tsx
```tsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### store.ts
```ts
import {
applyNodeChanges,
applyEdgeChanges,
type Edge,
type EdgeChange,
type Node,
type NodeChange,
type OnNodesChange,
type OnEdgesChange,
type XYPosition,
} from '@xyflow/react';
import { create } from 'zustand';
import { nanoid } from 'nanoid/non-secure';
export type RFState = {
nodes: Node[];
edges: Edge[];
onNodesChange: OnNodesChange;
onEdgesChange: OnEdgesChange;
addChildNode: (parentNode: Node, position: XYPosition) => void;
};
const useStore = create((set, get) => ({
nodes: [
{
id: 'root',
type: 'mindmap',
data: { label: 'React Flow Mind Map' },
position: { x: 0, y: 0 },
},
],
edges: [],
onNodesChange: (changes: NodeChange[]) => {
set({
nodes: applyNodeChanges(changes, get().nodes),
});
},
onEdgesChange: (changes: EdgeChange[]) => {
set({
edges: applyEdgeChanges(changes, get().edges),
});
},
addChildNode: (parentNode: Node, position: XYPosition) => {
const newNode = {
id: nanoid(),
type: 'mindmap',
data: { label: 'New Node' },
position,
parentId: parentNode.id,
};
const newEdge = {
id: nanoid(),
source: parentNode.id,
target: newNode.id,
};
set({
nodes: [...get().nodes, newNode],
edges: [...get().edges, newEdge],
});
},
}));
export default useStore;
```
To test the new functionality you can start a connection from a handle and then end it on the pane. You should see a new node being added to the mind map.
#### Keep data in sync
We can already update the labels but we are not updating the nodes data object. This is important to keep our app in sync and if we want to save our nodes on the server for example. To achieve this we add a new action called `updateNodeLabel` to the store. This action takes a node id and a label. The implementation is pretty straight forward: we iterate over the existing nodes and update the matching one with the passed label:
###### src/store.ts
```ts
updateNodeLabel: (nodeId: string, label: string) => {
set({
nodes: get().nodes.map((node) => {
if (node.id === nodeId) {
// it's important to create a new object here, to inform React Flow about the changes
node.data = { ...node.data, label };
}
return node;
}),
});
},
```
Let's use the new action in our `MindmapNode` component:
###### src/App/MindmapNode/index.tsx
```tsx
import { Handle, NodeProps, Position } from '@xyflow/react';
import useStore from '../store';
export type NodeData = {
label: string;
};
function MindMapNode({ id, data }: NodeProps) {
const updateNodeLabel = useStore((state) => state.updateNodeLabel);
return (
<>
updateNodeLabel(id, evt.target.value)}
className="input"
/>
>
);
}
export default MindMapNode;
```
That was quick! The input fields of the custom nodes now display the current label of the nodes. You could take your nodes data, save it on the server and then load it again.
#### Simpler UX and nicer styling
Functionality-wise we are finished with our mind map app! We can add new nodes, update their labels and move them around. But the UX and styling could use some improvements. Let's make it easier to drag the nodes and to create new nodes!
##### 1. A node as handle
Let's use the whole node as a handle, rather than displaying the default handles. This makes it easier to create nodes, because the area where you can start a new connection gets bigger. We need to style the source handle to be the size of the node and hide the target handle visually. React Flow still needs it to connect the nodes but we don't need to display it since we are creating new nodes by dropping an edge on the pane. We use plain old CSS to hide the target handle and position it in the center of the node:
###### src/index.css
```css
.react-flow__handle.target {
top: 50%;
pointer-events: none;
opacity: 0;
}
```
In order to make the whole node a handle, we also update the style of the source:
###### src/index.css
```css
.react-flow__handle.source {
top: 0;
left: 0;
transform: none;
background: #f6ad55;
height: 100%;
width: 100%;
border-radius: 2px;
border: none;
}
```
Example: tutorials/mindmap/node-as-handle
##### App.tsx
```tsx
import React, { useCallback, useRef } from 'react';
import {
ReactFlow,
Controls,
Panel,
useStoreApi,
useReactFlow,
ReactFlowProvider,
ConnectionLineType,
type NodeOrigin,
type InternalNode,
type OnConnectEnd,
type OnConnectStart,
} from '@xyflow/react';
import { useShallow } from 'zustand/shallow';
import useStore, { type RFState } from './store';
import MindMapNode from './MindMapNode';
import MindMapEdge from './MindMapEdge';
// we need to import the React Flow styles to make it work
import '@xyflow/react/dist/style.css';
const selector = (state: RFState) => ({
nodes: state.nodes,
edges: state.edges,
onNodesChange: state.onNodesChange,
onEdgesChange: state.onEdgesChange,
addChildNode: state.addChildNode,
});
const nodeTypes = {
mindmap: MindMapNode,
};
const edgeTypes = {
mindmap: MindMapEdge,
};
const nodeOrigin: NodeOrigin = [0.5, 0.5];
const connectionLineStyle = { stroke: '#F6AD55', strokeWidth: 3 };
const defaultEdgeOptions = { style: connectionLineStyle, type: 'mindmap' };
function Flow() {
// whenever you use multiple values, you should use shallow for making sure that the component only re-renders when one of the values change
const { nodes, edges, onNodesChange, onEdgesChange, addChildNode } = useStore(
useShallow(selector),
);
const connectingNodeId = useRef(null);
const store = useStoreApi();
const { screenToFlowPosition } = useReactFlow();
const getChildNodePosition = (
event: MouseEvent | TouchEvent,
parentNode?: InternalNode,
) => {
const { domNode } = store.getState();
if (
!domNode ||
// we need to check if these properties exist, because when a node is not initialized yet,
// it doesn't have a positionAbsolute nor a width or height
!parentNode?.internals.positionAbsolute ||
!parentNode?.measured.width ||
!parentNode?.measured.height
) {
return;
}
const isTouchEvent = 'touches' in event;
const x = isTouchEvent ? event.touches[0].clientX : event.clientX;
const y = isTouchEvent ? event.touches[0].clientY : event.clientY;
// we need to remove the wrapper bounds, in order to get the correct mouse position
const panePosition = screenToFlowPosition({
x,
y,
});
// we are calculating with positionAbsolute here because child nodes are positioned relative to their parent
return {
x:
panePosition.x -
parentNode.internals.positionAbsolute.x +
parentNode.measured.width / 2,
y:
panePosition.y -
parentNode.internals.positionAbsolute.y +
parentNode.measured.height / 2,
};
};
const onConnectStart: OnConnectStart = useCallback((_, { nodeId }) => {
connectingNodeId.current = nodeId;
}, []);
const onConnectEnd: OnConnectEnd = useCallback(
(event) => {
const { nodeLookup } = store.getState();
const targetIsPane = (event.target as Element).classList.contains(
'react-flow__pane',
);
if (targetIsPane && connectingNodeId.current) {
const parentNode = nodeLookup.get(connectingNodeId.current);
const childNodePosition = getChildNodePosition(event, parentNode);
if (parentNode && childNodePosition) {
addChildNode(parentNode, childNodePosition);
}
}
},
[getChildNodePosition],
);
return (
React Flow Mind Map
);
}
export default () => (
);
```
##### MindMapEdge.tsx
```tsx
import React from 'react';
import { BaseEdge, getStraightPath, type EdgeProps } from '@xyflow/react';
function MindMapEdge(props: EdgeProps) {
const { sourceX, sourceY, targetX, targetY } = props;
const [edgePath] = getStraightPath({
sourceX,
sourceY,
targetX,
targetY,
});
return ;
}
export default MindMapEdge;
```
##### MindMapNode.tsx
```tsx
import React from 'react';
import { Handle, Position, type Node, type NodeProps } from '@xyflow/react';
export type NodeData = {
label: string;
};
import useStore from './store';
function MindMapNode({ id, data }: NodeProps>) {
const updateNodeLabel = useStore((state) => state.updateNodeLabel);
return (
<>
updateNodeLabel(id, evt.target.value)}
/>
>
);
}
export default MindMapNode;
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
body {
margin: 0;
}
html,
body,
#root {
height: 100%;
}
.react-flow__node-mindmap {
background: #f6ad55;
border-radius: 2px;
border: 1px solid transparent;
padding: 2px 5px;
font-weight: 700;
}
.react-flow__handle.target {
top: 50%;
pointer-events: none;
opacity: 0;
}
.react-flow__handle.source {
top: 0;
left: 0;
transform: none;
background: #f6ad55;
height: 100%;
width: 100%;
border-radius: 2px;
border: none;
}
```
##### index.html
```html
React Flow Example
```
##### index.tsx
```tsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### store.ts
```ts
import {
applyNodeChanges,
applyEdgeChanges,
type Edge,
type EdgeChange,
type Node,
type NodeChange,
type OnNodesChange,
type OnEdgesChange,
type XYPosition,
} from '@xyflow/react';
import { create } from 'zustand';
import { nanoid } from 'nanoid/non-secure';
export type RFState = {
nodes: Node[];
edges: Edge[];
onNodesChange: OnNodesChange;
onEdgesChange: OnEdgesChange;
addChildNode: (parentNode: Node, position: XYPosition) => void;
updateNodeLabel: (nodeId: string, label: string) => void;
};
const useStore = create((set, get) => ({
nodes: [
{
id: 'root',
type: 'mindmap',
data: { label: 'React Flow Mind Map' },
position: { x: 0, y: 0 },
},
],
edges: [],
onNodesChange: (changes: NodeChange[]) => {
set({
nodes: applyNodeChanges(changes, get().nodes),
});
},
onEdgesChange: (changes: EdgeChange[]) => {
set({
edges: applyEdgeChanges(changes, get().edges),
});
},
addChildNode: (parentNode: Node, position: XYPosition) => {
const newNode = {
id: nanoid(),
type: 'mindmap',
data: { label: 'New Node' },
position,
parentId: parentNode.id,
};
const newEdge = {
id: nanoid(),
source: parentNode.id,
target: newNode.id,
};
set({
nodes: [...get().nodes, newNode],
edges: [...get().edges, newEdge],
});
},
updateNodeLabel: (nodeId: string, label: string) => {
set({
nodes: get().nodes.map((node) => {
if (node.id === nodeId) {
// Create a completely new node object to ensure React Flow detects the change
return {
...node,
data: {
...node.data,
label,
},
};
}
return node;
}),
});
},
}));
export default useStore;
```
This works but we can't move the nodes anymore because the source handle is now the whole node and covers the input field. We fix that by using the [`dragHandle` node option](/api-reference/types/node#drag-handle). It allows us to specify a selector for a DOM element that should be used as a drag handle. For this we adjust the custom node a bit:
###### src/App/MindmapNode/index.tsx
```tsx
import { Handle, NodeProps, Position } from '@xyflow/react';
import useStore from '../store';
export type NodeData = {
label: string;
};
function MindMapNode({ id, data }: NodeProps) {
const updateNodeLabel = useStore((state) => state.updateNodeLabel);
return (
<>
>
);
}
export default MindMapNode;
```
We add a wrapper div with the class name `inputWrapper` and a div with the class name `dragHandle` that acts as the drag handle (surprise!). Now we can style the new elements:
###### src/index.css
```css
.inputWrapper {
display: flex;
height: 20px;
z-index: 1;
position: relative;
}
.dragHandle {
background: transparent;
width: 14px;
height: 100%;
margin-right: 4px;
display: flex;
align-items: center;
}
.input {
border: none;
padding: 0 2px;
border-radius: 1px;
font-weight: 700;
background: transparent;
height: 100%;
color: #222;
}
```
Example: tutorials/mindmap/node-as-handle-2
##### App.tsx
```tsx
import React, { useCallback, useRef } from 'react';
import {
ReactFlow,
Controls,
Panel,
useStoreApi,
useReactFlow,
ReactFlowProvider,
ConnectionLineType,
type NodeOrigin,
type InternalNode,
type OnConnectEnd,
type OnConnectStart,
} from '@xyflow/react';
import { useShallow } from 'zustand/shallow';
import useStore, { type RFState } from './store';
import MindMapNode from './MindMapNode';
import MindMapEdge from './MindMapEdge';
import './index.css';
// we need to import the React Flow styles to make it work
import '@xyflow/react/dist/style.css';
const selector = (state: RFState) => ({
nodes: state.nodes,
edges: state.edges,
onNodesChange: state.onNodesChange,
onEdgesChange: state.onEdgesChange,
addChildNode: state.addChildNode,
});
const nodeTypes = {
mindmap: MindMapNode,
};
const edgeTypes = {
mindmap: MindMapEdge,
};
const nodeOrigin: NodeOrigin = [0.5, 0.5];
const connectionLineStyle = { stroke: '#F6AD55', strokeWidth: 3 };
const defaultEdgeOptions = { style: connectionLineStyle, type: 'mindmap' };
function Flow() {
// whenever you use multiple values, you should use shallow for making sure that the component only re-renders when one of the values change
const { nodes, edges, onNodesChange, onEdgesChange, addChildNode } = useStore(
useShallow(selector),
);
const connectingNodeId = useRef(null);
const store = useStoreApi();
const { screenToFlowPosition } = useReactFlow();
const getChildNodePosition = (
event: MouseEvent | TouchEvent,
parentNode?: InternalNode,
) => {
const { domNode } = store.getState();
if (
!domNode ||
// we need to check if these properties exist, because when a node is not initialized yet,
// it doesn't have a positionAbsolute nor a width or height
!parentNode?.internals.positionAbsolute ||
!parentNode?.measured.width ||
!parentNode?.measured.height
) {
return;
}
const isTouchEvent = 'touches' in event;
const x = isTouchEvent ? event.touches[0].clientX : event.clientX;
const y = isTouchEvent ? event.touches[0].clientY : event.clientY;
// we need to remove the wrapper bounds, in order to get the correct mouse position
const panePosition = screenToFlowPosition({
x,
y,
});
// we are calculating with positionAbsolute here because child nodes are positioned relative to their parent
return {
x:
panePosition.x -
parentNode.internals.positionAbsolute.x +
parentNode.measured.width / 2,
y:
panePosition.y -
parentNode.internals.positionAbsolute.y +
parentNode.measured.height / 2,
};
};
const onConnectStart: OnConnectStart = useCallback((_, { nodeId }) => {
connectingNodeId.current = nodeId;
}, []);
const onConnectEnd: OnConnectEnd = useCallback(
(event) => {
const { nodeLookup } = store.getState();
const targetIsPane = (event.target as Element).classList.contains(
'react-flow__pane',
);
if (targetIsPane && connectingNodeId.current) {
const parentNode = nodeLookup.get(connectingNodeId.current);
const childNodePosition = getChildNodePosition(event, parentNode);
if (parentNode && childNodePosition) {
addChildNode(parentNode, childNodePosition);
}
}
},
[getChildNodePosition],
);
return (
React Flow Mind Map
);
}
export default () => (
);
```
##### MindMapEdge.tsx
```tsx
import React from 'react';
import { BaseEdge, getStraightPath, type EdgeProps } from '@xyflow/react';
function MindMapEdge(props: EdgeProps) {
const { sourceX, sourceY, targetX, targetY } = props;
const [edgePath] = getStraightPath({
sourceX,
sourceY,
targetX,
targetY,
});
return ;
}
export default MindMapEdge;
```
##### MindMapNode.tsx
```tsx
import React from 'react';
import { Handle, Position, type Node, type NodeProps } from '@xyflow/react';
import useStore from './store';
export type NodeData = {
label: string;
};
function MindMapNode({ id, data }: NodeProps>) {
const updateNodeLabel = useStore((state) => state.updateNodeLabel);
return (
<>
>
);
}
export default MindMapNode;
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
body {
margin: 0;
}
html,
body,
#root {
height: 100%;
}
.react-flow__node-mindmap {
background: #f6ad55;
border-radius: 2px;
border: 1px solid transparent;
padding: 2px 5px;
font-weight: 700;
}
.react-flow__handle.target {
top: 50%;
pointer-events: none;
opacity: 0;
}
.react-flow__handle.source {
top: 0;
left: 0;
transform: none;
background: #f6ad55;
height: 100%;
width: 100%;
border-radius: 2px;
border: none;
}
.inputWrapper {
display: flex;
height: 20px;
z-index: 1;
position: relative;
}
.dragHandle {
background: transparent;
width: 14px;
height: 100%;
margin-right: 4px;
display: flex;
align-items: center;
}
.input {
border: none;
padding: 0 2px;
border-radius: 1px;
font-weight: 700;
background: transparent;
height: 100%;
color: #222;
}
```
##### index.html
```html
React Flow Example
```
##### index.tsx
```tsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### store.ts
```ts
import {
applyNodeChanges,
applyEdgeChanges,
type Edge,
type EdgeChange,
type Node,
type NodeChange,
type OnNodesChange,
type OnEdgesChange,
type XYPosition,
} from '@xyflow/react';
import { create } from 'zustand';
import { nanoid } from 'nanoid/non-secure';
export type RFState = {
nodes: Node[];
edges: Edge[];
onNodesChange: OnNodesChange;
onEdgesChange: OnEdgesChange;
addChildNode: (parentNode: Node, position: XYPosition) => void;
updateNodeLabel: (nodeId: string, label: string) => void;
};
const useStore = create((set, get) => ({
nodes: [
{
id: 'root',
type: 'mindmap',
data: { label: 'React Flow Mind Map' },
position: { x: 0, y: 0 },
},
],
edges: [],
onNodesChange: (changes: NodeChange[]) => {
set({
nodes: applyNodeChanges(changes, get().nodes),
});
},
onEdgesChange: (changes: EdgeChange[]) => {
set({
edges: applyEdgeChanges(changes, get().edges),
});
},
addChildNode: (parentNode: Node, position: XYPosition) => {
const newNode = {
id: nanoid(),
type: 'mindmap',
data: { label: 'New Node' },
position,
parentId: parentNode.id,
};
const newEdge = {
id: nanoid(),
source: parentNode.id,
target: newNode.id,
};
set({
nodes: [...get().nodes, newNode],
edges: [...get().edges, newEdge],
});
},
updateNodeLabel: (nodeId: string, label: string) => {
set({
nodes: get().nodes.map((node) => {
if (node.id === nodeId) {
// Create a completely new node object to ensure React Flow detects the change
return {
...node,
data: {
...node.data,
label,
},
};
}
return node;
}),
});
},
}));
export default useStore;
```
##### 2. Activate input on focus
We are almost there but we need to adjust some more details. We want to start our new connection from the center of the node. For this we set the pointer events of the input to "none" and check if the user releases the button on top of the node. Only then we want to activate the input field. We can use our `onConnectEnd` function to achieve this:
###### src/App/index.tsx
```tsx
const onConnectEnd: OnConnectEnd = useCallback(
(event) => {
const { nodeLookup } = store.getState();
const targetIsPane = (event.target as Element).classList.contains('react-flow__pane');
const node = (event.target as Element).closest('.react-flow__node');
if (node) {
node.querySelector('input')?.focus({ preventScroll: true });
} else if (targetIsPane && connectingNodeId.current) {
const parentNode = nodeLookup.get(connectingNodeId.current);
const childNodePosition = getChildNodePosition(event, parentNode);
if (parentNode && childNodePosition) {
addChildNode(parentNode, childNodePosition);
}
}
},
[getChildNodePosition],
);
```
As you see we are focusing the input field if the user releases the mouse button on top of a node. We can now add some styling so that the input field is activated (pointerEvents: all) only when it's focused:
```css
/* we want the connection line to be below the node */
.react-flow .react-flow__connectionline {
z-index: 0;
}
/* pointer-events: none so that the click for the connection goes through */
.inputWrapper {
display: flex;
height: 20px;
position: relative;
z-index: 1;
pointer-events: none;
}
/* pointer-events: all so that we can use the drag handle (here the user cant start a new connection) */
.dragHandle {
background: transparent;
width: 14px;
height: 100%;
margin-right: 4px;
display: flex;
align-items: center;
pointer-events: all;
}
/* pointer-events: none by default */
.input {
border: none;
padding: 0 2px;
border-radius: 1px;
font-weight: 700;
background: transparent;
height: 100%;
color: #222;
pointer-events: none;
}
/* pointer-events: all when it's focused so that we can type in it */
.input:focus {
border: none;
outline: none;
background: rgba(255, 255, 255, 0.25);
pointer-events: all;
}
```
Example: tutorials/mindmap/node-as-handle-3
##### App.tsx
```tsx
import React, { useCallback, useRef } from 'react';
import {
ReactFlow,
Controls,
Panel,
useStoreApi,
useReactFlow,
ReactFlowProvider,
ConnectionLineType,
type NodeOrigin,
type InternalNode,
type OnConnectEnd,
type OnConnectStart,
} from '@xyflow/react';
import { useShallow } from 'zustand/shallow';
import useStore, { type RFState } from './store';
import MindMapNode from './MindMapNode';
import MindMapEdge from './MindMapEdge';
import './index.css';
// we need to import the React Flow styles to make it work
import '@xyflow/react/dist/style.css';
const selector = (state: RFState) => ({
nodes: state.nodes,
edges: state.edges,
onNodesChange: state.onNodesChange,
onEdgesChange: state.onEdgesChange,
addChildNode: state.addChildNode,
});
const nodeTypes = {
mindmap: MindMapNode,
};
const edgeTypes = {
mindmap: MindMapEdge,
};
const nodeOrigin: NodeOrigin = [0.5, 0.5];
const connectionLineStyle = { stroke: '#F6AD55', strokeWidth: 3 };
const defaultEdgeOptions = { style: connectionLineStyle, type: 'mindmap' };
function Flow() {
// whenever you use multiple values, you should use shallow for making sure that the component only re-renders when one of the values change
const { nodes, edges, onNodesChange, onEdgesChange, addChildNode } = useStore(
useShallow(selector),
);
const connectingNodeId = useRef(null);
const store = useStoreApi();
const { screenToFlowPosition } = useReactFlow();
const getChildNodePosition = (
event: MouseEvent | TouchEvent,
parentNode?: InternalNode,
) => {
const { domNode } = store.getState();
if (
!domNode ||
// we need to check if these properties exist, because when a node is not initialized yet,
// it doesn't have a positionAbsolute nor a width or height
!parentNode?.internals.positionAbsolute ||
!parentNode?.measured.width ||
!parentNode?.measured.height
) {
return;
}
const isTouchEvent = 'touches' in event;
const x = isTouchEvent ? event.touches[0].clientX : event.clientX;
const y = isTouchEvent ? event.touches[0].clientY : event.clientY;
// we need to remove the wrapper bounds, in order to get the correct mouse position
const panePosition = screenToFlowPosition({
x,
y,
});
// we are calculating with positionAbsolute here because child nodes are positioned relative to their parent
return {
x:
panePosition.x -
parentNode.internals.positionAbsolute.x +
parentNode.measured.width / 2,
y:
panePosition.y -
parentNode.internals.positionAbsolute.y +
parentNode.measured.height / 2,
};
};
const onConnectStart: OnConnectStart = useCallback((_, { nodeId }) => {
connectingNodeId.current = nodeId;
}, []);
const onConnectEnd: OnConnectEnd = useCallback(
(event) => {
const { nodeLookup } = store.getState();
const targetIsPane = (event.target as Element).classList.contains(
'react-flow__pane',
);
const node = (event.target as Element).closest('.react-flow__node');
if (node) {
node.querySelector('input')?.focus({ preventScroll: true });
} else if (targetIsPane && connectingNodeId.current) {
const parentNode = nodeLookup.get(connectingNodeId.current);
const childNodePosition = getChildNodePosition(event, parentNode);
if (parentNode && childNodePosition) {
addChildNode(parentNode, childNodePosition);
}
}
},
[getChildNodePosition],
);
return (
React Flow Mind Map
);
}
export default () => (
);
```
##### MindMapEdge.tsx
```tsx
import React from 'react';
import { BaseEdge, getStraightPath, type EdgeProps } from '@xyflow/react';
function MindMapEdge(props: EdgeProps) {
const { sourceX, sourceY, targetX, targetY } = props;
const [edgePath] = getStraightPath({
sourceX,
sourceY,
targetX,
targetY,
});
return ;
}
export default MindMapEdge;
```
##### MindMapNode.tsx
```tsx
import React from 'react';
import { Handle, Position, type Node, type NodeProps } from '@xyflow/react';
import useStore from './store';
export type NodeData = {
label: string;
};
function MindMapNode({ id, data }: NodeProps>) {
const updateNodeLabel = useStore((state) => state.updateNodeLabel);
return (
<>
>
);
}
export default MindMapNode;
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
body {
margin: 0;
}
html,
body,
#root {
height: 100%;
}
.react-flow__node-mindmap {
background: #f6ad55;
border-radius: 2px;
border: 1px solid transparent;
padding: 2px 5px;
font-weight: 700;
}
.react-flow__handle.target {
top: 50%;
pointer-events: none;
opacity: 0;
}
.react-flow__handle.source {
top: 0;
left: 0;
transform: none;
background: #f6ad55;
height: 100%;
width: 100%;
border-radius: 2px;
border: none;
}
.react-flow .react-flow__connectionline {
z-index: 0;
}
.inputWrapper {
display: flex;
height: 20px;
z-index: 1;
position: relative;
pointer-events: none;
}
.dragHandle {
background: transparent;
width: 14px;
height: 100%;
margin-right: 4px;
display: flex;
align-items: center;
pointer-events: all;
}
.input {
border: none;
padding: 0 2px;
border-radius: 1px;
font-weight: 700;
background: transparent;
height: 100%;
color: #222;
pointer-events: none;
}
.input:focus {
border: none;
outline: none;
background: rgba(255, 255, 255, 0.25);
pointer-events: all;
}
```
##### index.html
```html
React Flow Example
```
##### index.tsx
```tsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### store.ts
```ts
import {
applyNodeChanges,
applyEdgeChanges,
type Edge,
type EdgeChange,
type Node,
type NodeChange,
type OnNodesChange,
type OnEdgesChange,
type XYPosition,
} from '@xyflow/react';
import { create } from 'zustand';
import { nanoid } from 'nanoid/non-secure';
export type RFState = {
nodes: Node[];
edges: Edge[];
onNodesChange: OnNodesChange;
onEdgesChange: OnEdgesChange;
addChildNode: (parentNode: Node, position: XYPosition) => void;
updateNodeLabel: (nodeId: string, label: string) => void;
};
const useStore = create((set, get) => ({
nodes: [
{
id: 'root',
type: 'mindmap',
data: { label: 'React Flow Mind Map' },
position: { x: 0, y: 0 },
},
],
edges: [],
onNodesChange: (changes: NodeChange[]) => {
set({
nodes: applyNodeChanges(changes, get().nodes),
});
},
onEdgesChange: (changes: EdgeChange[]) => {
set({
edges: applyEdgeChanges(changes, get().edges),
});
},
addChildNode: (parentNode: Node, position: XYPosition) => {
const newNode = {
id: nanoid(),
type: 'mindmap',
data: { label: 'New Node' },
position,
parentId: parentNode.id,
};
const newEdge = {
id: nanoid(),
source: parentNode.id,
target: newNode.id,
};
set({
nodes: [...get().nodes, newNode],
edges: [...get().edges, newEdge],
});
},
updateNodeLabel: (nodeId: string, label: string) => {
set({
nodes: get().nodes.map((node) => {
if (node.id === nodeId) {
// Create a completely new node object to ensure React Flow detects the change
return {
...node,
data: {
...node.data,
label,
},
};
}
return node;
}),
});
},
}));
export default useStore;
```
##### 3. Dynamic width and auto focus
Almost done! We want to have a dynamic width for the nodes based on the length of the text. To keep it simple we do a calculation based on the length of text for this:
###### Added effect in src/app/MindMapNode.tsx
```jsx
useLayoutEffect(() => {
if (inputRef.current) {
inputRef.current.style.width = `${data.label.length * 8}px`;
}
}, [data.label.length]);
```
We also want to focus / activate a node right after it gets created:
###### Added effect in src/app/MindMapNode.tsx
```jsx
useEffect(() => {
setTimeout(() => {
if (inputRef.current) {
inputRef.current.focus({ preventScroll: true });
}
}, 1);
}, []);
```
Example: tutorials/mindmap/node-as-handle-4
##### App.tsx
```tsx
import React, { useCallback, useRef } from 'react';
import {
ReactFlow,
Controls,
Panel,
useStoreApi,
useReactFlow,
ReactFlowProvider,
ConnectionLineType,
type NodeOrigin,
type InternalNode,
type OnConnectEnd,
type OnConnectStart,
} from '@xyflow/react';
import { useShallow } from 'zustand/shallow';
import useStore, { type RFState } from './store';
import MindMapNode from './MindMapNode';
import MindMapEdge from './MindMapEdge';
import './index.css';
// we need to import the React Flow styles to make it work
import '@xyflow/react/dist/style.css';
const selector = (state: RFState) => ({
nodes: state.nodes,
edges: state.edges,
onNodesChange: state.onNodesChange,
onEdgesChange: state.onEdgesChange,
addChildNode: state.addChildNode,
});
const nodeTypes = {
mindmap: MindMapNode,
};
const edgeTypes = {
mindmap: MindMapEdge,
};
const nodeOrigin: NodeOrigin = [0.5, 0.5];
const connectionLineStyle = { stroke: '#F6AD55', strokeWidth: 3 };
const defaultEdgeOptions = { style: connectionLineStyle, type: 'mindmap' };
function Flow() {
// whenever you use multiple values, you should use shallow for making sure that the component only re-renders when one of the values change
const { nodes, edges, onNodesChange, onEdgesChange, addChildNode } = useStore(
useShallow(selector),
);
const connectingNodeId = useRef(null);
const store = useStoreApi();
const { screenToFlowPosition } = useReactFlow();
const getChildNodePosition = (
event: MouseEvent | TouchEvent,
parentNode?: InternalNode,
) => {
const { domNode } = store.getState();
if (
!domNode ||
// we need to check if these properties exist, because when a node is not initialized yet,
// it doesn't have a positionAbsolute nor a width or height
!parentNode?.internals.positionAbsolute ||
!parentNode?.measured.width ||
!parentNode?.measured.height
) {
return;
}
const isTouchEvent = 'touches' in event;
const x = isTouchEvent ? event.touches[0].clientX : event.clientX;
const y = isTouchEvent ? event.touches[0].clientY : event.clientY;
// we need to remove the wrapper bounds, in order to get the correct mouse position
const panePosition = screenToFlowPosition({
x,
y,
});
// we are calculating with positionAbsolute here because child nodes are positioned relative to their parent
return {
x:
panePosition.x -
parentNode.internals.positionAbsolute.x +
parentNode.measured.width / 2,
y:
panePosition.y -
parentNode.internals.positionAbsolute.y +
parentNode.measured.height / 2,
};
};
const onConnectStart: OnConnectStart = useCallback((_, { nodeId }) => {
connectingNodeId.current = nodeId;
}, []);
const onConnectEnd: OnConnectEnd = useCallback(
(event) => {
const { nodeLookup } = store.getState();
const targetIsPane = (event.target as Element).classList.contains(
'react-flow__pane',
);
const node = (event.target as Element).closest('.react-flow__node');
if (node) {
node.querySelector('input')?.focus({ preventScroll: true });
} else if (targetIsPane && connectingNodeId.current) {
const parentNode = nodeLookup.get(connectingNodeId.current);
const childNodePosition = getChildNodePosition(event, parentNode);
if (parentNode && childNodePosition) {
addChildNode(parentNode, childNodePosition);
}
}
},
[getChildNodePosition],
);
return (
React Flow Mind Map
);
}
export default () => (
);
```
##### MindMapEdge.tsx
```tsx
import React from 'react';
import { BaseEdge, getStraightPath, type EdgeProps } from '@xyflow/react';
function MindMapEdge(props: EdgeProps) {
const { sourceX, sourceY, targetX, targetY } = props;
const [edgePath] = getStraightPath({
sourceX,
sourceY: sourceY + 20,
targetX,
targetY,
});
return ;
}
export default MindMapEdge;
```
##### MindMapNode.tsx
```tsx
import React, { useRef, useEffect, useLayoutEffect } from 'react';
import { Handle, Position, type Node, type NodeProps } from '@xyflow/react';
import useStore from './store';
export type NodeData = {
label: string;
};
function MindMapNode({ id, data }: NodeProps>) {
const inputRef = useRef();
const updateNodeLabel = useStore((state) => state.updateNodeLabel);
useLayoutEffect(() => {
if (inputRef.current) {
inputRef.current.style.width = `${data.label.length * 8}px`;
}
}, [data.label.length]);
useEffect(() => {
setTimeout(() => {
if (inputRef.current) {
inputRef.current.focus({ preventScroll: true });
}
}, 1);
}, []);
return (
<>
>
);
}
export default MindMapNode;
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
body {
margin: 0;
background-color: #f8f8f8;
}
html,
body,
#root {
height: 100%;
}
.header {
color: #cdcdcd;
}
.react-flow__node-mindmap {
background: #f6ad55;
border-radius: 2px;
border: 1px solid transparent;
padding: 2px 5px;
font-weight: 700;
}
.react-flow__handle.target {
top: 50%;
pointer-events: none;
opacity: 0;
}
.react-flow__handle.source {
top: 0;
left: 0;
transform: none;
background: #f6ad55;
height: 100%;
width: 100%;
border-radius: 2px;
border: none;
}
.react-flow .react-flow__connectionline {
z-index: 0;
}
.inputWrapper {
display: flex;
height: 20px;
z-index: 1;
position: relative;
pointer-events: none;
}
.dragHandle {
background: transparent;
width: 14px;
height: 100%;
margin-right: 4px;
display: flex;
align-items: center;
pointer-events: all;
}
.input {
border: none;
padding: 0 2px;
border-radius: 1px;
font-weight: 700;
background: transparent;
height: 100%;
color: #222;
pointer-events: none;
}
.input:focus {
border: none;
outline: none;
background: rgba(255, 255, 255, 0.25);
pointer-events: all;
}
```
##### index.html
```html
React Flow Example
```
##### index.tsx
```tsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### store.ts
```ts
import {
applyNodeChanges,
applyEdgeChanges,
type Edge,
type EdgeChange,
type Node,
type NodeChange,
type OnNodesChange,
type OnEdgesChange,
type XYPosition,
} from '@xyflow/react';
import { create } from 'zustand';
import { nanoid } from 'nanoid/non-secure';
export type RFState = {
nodes: Node[];
edges: Edge[];
onNodesChange: OnNodesChange;
onEdgesChange: OnEdgesChange;
addChildNode: (parentNode: Node, position: XYPosition) => void;
updateNodeLabel: (nodeId: string, label: string) => void;
};
const useStore = create((set, get) => ({
nodes: [
{
id: 'root',
type: 'mindmap',
data: { label: 'React Flow Mind Map' },
position: { x: 0, y: 0 },
},
],
edges: [],
onNodesChange: (changes: NodeChange[]) => {
set({
nodes: applyNodeChanges(changes, get().nodes),
});
},
onEdgesChange: (changes: EdgeChange[]) => {
set({
edges: applyEdgeChanges(changes, get().edges),
});
},
addChildNode: (parentNode: Node, position: XYPosition) => {
const newNode = {
id: nanoid(),
type: 'mindmap',
data: { label: 'New Node' },
position,
parentId: parentNode.id,
};
const newEdge = {
id: nanoid(),
source: parentNode.id,
target: newNode.id,
};
set({
nodes: [...get().nodes, newNode],
edges: [...get().edges, newEdge],
});
},
updateNodeLabel: (nodeId: string, label: string) => {
set({
nodes: get().nodes.map((node) => {
if (node.id === nodeId) {
// Create a completely new node object to ensure React Flow detects the change
return {
...node,
data: {
...node.data,
label,
},
};
}
return node;
}),
});
},
}));
export default useStore;
```
Now when you adjust a node label, the width of the node will adjust accordingly. You can also create a new node and it will be focused right away.
##### 4. Centered edges and styling details
You may have noticed that the edges are not centered. We created a custom edge at the beginning for this, and now we can adjust it a bit so that the edge starts in the center of the node and not at the top of the handle (the default behavior):
###### src/App/MindMapEdge.tsx
```tsx
import { BaseEdge, EdgeProps, getStraightPath } from '@xyflow/react';
function MindMapEdge(props: EdgeProps) {
const { sourceX, sourceY, targetX, targetY } = props;
const [edgePath] = getStraightPath({
sourceX,
sourceY: sourceY + 20,
targetX,
targetY,
});
return ;
}
export default MindMapEdge;
```
We are passing all props to the [`getStraightPath`](/api-reference/utils/get-straight-path) helper function but adjust the sourceY so that it is in the center of the node.
More over we want the title to be a bit more subtle and choose a color for our background. We can do this by adjusting the color of the panel (we added the class name `"header"`) and the background color of the body element:
```css
body {
margin: 0;
background-color: #f8f8f8;
height: 100%;
}
.header {
color: #cdcdcd;
}
```
Nicely done! You can find the final code here:
Example: tutorials/mindmap/node-as-handle-4
##### App.tsx
```tsx
import React, { useCallback, useRef } from 'react';
import {
ReactFlow,
Controls,
Panel,
useStoreApi,
useReactFlow,
ReactFlowProvider,
ConnectionLineType,
type NodeOrigin,
type InternalNode,
type OnConnectEnd,
type OnConnectStart,
} from '@xyflow/react';
import { useShallow } from 'zustand/shallow';
import useStore, { type RFState } from './store';
import MindMapNode from './MindMapNode';
import MindMapEdge from './MindMapEdge';
import './index.css';
// we need to import the React Flow styles to make it work
import '@xyflow/react/dist/style.css';
const selector = (state: RFState) => ({
nodes: state.nodes,
edges: state.edges,
onNodesChange: state.onNodesChange,
onEdgesChange: state.onEdgesChange,
addChildNode: state.addChildNode,
});
const nodeTypes = {
mindmap: MindMapNode,
};
const edgeTypes = {
mindmap: MindMapEdge,
};
const nodeOrigin: NodeOrigin = [0.5, 0.5];
const connectionLineStyle = { stroke: '#F6AD55', strokeWidth: 3 };
const defaultEdgeOptions = { style: connectionLineStyle, type: 'mindmap' };
function Flow() {
// whenever you use multiple values, you should use shallow for making sure that the component only re-renders when one of the values change
const { nodes, edges, onNodesChange, onEdgesChange, addChildNode } = useStore(
useShallow(selector),
);
const connectingNodeId = useRef(null);
const store = useStoreApi();
const { screenToFlowPosition } = useReactFlow();
const getChildNodePosition = (
event: MouseEvent | TouchEvent,
parentNode?: InternalNode,
) => {
const { domNode } = store.getState();
if (
!domNode ||
// we need to check if these properties exist, because when a node is not initialized yet,
// it doesn't have a positionAbsolute nor a width or height
!parentNode?.internals.positionAbsolute ||
!parentNode?.measured.width ||
!parentNode?.measured.height
) {
return;
}
const isTouchEvent = 'touches' in event;
const x = isTouchEvent ? event.touches[0].clientX : event.clientX;
const y = isTouchEvent ? event.touches[0].clientY : event.clientY;
// we need to remove the wrapper bounds, in order to get the correct mouse position
const panePosition = screenToFlowPosition({
x,
y,
});
// we are calculating with positionAbsolute here because child nodes are positioned relative to their parent
return {
x:
panePosition.x -
parentNode.internals.positionAbsolute.x +
parentNode.measured.width / 2,
y:
panePosition.y -
parentNode.internals.positionAbsolute.y +
parentNode.measured.height / 2,
};
};
const onConnectStart: OnConnectStart = useCallback((_, { nodeId }) => {
connectingNodeId.current = nodeId;
}, []);
const onConnectEnd: OnConnectEnd = useCallback(
(event) => {
const { nodeLookup } = store.getState();
const targetIsPane = (event.target as Element).classList.contains(
'react-flow__pane',
);
const node = (event.target as Element).closest('.react-flow__node');
if (node) {
node.querySelector('input')?.focus({ preventScroll: true });
} else if (targetIsPane && connectingNodeId.current) {
const parentNode = nodeLookup.get(connectingNodeId.current);
const childNodePosition = getChildNodePosition(event, parentNode);
if (parentNode && childNodePosition) {
addChildNode(parentNode, childNodePosition);
}
}
},
[getChildNodePosition],
);
return (
React Flow Mind Map
);
}
export default () => (
);
```
##### MindMapEdge.tsx
```tsx
import React from 'react';
import { BaseEdge, getStraightPath, type EdgeProps } from '@xyflow/react';
function MindMapEdge(props: EdgeProps) {
const { sourceX, sourceY, targetX, targetY } = props;
const [edgePath] = getStraightPath({
sourceX,
sourceY: sourceY + 20,
targetX,
targetY,
});
return ;
}
export default MindMapEdge;
```
##### MindMapNode.tsx
```tsx
import React, { useRef, useEffect, useLayoutEffect } from 'react';
import { Handle, Position, type Node, type NodeProps } from '@xyflow/react';
import useStore from './store';
export type NodeData = {
label: string;
};
function MindMapNode({ id, data }: NodeProps>) {
const inputRef = useRef();
const updateNodeLabel = useStore((state) => state.updateNodeLabel);
useLayoutEffect(() => {
if (inputRef.current) {
inputRef.current.style.width = `${data.label.length * 8}px`;
}
}, [data.label.length]);
useEffect(() => {
setTimeout(() => {
if (inputRef.current) {
inputRef.current.focus({ preventScroll: true });
}
}, 1);
}, []);
return (
<>
>
);
}
export default MindMapNode;
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
body {
margin: 0;
background-color: #f8f8f8;
}
html,
body,
#root {
height: 100%;
}
.header {
color: #cdcdcd;
}
.react-flow__node-mindmap {
background: #f6ad55;
border-radius: 2px;
border: 1px solid transparent;
padding: 2px 5px;
font-weight: 700;
}
.react-flow__handle.target {
top: 50%;
pointer-events: none;
opacity: 0;
}
.react-flow__handle.source {
top: 0;
left: 0;
transform: none;
background: #f6ad55;
height: 100%;
width: 100%;
border-radius: 2px;
border: none;
}
.react-flow .react-flow__connectionline {
z-index: 0;
}
.inputWrapper {
display: flex;
height: 20px;
z-index: 1;
position: relative;
pointer-events: none;
}
.dragHandle {
background: transparent;
width: 14px;
height: 100%;
margin-right: 4px;
display: flex;
align-items: center;
pointer-events: all;
}
.input {
border: none;
padding: 0 2px;
border-radius: 1px;
font-weight: 700;
background: transparent;
height: 100%;
color: #222;
pointer-events: none;
}
.input:focus {
border: none;
outline: none;
background: rgba(255, 255, 255, 0.25);
pointer-events: all;
}
```
##### index.html
```html
React Flow Example
```
##### index.tsx
```tsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### store.ts
```ts
import {
applyNodeChanges,
applyEdgeChanges,
type Edge,
type EdgeChange,
type Node,
type NodeChange,
type OnNodesChange,
type OnEdgesChange,
type XYPosition,
} from '@xyflow/react';
import { create } from 'zustand';
import { nanoid } from 'nanoid/non-secure';
export type RFState = {
nodes: Node[];
edges: Edge[];
onNodesChange: OnNodesChange;
onEdgesChange: OnEdgesChange;
addChildNode: (parentNode: Node, position: XYPosition) => void;
updateNodeLabel: (nodeId: string, label: string) => void;
};
const useStore = create((set, get) => ({
nodes: [
{
id: 'root',
type: 'mindmap',
data: { label: 'React Flow Mind Map' },
position: { x: 0, y: 0 },
},
],
edges: [],
onNodesChange: (changes: NodeChange[]) => {
set({
nodes: applyNodeChanges(changes, get().nodes),
});
},
onEdgesChange: (changes: EdgeChange[]) => {
set({
edges: applyEdgeChanges(changes, get().edges),
});
},
addChildNode: (parentNode: Node, position: XYPosition) => {
const newNode = {
id: nanoid(),
type: 'mindmap',
data: { label: 'New Node' },
position,
parentId: parentNode.id,
};
const newEdge = {
id: nanoid(),
source: parentNode.id,
target: newNode.id,
};
set({
nodes: [...get().nodes, newNode],
edges: [...get().edges, newEdge],
});
},
updateNodeLabel: (nodeId: string, label: string) => {
set({
nodes: get().nodes.map((node) => {
if (node.id === nodeId) {
// Create a completely new node object to ensure React Flow detects the change
return {
...node,
data: {
...node.data,
label,
},
};
}
return node;
}),
});
},
}));
export default useStore;
```
#### Final thoughts
What a trip! We started with an empty pane and ended with a fully functional mind map app. If you want to move on you could work on some of the following features:
* Add new nodes by clicking on the pane
* Save and restore button to store current state to local storage
* Export and import UI
* Collaborative editing
I hope you enjoyed this tutorial and learned something new! If you have any questions or feedback, feel free to reach out to me on [Twitter](https://twitter.com/moklick) or join our [Discord server](https://discord.com/invite/RVmnytFmGW). React Flow is an independent company financed by its users. If you want to support us you can [sponsor us on Github](https://github.com/sponsors/xyflow) or [subscribe to one of our Pro plans](/pro/).
### Integrating React Flow and the Web Audio API
Today we'll be looking at how to create an interactive audio playground using React Flow
and the Web Audio API. We'll start from scratch, first learning about the Web Audio API
before looking at how to handle many common scenarios in React Flow: state management,
implementing custom nodes, and adding interactivity.
A while back I shared a project I was working on to the React Flow [discord
server](https://discord.com/invite/RVmnytFmGW). It's called
[bleep.cafe](https://bleep.cafe) and it's a little web app for learning digital synthesis
all inside the browser. A lot of folks were interested to see how something like that was
put together: most people don't even know **their browser has a whole synth engine built
in!**
This tutorial will take us step-by-step to build something similar. We may skip over some
bits here and there, but for the most part if you're new to React Flow *or* the Web Audio
API you should be able to follow along and have something working by the end.
If you're already a React Flow wizard you might want to read the first section covering
the Web Audio API and then jump to the third to see how things are tied together!
But first...
#### A demo!
This and other examples in this tutorial _make sound_. To avoid creating an avant-garde
masterpiece, remember to mute each example before moving on!
#### The Web Audio API
Before we get stuck in to React Flow and interactive node editor goodness, we need to take
a crash course on the
[Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API). Here are
the highlights you need to know:
* The Web Audio API provides a variety of different audio nodes, including sources (e.g.
[OscillatorNode](https://developer.mozilla.org/en-US/docs/Web/API/OscillatorNode),
[MediaElementAudioSourceNode](https://developer.mozilla.org/en-US/docs/Web/API/MediaElementAudioSourceNode)),
effects (e.g. [GainNode](https://developer.mozilla.org/en-US/docs/Web/API/GainNode),
[DelayNode](https://developer.mozilla.org/en-US/docs/Web/API/DelayNode),
[ConvolverNode](https://developer.mozilla.org/en-US/docs/Web/API/ConvolverNode)), and
outputs (e.g.
[AudioDestinationNode](https://developer.mozilla.org/en-US/docs/Web/API/AudioDestinationNode)).
* Audio nodes can be connected together to form a (potentially cyclic) graph. We tend to
call this the audio-processing graph, signal graph, or signal chain.
* Audio processing is handled in a separate thread by native code. This means we can keep
generating sounds even when the main UI thread is busy or blocked.
* An [AudioContext](https://developer.mozilla.org/en-US/docs/Web/API/AudioContext) acts as
the brain of an audio-processing graph. We can use it to create new audio nodes and
suspend or resume audio processing entirely.
##### Hello, sound!
Let's see some of this stuff in action and build our first Web Audio app! We won't be
doing anything too wild: we'll make a simple mouse
[theremin](http://www.thereminworld.com/Article/14232/what-s-a-theremin-). We'll use React
for these examples and everything else moving forward (we're called React Flow after all!)
and [`vite`](https://vitejs.dev) to handle bundling and hot reloading.
If you prefer another bundler like parcel or Create React App that's cool too, they all do
largely the same thing. You could also choose to use TypeScript instead of JavaScript. To
keep things simple we won't use it today, but React Flow is fully typed (and written
entirely in TypeScript) so it's a breeze to use!
```bash npm2yarn
npm create vite@latest -- --template react
```
Vite will scaffold out a simple React application for us, but can delete the assets and
jump right into `App.jsx`. Remove the demo component generated for us and start by
creating a new AudioContext and putting together the nodes we need. We want an
OscillatorNode to generate some tones and a GainNode to control the volume.
```js filename="./src/App.jsx"
// Create the brain of our audio-processing graph
const context = new AudioContext();
// Create an oscillator node to generate tones
const osc = context.createOscillator();
// Create a gain node to control the volume
const amp = context.createGain();
// Pass the oscillator's output through the gain node and to our speakers
osc.connect(amp);
amp.connect(context.destination);
// Start generating those tones!
osc.start();
```
Oscillator nodes need to be started.
Don't forget that call to `osc.start`. The oscillator won't start generating tones without
it!
For our app, we'll track the mouse's position on the screen and use that to set the pitch
of the oscillator node and the volume of the gain node.
```jsx filename="./src/App.jsx" {12-27}
import React from 'react';
const context = new AudioContext();
const osc = context.createOscillator();
const amp = context.createGain();
osc.connect(amp);
amp.connect(context.destination);
osc.start();
const updateValues = (e) => {
const freq = (e.clientX / window.innerWidth) * 1000;
const gain = e.clientY / window.innerHeight;
osc.frequency.value = freq;
amp.gain.value = gain;
};
export default function App() {
return
;
}
```
`osc.frequency.value`, `amp.gain.value`...
The Web Audio API makes a distinction between simple object properties and audio node
*parameters*. That distinction appears in the form of an `AudioParam`. You can read up on
them in the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/AudioParam) but
for now it's enough to know that you need to use `.value` to set the value of an
`AudioParam` rather than just assigning a value to the property directly.
If you try this example as it is, you'll probably find that nothing happens. An
AudioContext often starts in a suspended state in an attempt to avoid ads hijacking our
speakers. We can fix that easily by adding a click handler on the `
` to resume the
context if it's suspended.
```jsx filename="./src/App.jsx" {1-7,12}
const toggleAudio = () => {
if (context.state === 'suspended') {
context.resume();
} else {
context.suspend();
}
};
export default function App() {
return (
);
};
```
And that's everything we need to start making some sounds with the Web Audio API! Here's
what we put together, in case you weren't following along at home:
Example: tutorials/webaudio/mouse-theremin
##### App.jsx
```jsx
import React, { useCallback, useState } from 'react';
const ctx = new AudioContext();
const osc = ctx.createOscillator();
const amp = ctx.createGain();
ctx.suspend();
osc.connect(amp);
amp.connect(ctx.destination);
osc.start();
export default function App() {
const [running, isRunning] = useState(ctx.state === 'running');
const updateAudio = useCallback((e) => {
const freq = (e.clientX / window.innerWidth) * 1000;
const gain = 1 - e.clientY / window.innerHeight;
osc.frequency.value = freq;
amp.gain.value = gain;
});
const toggleDSP = useCallback(() => {
if (ctx.state === 'suspended') {
ctx.resume().then(() => isRunning(true));
} else {
ctx.suspend().then(() => isRunning(false));
}
});
return (
{running ? '🔊' : '🔇'}
);
}
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
body {
margin: 0;
background-color: #f8f8f8;
}
html,
body,
#root {
height: 100%;
}
main {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
button {
background-color: #f8f8f8;
/* border: none; */
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
font-size: 16px;
display: inline-block;
margin: 4px 2px;
cursor: pointer;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
Now let's put this knowledge to one side and take a look at how to build a React Flow
project from scratch.
Already a React Flow pro? If you're already familiar with React Flow, you can
comfortably skip over the next section and head straight on over to [making some
sounds](#do-sound-to-it). For everyone else, let's take a look at how to build a React
Flow project from scratch.
#### Scaffolding a React Flow project
Later on we'll take what we've learned about the Web Audio API, oscillators, and gain
nodes and use React Flow to interactively build audio-processing graphs. For now though,
we need to put together an empty React Flow app.
We already have a React app set up with Vite, so we'll keep using that. If you skipped
over the last section, we ran `npm create vite@latest -- --template react` to get started.
You can use whatever bundler and/or dev server you like, though. Nothing here is vite
specific.
We only need three additional dependencies for this project: `@xyflow/react` for our UI
(obviously!), `zustand` as our simple state management library (that's what we use under
the hood at React Flow) and `nanoid` as a lightweight id generator.
```bash npm2yarn
npm install @xyflow/react zustand nanoid
```
We're going to remove everything from our Web Audio crash course and start from scratch.
Start by modifying `main.jsx` to match the following:
```jsx filename="./src/main.jsx"
import App from './App';
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ReactFlowProvider } from '@xyflow/react';
// 👇 Don't forget to import the styles!
import '@xyflow/react/dist/style.css';
import './index.css';
const root = document.querySelector('#root');
ReactDOM.createRoot(root).render(
{/* React flow needs to be inside an element with a known height and width to work */}
,
);
```
There are three important things to pay attention to here:
1. You need to remember to **import the React Flow CSS styles** to make sure everything
works correctly.
2. The React Flow renderer needs to be inside an element with a known height and width, so
we've set the containing `
` to take up the entire screen.
3. To use some of the hooks React Flow provides, your components need to be inside a
` ` or inside the ` ` component itself, so we've
wrapped the entire app in the provider to be sure.
Next, hop into `App.jsx` and create an empty flow:
```jsx filename="./src/App.jsx"
import React from 'react';
import { ReactFlow, Background } from '@xyflow/react';
export default function App() {
return (
);
}
```
We'll expand and add on to this component over time. For now, we've added one of React
Flow's built-in components - [` `](/api-reference/components/background) - to
check if everything is setup correctly. Go ahead and run `npm run dev` (or whatever you
need to do to spin up a dev server if you didn't choose vite) and check out your browser.
You should see an empty flow:
Leave the dev server running. We can keep checking back on our progress as we add new bits
and bobs.
##### 1. State management with Zustand
A Zustand store will hold all the UI state for our application. In practical terms that
means it'll hold the nodes and edges of our React Flow graph, a few other pieces of state,
and a handful of *actions* to update that state.
To get a basic interactive React Flow graph going we need three actions:
1. `onNodesChange` to handle nodes being moved around or deleted.
2. `onEdgesChange` to handle *edges* being moved around or deleted.
3. `addEdge` to connect two nodes in the graph.
Go ahead and create a new file, `store.js`, and add the following:
```js filename="./src/store.js"
import { applyNodeChanges, applyEdgeChanges } from '@xyflow/react';
import { nanoid } from 'nanoid';
import { createWithEqualityFn } from 'zustand/traditional';
export const useStore = createWithEqualityFn((set, get) => ({
nodes: [],
edges: [],
onNodesChange(changes) {
set({
nodes: applyNodeChanges(changes, get().nodes),
});
},
onEdgesChange(changes) {
set({
edges: applyEdgeChanges(changes, get().edges),
});
},
addEdge(data) {
const id = nanoid(6);
const edge = { id, ...data };
set({ edges: [edge, ...get().edges] });
},
}));
```
Zustand is dead simple to use. We create a function that receives both a `set` and a `get`
function and returns an object with our initial state along with the actions we can use to
update that state. Updates happen immutably and we can use the `set` function for that.
The `get` function is how we read the current state. And... that's it for zustand.
The `changes` argument in both `onNodesChange` and `onEdgesChange` represents events like
a node or edge being moved or deleted. Fortunately, React Flow provides some
[helper](/api-reference/utils/apply-node-changes)
[functions](/api-reference/utils/apply-edge-changes) to apply those changes for us. We
just need to update the store with the new array of nodes.
`addEdge` will be called whenever two nodes get connected. The `data` argument is *almost*
a valid edge, it's just missing an id. Here we're getting nanoid to generate a 6 character
random id and then adding the edge to our graph, nothing exciting.
If we hop back over to our ` ` component we can hook React Flow up to our actions
and get something working.
```jsx filename="./src/App.jsx" {3,5,7-13,16,20-24}
import React from 'react';
import { ReactFlow, Background } from '@xyflow/react';
import { shallow } from 'zustand/shallow';
import { useStore } from './store';
const selector = (store) => ({
nodes: store.nodes,
edges: store.edges,
onNodesChange: store.onNodesChange,
onEdgesChange: store.onEdgesChange,
addEdge: store.addEdge,
});
export default function App() {
const store = useStore(selector, shallow);
return (
);
}
```
So what's this `selector` thing all about? Zustand let's us supply a selector function to
pluck out the exact bits of state we need from the store. Combined with the `shallow`
equality function, this means we typically don't have re-renders when state we don't care
about changes.
Right now, our store is small and we actually want everything from it to help render our
React Flow graph, but as we expand on it this selector will make sure we're not
re-rendering *everything* all the time.
This is everything we need to have an interactive graph: we can move nodes around, connect
them together, and remove them. To demonstrate, *temporarily* add some dummy nodes to your
store:
```js filename="./store.jsx" {2-6}
const useStore = createWithEqualityFn((set, get) => ({
nodes: [
{ id: 'a', data: { label: 'oscillator' }, position: { x: 0, y: 0 } },
{ id: 'b', data: { label: 'gain' }, position: { x: 50, y: 50 } },
{ id: 'c', data: { label: 'output' }, position: { x: -50, y: 100 } }
],
...
}));
```
Example: tutorials/webaudio/state-management
##### App.jsx
```jsx
import React from 'react';
import { ReactFlow, ReactFlowProvider, Background } from '@xyflow/react';
import { shallow } from 'zustand/shallow';
import { useStore } from './store';
import '@xyflow/react/dist/style.css';
const selector = (store) => ({
nodes: store.nodes,
edges: store.edges,
onNodesChange: store.onNodesChange,
onEdgesChange: store.onEdgesChange,
addEdge: store.addEdge,
});
export default function App() {
const store = useStore(selector, shallow);
return (
);
}
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### store.js
```js
import { applyNodeChanges, applyEdgeChanges } from '@xyflow/react';
import { nanoid } from 'nanoid';
import { createWithEqualityFn } from 'zustand/traditional';
export const useStore = createWithEqualityFn((set, get) => ({
nodes: [
{ id: 'a', data: { label: 'oscillator' }, position: { x: 0, y: 0 } },
{ id: 'b', data: { label: 'gain' }, position: { x: 50, y: 100 } },
{ id: 'c', data: { label: 'output' }, position: { x: -50, y: 200 } },
],
edges: [],
onNodesChange(changes) {
set({
nodes: applyNodeChanges(changes, get().nodes),
});
},
onEdgesChange(changes) {
set({
edges: applyEdgeChanges(changes, get().edges),
});
},
addEdge(data) {
const id = nanoid(6);
const edge = { id, ...data };
set({ edges: [edge, ...get().edges] });
},
}));
```
##### 2. Custom nodes
OK great, we have an interactive React Flow instance we can start playing with. We added
some dummy nodes but they're just the default unstyled ones right now. In this step we'll
add three custom nodes with interactive controls:
1. An oscillator node and controls for the pitch and waveform type.
2. A gain node and a control for the volume
3. An output node and a button to toggle audio processing on and off.
Let's create a new folder, `nodes/`, and create a file for each custom node we want to
create. Starting with the oscillator we need two controls and a source handle to connect
the output of the oscillator to other nodes.
```jsx filename="./src/nodes/Osc.jsx"
import React from 'react';
import { Handle } from '@xyflow/react';
import { useStore } from '../store';
export default function Osc({ id, data }) {
return (
);
};
```
"nodrag" is important.
Pay attention to the `"nodrag"` class being added to both the ` ` and ` `
elements. It's *super important* that you remember to add this class otherwise you'll find
that React Flow intercepts the mouse events and you'll be stuck dragging the node around
forever!
If we try rendering this custom node we'll find that the inputs don't do anything. That's
because the input values are fixed by `data.frequency` and `data.type` but we have no
event handlers listening to changes and no mechanism to update a node's data!
To fix the situation we need to jump back to our store and add an `updateNode` action:
```js filename="./src/store.js"
export const useStore = createWithEqualityFn((set, get) => ({
...
updateNode(id, data) {
set({
nodes: get().nodes.map(node =>
node.id === id
? { ...node, data: { ...node.data, ...data } }
: node
)
});
},
...
}));
```
This action will handle partial data updates, such that if we only want to update a node's
`frequency`, for example, we could just call `updateNode(id, { frequency: 220 }`. Now we
just need to bring the action into our ` ` component and call it whenever an input
changes.
```jsx filename="./src/nodes/Osc.jsx" {3,7-10,13,28,35}
import React from 'react';
import { Handle } from '@xyflow/react';
import { shallow } from 'zustand/shallow';
import { useStore } from '../store';
const selector = (id) => (store) => ({
setFrequency: (e) => store.updateNode(id, { frequency: +e.target.value }),
setType: (e) => store.updateNode(id, { type: e.target.value }),
});
export default function Osc({ id, data }) {
const { setFrequency, setType } = useStore(selector(id), shallow);
return (
);
}
```
Hey, that `selector` is back! Notice how this time we're using it to derive two event
handlers, `setFrequency` and `setType`, from the general `updateNode` action.
The last piece of the puzzle is to tell React Flow how to render our custom node. For that
we need to create a `nodeTypes` object: the keys should correspond to a node's `type` and
the value will be the React component to render.
```jsx filename="./src/App.jsx" {5,16-18,26}
import React from 'react';
import { ReactFlow } from '@xyflow/react';
import { shallow } from 'zustand/shallow';
import { useStore } from './store';
import Osc from './nodes/Osc';
const selector = (store) => ({
nodes: store.nodes,
edges: store.edges,
onNodesChange: store.onNodesChange,
onEdgesChange: store.onEdgesChange,
addEdge: store.addEdge,
});
const nodeTypes = {
osc: Osc,
};
export default function App() {
const store = useStore(selector, shallow);
return (
);
}
```
Avoid unnecessary renders.
It's important to define `nodeTypes` outside of the ` ` component (or use React's
[`useMemo`](https://react.dev/reference/react/useMemo)) to avoid recomputing it every
render.
If you've got the dev server running, don't panic if things haven't changed yet! None of
our temporary nodes have been given the right type yet, so React Flow just falls back to
rendering the default node. If we change one of those nodes to be an `osc` with some
initial values for `frequency` and `type` we should see our custom node being rendered.
```js title"./src/store.js"
const useStore = createWithEqualityFn((set, get) => ({
nodes: [
{ type: 'osc',
id: 'a',
data: { frequency: 220, type: 'square' },
position: { x: 0, y: 0 }
},
...
],
...
}));
```
Example: tutorials/webaudio/custom-node
##### App.jsx
```jsx
import React from 'react';
import { ReactFlow, ReactFlowProvider, Background } from '@xyflow/react';
import { shallow } from 'zustand/shallow';
import { useStore } from './store';
import Osc from './nodes/Osc';
import '@xyflow/react/dist/style.css';
const nodeTypes = {
osc: Osc,
};
const selector = (store) => ({
nodes: store.nodes,
edges: store.edges,
onNodesChange: store.onNodesChange,
onEdgesChange: store.onEdgesChange,
addEdge: store.addEdge,
});
export default function App() {
const store = useStore(selector, shallow);
return (
);
}
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### store.js
```js
import { applyNodeChanges, applyEdgeChanges } from '@xyflow/react';
import { nanoid } from 'nanoid';
import { createWithEqualityFn } from 'zustand/traditional';
export const useStore = createWithEqualityFn((set, get) => ({
nodes: [
{
id: 'a',
type: 'osc',
data: { frequency: 220, type: 'square' },
position: { x: 0, y: 0 },
},
],
edges: [],
onNodesChange(changes) {
set({
nodes: applyNodeChanges(changes, get().nodes),
});
},
updateNode(id, data) {
set({
nodes: get().nodes.map((node) =>
node.id === id
? { ...node, data: Object.assign(node.data, data) }
: node,
),
});
},
onEdgesChange(changes) {
set({
edges: applyEdgeChanges(changes, get().edges),
});
},
addEdge(data) {
const id = nanoid(6);
const edge = { id, ...data };
set({ edges: [edge, ...get().edges] });
},
}));
```
##### nodes/Osc.jsx
```jsx
import React from 'react';
import { Handle } from '@xyflow/react';
import { shallow } from 'zustand/shallow';
import { tw } from 'twind';
import { useStore } from '../store';
const selector = (id) => (store) => ({
setFrequency: (e) => store.updateNode(id, { frequency: +e.target.value }),
setType: (e) => store.updateNode(id, { type: e.target.value }),
});
export default function Osc({ id, data }) {
const { setFrequency, setType } = useStore(selector(id), shallow);
return (
Osc
Frequency
{data.frequency} Hz
Waveform
sine
triangle
sawtooth
square
);
}
```
Stuck on styling?
If you're just implementing the code from this post as you go along, you'll see that your
custom node doesn't look like the one in the preview above. To keep things easy to digest,
we've left out styling in the code snippets.
To learn how to style your custom nodes, check out our docs on
[theming](/learn/customization/theming) or our example using
[Tailwind](/examples/styling/tailwind).
Implementing a gain node is pretty much the same process, so we'll leave that one to you.
Instead, we'll turn our attention to the output node. This node will have no parameters
control, but we do want to toggle signal processing on and off. That's a bit difficult
right now when we haven't implemented any audio code yet, so in the meantime we'll add
just a flag to our store and an action to toggle it.
```js filename="./src/store.js"
const useStore = createWithEqualityFn((set, get) => ({
...
isRunning: false,
toggleAudio() {
set({ isRunning: !get().isRunning });
},
...
}));
```
The custom node itself is then pretty simple:
```jsx filename="./src/nodes/Out.jsx"
import React from 'react';
import { Handle } from '@xyflow/react';
import { shallow } from 'zustand/shallow';
import { useStore } from '../store';
const selector = (store) => ({
isRunning: store.isRunning,
toggleAudio: store.toggleAudio,
});
export default function Out({ id, data }) {
const { isRunning, toggleAudio } = useStore(selector, shallow);
return (
Output Node
{isRunning ? (
🔇
) : (
🔈
)}
);
}
```
Things are starting to shape up quite nicely!
Example: tutorials/webaudio/custom-nodes
##### App.jsx
```jsx
import React from 'react';
import { ReactFlow, ReactFlowProvider, Background } from '@xyflow/react';
import { shallow } from 'zustand/shallow';
import { useStore } from './store';
import Osc from './nodes/Osc';
import Amp from './nodes/Amp';
import Out from './nodes/Out';
import '@xyflow/react/dist/style.css';
const nodeTypes = {
osc: Osc,
amp: Amp,
out: Out,
};
const selector = (store) => ({
nodes: store.nodes,
edges: store.edges,
onNodesChange: store.onNodesChange,
onEdgesChange: store.onEdgesChange,
addEdge: store.addEdge,
});
export default function App() {
const store = useStore(selector, shallow);
return (
);
}
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### store.js
```js
import { applyNodeChanges, applyEdgeChanges } from '@xyflow/react';
import { nanoid } from 'nanoid';
import { createWithEqualityFn } from 'zustand/traditional';
export const useStore = createWithEqualityFn((set, get) => ({
nodes: [
{
id: 'a',
type: 'osc',
data: { frequency: 220, type: 'square' },
position: { x: 0, y: 0 },
},
{
id: 'b',
type: 'amp',
data: { gain: 0.5 },
position: { x: -100, y: 250 },
},
{ id: 'c', type: 'out', position: { x: 100, y: 500 } },
],
edges: [],
isRunning: false,
toggleAudio() {
set({ isRunning: !get().isRunning });
},
onNodesChange(changes) {
set({
nodes: applyNodeChanges(changes, get().nodes),
});
},
updateNode(id, data) {
set({
nodes: get().nodes.map((node) =>
node.id === id
? { ...node, data: Object.assign(node.data, data) }
: node,
),
});
},
onEdgesChange(changes) {
set({
edges: applyEdgeChanges(changes, get().edges),
});
},
addEdge(data) {
const id = nanoid(6);
const edge = { id, ...data };
set({ edges: [edge, ...get().edges] });
},
}));
```
##### nodes/Amp.jsx
```jsx
import React from 'react';
import { Handle } from '@xyflow/react';
import { shallow } from 'zustand/shallow';
import { tw } from 'twind';
import { useStore } from '../store';
const selector = (id) => (store) => ({
setGain: (e) => store.updateNode(id, { gain: +e.target.value }),
});
export default function Osc({ id, data }) {
const { setGain } = useStore(selector(id), shallow);
return (
Amp
Gain
{data.gain.toFixed(2)}
);
}
```
##### nodes/Osc.jsx
```jsx
import React from 'react';
import { Handle } from '@xyflow/react';
import { shallow } from 'zustand/shallow';
import { tw } from 'twind';
import { useStore } from '../store';
const selector = (id) => (store) => ({
setFrequency: (e) => store.updateNode(id, { frequency: +e.target.value }),
setType: (e) => store.updateNode(id, { type: e.target.value }),
});
export default function Osc({ id, data }) {
const { setFrequency, setType } = useStore(selector(id), shallow);
return (
Osc
Frequency
{data.frequency} Hz
Waveform
sine
triangle
sawtooth
square
);
}
```
##### nodes/Out.jsx
```jsx
import React from 'react';
import { Handle } from '@xyflow/react';
import { shallow } from 'zustand/shallow';
import { tw } from 'twind';
import { useStore } from '../store';
const selector = (store) => ({
isRunning: store.isRunning,
toggleAudio: store.toggleAudio,
});
export default function Out({ id, data }) {
const { isRunning, toggleAudio } = useStore(selector, shallow);
return (
{isRunning ? (
🔈
) : (
🔇
)}
);
}
```
The next step, then, is to…
#### Do sound to it
We have an interactive graph and we're able to update node data, now let's add in what we
know about the Web Audio API. Start by creating a new file, `audio.js`, and create a new
audio context and an empty `Map`.
```js filename="./src/audio.js"
const context = new AudioContext();
const nodes = new Map();
```
The way we'll manage our audio graph is by hooking into the different actions in our
store. So we might connect two audio nodes when the `addEdge` action is called, or update
an audio node's properties when `updateNode` is called, and so on.
Hardcoded nodes
We hardcoded a couple of nodes in our store earlier on in this post but our audio graph
doesn't know anything about them! For the finished project we can do away with all these
hardcoded bits, but for now it's **really important** that we also hardcode some audio
nodes.
Here's how we did it:
```js filename="./src/audio.js" {4-7,9-10,12,14-16}
const context = new AudioContext();
const nodes = new Map();
const osc = context.createOscillator();
osc.frequency.value = 220;
osc.type = 'square';
osc.start();
const amp = context.createGain();
amp.gain.value = 0.5;
const out = context.destination;
nodes.set('a', osc);
nodes.set('b', amp);
nodes.set('c', out);
```
##### 1. Node changes
Right now, there are two types of node changes that can happen in our graph and that we
need to respond to: updating a node's `data`, and removing a node from the graph. We
already have an action for the former, so let's handle that first.
In `audio.js` we'll define a function, `updateAudioNode`, that we'll call with a node's id
and a partial `data` object and use it to update an existing node in the `Map`:
```js filename="./src/audio.js"
export function updateAudioNode(id, data) {
const node = nodes.get(id);
for (const [key, val] of Object.entries(data)) {
if (node[key] instanceof AudioParam) {
node[key].value = val;
} else {
node[key] = val;
}
}
}
```
Remember that properties on an audio node may be special `AudioParams` that must be
updated differently to regular object properties.
Now we'll want to update our `updateNode` action in the store to call this function as
part of the update:
```js filename="./src/store.js"
import { updateAudioNode } from './audio';
export const useStore = createWithEqualityFn((set, get) => ({
...
updateNode(id, data) {
updateAudioNode(id, data);
set({ nodes: ... });
},
...
}));
```
The next change we need to handle is removing a node from the graph. If you select a node
in the graph and hit backspace, React Flow will remove it. This is implicitly handled for
us by the `onNodesChange` action we hooked up, but now we want some additional handling
we'll need to wire up a new action to React Flow's `onNodesDelete` event.
This is actually pretty simple, so I'll save you some reading and present the next three
snippets of code without comment.
\
\
```js
export function removeAudioNode(id) {
const node = nodes.get(id);
node.disconnect();
node.stop?.();
nodes.delete(id);
}
```
\
\
```js
import { ..., removeAudioNode } from './audio';
export const useStore = createWithEqualityFn((set, get) => ({
...
removeNodes(nodes) {
for (const { id } of nodes) {
removeAudioNode(id)
}
},
...
}));
```
\
\
```jsx
const selector = store => ({
...,
onNodesDelete: store.removeNodes
});
export default function App() {
const store = useStore(selector, shallow);
return (
)
};
```
\
The only thing to note is that `onNodesDelete` calls the provided callback with an *array*
of deleted nodes, because it is possible to delete more than one node at once!
##### 2. Edge changes
We're getting super close to actually making some sounds! All that's left is to handle
changes to our graph's edges. Like with node changes, we already have an action to handle
creating new edges and we're also implicitly handling removed edges in `onEdgesChange`.
To handle new connections, we just need the `source` and `target` ids from the edge
created in our `addEdge` action. Then we can just look up the two nodes in our `Map` and
connect them up.
\
\
```js
export function connect(sourceId, targetId) {
const source = nodes.get(sourceId);
const target = nodes.get(targetId);
source.connect(target);
}
```
\
\
```js
import { ..., connect } from './audio';
export const useStore = createWithEqualityFn((set, get) => ({
...
addEdge(data) {
...
connect(data.source, data.target);
},
...
}));
```
\
We saw React Flow accepted an `onNodesDelete` handler and wouldn't you know it, there's an
`onEdgesDelete` handler too! The approach we'd take to implement `disconnect` and hook it
up to our store and React Flow instance is pretty much the same as before, so we'll leave
that one down to you as well!
##### 3. Switching the speakers on
You'll remember that our `AudioContext` probably begins in a suspended state to prevent
potentially annoying autoplay issues. We already faked the data and actions we need for
our ` ` component in the store, now we just need to replace them with the real
context's state and resume/suspend methods.
```js filename="./src/audio.js"
export function isRunning() {
return context.state === 'running';
}
export function toggleAudio() {
return isRunning() ? context.suspend() : context.resume();
}
```
Although we haven't been returning anything from our audio functions up until now, we need
to return from `toggleAudio` because those methods are asynchronous and we don't want to
update the store prematurely!
```js filename="./src/store.js"
import { ..., isRunning, toggleAudio } from './audio'
export const useStore = createWithEqualityFn((set, get) => ({
...
isRunning: isRunning(),
toggleAudio() {
toggleAudio().then(() => {
set({ isRunning: isRunning() });
});
}
}));
```
Et voilà, we did it! We've now put enough together to actually *make sounds*! Let's see
what we have in action.
Example: tutorials/webaudio/hardcoded-audio
##### App.jsx
```jsx
import React from 'react';
import { ReactFlow, ReactFlowProvider, Background } from '@xyflow/react';
import { shallow } from 'zustand/shallow';
import { useStore } from './store';
import Osc from './nodes/Osc';
import Amp from './nodes/Amp';
import Out from './nodes/Out';
import '@xyflow/react/dist/style.css';
const nodeTypes = {
osc: Osc,
amp: Amp,
out: Out,
};
const selector = (store) => ({
nodes: store.nodes,
edges: store.edges,
onNodesChange: store.onNodesChange,
onNodesDelete: store.onNodesDelete,
onEdgesChange: store.onEdgesChange,
onEdgesDelete: store.onEdgesDelete,
addEdge: store.addEdge,
});
export default function App() {
const store = useStore(selector, shallow);
return (
);
}
```
##### audio.js
```js
const context = new AudioContext();
const nodes = new Map();
context.suspend();
const osc = context.createOscillator();
osc.frequency.value = 220;
osc.type = 'square';
osc.start();
const amp = context.createGain();
amp.gain.value = 0.5;
const out = context.destination;
nodes.set('a', osc);
nodes.set('b', amp);
nodes.set('c', out);
export function isRunning() {
return context.state === 'running';
}
export function toggleAudio() {
return isRunning() ? context.suspend() : context.resume();
}
export function updateAudioNode(id, data) {
const node = nodes.get(id);
for (const [key, val] of Object.entries(data)) {
if (node[key] instanceof AudioParam) {
node[key].value = val;
} else {
node[key] = val;
}
}
}
export function removeAudioNode(id) {
const node = nodes.get(id);
node.disconnect();
node.stop?.();
nodes.delete(id);
}
export function connect(sourceId, targetId) {
const source = nodes.get(sourceId);
const target = nodes.get(targetId);
source.connect(target);
}
export function disconnect(sourceId, targetId) {
const source = nodes.get(sourceId);
const target = nodes.get(targetId);
source.disconnect(target);
}
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### store.js
```js
import { applyNodeChanges, applyEdgeChanges } from '@xyflow/react';
import { nanoid } from 'nanoid';
import { createWithEqualityFn } from 'zustand/traditional';
import {
isRunning,
toggleAudio,
updateAudioNode,
removeAudioNode,
connect,
disconnect,
} from './audio';
export const useStore = createWithEqualityFn((set, get) => ({
nodes: [
{
id: 'a',
type: 'osc',
data: { frequency: 220, type: 'square' },
position: { x: 0, y: 0 },
},
{
id: 'b',
type: 'amp',
data: { gain: 0.5 },
position: { x: -100, y: 250 },
},
{ id: 'c', type: 'out', position: { x: 100, y: 500 } },
],
edges: [],
isRunning: isRunning(),
toggleAudio() {
toggleAudio().then(() => {
set({ isRunning: isRunning() });
});
},
onNodesChange(changes) {
set({
nodes: applyNodeChanges(changes, get().nodes),
});
},
updateNode(id, data) {
updateAudioNode(id, data);
set({
nodes: get().nodes.map((node) =>
node.id === id
? { ...node, data: Object.assign(node.data, data) }
: node,
),
});
},
onNodesDelete(deleted) {
for (const { id } of deleted) {
removeAudioNode(id);
}
},
onEdgesChange(changes) {
set({
edges: applyEdgeChanges(changes, get().edges),
});
},
addEdge(data) {
const id = nanoid(6);
const edge = { id, ...data };
connect(edge.source, edge.target);
set({ edges: [edge, ...get().edges] });
},
onEdgesDelete(deleted) {
for ({ source, target } of deleted) {
disconnect(source, target);
}
},
}));
```
##### nodes/Amp.jsx
```jsx
import React from 'react';
import { Handle } from '@xyflow/react';
import { shallow } from 'zustand/shallow';
import { tw } from 'twind';
import { useStore } from '../store';
const selector = (id) => (store) => ({
setGain: (e) => store.updateNode(id, { gain: +e.target.value }),
});
export default function Osc({ id, data }) {
const { setGain } = useStore(selector(id), shallow);
return (
Amp
Gain
{data.gain.toFixed(2)}
);
}
```
##### nodes/Osc.jsx
```jsx
import React from 'react';
import { Handle } from '@xyflow/react';
import { shallow } from 'zustand/shallow';
import { tw } from 'twind';
import { useStore } from '../store';
const selector = (id) => (store) => ({
setFrequency: (e) => store.updateNode(id, { frequency: +e.target.value }),
setType: (e) => store.updateNode(id, { type: e.target.value }),
});
export default function Osc({ id, data }) {
const { setFrequency, setType } = useStore(selector(id), shallow);
return (
Osc
Frequency
{data.frequency} Hz
Waveform
sine
triangle
sawtooth
square
);
}
```
##### nodes/Out.jsx
```jsx
import React from 'react';
import { Handle } from '@xyflow/react';
import { shallow } from 'zustand/shallow';
import { tw } from 'twind';
import { useStore } from '../store';
const selector = (store) => ({
isRunning: store.isRunning,
toggleAudio: store.toggleAudio,
});
export default function Out({ id, data }) {
const { isRunning, toggleAudio } = useStore(selector, shallow);
return (
{isRunning ? (
🔈
) : (
🔇
)}
);
}
```
##### 4. Creating new nodes
Up until now we have been dealing with a hard-coded set of nodes in our graph. This has
been fine for prototyping but for it to actually be useful we'll want a way to add new
nodes to the graph dynamically. Our final task will be adding this functionality: we'll
work backwards starting with the audio code and ending by creating a basic toolbar.
Implementing a `createAudioNode` function will be simple enough. All we need is an id for
the new node, the type of node to create, and its initial data:
```js filename="./src/audio.js"
export function createAudioNode(id, type, data) {
switch (type) {
case 'osc': {
const node = context.createOscillator();
node.frequency.value = data.frequency;
node.type = data.type;
node.start();
nodes.set(id, node);
break;
}
case 'amp': {
const node = context.createGain();
node.gain.value = data.gain;
nodes.set(id, node);
break;
}
}
}
```
Next we'll need a `createNode` function in our store. The node id will be generated by
nanoid and we'll hardcode some initial data for each of the node types, so the only thing
we need to pass in is the type of node to create:
```js filename="./src/store.js"
import { ..., createAudioNode } from './audio';
export const useStore = createWithEqualityFn((set, get) => ({
...
createNode(type) {
const id = nanoid();
switch(type) {
case 'osc': {
const data = { frequency: 440, type: 'sine' };
const position = { x: 0, y: 0 };
createAudioNode(id, type, data);
set({ nodes: [...get().nodes, { id, type, data, position }] });
break;
}
case 'amp': {
const data = { gain: 0.5 };
const position = { x: 0, y: 0 };
createAudioNode(id, type, data);
set({ nodes: [...get().nodes, { id, type, data, position }] });
break;
}
}
}
}));
```
We could be a bit smarter about calculating the position of the new node, but to keep
things simple we'll just hardcode it to `{ x: 0, y: 0 }` for now.
The final piece of the puzzle is to create a toolbar component that can trigger the new
`createNode` action. To do that we'll jump back to `App.jsx` and make use of the
[` `](/docs/api-reference/components/panel/) built-in component.
```jsx filename="./src/App.jsx"
...
import { ReactFlow, Panel } from '@xyflow/react';
...
const selector = (store) => ({
...,
createNode: store.createNode,
});
export default function App() {
const store = useStore(selector, shallow);
return (
...
);
};
```
We don't need anything fancy here, just a couple of buttons that trigger the `createNode`
action with the appropriate type:
```jsx filename="./src/App.jsx"
store.createNode('osc')}>osc
store.createNode('amp')}>amp
```
And that's... everything! We've now got a fully functional audio graph editor that can:
* Create new audio nodes
* Update node data with some UI controls
* Connect nodes together
* Delete nodes and connections
* Start and stop audio processing
Here's the demo from the beginning, but this time you can see the source code to make sure
you haven't missed anything.
Example: tutorials/webaudio/demo
##### App.jsx
```jsx
import React from 'react';
import { ReactFlow, ReactFlowProvider, Background, Panel } from '@xyflow/react';
import { shallow } from 'zustand/shallow';
import { tw } from 'twind';
import { useStore } from './store';
import Osc from './nodes/Osc';
import Amp from './nodes/Amp';
import Out from './nodes/Out';
import '@xyflow/react/dist/style.css';
const nodeTypes = {
osc: Osc,
amp: Amp,
out: Out,
};
const selector = (store) => ({
nodes: store.nodes,
edges: store.edges,
onNodesChange: store.onNodesChange,
onNodesDelete: store.onNodesDelete,
onEdgesChange: store.onEdgesChange,
onEdgesDelete: store.onEdgesDelete,
addEdge: store.addEdge,
createNode: store.createNode,
});
export default function App() {
const store = useStore(selector, shallow);
return (
store.createNode('osc')}
>
Add Osc
store.createNode('amp')}
>
Add Amp
);
}
```
##### audio.js
```js
const context = new AudioContext();
const nodes = new Map();
context.suspend();
nodes.set('output', context.destination);
export function isRunning() {
return context.state === 'running';
}
export function toggleAudio() {
return isRunning() ? context.suspend() : context.resume();
}
export function createAudioNode(id, type, data) {
switch (type) {
case 'osc': {
const node = context.createOscillator();
node.frequency.value = data.frequency;
node.type = data.type;
node.start();
nodes.set(id, node);
break;
}
case 'amp': {
const node = context.createGain();
node.gain.value = data.gain;
nodes.set(id, node);
break;
}
}
}
export function updateAudioNode(id, data) {
const node = nodes.get(id);
for (const [key, val] of Object.entries(data)) {
if (node[key] instanceof AudioParam) {
node[key].value = val;
} else {
node[key] = val;
}
}
}
export function removeAudioNode(id) {
const node = nodes.get(id);
node.disconnect();
node.stop?.();
nodes.delete(id);
}
export function connect(sourceId, targetId) {
const source = nodes.get(sourceId);
const target = nodes.get(targetId);
source.connect(target);
}
export function disconnect(sourceId, targetId) {
const source = nodes.get(sourceId);
const target = nodes.get(targetId);
source.disconnect(target);
}
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
```
##### index.html
```html
React Flow Example
```
##### index.jsx
```jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### store.js
```js
import { applyNodeChanges, applyEdgeChanges } from '@xyflow/react';
import { nanoid } from 'nanoid';
import { createWithEqualityFn } from 'zustand/traditional';
import {
isRunning,
toggleAudio,
createAudioNode,
updateAudioNode,
removeAudioNode,
connect,
disconnect,
} from './audio';
export const useStore = createWithEqualityFn((set, get) => ({
nodes: [{ id: 'output', type: 'out', position: { x: 0, y: 0 } }],
edges: [],
isRunning: isRunning(),
toggleAudio() {
toggleAudio().then(() => {
set({ isRunning: isRunning() });
});
},
onNodesChange(changes) {
set({
nodes: applyNodeChanges(changes, get().nodes),
});
},
createNode(type, x, y) {
const id = nanoid();
switch (type) {
case 'osc': {
const data = { frequency: 440, type: 'sine' };
const position = { x: 0, y: 0 };
createAudioNode(id, type, data);
set({ nodes: [...get().nodes, { id, type, data, position }] });
break;
}
case 'amp': {
const data = { gain: 0.5 };
const position = { x: 0, y: 0 };
createAudioNode(id, type, data);
set({ nodes: [...get().nodes, { id, type, data, position }] });
break;
}
}
},
updateNode(id, data) {
updateAudioNode(id, data);
set({
nodes: get().nodes.map((node) =>
node.id === id
? { ...node, data: Object.assign(node.data, data) }
: node,
),
});
},
onNodesDelete(deleted) {
for (const { id } of deleted) {
removeAudioNode(id);
}
},
onEdgesChange(changes) {
set({
edges: applyEdgeChanges(changes, get().edges),
});
},
addEdge(data) {
const id = nanoid(6);
const edge = { id, ...data };
connect(edge.source, edge.target);
set({ edges: [edge, ...get().edges] });
},
onEdgesDelete(deleted) {
for ({ source, target } of deleted) {
disconnect(source, target);
}
},
}));
```
##### nodes/Amp.jsx
```jsx
import React from 'react';
import { Handle } from '@xyflow/react';
import { shallow } from 'zustand/shallow';
import { tw } from 'twind';
import { useStore } from '../store';
const selector = (id) => (store) => ({
setGain: (e) => store.updateNode(id, { gain: +e.target.value }),
});
export default function Osc({ id, data }) {
const { setGain } = useStore(selector(id), shallow);
return (
Amp
Gain
{data.gain.toFixed(2)}
);
}
```
##### nodes/Osc.jsx
```jsx
import React from 'react';
import { Handle } from '@xyflow/react';
import { shallow } from 'zustand/shallow';
import { tw } from 'twind';
import { useStore } from '../store';
const selector = (id) => (store) => ({
setFrequency: (e) => store.updateNode(id, { frequency: +e.target.value }),
setType: (e) => store.updateNode(id, { type: e.target.value }),
});
export default function Osc({ id, data }) {
const { setFrequency, setType } = useStore(selector(id), shallow);
return (
Osc
Frequency
{data.frequency} Hz
Waveform
sine
triangle
sawtooth
square
);
}
```
##### nodes/Out.jsx
```jsx
import React from 'react';
import { Handle } from '@xyflow/react';
import { shallow } from 'zustand/shallow';
import { tw } from 'twind';
import { useStore } from '../store';
const selector = (store) => ({
isRunning: store.isRunning,
toggleAudio: store.toggleAudio,
});
export default function Out({ id, data }) {
const { isRunning, toggleAudio } = useStore(selector, shallow);
return (
{isRunning ? (
🔈
) : (
🔇
)}
);
}
```
#### Final thoughts
Whew that was a long one, but we made it! For our efforts we've come out the other side
with a fun little interactive audio playground, learned a little bit about the Web Audio
API along the way, and have a better idea of one approach to "running" a React Flow graph.
If you've made it this far and are thinking "Hayleigh, I'm never going to write a Web
Audio app. Did I learn *anything* useful?" Then you're in luck, because you did! You could
take our approach to connecting to the Web Audio API and apply it to some other
graph-based computation engine like
[behave-graph](https://github.com/bhouston/behave-graph). In fact, some has done just that
and created [behave-flow](https://github.com/beeglebug/behave-flow)!
There are still plenty of ways to expand on this project. If you'd like to keep working on
it, here are some ideas:
* Add more node types.
* Allow nodes to connect to `AudioParams` on other nodes.
* Use the [`AnalyserNode`](https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode)
to visualize the output of a node or signal.
* Anything else you can think of!
And if you're looking for inspiration, there are quite a few projects out in the wild that
are using node-based UIs for audio things. Some of my favorites are
[Max/MSP](https://cycling74.com/products/max/),
[Reaktor](https://www.native-instruments.com/en/products/komplete/synths/reaktor-6/), and
[Pure Data](https://puredata.info/). Max and Reaktor are closed-source commercial
software, but you can still steal some ideas from them .
You can use the completed [source code](https://github.com/xyflow/react-flow-web-audio) as
a starting point, or you can just keep building on top of what we've made today. We'd love
to see what you build so please share it with us over on our
[Discord server](https://discord.com/invite/RVmnytFmGW) or
[Twitter](https://twitter.com/xyflowdev).
React Flow is an independent company financed by its users. If you want to support us you
can [sponsor us on Github](https://github.com/sponsors/xyflow) or
[subscribe to one of our Pro plans](/pro/).
### Create a slide show presentation with React Flow
We recently published the findings from our React Flow 2023 end-of-year survey with an
[interactive presentation](/developer-survey-2023) of the key findings, using React Flow
itself. There were lots of useful bits built into this slideshow app, so we wanted to
share how we built it!
By the end of this tutorial, you will have built a presentation app with
* Support for markdown slides
* Keyboard navigation around the viewport
* Automatic layouting
* Click-drag panning navigation (à la Prezi)
Along the way, you'll learn a bit about the basics of layouting algorithms, creating
static flows, and custom nodes.
Once you're done, the app will look like this!
Example: tutorials/presentation/app
##### App.tsx
```tsx
import { type KeyboardEventHandler, useCallback, useState } from 'react';
import {
ReactFlow,
ReactFlowProvider,
useReactFlow,
Background,
BackgroundVariant,
type NodeMouseHandler,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { Slide, type SlideData } from './Slide';
import { slides, slidesToElements } from './slides';
const nodeTypes = {
slide: Slide,
};
const initialSlide = '01';
const { nodes, edges } = slidesToElements(initialSlide, slides);
function App() {
const [currentSlide, setCurrentSlide] = useState(initialSlide);
const { fitView } = useReactFlow();
const handleKeyPress = useCallback(
(event) => {
const slide = slides[currentSlide];
switch (event.key) {
case 'ArrowLeft':
case 'ArrowUp':
case 'ArrowDown':
case 'ArrowRight': {
const direction = event.key.slice(5).toLowerCase() as keyof SlideData;
const target = slide[direction];
// Prevent the arrow keys from scrolling the page when React Flow is
// only part of a larger application.
event.preventDefault();
if (target) {
setCurrentSlide(target);
fitView({ nodes: [{ id: target }], duration: 100 });
}
}
}
},
[fitView, currentSlide],
);
const handleNodeClick = useCallback(
(_, node) => {
if (node.id !== currentSlide) {
setCurrentSlide(node.id);
fitView({ nodes: [{ id: node.id }], duration: 100 });
}
},
[fitView, currentSlide],
);
return (
);
}
export default () => (
);
```
##### Slide.tsx
```tsx
import { type Node, type NodeProps, useReactFlow } from '@xyflow/react';
import { Remark } from 'react-remark';
import { useCallback } from 'react';
export type SlideNode = Node;
export type SlideData = {
source: string;
left?: string;
up?: string;
down?: string;
right?: string;
};
export const SLIDE_WIDTH = 1920;
export const SLIDE_HEIGHT = 1080;
// The padding constant is used when computing the presentation layout. It adds
// a bit of space between each slide
export const SLIDE_PADDING = 100;
const style = {
width: `${SLIDE_WIDTH}px`,
height: `${SLIDE_HEIGHT}px`,
} satisfies React.CSSProperties;
export function Slide({ data }: NodeProps) {
const { source, left, up, down, right } = data;
const { fitView } = useReactFlow();
const moveToNextSlide = useCallback(
(event: React.MouseEvent, id: string) => {
// Prevent the click event from propagating so `onNodeClick` is not
// triggered when clicking on the control buttons.
event.stopPropagation();
fitView({ nodes: [{ id }], duration: 100 });
},
[fitView],
);
return (
{source}
{left && moveToNextSlide(e, left)}>← }
{up && moveToNextSlide(e, up)}>↑ }
{down && moveToNextSlide(e, down)}>↓ }
{right && moveToNextSlide(e, right)}>→ }
);
}
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
body {
margin: 0;
background-color: #f8f8f8;
font-family: sans-serif;
color: #111;
}
html,
body,
#root {
height: 100%;
}
.slide {
box-sizing: border-box;
box-shadow: 0rem 1rem 4rem 0.25rem rgba(0, 0, 0, 0.06);
width: 1920px;
height: 1080px;
overflow: hidden;
position: relative;
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 6rem;
padding-top: 10rem;
padding-left: 8rem;
font-size: 4rem;
}
.slide h1 {
font-size: 8rem;
margin-bottom: 1rem;
}
.slide ul li {
margin-bottom: 1rem;
}
.slide__controls {
position: absolute;
bottom: 4rem;
right: 4rem;
display: flex;
justify-content: end;
gap: 1rem;
}
.slide__controls button {
font-size: 4rem;
padding: 1rem 2rem;
border-radius: 1rem;
border: 2px solid #000;
background-color: #fff;
color: #111;
&:hover {
background-color: #f8f8f8;
}
&:active {
background-color: #e0e0e0;
}
}
```
##### index.html
```html
React Flow Example
```
##### index.tsx
```tsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### slides.ts
```ts
import { type Edge, type Node } from '@xyflow/react';
import {
SLIDE_WIDTH,
SLIDE_HEIGHT,
SLIDE_PADDING,
type SlideData,
} from './Slide';
const slide01 = {
id: '01',
data: {
right: '02',
source: `
# Slide 1
- This is the first slide
- It has a right arrow to go to the next slide
`,
},
};
const slide02 = {
id: '02',
data: {
left: '01',
up: '03',
right: '04',
source: `
# Slide 2
- This is the second slide
- It has a left arrow to go back to the first slide
- It has an up arrow to go to the third slide
- It has a right arrow to go to the fourth slide
`,
},
};
const slide03 = {
id: '03',
data: {
down: '02',
source: `
# Slide 3
- This is the third slide
- It has a down arrow to go back to the second slide
`,
},
};
const slide04 = {
id: '04',
data: {
left: '02',
source: `
# Slide 4
- This is the fourth slide
- It has a left arrow to go back to the second slide
`,
},
};
export const slides = Object.fromEntries(
[slide01, slide02, slide03, slide04].map(({ id, data }) => [id, data]),
) as Record;
export const slidesToElements = (
initial: string,
slides: Record,
) => {
const stack = [{ id: initial, position: { x: 0, y: 0 } }];
const visited = new Set();
const nodes: Node[] = [];
const edges: Edge[] = [];
while (stack.length) {
const { id, position } = stack.pop()!;
const data = slides[id];
const node = { id, type: 'slide', position, data };
if (data.left && !visited.has(data.left)) {
const nextPosition = {
x: position.x - (SLIDE_WIDTH + SLIDE_PADDING),
y: position.y,
};
stack.push({ id: data.left, position: nextPosition });
edges.push({
id: `${id}->${data.left}`,
source: id,
target: data.left,
});
}
if (data.up && !visited.has(data.up)) {
const nextPosition = {
x: position.x,
y: position.y - (SLIDE_HEIGHT + SLIDE_PADDING),
};
stack.push({ id: data.up, position: nextPosition });
edges.push({ id: `${id}->${data.up}`, source: id, target: data.up });
}
if (data.down && !visited.has(data.down)) {
const nextPosition = {
x: position.x,
y: position.y + (SLIDE_HEIGHT + SLIDE_PADDING),
};
stack.push({ id: data.down, position: nextPosition });
edges.push({
id: `${id}->${data.down}`,
source: id,
target: data.down,
});
}
if (data.right && !visited.has(data.right)) {
const nextPosition = {
x: position.x + (SLIDE_WIDTH + SLIDE_PADDING),
y: position.y,
};
stack.push({ id: data.right, position: nextPosition });
edges.push({
id: `${id}->${data.down}`,
source: id,
target: data.right,
});
}
nodes.push(node);
visited.add(id);
}
return { nodes, edges };
};
```
To follow along with this tutorial we'll assume you have a basic understanding of
[React](https://reactjs.org/docs/getting-started.html) and
[React Flow](/learn/concepts/terms-and-definitions), but if you get stuck on the way feel
free to reach out to us on [Discord](https://discord.com/invite/RVmnytFmGW)!
Here's the [repo with the final code](https://github.com/xyflow/react-flow-slide-show) if
you'd like to skip ahead or refer to it as we go.
Let's get started!
#### Setting up the project
We like to recommend using [Vite](https://vitejs.dev) when starting new React Flow
projects, and this time we'll use TypeScript too. You can scaffold a new project with the
following command:
```bash npm2yarn
npm create vite@latest -- --template react-ts
```
If you'd prefer to follow along with JavaScript feel free to use the `react` template
instead. You can also follow along in your browser by using our Codesandbox templates:
JS
}
/>
TS
}
/>
Besides React Flow we only need to pull in one dependency,
[`react-remark`](https://www.npmjs.com/package/react-remark), to help us render markdown
in our slides.
```bash npm2yarn
npm install @xyflow/react react-remark
```
We'll modify the generated `main.tsx` to include React Flow's styles, as well as wrap the
app in a ` ` to make sure we can access the React Flow instance inside
our components;
```tsx filename="main.tsx" {3,7,12,20}
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ReactFlowProvider } from '@xyflow/react';
import App from './App';
import '@xyflow/react/dist/style.css';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
{/* The parent element of the React Flow component needs a width and a height
to work properly. If you're styling your app as you follow along, you
can remove this div and apply styles to the #root element in your CSS.
*/}
,
);
```
This tutorial is going to gloss over the styling of the app, so feel free to use any CSS
framework or styling solution you're familiar with. If you're going to style your app
differently from just writing CSS, [Tailwind CSS](/examples/styling/tailwind), you can
skip the import to `index.css`.
How you style your app is up to you, but you must **always** include React Flow's
styles! If you don't need the default styles, at a minimum you should include the base
styles from `@xyflow/react/dist/base.css`.
Each slide of our presentation will be a node on the canvas, so let's create a new file
`Slide.tsx` that will be our custom node used to render each slide.
```tsx filename="Slide.tsx"
import { type Node, type NodeProps } from '@xyflow/react';
export const SLIDE_WIDTH = 1920;
export const SLIDE_HEIGHT = 1080;
export type SlideNode = Node;
export type SlideData = {};
const style = {
width: `${SLIDE_WIDTH}px`,
height: `${SLIDE_HEIGHT}px`,
} satisfies React.CSSProperties;
export function Slide({ data }: NodeProps) {
return (
Hello, React Flow!
);
}
```
We're setting the slide width and height as constants here (rather than styling the node
in CSS) because we'll want access to those dimensions later on. We've also stubbed out the
`SlideData` type so we can properly type the component's props.
The last thing to do is to register our new custom node and show something on the screen.
```tsx filename="App.tsx"
import { ReactFlow } from '@xyflow/react';
import { Slide } from './Slide.tsx';
const nodeTypes = {
slide: Slide,
};
export default function App() {
const nodes = [{ id: '0', type: 'slide', position: { x: 0, y: 0 }, data: {} }];
return ;
}
```
It's important to remember to define your `nodeTypes` object _outside_ of the component
(or to use React's `useMemo` hook)! When the `nodeTypes` object changes, the entire flow
is re-rendered.
With the basics put together, you can start the development server by running
`npm run dev` and see the following:
Example: tutorials/presentation/scaffold
##### App.tsx
```tsx
import {
ReactFlow,
ReactFlowProvider,
Background,
BackgroundVariant,
} from '@xyflow/react';
import { Slide } from './Slide';
import '@xyflow/react/dist/style.css';
const nodeTypes = {
slide: Slide,
};
function App() {
const nodes = [
{ id: '0', type: 'slide', position: { x: 0, y: 0 }, data: {} },
];
return (
);
}
export default () => (
);
```
##### Slide.tsx
```tsx
import { type Node, type NodeProps } from '@xyflow/react';
export const SLIDE_WIDTH = 1920;
export const SLIDE_HEIGHT = 1080;
export type SlideNode = Node;
export type SlideData = {};
const style = {
width: `${SLIDE_WIDTH}px`,
height: `${SLIDE_HEIGHT}px`,
} satisfies React.CSSProperties;
export function Slide({ data }: NodeProps) {
return (
Hello, React Flow!
);
}
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
body {
margin: 0;
background-color: #f8f8f8;
}
html,
body,
#root {
height: 100%;
}
.slide {
box-sizing: border-box;
width: 1920px;
height: 1080px;
overflow: hidden;
position: relative;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 4rem;
font-size: 4rem;
}
.slide__controls {
position: absolute;
bottom: 2rem;
right: 2rem;
display: flex;
justify-content: end;
gap: 4px;
pointer-events: all;
}
.slide__controls button {
font-size: 2rem;
}
```
##### index.html
```html
React Flow Example
```
##### index.tsx
```tsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
Not super exciting yet, but let's add markdown rendering and create a few slides side by
side!
#### Rendering markdown
We want to make it easy to add content to our slides, so we'd like the ability to write
[Markdown](https://www.markdownguide.org/basic-syntax/) in our slides. If you're not
familiar, Markdown is a simple markup language for creating formatted text documents. If
you've ever written a README on GitHub, you've used Markdown!
Thanks to the `react-remark` package we installed earlier, this step is a simple one. We
can use the ` ` component to render a string of markdown content into our slides.
```tsx filename="Slide.tsx" {2,9-11,21}
import { type Node, type NodeProps } from '@xyflow/react';
import { Remark } from 'react-remark';
export const SLIDE_WIDTH = 1920;
export const SLIDE_HEIGHT = 1080;
export type SlideNode = Node;
export type SlideData = {
source: string;
};
const style = {
width: `${SLIDE_WIDTH}px`,
height: `${SLIDE_HEIGHT}px`,
} satisfies React.CSSProperties;
export function Slide({ data }: NodeProps) {
return (
{data.source}
);
}
```
In React Flow, nodes can have data stored on them that can be used during rendering. In
this case we're storing the markdown content to display by adding a `source` property to
the `SlideData` type and passing that to the ` ` component. We can update our
hardcoded nodes with some markdown content to see it in action:
```tsx filename="App.tsx" {2, 10-27, 34}
import { ReactFlow } from '@xyflow/react';
import { Slide, SLIDE_WIDTH } from './Slide';
const nodeTypes = {
slide: Slide,
};
export default function App() {
const nodes = [
{
id: '0',
type: 'slide',
position: { x: 0, y: 0 },
data: { source: '# Hello, React Flow!' },
},
{
id: '1',
type: 'slide',
position: { x: SLIDE_WIDTH, y: 0 },
data: { source: '...' },
},
{
id: '2',
type: 'slide',
position: { x: SLIDE_WIDTH * 2, y: 0 },
data: { source: '...' },
},
];
return ;
}
```
Note that we've added the `minZoom` prop to the ` ` component. Our slides are
quite large, and the default minimum zoom level is not enough to zoom out and see multiple
slides at once.
Example: tutorials/presentation/rendering-markdown
##### App.tsx
```tsx
import {
ReactFlow,
ReactFlowProvider,
Background,
BackgroundVariant,
} from '@xyflow/react';
import { Slide, SLIDE_WIDTH } from './Slide';
import '@xyflow/react/dist/style.css';
const nodeTypes = {
slide: Slide,
};
function App() {
const nodes = [
{
id: '0',
type: 'slide',
position: { x: 0, y: 0 },
data: { source: '# Hello, React Flow!' },
},
{
id: '1',
type: 'slide',
position: { x: SLIDE_WIDTH, y: 0 },
data: { source: '- these are\n- some\n- bullet points!' },
},
{
id: '2',
type: 'slide',
position: { x: SLIDE_WIDTH * 2, y: 0 },
data: {
source:
"It's markdown so we can write **bold text** or `code snippets` too!",
},
},
];
return (
);
}
export default () => (
);
```
##### Slide.tsx
```tsx
import { Remark } from 'react-remark';
import { type Node, type NodeProps } from '@xyflow/react';
export const SLIDE_WIDTH = 1920;
export const SLIDE_HEIGHT = 1080;
export type SlideNode = Node;
export type SlideData = {
source: string;
};
const style = {
width: `${SLIDE_WIDTH}px`,
height: `${SLIDE_HEIGHT}px`,
} satisfies React.CSSProperties;
export function Slide({ data }: NodeProps) {
return (
{data.source}
);
}
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
body {
margin: 0;
background-color: #f8f8f8;
font-family: sans-serif;
color: #111;
}
html,
body,
#root {
height: 100%;
}
.slide {
box-sizing: border-box;
box-shadow: 0rem 1rem 4rem 0.25rem rgba(0, 0, 0, 0.06);
width: 1920px;
height: 1080px;
overflow: hidden;
position: relative;
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 6rem;
padding-top: 10rem;
padding-left: 8rem;
font-size: 4rem;
}
.slide h1 {
font-size: 8rem;
margin-bottom: 1rem;
}
.slide ul li {
margin-bottom: 1rem;
}
.slide__controls {
position: absolute;
bottom: 4rem;
right: 4rem;
display: flex;
justify-content: end;
gap: 1rem;
}
.slide__controls button {
font-size: 4rem;
padding: 1rem 2rem;
border-radius: 1rem;
border: 2px solid #000;
background-color: #fff;
color: #111;
&:hover {
background-color: #f8f8f8;
}
&:active {
background-color: #e0e0e0;
}
}
```
##### index.html
```html
React Flow Example
```
##### index.tsx
```tsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
In the nodes array above, we've made sure to space the slides out by doing some manual
math with the `SLIDE_WIDTH` constant. In the next section we'll come up with an algorithm
to automatically lay out the slides in a grid.
#### Laying out the nodes
We often get asked how to automatically lay out nodes in a flow, and we have some
documentation on how to use common layouting libraries like dagre and d3-hierarchy in our
[layouting guide](/learn/layouting/layouting). Here you'll be writing your own
super-simple layouting algorithm, which gets a bit nerdy, but stick with us!
For our presentation app we'll construct a simple grid layout by starting from 0,0 and
updating the x or y coordinates any time we have a new slide to the left, right, up, or
down.
First, we need to update our `SlideData` type to include optional ids for the slides to
the left, right, up, and down of the current slide.
```tsx filename="Slide.tsx" {3-6}
export type SlideData = {
source: string;
left?: string;
up?: string;
down?: string;
right?: string;
};
```
Storing this information on the node data directly gives us some useful benefits:
* We can write fully declarative slides without worrying about the concept of nodes and
edges
* We can compute the layout of the presentation by visiting connecting slides
* We can add navigation buttons to each slide to navigate between them automatically.
We'll handle that in a later step.
The magic happens in a function we're going to define called `slidesToElements`. This
function will take an object containing all our slides addressed by their id, and an id
for the slide to start at. Then it will work through each connecting slide to build an
array of nodes and edges that we can pass to the ` ` component.
The algorithm will go something like this:
* Push the initial slide's id and the position `{ x: 0, y: 0 }` onto a stack.
* While that stack is not empty...
* Pop the current position and slide id off the stack.
* Look up the slide data by id.
* Push a new node onto the nodes array with the current id, position, and slide data.
* Add the slide's id to a set of visited slides.
* For every direction (left, right, up, down)...
* Make sure the slide has not already been visited.
* Take the current position and update the x or y coordinate by adding or subtracting
`SLIDE_WIDTH` or `SLIDE_HEIGHT` depending on the direction.
* Push the new position and the new slide's id onto a stack.
* Push a new edge onto the edges array connecting the current slide to the new slide.
* Repeat for the remaining directions...
If all goes to plan, we should be able to take a stack of slides shown below and turn them
into a neatly laid out grid!
Let's see the code. In a file called `slides.ts` add the following:
```tsx filename="slides.ts"
import { SlideData, SLIDE_WIDTH, SLIDE_HEIGHT } from './Slide';
export const slidesToElements = (initial: string, slides: Record) => {
// Push the initial slide's id and the position `{ x: 0, y: 0 }` onto a stack.
const stack = [{ id: initial, position: { x: 0, y: 0 } }];
const visited = new Set();
const nodes = [];
const edges = [];
// While that stack is not empty...
while (stack.length) {
// Pop the current position and slide id off the stack.
const { id, position } = stack.pop();
// Look up the slide data by id.
const data = slides[id];
const node = { id, type: 'slide', position, data };
// Push a new node onto the nodes array with the current id, position, and slide
// data.
nodes.push(node);
// add the slide's id to a set of visited slides.
visited.add(id);
// For every direction (left, right, up, down)...
// Make sure the slide has not already been visited.
if (data.left && !visited.has(data.left)) {
// Take the current position and update the x or y coordinate by adding or
// subtracting `SLIDE_WIDTH` or `SLIDE_HEIGHT` depending on the direction.
const nextPosition = {
x: position.x - SLIDE_WIDTH,
y: position.y,
};
// Push the new position and the new slide's id onto a stack.
stack.push({ id: data.left, position: nextPosition });
// Push a new edge onto the edges array connecting the current slide to the
// new slide.
edges.push({ id: `${id}->${data.left}`, source: id, target: data.left });
}
// Repeat for the remaining directions...
}
return { nodes, edges };
};
```
We've left out the code for the right, up, and down directions for brevity, but the logic
is the same for each direction. We've also included the same breakdown of the algorithm as
comments, to help you navigate the code.
Below is a demo app of the layouting algorithm, you can edit the `slides` object to see
how adding slides to different directions affects the layout. For example, try extending
4's data to include `down: '5'` and see how the layout updates.
Example: tutorials/presentation/layout-demo
##### App.tsx
```tsx
import Flow from './Flow';
// add more slides and create different layouts by
// linking slides in different ways.
const slides = {
'1': { right: '2' },
'2': { left: '1', up: '3', right: '4' },
'3': { down: '2' },
'4': { left: '2' },
};
export default function App() {
return ;
}
```
##### Flow\.tsx
```tsx
import { useMemo } from 'react';
import { ReactFlow, Background, BackgroundVariant } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
const SLIDE_WIDTH = 100;
const SLIDE_HEIGHT = 100;
const SLIDE_PADDING = 10;
function Slide({ id }) {
const style = { width: `${SLIDE_WIDTH}px`, height: `${SLIDE_HEIGHT}px` };
return (
{id}
);
}
export default function Flow({ slides }) {
const nodeTypes = useMemo(() => ({ slide: Slide }), []);
const { nodes, edges } = useMemo(() => {
const stack = [{ id: '1', position: { x: 0, y: 0 } }];
const visited = new Set();
const nodes = [];
const edges = [];
while (stack.length) {
const { id, position } = stack.pop();
const data = slides[id];
// remember to add `type: 'slide'` to the node!
const node = { id, type: 'slide', position, data };
nodes.push(node);
visited.add(id);
if (!data) continue;
if (data.left && !visited.has(data.left)) {
// a node on left we haven't seen means we need to subtract SLIDE_WIDTH
// from the current position
const nextPosition = {
x: position.x - SLIDE_WIDTH - SLIDE_PADDING,
y: position.y,
};
stack.push({ id: data.left, position: nextPosition });
edges.push({
id: `${id}->${data.left}`,
source: id,
target: data.left,
});
}
if (data.right && !visited.has(data.right)) {
const nextPosition = {
x: position.x + SLIDE_WIDTH + SLIDE_PADDING,
y: position.y,
};
stack.push({ id: data.right, position: nextPosition });
edges.push({
id: `${id}->${data.right}`,
source: id,
target: data.right,
});
}
if (data.up && !visited.has(data.up)) {
const nextPosition = {
x: position.x,
y: position.y - SLIDE_HEIGHT - SLIDE_PADDING,
};
stack.push({ id: data.up, position: nextPosition });
edges.push({ id: `${id}->${data.up}`, source: id, target: data.up });
}
if (data.down && !visited.has(data.down)) {
const nextPosition = {
x: position.x,
y: position.y + SLIDE_HEIGHT + SLIDE_PADDING,
};
stack.push({ id: data.down, position: nextPosition });
edges.push({
id: `${id}->${data.down}`,
source: id,
target: data.down,
});
}
}
return { nodes, edges };
}, [slides]);
return (
);
}
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
body {
margin: 0;
background-color: #f8f8f8;
}
html,
body,
#root {
height: 100%;
}
.slide {
border: 1px solid hsl(333 100% 50%);
border-radius: 4px;
background-color: white;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
```
##### index.html
```html
React Flow Example
```
##### index.tsx
```tsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
If you spend a little time playing with this demo, you'll likely run across two
limitations of this algorithm:
1. It is possible to construct a layout that overlaps two slides in the same position.
2. The algorithm will ignore nodes that cannot be reached from the initial slide.
Addressing these shortcomings is totally possible, but a bit beyond the scope of this
tutorial. If you give a shot, be sure to share your solution with us on the
[discord server](https://discord.com/invite/RVmnytFmGW)!
With our layouting algorithm written, we can hop back to `App.tsx` and remove the
hardcoded nodes array in favor of the new `slidesToElements` function.
```tsx filename="App.tsx" {2,3,5-9,15-16,24}
import { ReactFlow } from '@xyflow/react';
import { slidesToElements } from './slides';
import { Slide, SlideData, SLIDE_WIDTH } from './Slide';
const slides: Record = {
'0': { source: '# Hello, React Flow!', right: '1' },
'1': { source: '...', left: '0', right: '2' },
'2': { source: '...', left: '1' },
};
const nodeTypes = {
slide: Slide,
};
const initialSlide = '0';
const { nodes, edges } = slidesToElements(initialSlide, slides);
export default function App() {
return (
);
}
```
The slides in our flow are static, so we can move the `slidesToElements` call *outside*
the component to make sure we're not recalculating the layout if the component re-renders.
Alternatively, you could use React's `useMemo` hook to define things inside the component
but only calculate them once.
Because we have the idea of an "initial" slide now, we're also using the `fitViewOptions`
to ensure the initial slide is the one that is focused when the canvas is first loaded.
#### Navigating between slides
So far we have our presentation laid out in a grid but we have to manually pan the canvas
to see each slide, which isn't very practical for a presentation! We're going to add three
different ways to navigate between slides:
* Click-to-focus on nodes for jumping to different slides by clicking on them.
* Navigation buttons on each slide for moving sequentially between slides in any valid
direction.
* Keyboard navigation using the arrow keys for moving around the presentation without
using the mouse or interacting with a slide directly.
##### Focus on click
The ` ` element can receive an
[`onNodeClick`](/api-reference/react-flow#on-node-click) callback that fires when *any*
node is clicked. Along with the mouse event itself, we also receive a reference to the
node that was clicked on, and we can use that to pan the canvas thanks to the `fitView`
method.
[`fitView`](/api-reference/types/react-flow-instance#fit-view) is a method on the React
Flow instance, and we can get access to it by using the
[`useReactFlow`](/api-reference/types/react-flow-instance#use-react-flow) hook.
```tsx filename="App.tsx" {1-2,17-23,29}
import { useCallback } from 'react';
import { ReactFlow, useReactFlow, type NodeMouseHandler } from '@xyflow/react';
import { Slide, SlideData, SLIDE_WIDTH } from './Slide';
const slides: Record = {
...
}
const nodeTypes = {
slide: Slide,
};
const initialSlide = '0';
const { nodes, edges } = slidesToElements(initialSlide, slides);
export default function App() {
const { fitView } = useReactFlow();
const handleNodeClick = useCallback(
(_, node) => {
fitView({ nodes: [node], duration: 150 });
},
[fitView],
);
return (
);
}
```
It's important to remember to include `fitView` as in the dependency array of our
`handleNodeClick` callback. That's because the `fitView` function is replaced once React
Flow has initialized the viewport. If you forget this step you'll likely find out that
`handleNodeClick` does nothing at all (and yes, we also forget this ourselves sometimes
too
).
Calling `fitView` with no arguments would attempt to fit every node in the graph into
view, but we only want to focus on the node that was clicked! The
[`FitViewOptions`](/api-reference/types/fit-view-options) object lets us provide an array
of just the nodes we want to focus on: in this case, that's just the node that was
clicked.
Example: tutorials/presentation/focus-on-click
##### App.tsx
```tsx
import { useCallback, useMemo } from 'react';
import {
ReactFlow,
ReactFlowProvider,
useReactFlow,
Background,
BackgroundVariant,
type NodeMouseHandler,
} from '@xyflow/react';
import { Slide } from './Slide';
import { slides, slidesToElements } from './slides';
import '@xyflow/react/dist/style.css';
const nodeTypes = {
slide: Slide,
};
function App() {
const start = '01';
const { fitView } = useReactFlow();
const { nodes, edges } = useMemo(() => slidesToElements(start, slides), []);
const handleNodeClick = useCallback(
(_, node) => {
fitView({ nodes: [{ id: node.id }], duration: 150 });
},
[fitView],
);
return (
);
}
export default () => (
);
```
##### Slide.tsx
```tsx
import { type Node, type NodeProps } from '@xyflow/react';
import { Remark } from 'react-remark';
export const SLIDE_WIDTH = 1920;
export const SLIDE_HEIGHT = 1080;
export type SlideNode = Node;
export type SlideData = {
source: string;
left?: string;
up?: string;
down?: string;
right?: string;
};
const style = {
width: `${SLIDE_WIDTH}px`,
height: `${SLIDE_HEIGHT}px`,
} satisfies React.CSSProperties;
export function Slide({ data }: NodeProps) {
return (
{data.source}
);
}
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
body {
margin: 0;
background-color: #f8f8f8;
font-family: sans-serif;
color: #111;
}
html,
body,
#root {
height: 100%;
}
.slide {
box-sizing: border-box;
box-shadow: 0rem 1rem 4rem 0.25rem rgba(0, 0, 0, 0.06);
width: 1920px;
height: 1080px;
overflow: hidden;
position: relative;
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 6rem;
padding-top: 10rem;
padding-left: 8rem;
font-size: 4rem;
}
.slide h1 {
font-size: 8rem;
margin-bottom: 1rem;
}
.slide ul li {
margin-bottom: 1rem;
}
.slide__controls {
position: absolute;
bottom: 4rem;
right: 4rem;
display: flex;
justify-content: end;
gap: 1rem;
}
.slide__controls button {
font-size: 4rem;
padding: 1rem 2rem;
border-radius: 1rem;
border: 2px solid #000;
background-color: #fff;
color: #111;
&:hover {
background-color: #f8f8f8;
}
&:active {
background-color: #e0e0e0;
}
}
```
##### index.html
```html
React Flow Example
```
##### index.tsx
```tsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### slides.ts
```ts
import { type Edge, type Node } from '@xyflow/react';
import { SLIDE_WIDTH, SLIDE_HEIGHT, type SlideData } from './Slide';
const slide01 = {
id: '01',
data: {
right: '02',
source: `
# Slide 1
- This is the first slide
- Zoom out and click on another slide to focus on it!
`,
},
};
const slide02 = {
id: '02',
data: {
left: '01',
up: '03',
right: '04',
source: `
# Slide 2
- ...
`,
},
};
const slide03 = {
id: '03',
data: {
down: '02',
source: `
# Slide 3
- ...
`,
},
};
const slide04 = {
id: '04',
data: {
left: '02',
source: `
# Slide 4
- ...
`,
},
};
export const slides = Object.fromEntries(
[slide01, slide02, slide03, slide04].map(({ id, data }) => [id, data]),
) as Record;
export const slidesToElements = (
start: string,
slides: Record,
) => {
const stack = [{ id: start, position: { x: 0, y: 0 } }];
const visited = new Set();
const nodes = [] as Node[];
const edges = [] as Edge[];
while (stack.length) {
const { id, position } = stack.pop()!;
const data = slides[id];
const node = { id, type: 'slide', position, data };
if (data.left && !visited.has(data.left)) {
const nextPosition = {
x: position.x - SLIDE_WIDTH,
y: position.y,
};
stack.push({ id: data.left, position: nextPosition });
edges.push({
id: `${id}->${data.left}`,
source: id,
target: data.left,
});
}
if (data.up && !visited.has(data.up)) {
const nextPosition = {
x: position.x,
y: position.y - SLIDE_HEIGHT,
};
stack.push({ id: data.up, position: nextPosition });
edges.push({ id: `${id}->${data.up}`, source: id, target: data.up });
}
if (data.down && !visited.has(data.down)) {
const nextPosition = {
x: position.x,
y: position.y + SLIDE_HEIGHT,
};
stack.push({ id: data.down, position: nextPosition });
edges.push({
id: `${id}->${data.down}`,
source: id,
target: data.down,
});
}
if (data.right && !visited.has(data.right)) {
const nextPosition = {
x: position.x + SLIDE_WIDTH,
y: position.y,
};
stack.push({ id: data.right, position: nextPosition });
edges.push({
id: `${id}->${data.right}`,
source: id,
target: data.right,
});
}
nodes.push(node);
visited.add(id);
}
return { start, nodes, edges };
};
```
##### Slide controls
Clicking to focus a node is handy for zooming out to see the big picture before focusing
back in on a specific slide, but it's not a very practical way for navigating around a
presentation. In this step we'll add some controls to each slide that allow us to move to
a connected slide in any direction.
Let's add a `` to each slide that conditionally renders a button in any direction
with a connected slide. We'll also preemptively create a `moveToNextSlide` callback that
we'll use in a moment.
```tsx filename="Slide.tsx" {3,8,13-18}
import { type NodeProps, fitView } from '@xyflow/react';
import { Remark } from 'react-remark';
import { useCallback } from 'react';
...
export function Slide({ data }: NodeProps) {
const moveToNextSlide = useCallback((id: string) => {}, []);
return (
{data.source}
{data.left && ( moveToNextSlide(data.left)}>← )}
{data.up && ( moveToNextSlide(data.up)}>↑ )}
{data.down && ( moveToNextSlide(data.down)}>↓ )}
{data.right && ( moveToNextSlide(data.right)}>→ )}
);
}
```
You can style the footer however you like, but it's important to add the `"nopan"` class
to prevent prevent the canvas from panning as you interact with any of the buttons.
To implement `moveToSlide`, we'll make use of `fitView` again. Previously we had a
reference to the actual node that was clicked on to pass to `fitView`, but this time we
only have a node's id. You might be tempted to look up the target node by its id, but
actually that's not necessary! If we look at the type of
[`FitViewOptions`](/api-reference/types/fit-view-options) we can see that the array of
nodes we pass in only *needs* to have an `id` property:
```ts filename="https://reactflow.dev/api-reference/types/fit-view-options" {7}
export type FitViewOptions = {
padding?: number;
includeHiddenNodes?: boolean;
minZoom?: number;
maxZoom?: number;
duration?: number;
nodes?: (Partial & { id: Node['id'] })[];
};
```
`Partial` means that all of the fields of the `Node` object type get marked as
optional, and then we intersect that with `{ id: Node['id'] }` to ensure that the `id`
field is always required. This means we can just pass in an object with an `id` property
and nothing else, and `fitView` will know what to do with it!
```tsx filename="Slide.tsx" {1,4,6-9}
import { type NodeProps, useReactFlow } from '@xyflow/react';
export function Slide({ data }: NodeProps) {
const { fitView } = useReactFlow();
const moveToNextSlide = useCallback(
(id: string) => fitView({ nodes: [{ id }] }),
[fitView],
);
return (
...
);
}
```
Example: tutorials/presentation/slide-controls
##### App.tsx
```tsx
import React, { useCallback, useMemo } from 'react';
import {
ReactFlow,
useReactFlow,
ReactFlowProvider,
Background,
BackgroundVariant,
type Node,
type NodeMouseHandler,
} from '@xyflow/react';
// we need to import the React Flow styles to make it work
import '@xyflow/react/dist/style.css';
import slides from './slides';
import {
Slide,
SLIDE_WIDTH,
SLIDE_HEIGHT,
SLIDE_PADDING,
type SlideData,
} from './Slide';
const slidesToElements = () => {
const start = Object.keys(slides)[0];
const stack = [{ id: start, position: { x: 0, y: 0 } }];
const visited = new Set();
const nodes = [];
const edges = [];
while (stack.length) {
const { id, position } = stack.pop();
const slide = slides[id];
const node = {
id,
type: 'slide',
position,
data: slide,
draggable: false,
} satisfies Node;
if (slide.left && !visited.has(slide.left)) {
const nextPosition = {
x: position.x - (SLIDE_WIDTH + SLIDE_PADDING),
y: position.y,
};
stack.push({ id: slide.left, position: nextPosition });
edges.push({
id: `${id}->${slide.left}`,
source: id,
target: slide.left,
});
}
if (slide.up && !visited.has(slide.up)) {
const nextPosition = {
x: position.x,
y: position.y - (SLIDE_HEIGHT + SLIDE_PADDING),
};
stack.push({ id: slide.up, position: nextPosition });
edges.push({ id: `${id}->${slide.up}`, source: id, target: slide.up });
}
if (slide.down && !visited.has(slide.down)) {
const nextPosition = {
x: position.x,
y: position.y + (SLIDE_HEIGHT + SLIDE_PADDING),
};
stack.push({ id: slide.down, position: nextPosition });
edges.push({
id: `${id}->${slide.down}`,
source: id,
target: slide.down,
});
}
if (slide.right && !visited.has(slide.right)) {
const nextPosition = {
x: position.x + (SLIDE_WIDTH + SLIDE_PADDING),
y: position.y,
};
stack.push({ id: slide.right, position: nextPosition });
edges.push({
id: `${id}->${slide.down}`,
source: id,
target: slide.down,
});
}
nodes.push(node);
visited.add(id);
}
return { start, nodes, edges };
};
const nodeTypes = {
slide: Slide,
};
function Flow() {
const { fitView } = useReactFlow();
const { start, nodes, edges } = useMemo(() => slidesToElements(), []);
const handleNodeClick = useCallback(
(_, node) => {
fitView({ nodes: [{ id: node.id }], duration: 150 });
},
[fitView],
);
return (
);
}
export default () => (
);
```
##### Slide.tsx
```tsx
import { useCallback } from 'react';
import { Remark } from 'react-remark';
import { type Node, type NodeProps, useReactFlow } from '@xyflow/react';
export type SlideNode = Node;
export type SlideData = {
source: string;
left?: string;
up?: string;
down?: string;
right?: string;
};
export const SLIDE_WIDTH = 1920;
export const SLIDE_HEIGHT = 1080;
export const SLIDE_PADDING = 100;
const style = {
width: `${SLIDE_WIDTH}px`,
height: `${SLIDE_HEIGHT}px`,
} satisfies React.CSSProperties;
export function Slide({ data }: NodeProps) {
const { source, left, up, down, right } = data;
const { fitView } = useReactFlow();
const moveToNextSlide = useCallback(
(event: React.MouseEvent, id: string) => {
event.stopPropagation();
fitView({ nodes: [{ id }], duration: 150 });
},
[fitView],
);
return (
{source}
{left && moveToNextSlide(e, left)}>← }
{up && moveToNextSlide(e, up)}>↑ }
{down && moveToNextSlide(e, down)}>↓ }
{right && moveToNextSlide(e, right)}>→ }
);
}
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
body {
margin: 0;
background-color: #f8f8f8;
font-family: sans-serif;
color: #111;
}
html,
body,
#root {
height: 100%;
}
.slide {
box-sizing: border-box;
box-shadow: 0rem 1rem 4rem 0.25rem rgba(0, 0, 0, 0.06);
width: 1920px;
height: 1080px;
overflow: hidden;
position: relative;
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 6rem;
padding-top: 10rem;
padding-left: 8rem;
font-size: 4rem;
}
.slide h1 {
font-size: 8rem;
margin-bottom: 1rem;
}
.slide ul li {
margin-bottom: 1rem;
}
.slide__controls {
position: absolute;
bottom: 4rem;
right: 4rem;
display: flex;
justify-content: end;
gap: 1rem;
}
.slide__controls button {
font-size: 4rem;
padding: 1rem 2rem;
border-radius: 1rem;
border: 2px solid #000;
background-color: #fff;
color: #111;
&:hover {
background-color: #f8f8f8;
}
&:active {
background-color: #e0e0e0;
}
}
```
##### index.html
```html
React Flow Example
```
##### index.tsx
```tsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### slides.ts
```ts
import { type SlideData } from './Slide';
const slide01 = {
id: '01',
data: {
right: '02',
source: `
# Slide 1
- This is the first slide
- It has a right arrow to go to the next slide
`,
},
};
const slide02 = {
id: '02',
data: {
left: '01',
up: '03',
right: '04',
source: `
# Slide 2
- This is the second slide
- It has a left arrow to go back to the first slide
- It has an up arrow to go to the third slide
- It has a right arrow to go to the fourth slide
`,
},
};
const slide03 = {
id: '03',
data: {
down: '02',
source: `
# Slide 3
- This is the third slide
- It has a down arrow to go back to the second slide
`,
},
};
const slide04 = {
id: '04',
data: {
left: '02',
source: `
# Slide 4
- This is the fourth slide
- It has a left arrow to go back to the second slide
`,
},
};
export default [slide01, slide02, slide03, slide04].reduce(
(slides, { id, data }) => ({ ...slides, [id]: data }),
{},
) satisfies Record;
```
##### Keyboard navigation
The final piece of the puzzle is to add keyboard navigation to our presentation. It's not
very convenient to have to *always* click on a slide to move to the next one, so we'll add
some keyboard shortcuts to make it easier. React Flow lets us listen to keyboard events on
the ` ` component through handlers like
[`onKeyDown`](/api-reference/react-flow#on-key-down).
Up until now the slide currently focused is implied by the position of the canvas, but if
we want to handle key presses on the entire canvas we need to *explicitly* track the
current slide. We need to this because we need to know which slide to navigate to when an
arrow key is pressed!
```tsx filename="App.tsx" {1,2,13-14,17,23}
import { useState, useCallback } from 'react';
import { ReactFlow, useReactFlow } from '@xyflow/react';
import { Slide, SlideData, SLIDE_WIDTH } from './Slide';
const slides: Record = {
...
}
const nodeTypes = {
slide: Slide,
};
const initialSlide = '0';
const { nodes, edges } = slidesToElements(initialSlide, slides)
export default function App() {
const [currentSlide, setCurrentSlide] = useState(initialSlide);
const { fitView } = useReactFlow();
const handleNodeClick = useCallback(
(_, node) => {
fitView({ nodes: [node] });
setCurrentSlide(node.id);
},
[fitView],
);
return (
);
}
```
Here we've added a bit of state, `currentSlide`, to our flow component and we're making
sure to update it whenever a node is clicked. Next, we'll write a callback to handle
keyboard events on the canvas:
```tsx filename="App.tsx"
export default function App() {
const [currentSlide, setCurrentSlide] = useState(initialSlide);
const { fitView } = useReactFlow();
...
const handleKeyPress = useCallback(
(event) => {
const slide = slides[currentSlide];
switch (event.key) {
case 'ArrowLeft':
case 'ArrowUp':
case 'ArrowDown':
case 'ArrowRight':
const direction = event.key.slice(5).toLowerCase();
const target = slide[direction];
if (target) {
event.preventDefault();
setCurrentSlide(target);
fitView({ nodes: [{ id: target }] });
}
}
},
[currentSlide, fitView],
);
return (
);
}
```
To save some typing we're extracting the direction from the key pressed - if the user
pressed `'ArrowLeft'` we'll get `'left'` and so on. Then, if there is actually a slide
connected in that direction we'll update the current slide and call `fitView` to navigate
to it!
We're also preventing the default behavior of the arrow keys to prevent the window from
scrolling up and down. This is necessary for this tutorial because the canvas is only one
part of the page, but for an app where the canvas is the entire viewport you might not
need to do this.
And that's everything! To recap let's look at the final result and talk about what we've
learned.
Example: tutorials/presentation/app
##### App.tsx
```tsx
import { type KeyboardEventHandler, useCallback, useState } from 'react';
import {
ReactFlow,
ReactFlowProvider,
useReactFlow,
Background,
BackgroundVariant,
type NodeMouseHandler,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { Slide, type SlideData } from './Slide';
import { slides, slidesToElements } from './slides';
const nodeTypes = {
slide: Slide,
};
const initialSlide = '01';
const { nodes, edges } = slidesToElements(initialSlide, slides);
function App() {
const [currentSlide, setCurrentSlide] = useState(initialSlide);
const { fitView } = useReactFlow();
const handleKeyPress = useCallback(
(event) => {
const slide = slides[currentSlide];
switch (event.key) {
case 'ArrowLeft':
case 'ArrowUp':
case 'ArrowDown':
case 'ArrowRight': {
const direction = event.key.slice(5).toLowerCase() as keyof SlideData;
const target = slide[direction];
// Prevent the arrow keys from scrolling the page when React Flow is
// only part of a larger application.
event.preventDefault();
if (target) {
setCurrentSlide(target);
fitView({ nodes: [{ id: target }], duration: 100 });
}
}
}
},
[fitView, currentSlide],
);
const handleNodeClick = useCallback(
(_, node) => {
if (node.id !== currentSlide) {
setCurrentSlide(node.id);
fitView({ nodes: [{ id: node.id }], duration: 100 });
}
},
[fitView, currentSlide],
);
return (
);
}
export default () => (
);
```
##### Slide.tsx
```tsx
import { type Node, type NodeProps, useReactFlow } from '@xyflow/react';
import { Remark } from 'react-remark';
import { useCallback } from 'react';
export type SlideNode = Node;
export type SlideData = {
source: string;
left?: string;
up?: string;
down?: string;
right?: string;
};
export const SLIDE_WIDTH = 1920;
export const SLIDE_HEIGHT = 1080;
// The padding constant is used when computing the presentation layout. It adds
// a bit of space between each slide
export const SLIDE_PADDING = 100;
const style = {
width: `${SLIDE_WIDTH}px`,
height: `${SLIDE_HEIGHT}px`,
} satisfies React.CSSProperties;
export function Slide({ data }: NodeProps) {
const { source, left, up, down, right } = data;
const { fitView } = useReactFlow();
const moveToNextSlide = useCallback(
(event: React.MouseEvent, id: string) => {
// Prevent the click event from propagating so `onNodeClick` is not
// triggered when clicking on the control buttons.
event.stopPropagation();
fitView({ nodes: [{ id }], duration: 100 });
},
[fitView],
);
return (
{source}
{left && moveToNextSlide(e, left)}>← }
{up && moveToNextSlide(e, up)}>↑ }
{down && moveToNextSlide(e, down)}>↓ }
{right && moveToNextSlide(e, right)}>→ }
);
}
```
##### index.css
```css
html,
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
body {
margin: 0;
background-color: #f8f8f8;
font-family: sans-serif;
color: #111;
}
html,
body,
#root {
height: 100%;
}
.slide {
box-sizing: border-box;
box-shadow: 0rem 1rem 4rem 0.25rem rgba(0, 0, 0, 0.06);
width: 1920px;
height: 1080px;
overflow: hidden;
position: relative;
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 6rem;
padding-top: 10rem;
padding-left: 8rem;
font-size: 4rem;
}
.slide h1 {
font-size: 8rem;
margin-bottom: 1rem;
}
.slide ul li {
margin-bottom: 1rem;
}
.slide__controls {
position: absolute;
bottom: 4rem;
right: 4rem;
display: flex;
justify-content: end;
gap: 1rem;
}
.slide__controls button {
font-size: 4rem;
padding: 1rem 2rem;
border-radius: 1rem;
border: 2px solid #000;
background-color: #fff;
color: #111;
&:hover {
background-color: #f8f8f8;
}
&:active {
background-color: #e0e0e0;
}
}
```
##### index.html
```html
React Flow Example
```
##### index.tsx
```tsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.querySelector('#app');
const root = createRoot(container);
root.render( );
```
##### slides.ts
```ts
import { type Edge, type Node } from '@xyflow/react';
import {
SLIDE_WIDTH,
SLIDE_HEIGHT,
SLIDE_PADDING,
type SlideData,
} from './Slide';
const slide01 = {
id: '01',
data: {
right: '02',
source: `
# Slide 1
- This is the first slide
- It has a right arrow to go to the next slide
`,
},
};
const slide02 = {
id: '02',
data: {
left: '01',
up: '03',
right: '04',
source: `
# Slide 2
- This is the second slide
- It has a left arrow to go back to the first slide
- It has an up arrow to go to the third slide
- It has a right arrow to go to the fourth slide
`,
},
};
const slide03 = {
id: '03',
data: {
down: '02',
source: `
# Slide 3
- This is the third slide
- It has a down arrow to go back to the second slide
`,
},
};
const slide04 = {
id: '04',
data: {
left: '02',
source: `
# Slide 4
- This is the fourth slide
- It has a left arrow to go back to the second slide
`,
},
};
export const slides = Object.fromEntries(
[slide01, slide02, slide03, slide04].map(({ id, data }) => [id, data]),
) as Record;
export const slidesToElements = (
initial: string,
slides: Record,
) => {
const stack = [{ id: initial, position: { x: 0, y: 0 } }];
const visited = new Set();
const nodes: Node[] = [];
const edges: Edge[] = [];
while (stack.length) {
const { id, position } = stack.pop()!;
const data = slides[id];
const node = { id, type: 'slide', position, data };
if (data.left && !visited.has(data.left)) {
const nextPosition = {
x: position.x - (SLIDE_WIDTH + SLIDE_PADDING),
y: position.y,
};
stack.push({ id: data.left, position: nextPosition });
edges.push({
id: `${id}->${data.left}`,
source: id,
target: data.left,
});
}
if (data.up && !visited.has(data.up)) {
const nextPosition = {
x: position.x,
y: position.y - (SLIDE_HEIGHT + SLIDE_PADDING),
};
stack.push({ id: data.up, position: nextPosition });
edges.push({ id: `${id}->${data.up}`, source: id, target: data.up });
}
if (data.down && !visited.has(data.down)) {
const nextPosition = {
x: position.x,
y: position.y + (SLIDE_HEIGHT + SLIDE_PADDING),
};
stack.push({ id: data.down, position: nextPosition });
edges.push({
id: `${id}->${data.down}`,
source: id,
target: data.down,
});
}
if (data.right && !visited.has(data.right)) {
const nextPosition = {
x: position.x + (SLIDE_WIDTH + SLIDE_PADDING),
y: position.y,
};
stack.push({ id: data.right, position: nextPosition });
edges.push({
id: `${id}->${data.down}`,
source: id,
target: data.right,
});
}
nodes.push(node);
visited.add(id);
}
return { nodes, edges };
};
```
#### Final thoughts
Even if you're not planning on making the next [Prezi](https://prezi.com), we've still
looked at a few useful features of React Flow in this tutorial:
* The [`useReactFlow`](/api-reference/hooks/use-react-flow) hook to access the `fitView`
method.
* The [`onNodeClick`](/api-reference/react-flow#on-node-click) event handler to listen to
clicks on every node in a flow.
* The [`onKeyPress`](/api-reference/react-flow#on-key-press) event handler to listen to
keyboard events on the entire canvas.
We've also looked at how to implement a simple layouting algorithm ourselves. Layouting is
a *really* common question we get asked about, but if your needs aren't that complex you
can get quite far rolling your own solution!
If you're looking for ideas on how to extend this project, you could try addressing the
issues we pointed out with the layouting algorithm, coming up with a more sophisticated
`Slide` component with different layouts, or something else entirely.
You can use the completed [source code](https://github.com/xyflow/react-flow-slide-show)
as a starting point, or you can just keep building on top of what we've made today. We'd
love to see what you build so please share it with us over on our
[Discord server](https://discord.com/invite/RVmnytFmGW) or
[Twitter](https://twitter.com/reactflowdev).
## UI
### React Flow UI
Ready-to-use React Flow components built with [shadcn/ui](https://ui.shadcn.com/)
components and [Tailwind CSS](https://tailwindcss.com/). Useful for new projects, MVPs, or
when you need to get up and running quickly.
> \[!NOTE]
>
> React Flow UI has been updated to support the latest version of shadcn/ui, on **React 19
> and Tailwind 4**! Read more about the changes and how to upgrade
> [here](/whats-new/2025-10-28).
#### Prerequisites
You need to have **shadcn and tailwind configured in your project**. If you haven't
installed it, you can follow the steps explained in the
[shadcn installation guide](https://ui.shadcn.com/docs/installation). If shadcn and
tailwind are part of your project, you can initialize shadcn-ui by running:
```bash copy npm2yarn
npx shadcn@latest init
```
If you want to learn more about the motivation behind this project, you can find a
detailed blog post [here](https://xyflow.com/blog/react-flow-components). For a more
in-depth tutorial, we also recently published a new guide on
[getting started with React Flow UI](/learn/tutorials/getting-started-with-react-flow-components).
> \[!IMPORTANT]
>
> Using React Flow UI components **requires importing the main React Flow CSS stylesheet**
> before you import the **shadcn UI stylesheet**. You usually do so in the Typescript or
> Javascript file where you use the main [``](/api-reference/react-flow)
> component.
```tsx filename="App.tsx"
import '@xyflow/react/dist/style.css';
```
#### Usage
Find a component you like and run the command to add it to your project.
```bash copy npm2yarn
npx shadcn@latest add https://ui.reactflow.dev/component-name
```
* This command copies the component code inside your components folder. You can change
this folder by adding an alias inside your `components.json`.
* It automatically installs all necessary dependencies
* It utilizes previously added and even modified components or asks you if you'd like to
overwrite them.
* It uses your existing tailwind configuration.
* The components are **not black-boxes** and can be **modified and extended** to fit your
needs.
For more information visit the [shadcn documentation](https://ui.shadcn.com/docs).
### Animated SVG Edge
An edge that animates a custom SVG element along the edge's path. This component
is based on the [animating SVG elements example](/examples/edges/animating-edges).
UI Component: animated-svg-edge
##### index.tsx
```tsx
import React from "react";
import type { Edge, EdgeProps, Position } from "@xyflow/react";
import {
BaseEdge,
getBezierPath,
getStraightPath,
getSmoothStepPath,
} from "@xyflow/react";
export type AnimatedSvgEdge = Edge<{
/**
* The amount of time it takes, in seconds, to move the shape one from end of
* the edge path to the other.
*/
duration: number;
/**
* The direction in which the shape moves along the edge path. Each value
* corresponds to the following behavior:
*
* - `forward`: The shape moves from the source node to the target node.
*
* - `reverse`: The shape moves from the target node to the source node.
*
* - `alternate`: The shape moves from the source node to the target node and
* then back to the source node.
*
* - `alternate-reverse`: The shape moves from the target node to the source
* node and then back to the target node.
*
* If not provided, this defaults to `"forward"`.
*/
direction?: "forward" | "reverse" | "alternate" | "alternate-reverse";
/**
* Which of React Flow's path algorithms to use. Each value corresponds to one
* of React Flow's built-in edge types.
*
* If not provided, this defaults to `"bezier"`.
*/
path?: "bezier" | "smoothstep" | "step" | "straight";
/**
* The number of times to repeat the animation before stopping. If set to
* `"indefinite"`, the animation will repeat indefinitely.
*
* If not provided, this defaults to `"indefinite"`.
*/
repeat?: number | "indefinite";
shape: keyof typeof shapes;
}>;
/**
* The `AnimatedSvgEdge` component renders a typical React Flow edge and animates
* an SVG shape along the edge's path.
*/
export function AnimatedSvgEdge({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
data = {
duration: 2,
direction: "forward",
path: "bezier",
repeat: "indefinite",
shape: "circle",
},
...delegated
}: EdgeProps) {
const Shape = shapes[data.shape];
const [path] = getPath({
type: data.path ?? "bezier",
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
const animateMotionProps = getAnimateMotionProps({
duration: data.duration,
direction: data.direction ?? "forward",
repeat: data.repeat ?? "indefinite",
path,
});
return (
<>
>
);
}
type AnimateMotionProps = {
dur: string;
keyTimes: string;
keyPoints: string;
repeatCount: number | "indefinite";
path: string;
};
type AnimatedSvg = ({
animateMotionProps,
}: {
animateMotionProps: AnimateMotionProps;
}) => React.ReactElement;
const shapes = {
circle: ({ animateMotionProps }) => (
),
package: ({ animateMotionProps }) => (
),
} satisfies Record;
/**
* Chooses which of React Flow's edge path algorithms to use based on the provided
* `type`.
*/
function getPath({
type,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
}: {
type: "bezier" | "smoothstep" | "step" | "straight";
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
sourcePosition: Position;
targetPosition: Position;
}) {
switch (type) {
case "bezier":
return getBezierPath({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
});
case "smoothstep":
return getSmoothStepPath({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
});
case "step":
return getSmoothStepPath({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
borderRadius: 0,
});
case "straight":
return getStraightPath({
sourceX,
sourceY,
targetX,
targetY,
});
}
}
/**
* Construct the props for an ` ` element based on an
* `AnimatedSvgEdge`'s data.
*/
function getAnimateMotionProps({
duration,
direction,
repeat,
path,
}: {
duration: number;
direction: "forward" | "reverse" | "alternate" | "alternate-reverse";
repeat: number | "indefinite";
path: string;
}) {
const base = {
path,
repeatCount: repeat,
// The default calcMode for the ` ` element is "paced", which
// is not compatible with the `keyPoints` attribute. Setting this to "linear"
// ensures that the shape correct follows the path.
calcMode: "linear",
};
switch (direction) {
case "forward":
return {
...base,
dur: `${duration}s`,
keyTimes: "0;1",
keyPoints: "0;1",
};
case "reverse":
return {
...base,
dur: `${duration}s`,
keyTimes: "0;1",
keyPoints: "1;0",
};
case "alternate":
return {
...base,
// By doubling the animation duration, the time spent moving from one end
// to the other remains consistent when switching between directions.
dur: `${duration * 2}s`,
keyTimes: "0;0.5;1",
keyPoints: "0;1;0",
};
case "alternate-reverse":
return {
...base,
dur: `${duration * 2}s`,
keyTimes: "0;0.5;1",
keyPoints: "1;0;1",
};
}
}
```
#### Custom shapes
It is intended that you add your own SVG shapes to the module. Each shape should
be a React component that takes one prop, `animateMotionProps`, and returns some
SVG.
You can define these shapes in a separate file or in the same file as the edge
component. In order to use them, you need to add them to the `shapes` record like
so:
```tsx
const shapes = {
box: ({ animateMotionProps }) => (
),
} satisfies Record;
```
The keys of the `shapes` record are valid values for the `shape` field of the
edge's data:
```ts
const initialEdges = [
{
// ...
type: "animatedSvgEdge",
data: {
duration: 2,
shape: "box",
},
} satisfies AnimatedSvgEdge,
];
```
If you want to render regular HTML elements, be sure to wrap them in an SVG
` ` element. Make sure to give the ` ` an `id`
attribute and use that as the `href` attribute when rendering the ` `
element.
### Base Handle
A handle with some basic styling used for creating a shared design among all handles in your application.
UI Component: base-handle
##### index.tsx
```tsx
import type { ComponentProps } from "react";
import { Handle, type HandleProps } from "@xyflow/react";
import { cn } from "@/lib/utils";
export type BaseHandleProps = HandleProps;
export function BaseHandle({
className,
children,
...props
}: ComponentProps) {
return (
{children}
);
}
```
##### component-example.tsx
```tsx
import React, { memo } from "react";
import { NodeProps, Position } from "@xyflow/react";
import { BaseHandle } from "@/registry/components/base-handle";
import { BaseNode, BaseNodeContent } from "@/registry/components/base-node";
const BaseHandleDemo = memo(() => {
return (
A node with two handles
);
});
export default BaseHandleDemo;
```
### Base Node
A node wrapper with some basic styling used for creating a shared design among all nodes in your application.
Similarly to [shadcn ui's card](https://ui.shadcn.com/docs/components/card) the components file exports:
* The `BaseNode` main container,
* The `BaseNodeHeader` container where you would usually add actions and a `BaseNodeHeaderTitle`
* The `BaseNodeContent` container where you would add the main contents of the node.
* The `BaseNodeFooter` container where you may want to add extra information, or visible actions.
In case you need to fine-tune how interactions like dragging and scrolling work with your custom components,
React Flow provides [several CSS utility classes](/learn/customization/utility-classes)
You should use the `nodrag` [React Flow utility class](/learn/customization/utility-classes)
in interactive components of your node such as buttons, to disable dragging
the node inside the flow when the user is interacting with buttons or sliders.
UI Component: base-node
##### index.tsx
```tsx
import type { ComponentProps } from "react";
import { cn } from "@/lib/utils";
export function BaseNode({ className, ...props }: ComponentProps<"div">) {
return (
);
}
/**
* A container for a consistent header layout intended to be used inside the
* ` ` component.
*/
export function BaseNodeHeader({
className,
...props
}: ComponentProps<"header">) {
return (
` component.
className,
)}
/>
);
}
/**
* The title text for the node. To maintain a native application feel, the title
* text is not selectable.
*/
export function BaseNodeHeaderTitle({
className,
...props
}: ComponentProps<"h3">) {
return (
);
}
export function BaseNodeContent({
className,
...props
}: ComponentProps<"div">) {
return (
);
}
export function BaseNodeFooter({ className, ...props }: ComponentProps<"div">) {
return (
);
}
```
##### component-example.tsx
```tsx
import { memo } from "react";
import { Button } from "@/components/ui/button";
import {
BaseNode,
BaseNodeContent,
BaseNodeFooter,
BaseNodeHeader,
BaseNodeHeaderTitle,
} from "@/registry/components/base-node";
import { Rocket } from "lucide-react";
export const BaseNodeFullDemo = memo(() => {
return (
Header
Content
This is a full-featured node with a header, content, and footer. You
can customize it as needed.
Footer
Action 1
);
});
BaseNodeFullDemo.displayName = "BaseNodeFullDemo";
```
#### Theming
To customize the visual appearance of your custom nodes, you can simply use
[Tailwind CSS](https://tailwindcss.com/) classes. All of the React Flow
components are based on [shadcn UI](https://ui.shadcn.com/), and you should follow
the [shadcn UI theming guide](https://ui.shadcn.com/docs/theming) to customize aspects like typography and colors
in your application.
In most occasions though, when developing custom nodes, you may simply need to
add custom Tailwind CSS classes. All of the `BaseNode` components are just light wrappers around ``.
For example, to change the border color of a node, based on an hypothetical execution status,
you can pass extra `className`s:
```tsx
// Assuming your component is receiving a `data` prop
export const BaseNodeSimpleDemo = memo(({ data }: NodeProps) => {
return (
{/* Your custom node definiton goes here */}
);
});
```
### Button Edge
An edge with a button that can be used to trigger a custom action.
UI Component: button-edge
##### index.tsx
```tsx
import { type ReactNode } from "react";
import {
BaseEdge,
EdgeLabelRenderer,
getBezierPath,
type EdgeProps,
} from "@xyflow/react";
export function ButtonEdge({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
markerEnd,
children,
}: EdgeProps & { children: ReactNode }) {
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
return (
<>
{children}
>
);
}
```
##### component-example.tsx
```tsx
import { EdgeProps } from "@xyflow/react";
import { memo } from "react";
import { Button } from "@/components/ui/button";
import { MousePointerClick } from "lucide-react";
import { ButtonEdge } from "@/registry/components/button-edge";
const ButtonEdgeDemo = memo((props: EdgeProps) => {
const onEdgeClick = () => {
window.alert(`Edge has been clicked!`);
};
return (
);
});
export default ButtonEdgeDemo;
```
### Button Handle
A handle component with a button attached.
UI Component: button-handle
##### index.tsx
```tsx
import { Position, type HandleProps } from "@xyflow/react";
import { BaseHandle } from "@/registry/components/base-handle";
const wrapperClassNames: Record
= {
[Position.Top]:
"flex-col-reverse left-1/2 -translate-y-full -translate-x-1/2",
[Position.Bottom]: "flex-col left-1/2 translate-y-[10px] -translate-x-1/2",
[Position.Left]:
"flex-row-reverse top-1/2 -translate-x-full -translate-y-1/2",
[Position.Right]: "top-1/2 -translate-y-1/2 translate-x-[10px]",
};
export function ButtonHandle({
showButton = true,
position = Position.Bottom,
children,
...props
}: HandleProps & { showButton?: boolean }) {
const wrapperClassName = wrapperClassNames[position || Position.Bottom];
const vertical = position === Position.Top || position === Position.Bottom;
return (
{showButton && (
)}
);
}
```
##### component-example.tsx
```tsx
import { Plus } from "lucide-react";
import { ConnectionState, Position, useConnection } from "@xyflow/react";
import { ButtonHandle } from "@/registry/components/button-handle";
import { BaseNode, BaseNodeContent } from "@/registry/components/base-node";
import { Button } from "@/components/ui/button";
const onClick = () => {
window.alert(`Handle button has been clicked!`);
};
const selector = (connection: ConnectionState) => {
return connection.inProgress;
};
const ButtonHandleDemo = () => {
const connectionInProgress = useConnection(selector);
return (
Node with a handle button
);
};
export default ButtonHandleDemo;
```
### Data Edge
An edge that displays one field from the source node's `data` object.
UI Component: data-edge
##### index.tsx
```tsx
import { useMemo } from "react";
import {
BaseEdge,
EdgeLabelRenderer,
getBezierPath,
getSmoothStepPath,
getStraightPath,
Position,
useStore,
type Edge,
type EdgeProps,
type Node,
} from "@xyflow/react";
export type DataEdge = Edge<{
/**
* The key to lookup in the source node's `data` object. For additional safety,
* you can parameterize the `DataEdge` over the type of one of your nodes to
* constrain the possible values of this key.
*
* If no key is provided this edge behaves identically to React Flow's default
* edge component.
*/
key?: keyof T["data"];
/**
* Which of React Flow's path algorithms to use. Each value corresponds to one
* of React Flow's built-in edge types.
*
* If not provided, this defaults to `"bezier"`.
*/
path?: "bezier" | "smoothstep" | "step" | "straight";
}>;
export function DataEdge({
data = { path: "bezier" },
id,
markerEnd,
source,
sourcePosition,
sourceX,
sourceY,
style,
targetPosition,
targetX,
targetY,
}: EdgeProps) {
const nodeData = useStore((state) => state.nodeLookup.get(source)?.data);
const [edgePath, labelX, labelY] = getPath({
type: data.path ?? "bezier",
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
const label = useMemo(() => {
if (data.key && nodeData) {
const value = nodeData[data.key];
switch (typeof value) {
case "string":
case "number":
return value;
case "object":
return JSON.stringify(value);
default:
return "";
}
}
}, [data, nodeData]);
const transform = `translate(${labelX}px,${labelY}px) translate(-50%, -50%)`;
return (
<>
{data.key && (
)}
>
);
}
/**
* Chooses which of React Flow's edge path algorithms to use based on the provided
* `type`.
*/
function getPath({
type,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
}: {
type: "bezier" | "smoothstep" | "step" | "straight";
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
sourcePosition: Position;
targetPosition: Position;
}) {
switch (type) {
case "bezier":
return getBezierPath({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
});
case "smoothstep":
return getSmoothStepPath({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
});
case "step":
return getSmoothStepPath({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
borderRadius: 0,
});
case "straight":
return getStraightPath({
sourceX,
sourceY,
targetX,
targetY,
});
}
}
```
##### component-example.tsx
```tsx
import { Handle, NodeProps, Position, useReactFlow, Node } from "@xyflow/react";
import { memo } from "react";
import { BaseNode, BaseNodeContent } from "@/registry/components/base-node";
import { Slider } from "@/components/ui/slider";
export type CounterNodeType = Node<{ value: number }>;
export const CounterNode = memo(({ id, data }: NodeProps) => {
const { updateNodeData } = useReactFlow();
return (
{
updateNodeData(id, (node) => ({
...node.data,
value,
}));
}}
/>
);
});
```
#### Additional type safety
When creating new edges of this type, you can use TypeScript's `satisfies` predicate
along with the specific type of a node in your application to ensure the `key`
property of the edge's data is a valid key of the node's data.
```ts
type CounterNode = Node<{ count: number }>;
const initialEdges = [
{
id: 'edge-1',
source: 'node-1',
target: 'node-2',
type: 'dataEdge',
data: {
key: 'count',
} satisfies DataEdge,
},
];
```
If you try to use a key that is not present in the node's data, TypeScript will
show an error message like:
> ts: Type '"value"' is not assignable to type '"count"'.
### Database Schema Node
A node that can be used to visualize a database schema.
UI Component: database-schema-node
##### index.tsx
```tsx
import React, { type ReactNode } from "react";
import {
BaseNode,
BaseNodeContent,
BaseNodeHeader,
} from "@/registry/components/base-node";
import { TableBody, TableRow, TableCell } from "@/components/ui/table";
/* DATABASE SCHEMA NODE HEADER ------------------------------------------------ */
/**
* A container for the database schema node header.
*/
export type DatabaseSchemaNodeHeaderProps = {
children?: ReactNode;
};
export const DatabaseSchemaNodeHeader = ({
children,
}: DatabaseSchemaNodeHeaderProps) => {
return (
{children}
);
};
/* DATABASE SCHEMA NODE BODY -------------------------------------------------- */
/**
* A container for the database schema node body that wraps the table.
*/
export type DatabaseSchemaNodeBodyProps = {
children?: ReactNode;
};
export const DatabaseSchemaNodeBody = ({
children,
}: DatabaseSchemaNodeBodyProps) => {
return (
);
};
/* DATABASE SCHEMA TABLE ROW -------------------------------------------------- */
/**
* A wrapper for individual table rows in the database schema node.
*/
export type DatabaseSchemaTableRowProps = {
children: ReactNode;
className?: string;
};
export const DatabaseSchemaTableRow = ({
children,
className,
}: DatabaseSchemaTableRowProps) => {
return (
{children}
);
};
/* DATABASE SCHEMA TABLE CELL ------------------------------------------------- */
/**
* A simplified table cell for the database schema node.
* Renders static content without additional dynamic props.
*/
export type DatabaseSchemaTableCellProps = {
className?: string;
children?: ReactNode;
};
export const DatabaseSchemaTableCell = ({
className,
children,
}: DatabaseSchemaTableCellProps) => {
return {children} ;
};
/* DATABASE SCHEMA NODE ------------------------------------------------------- */
/**
* The main DatabaseSchemaNode component that wraps the header and body.
* It maps over the provided schema data to render rows and cells.
*/
export type DatabaseSchemaNodeProps = {
className?: string;
children?: ReactNode;
};
export const DatabaseSchemaNode = ({
className,
children,
}: DatabaseSchemaNodeProps) => {
return {children} ;
};
```
##### component-example.tsx
```tsx
import { memo } from "react";
import { Position } from "@xyflow/react";
import { LabeledHandle } from "@/registry/components/labeled-handle";
import {
DatabaseSchemaNode,
DatabaseSchemaNodeHeader,
DatabaseSchemaNodeBody,
DatabaseSchemaTableRow,
DatabaseSchemaTableCell,
} from "@/registry/components/database-schema-node";
export type DatabaseSchemaNodeData = {
data: {
label: string;
schema: { title: string; type: string }[];
};
};
const DatabaseSchemaDemo = memo(({ data }: DatabaseSchemaNodeData) => {
return (
{data.label}
{data.schema.map((entry) => (
))}
);
});
export default DatabaseSchemaDemo;
```
### DevTools
### DevTools
A debugging tool that provides data on the viewport, the state of each node, and logs change events. This component
is based on [DevTools and Debugging](/learn/advanced-use/devtools-and-debugging) under Advanced Use.
You can import the entire ` ` component, or optionally, import individual components for greater flexibility. These components include:
* A ` ` component that shows the current position and zoom level of the viewport.
* A ` ` component that reveals the state of each node.
* A ` ` that wraps your flow’s onNodesChange handler and logs each change as it is dispatched.
You can read more about the individual components at [DevTools and Debugging](/learn/advanced-use/devtools-and-debugging). While we find these tools useful for making sure React Flow is working properly, you might also find them useful for debugging your applications as your flows and their interactions become more complex.
UI Component: devtools
##### index.tsx
```tsx
"use client";
import {
useEffect,
useState,
useCallback,
type Dispatch,
type SetStateAction,
} from "react";
import {
useNodes,
Panel,
useStore,
useStoreApi,
ViewportPortal,
useReactFlow,
PanelPosition,
type OnNodesChange,
type NodeChange,
type XYPosition,
} from "@xyflow/react";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
export const ViewportLogger = () => {
const viewport = useStore(
(s) =>
`x: ${s.transform[0].toFixed(2)}, y: ${s.transform[1].toFixed(2)}, zoom: ${s.transform[2].toFixed(2)}`,
);
return {viewport}
;
};
type ChangeLoggerProps = {
color?: string;
limit?: number;
};
type ChangeInfoProps = {
change: NodeChange;
};
const ChangeInfo = ({ change }: ChangeInfoProps) => {
const id = "id" in change ? change.id : "-";
const { type } = change;
return (
node id: {id}
{type === "add" ? JSON.stringify(change.item, null, 2) : null}
{type === "dimensions"
? `dimensions: ${change.dimensions?.width} × ${change.dimensions?.height}`
: null}
{type === "position"
? `position: ${change.position?.x.toFixed(1)}, ${change.position?.y.toFixed(1)}`
: null}
{type === "remove" ? "remove" : null}
{type === "select" ? (change.selected ? "select" : "unselect") : null}
);
};
export const ChangeLogger = ({ limit = 20 }: ChangeLoggerProps) => {
const [changes, setChanges] = useState([]);
const store = useStoreApi();
// Memoize the callback for handling node changes
const handleNodeChanges: OnNodesChange = useCallback(
(newChanges: NodeChange[]) => {
setChanges((prevChanges) =>
[...newChanges, ...prevChanges].slice(0, limit),
);
},
[limit],
);
useEffect(() => {
store.setState({ onNodesChange: handleNodeChanges });
return () => store.setState({ onNodesChange: undefined });
}, [handleNodeChanges, store]);
const NoChanges = () => No Changes Triggered
;
return (
<>
{changes.length === 0 ? (
) : (
changes.map((change, index) => (
))
)}
>
);
};
export const NodeInspector = () => {
const { getInternalNode } = useReactFlow();
const nodes = useNodes();
return (
{nodes.map((node) => {
const internalNode = getInternalNode(node.id);
if (!internalNode) {
return null;
}
const absPosition = internalNode?.internals.positionAbsolute;
return (
);
})}
);
};
type NodeInfoProps = {
id: string;
type: string;
selected: boolean;
position: XYPosition;
absPosition: XYPosition;
width?: number;
height?: number;
data: any;
};
const NodeInfo = ({
id,
type,
selected,
position,
absPosition,
width,
height,
data,
}: NodeInfoProps) => {
if (!width || !height) return null;
const absoluteTransform = `translate(${absPosition.x}px, ${absPosition.y + height}px)`;
const formattedPosition = `${position.x.toFixed(1)}, ${position.y.toFixed(1)}`;
const formattedDimensions = `${width} × ${height}`;
const selectionStatus = selected ? "Selected" : "Not Selected";
return (
id: {id}
type: {type}
selected: {selectionStatus}
position: {formattedPosition}
dimensions: {formattedDimensions}
data: {JSON.stringify(data, null, 2)}
);
};
type Tool = {
active: boolean;
setActive: Dispatch>;
label: string;
value: string;
};
type DevToolsToggleProps = {
tools: Tool[];
position: PanelPosition;
};
const DevToolsToggle = ({ tools, position }: DevToolsToggleProps) => {
return (
{tools.map(({ active, setActive, label, value }) => (
setActive((prev) => !prev)}
aria-pressed={active}
className="bg-card text-card-foreground hover:bg-secondary hover:text-secondary-foreground transition-colors duration-300"
>
{label}
))}
);
};
type DevToolsProps = {
position: PanelPosition;
};
export const DevTools = ({ position }: DevToolsProps) => {
const [nodeInspectorActive, setNodeInspectorActive] = useState(false);
const [changeLoggerActive, setChangeLoggerActive] = useState(false);
const [viewportLoggerActive, setViewportLoggerActive] = useState(false);
const tools = [
{
active: nodeInspectorActive,
setActive: setNodeInspectorActive,
label: "Node Inspector",
value: "node-inspector",
},
{
active: changeLoggerActive,
setActive: setChangeLoggerActive,
label: "Change Logger",
value: "change-logger",
},
{
active: viewportLoggerActive,
setActive: setViewportLoggerActive,
label: "Viewport Logger",
value: "viewport-logger",
},
];
return (
<>
{changeLoggerActive && (
)}
{nodeInspectorActive && }
{viewportLoggerActive && (
)}
>
);
};
DevTools.displayName = "DevTools";
```
### Labeled Group Node
A group node with an optional label.
UI Component: labeled-group-node
##### index.tsx
```tsx
import React, { type ReactNode, type ComponentProps } from "react";
import { Panel, type NodeProps, type PanelPosition } from "@xyflow/react";
import { BaseNode } from "@/registry/components/base-node";
import { cn } from "@/lib/utils";
/* GROUP NODE Label ------------------------------------------------------- */
export type GroupNodeLabelProps = ComponentProps<"div">;
export function GroupNodeLabel({
children,
className,
...props
}: GroupNodeLabelProps) {
return (
);
}
export type GroupNodeProps = Partial & {
label?: ReactNode;
position?: PanelPosition;
};
/* GROUP NODE -------------------------------------------------------------- */
export function GroupNode({ label, position, ...props }: GroupNodeProps) {
const getLabelClassName = (position?: PanelPosition) => {
switch (position) {
case "top-left":
return "rounded-br-sm";
case "top-center":
return "rounded-b-sm";
case "top-right":
return "rounded-bl-sm";
case "bottom-left":
return "rounded-tr-sm";
case "bottom-right":
return "rounded-tl-sm";
case "bottom-center":
return "rounded-t-sm";
default:
return "rounded-br-sm";
}
};
return (
{label && (
{label}
)}
);
}
```
##### component-example.tsx
```tsx
import { memo } from "react";
import { GroupNode } from "@/registry/components/labeled-group-node";
const LabeledGroupNodeDemo = memo(() => );
export default LabeledGroupNodeDemo;
```
### Labeled Handle
A handle with a label that can be used to display additional information.
UI Component: labeled-handle
##### index.tsx
```tsx
import React, { type ComponentProps } from "react";
import { type HandleProps } from "@xyflow/react";
import { cn } from "@/lib/utils";
import { BaseHandle } from "@/registry/components/base-handle";
const flexDirections = {
top: "flex-col",
right: "flex-row-reverse justify-end",
bottom: "flex-col-reverse justify-end",
left: "flex-row",
};
export function LabeledHandle({
className,
labelClassName,
handleClassName,
title,
position,
...props
}: HandleProps &
ComponentProps<"div"> & {
title: string;
handleClassName?: string;
labelClassName?: string;
}) {
const { ref, ...handleProps } = props;
return (
{title}
);
}
```
##### component-example.tsx
```tsx
import React, { memo } from "react";
import { NodeProps, Position } from "@xyflow/react";
import { LabeledHandle } from "@/registry/components/labeled-handle";
import { BaseNode } from "@/registry/components/base-node";
const LabeledHandleDemo = memo(() => {
return (
);
});
export default LabeledHandleDemo;
```
### Node Appendix
A wrapper component for dynamically appending information to nodes in an absolutely positioned container.
UI Component: node-appendix
##### index.tsx
```tsx
import type { ComponentProps } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const appendixVariants = cva(
"node-appendix absolute flex w-full flex-col items-center rounded-md border bg-card p-1 text-card-foreground",
{
variants: {
position: {
top: "-translate-y-full -my-1",
bottom: "top-full my-1",
left: "-left-full -mx-1",
right: "left-full mx-1",
},
},
defaultVariants: {
position: "top",
},
},
);
export interface NodeAppendixProps
extends ComponentProps<"div">,
VariantProps {
className?: string;
position?: "top" | "bottom" | "left" | "right";
}
export function NodeAppendix({
children,
className,
position,
...props
}: NodeAppendixProps) {
return (
{children}
);
}
```
##### component-example.tsx
```tsx
import { NodeAppendix } from "@/registry/components/node-appendix";
import {
BaseNode,
BaseNodeContent,
BaseNodeHeader,
BaseNodeHeaderTitle,
} from "../base-node";
export const NodeAppendixDemo = () => {
return (
Add custom content to the node appendix.
Custom Node
Node Content goes here.
);
};
```
### Node Search
A search bar component that can be used to search for nodes in the flow.
It uses the [Command](https://ui.shadcn.com/docs/components/command) component from [shadcn ui](https://ui.shadcn.com).
By default, it will check for lowercase string inclusion in the node's label, and
select the node and fit the view to the node when it is selected.
You can override this behavior by passing a custom `onSearch` function.
You can also override the default `onSelectNode` function to customize the behavior when a node is selected.
UI Component: node-search
##### index.tsx
```tsx
import { useCallback, useState } from "react";
import {
BuiltInEdge,
useReactFlow,
type Node,
type PanelProps,
} from "@xyflow/react";
import {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
export interface NodeSearchProps extends Omit {
// The function to search for nodes, should return an array of nodes that match the search string
// By default, it will check for lowercase string inclusion.
onSearch?: (searchString: string) => Node[];
// The function to select a node, should set the node as selected and fit the view to the node
// By default, it will set the node as selected and fit the view to the node.
onSelectNode?: (node: Node) => void;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
export function NodeSearchInternal({
className,
onSearch,
onSelectNode,
open,
onOpenChange,
...props
}: NodeSearchProps) {
const [searchResults, setSearchResults] = useState([]);
const [searchString, setSearchString] = useState("");
const { getNodes, fitView, setNodes } = useReactFlow();
const defaultOnSearch = useCallback(
(searchString: string) => {
const nodes = getNodes();
return nodes.filter((node) =>
(node.data.label as string)
.toLowerCase()
.includes(searchString.toLowerCase()),
);
},
[getNodes],
);
const onChange = useCallback(
(searchString: string) => {
setSearchString(searchString);
if (searchString.length > 0) {
onOpenChange?.(true);
const results = (onSearch || defaultOnSearch)(searchString);
setSearchResults(results);
}
},
[onSearch, onOpenChange],
);
const defaultOnSelectNode = useCallback(
(node: Node) => {
setNodes((nodes) =>
nodes.map((n) => (n.id === node.id ? { ...n, selected: true } : n)),
);
fitView({ nodes: [node], duration: 500 });
},
[fitView, setNodes],
);
const onSelect = useCallback(
(node: Node) => {
(onSelectNode || defaultOnSelectNode)?.(node);
setSearchString("");
onOpenChange?.(false);
},
[onSelectNode, defaultOnSelectNode, onOpenChange],
);
return (
<>
onOpenChange?.(true)}
/>
{open && (
{searchResults.length === 0 ? (
No results found. {searchString}
) : (
{searchResults.map((node) => {
return (
onSelect(node)}>
{node.data.label as string}
);
})}
)}
)}
>
);
}
export function NodeSearch({
className,
onSearch,
onSelectNode,
...props
}: NodeSearchProps) {
const [open, setOpen] = useState(false);
return (
);
}
export interface NodeSearchDialogProps extends NodeSearchProps {
title?: string;
}
export function NodeSearchDialog({
className,
onSearch,
onSelectNode,
open,
onOpenChange,
title = "Node Search",
...props
}: NodeSearchDialogProps) {
return (
);
}
```
### Node Status Indicator
A node wrapper that has multiple states for indicating the status of a node. Status can be one of the following: `"success"`, `"loading"`, `"error"` and `"initial"`.
Additionally, the `NodeStatusIndicator` component supports different loading variants: `"border"` and `"overlay"`, which can be set using the `loadingVariant` prop.
* The `"border"` variant is the default and shows a spinning border around the node when it is in loading state.
* The `"overlay"` variant shows a full overlay, with an animated spinner on the node when it is in loading state.
UI Component: node-status-indicator
##### index.tsx
```tsx
import { type ReactNode } from "react";
import { LoaderCircle } from "lucide-react";
import { cn } from "@/lib/utils";
export type NodeStatus = "loading" | "success" | "error" | "initial";
export type NodeStatusVariant = "overlay" | "border";
export type NodeStatusIndicatorProps = {
status?: NodeStatus;
variant?: NodeStatusVariant;
children: ReactNode;
};
export const SpinnerLoadingIndicator = ({
children,
}: {
children: ReactNode;
}) => {
return (
);
};
export const BorderLoadingIndicator = ({
children,
}: {
children: ReactNode;
}) => {
return (
<>
{children}
>
);
};
const StatusBorder = ({
children,
className,
}: {
children: ReactNode;
className?: string;
}) => {
return (
<>
{children}
>
);
};
export const NodeStatusIndicator = ({
status,
variant = "border",
children,
}: NodeStatusIndicatorProps) => {
switch (status) {
case "loading":
switch (variant) {
case "overlay":
return {children} ;
case "border":
return {children} ;
default:
return <>{children}>;
}
case "success":
return (
{children}
);
case "error":
return {children} ;
default:
return <>{children}>;
}
};
```
##### component-example.tsx
```tsx
import { BaseNode, BaseNodeContent } from "@/registry/components/base-node";
import { NodeStatusIndicator } from "@/registry/components/node-status-indicator";
export const LoadingNode = () => {
return (
This node is loading...
);
};
```
### Node Tooltip
A wrapper for node components that displays a tooltip when hovered.
Built on top of the [NodeToolbar](/api-reference/components/node-toolbar) component that comes with React Flow.
UI Component: node-tooltip
##### index.tsx
```tsx
"use client";
import React, {
createContext,
useCallback,
useContext,
useState,
type ComponentProps,
} from "react";
import { NodeToolbar, type NodeToolbarProps } from "@xyflow/react";
import { cn } from "@/lib/utils";
/* TOOLTIP CONTEXT ---------------------------------------------------------- */
type TooltipContextType = {
isVisible: boolean;
showTooltip: () => void;
hideTooltip: () => void;
};
const TooltipContext = createContext(null);
/* TOOLTIP NODE ------------------------------------------------------------- */
export function NodeTooltip({ children }: ComponentProps<"div">) {
const [isVisible, setIsVisible] = useState(false);
const showTooltip = useCallback(() => setIsVisible(true), []);
const hideTooltip = useCallback(() => setIsVisible(false), []);
return (
{children}
);
}
/* TOOLTIP TRIGGER ---------------------------------------------------------- */
export function NodeTooltipTrigger(props: ComponentProps<"div">) {
const tooltipContext = useContext(TooltipContext);
if (!tooltipContext) {
throw new Error("NodeTooltipTrigger must be used within NodeTooltip");
}
const { showTooltip, hideTooltip } = tooltipContext;
const onMouseEnter = useCallback(
(e: React.MouseEvent) => {
props.onMouseEnter?.(e);
showTooltip();
},
[props, showTooltip],
);
const onMouseLeave = useCallback(
(e: React.MouseEvent) => {
props.onMouseLeave?.(e);
hideTooltip();
},
[props, hideTooltip],
);
return (
);
}
/* TOOLTIP CONTENT ---------------------------------------------------------- */
// /**
// * A component that displays the tooltip content based on visibility context.
// */
export function NodeTooltipContent({
children,
position,
className,
...props
}: NodeToolbarProps) {
const tooltipContext = useContext(TooltipContext);
if (!tooltipContext) {
throw new Error("NodeTooltipContent must be used within NodeTooltip");
}
const { isVisible } = tooltipContext;
return (
{children}
);
}
```
##### component-example.tsx
```tsx
import React, { memo } from "react";
import { Position } from "@xyflow/react";
import {
NodeTooltip,
NodeTooltipContent,
NodeTooltipTrigger,
} from "@/registry/components/node-tooltip";
import { BaseNode, BaseNodeContent } from "../base-node";
const NodeTooltipDemo = memo(() => {
return (
You can display any content here, like text, images, or even components.{" "}
The tooltip will appear when you hover over the trigger.
Hover me! ⭐️
You can add more content that does not trigger the tooltip.
);
});
export default NodeTooltipDemo;
```
### Placeholder Node
A custom node that can be clicked to create a new node.
UI Component: placeholder-node
##### index.tsx
```tsx
"use client";
import React, { useCallback, type ReactNode } from "react";
import {
useReactFlow,
useNodeId,
Handle,
Position,
type NodeProps,
} from "@xyflow/react";
import { BaseNode } from "@/registry/components/base-node";
export type PlaceholderNodeProps = Partial & {
children?: ReactNode;
};
export function PlaceholderNode({ children }: PlaceholderNodeProps) {
const id = useNodeId();
const { setNodes, setEdges } = useReactFlow();
const handleClick = useCallback(() => {
if (!id) return;
setEdges((edges) =>
edges.map((edge) =>
edge.target === id ? { ...edge, animated: false } : edge,
),
);
setNodes((nodes) => {
const updatedNodes = nodes.map((node) => {
if (node.id === id) {
// Customize this function to update the node's data as needed.
// For example, you can change the label or other properties of the node.
return {
...node,
data: { ...node.data, label: "Node" },
type: "default",
};
}
return node;
});
return updatedNodes;
});
}, [id, setEdges, setNodes]);
return (
{children}
);
}
```
##### component-example.tsx
```tsx
import { memo } from "react";
import { PlaceholderNode } from "@/registry/components/placeholder-node";
const PlaceholderNodeDemo = memo(() => {
return (
+
);
});
export default PlaceholderNodeDemo;
```
### Zoom Select
A zoom control that lets you zoom in and out seamlessly using a select dropdown.
UI Component: zoom-select
##### index.tsx
```tsx
"use client";
import React, { useCallback } from "react";
import { Panel, useReactFlow, useStore, type PanelProps } from "@xyflow/react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
export function ZoomSelect({
className,
...props
}: Omit) {
const { zoomTo, fitView } = useReactFlow();
const handleZoomChange = useCallback(
(value: string) => {
if (value === "best-fit") {
fitView();
} else {
const zoomValue = parseFloat(value);
if (!isNaN(zoomValue)) {
zoomTo(zoomValue);
}
}
},
[fitView, zoomTo],
);
const zoomLevels = useStore((state) => {
const { minZoom, maxZoom } = state;
const levels = [];
const zoomIncrement = 50;
for (
let i = Math.ceil(minZoom * 100);
i <= Math.floor(maxZoom * 100);
i += zoomIncrement
) {
levels.push((i / 100).toString());
}
return levels;
});
return (
Best Fit
{zoomLevels.map((level) => (
{`${(parseFloat(level) * 100).toFixed(0)}%`}
))}
);
}
```
### Zoom Slider
A zoom control that lets you zoom in and out seamlessly using a slider.
UI Component: zoom-slider
##### index.tsx
```tsx
"use client";
import React from "react";
import { Maximize, Minus, Plus } from "lucide-react";
import {
Panel,
useViewport,
useStore,
useReactFlow,
type PanelProps,
} from "@xyflow/react";
import { Slider } from "@/components/ui/slider";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
export function ZoomSlider({
className,
orientation = "horizontal",
...props
}: Omit & {
orientation?: "horizontal" | "vertical";
}) {
const { zoom } = useViewport();
const { zoomTo, zoomIn, zoomOut, fitView } = useReactFlow();
const minZoom = useStore((state) => state.minZoom);
const maxZoom = useStore((state) => state.maxZoom);
return (
zoomOut({ duration: 300 })}
>
zoomTo(values[0])}
/>
zoomIn({ duration: 300 })}
>
zoomTo(1, { duration: 300 })}
>
{(100 * zoom).toFixed(0)}%
fitView({ duration: 300 })}
>
);
}
```
### AI Workflow Editor
### AI Workflow Editor
The AI Workflow Editor template is a Next.js template for getting started quickly with an
AI workflow app. It's based on our
[workflow editor template](/ui/templates/workflow-editor), [AI SDK](https://ai-sdk.dev/)
and [shadcn/ui](https://ui.shadcn.com/).
! THIS IS A PRO EXAMPLE. SUBSCRIBE TO TO ACCESS PRO EXAMPLES !
#### Tech Stack
* **React Flow UI**: The project uses [React Flow UI](/ui) for the nodes. These components
are designed to help you quickly get up to speed on projects.
* **shadcn CLI**: The project uses [shadcn CLI](https://ui.shadcn.com/docs/cli) to manage
UI components. This tool builds on top of [Tailwind CSS](https://tailwindcss.com/) and
[shadcn/ui](https://ui.shadcn.com/) components, making it easy to add and customize UI
elements.
* **AI SDK**: The template uses [AI SDK](https://ai-sdk.dev/) to provide AI-powered
features within the workflow editor.
* **State Management with Zustand**: The AI workflow editor template uses Zustand for
state management, providing a simple and efficient way to manage the state of nodes,
edges, and other workflow-related data.
### Workflow Editor
### Workflow Editor
The Workflow Editor template is a Next.js-based application designed to help you quickly
create, manage, and visualize workflows. Built with [React Flow UI](/ui) and styled using
[Tailwind CSS](https://tailwindcss.com/) and [shadcn/ui](https://ui.shadcn.com/), this
project provides a highly customizable foundation for building and extending workflow
editors.
! THIS IS A PRO EXAMPLE. SUBSCRIBE TO TO ACCESS PRO EXAMPLES !
#### Tech Stack
* **React Flow UI**: The project uses [React Flow UI](/ui) to build nodes. These
components are designed to help you quickly get up to speed on projects.
* **shadcn CLI**: The project uses the [shadcn CLI](https://ui.shadcn.com/docs/cli) to
manage UI components. This tool builds on top of
[Tailwind CSS](https://tailwindcss.com/) and [shadcn/ui](https://ui.shadcn.com/)
components, making it easy to add and customize UI elements.
* **State Management with Zustand**: The application uses Zustand for state management,
providing a simple and efficient way to manage the state of nodes, edges, and other
workflow-related data.
#### Features
* **Automatic Layouting**: Utilizes the [ELKjs](https://github.com/kieler/elkjs) layout
engine to automatically arrange nodes and edges.
* **Drag-and-Drop Sidebar**: Add and arrange nodes using a drag-and-drop mechanism.
* **Customizable Components**: Uses React Flow UI and the shadcn library to create
highly-customizable nodes and edges.
* **Dark Mode**: Toggles between light and dark themes, managed through the Zustand store.
* **Runner Functionality**: Executes and monitors nodes sequentially with a workflow
runner.