Whiteboard Features - React Flow
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);
}
This is a Pro example. Get all Pro examples , templates, 1:1 support from the xyflow team and prioritized Github issues with a React Flow Pro subscription.
solid
#ededed
;
--xy-node-boxshadow-default : 0 px 3.54 px 4.55 px 0 px #00000005 , 0 px 3.54 px 4.55 px 0 px #0000000d , 0 px 0.51 px 1.01 px 0 px #0000001a ;
lineSegmentsIntersect
(
p1
:
Point
,
p2
:
Point
,
p3
:
Point
,
p4
:
Point
)
:
boolean
{
const [ x1 , y1 ] = p1;
const [ x2 , y2 ] = p2;
--xy-node-border-radius-default : 8 px ;
--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 ;
const [ x3 , y3 ] = p3;
const [ x4 , y4 ] = p4;
--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 : 1 px solid #303238 ;
--xy-handle-background-color-default : #141519 ;
const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
--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 ;
if (Math. abs (denom) < 1e-10 ) return false ; // Lines are parallel
--xy-theme-subtle-border : #3f434c ;
const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom;
--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 {
const u = - ((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom;
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 : 10 px ;
return t >= 0 && t <= 1 && u >= 0 && u <= 1 ;
font-size : 12 px ;
flex-direction : column ;
border : var ( --xy-node-border-default );
color : var ( --xy-node-color , var ( --xy-node-color-default ));
}
.react-flow__node.selectable:focus {
}
// Check if a point is inside a rectangle
box-shadow : 0 px 0 px 0 px 4 px var ( --xy-theme-color-focus );
function pointInRectangle ( point : Point , rect : Rectangle ) : boolean {
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 {
const [ x , y ] = point;
return (
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 {
x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height
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 {
);
}
// 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],
background-color : var ( --xy-handle-background-color-default );
}
.react-flow__handle.connectionindicator:hover {
], // 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
];
}
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 {
// Check if a polyline (series of connected line segments) intersects with a rectangle
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 : 5 px ;
height : 5 px ;
}
export function polylineIntersectsRectangle ( points : Point [], rect : Rectangle ) : boolean {
/*
Custom Example CSS - This CSS is to improve the example experience.
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);
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 ;
}
for ( let i = 0 ; i < points. length - 1 ; i ++ ) {
.xy-theme__button-group {
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 ;
}
display : flex ;
align-items : center ;
. xy-theme __ button :first-child {
border-radius: 100 px 0 0 100 px ;
}
.xy-theme__button:last-child {
border-radius : 0 100 px 100 px 0 ;
margin : 0 ;
// Calculate distance between two points
}
}
/* Custom Button Styling */
.xy-theme__button {
display : inline-flex ;
align-items : center ;
justify-content : center ;
height : 2.5 rem ;
padding : 0 1 rem ;
border-radius : 100 px ;
function distanceBetweenPoints ( p1 : Point , p2 : Point ) : number {
border : 1 px solid var ( --color-primary );
background-color : var ( --color-background );
color : var ( --color-primary );
transition :
background-color 0.2 s ease ,
border-color 0.2 s ease ;
const [ x1 , y1 ] = p1;
const [ x2 , y2 ] = p2;
box-shadow : var ( --xy-node-boxshadow-default );
return Math. sqrt ((x2 - x1) ** 2 + (y2 - y1) ** 2 );
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 );
}
// 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;
opacity : 0.9 ;
}
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
.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 ;
const t = Math. max ( 0 , Math. min ( 1 , ((px - x1) * dx + (py - y1) * dy) / lengthSquared));
border : 1 px solid var ( --color-disabled );
}
.xy-theme__button > span {
margin-right : 0.2 rem ;
}
/* Add gap between adjacent buttons */
.xy-theme__button + .xy-theme__button {
margin-left : 0.3 rem ;
}
/* Example Input Styling */
.xy-theme__input {
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 {
padding : 0.5 rem 0.75 rem ;
border : 1 px solid var ( --color-primary );
border-radius : 7 px ;
background-color : var ( --color-background );
transition :
background-color 0.2 s ease ,
if (path1. length < 2 || path2. length < 2 ) return false ;
border-color 0.2 s ease ;
// First, do the more precise line segment intersection check
font-size : 1 rem ;
color : var ( --xy-theme-panel-text );
}
.xy-theme__input:focus {
outline : none ;
border-color : var ( --color-primary );
for ( let i = 0 ; i < path1. length - 1 ; i ++ ) {
box-shadow : 0 0 0 2 px rgba ( 255 , 0 , 115 , 0.3 );
for ( let j = 0 ; j < path2. length - 1 ; j ++ ) {
}
/* Specific Checkbox Styling */
.xy-theme__checkbox {
appearance : none ;
-webkit-appearance : none ;
width : 1.25 rem ;
height : 1.25 rem ;
border-radius : 7 px ;
border : 2 px solid var ( --color-primary );
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 ) {
background-color : var ( --color-background );
transition :
background-color 0.2 s ease ,
border-color 0.2 s ease ;
cursor : pointer ;
display : inline-block ;
vertical-align : middle ;
margin-right : 0.5 rem ;
}
for ( let i = 0 ; i < path1. length - 1 ; i ++ ) {
.xy-theme__checkbox:checked {
background-color : var ( --color-primary );
border-color : var ( --color-primary );
}
.xy-theme__checkbox:focus {
outline : none ;
const segment1Start = path1[i];
const segment1End = path1[i + 1 ];
box-shadow : 0 0 0 2 px rgba ( 255 , 0 , 115 , 0.3 );
for ( let j = 0 ; j < path2. length - 1 ; j ++ ) {
}
/* Dropdown Styling */
.xy-theme__select {
padding : 0.5 rem 0.75 rem ;
border : 1 px solid var ( --color-primary );
border-radius : 50 px ;
background-color : var ( --color-background );
transition :
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,
background-color 0.2 s ease ,
border-color 0.2 s ease ;
font-size : 1 rem ;
color : var ( --xy-theme-panel-text );
margin-right : 0.5 rem ;
box-shadow : var ( --xy-node-boxshadow-default );
}
.xy-theme__select:focus {
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
outline : none ;
export function samplePathPoints ( points : Point [], maxDistance : number = 5 ) : Point [] {
border-color : var ( --color-primary );
if (points. length < 2 ) return [ ... points];
const result : Point [] = [points[ 0 ]];
for ( let i = 1 ; i < points. length ; i ++ ) {
box-shadow : 0 0 0 2 px rgba ( 255 , 0 , 115 , 0.3 );
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);
}
.xy-theme__label {
margin-top : 10 px ;
margin-bottom : 3 px ;
display : inline-block ;
color : var ( --xy-theme-panel-text );
}
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;
}