Skip to Content
LearnAdvanced UseWhiteboard Features

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

🎯 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
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
// 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
/* 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; --xy-node-background-color-default: #ffffff; --xy-theme-panel-bg: #ffffff; --xy-theme-panel-text: #111827; --xy-theme-resize-handle-bg: #ffffff; --xy-theme-focus-border: #d9d9d9; --xy-theme-muted-bg: #f3f4f6; --xy-theme-subtle-border: #d1d5db; } .react-flow.dark { --xy-background-color: #101012; --xy-node-background-color-default: #1a1b1e; --xy-node-border-default: 1px solid #303238; --xy-handle-background-color-default: #141519; --xy-handle-border-color-default: #5a5d66; --xy-edge-label-color-default: #e6e7ea; --xy-theme-hover: #7a7d86; --xy-theme-edge-hover: #f3f4f6; --xy-theme-panel-bg: #141519; --xy-theme-panel-text: #f3f4f6; --xy-theme-resize-handle-bg: #141519; --xy-theme-focus-border: #5f6470; --xy-theme-muted-bg: #24262c; --xy-theme-subtle-border: #3f434c; --xy-node-boxshadow-default: none; --xy-theme-color-focus: #4b4e57; --color-background: #1a1b1e; --color-hover-bg: #24262c; --color-disabled: #8b9099; } .react-flow { background-color: var(--xy-background-color); } /* 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: var(--xy-theme-focus-border); } .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.dark .react-flow__node-group { background-color: rgba(123, 107, 148, 0.22); border-color: #7b6b94; } .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: var(--xy-handle-background-color-default); } .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: var(--xy-theme-resize-handle-bg); 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: var(--xy-theme-panel-text); } .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: var(--xy-theme-panel-text); 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; color: var(--xy-theme-panel-text); }

Whiteboard libraries

If you are looking for a more complete whiteboard solution, consider using libraries that are specifically designed for whiteboard applications like tldraw  or Excalidraw . These libraries provide a full set of features for collaborative drawing, shapes, text, and more.

Last updated on