Skip to Content
LearnAdvanced UseWhiteboard Features

Whiteboard Features

Vue Flow is designed for building node-based UIs like workflow editors, flowcharts, and diagrams. It isn’t a whiteboard library, but you might still want common whiteboard features to annotate or sketch alongside your nodes and edges. These examples show how to add drawing capabilities to your application.

Examples

✏️ Freehand draw (Pro)

Draw smooth curves on your Vue 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

Freehand draw is available as a Vue Flow Pro example.

🎯 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'] as (string | number)[], ); 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 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 = 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; }

📐 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
import type { Node } from '@xyflow/vue'; export type RectangleNodeType = Node<{ color: string }, 'rectangle'>;

Whiteboard libraries

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

Last updated on