React Flow Custom Nodes and Edges Create fully customized nodes and edges with React Flow. Build complex node-based editors with custom styling, behaviors, and interactions. Custom Node Component import { memo } from 'react' ; import { Handle , Position , type NodeProps , type Node } from '@xyflow/react' ; // Define custom node data type type TextUpdaterNodeData = { label : string ; onChange : ( value : string ) => void ; } ; type TextUpdaterNode = Node < TextUpdaterNodeData
; function TextUpdaterNode ( { data , isConnectable } : NodeProps < TextUpdaterNode
) { const onChange = ( evt : React . ChangeEvent < HTMLInputElement
) => { data . onChange ( evt . target . value ) ; } ; return ( < div className = " text-updater-node "
< Handle type = " target " position = { Position . Top } isConnectable = { isConnectable } /> < div
< label htmlFor = " text "
Text: </ label
< input id = " text " name = " text " onChange = { onChange } className = " nodrag " defaultValue = { data . label } /> </ div
< Handle type = " source " position = { Position . Bottom } id = " a " isConnectable = { isConnectable } /> </ div
) ; } // Memoize for performance export default memo ( TextUpdaterNode ) ; Registering Custom Nodes import { ReactFlow } from '@xyflow/react' ; import TextUpdaterNode from './TextUpdaterNode' ; import ColorPickerNode from './ColorPickerNode' ; // Define node types outside component to prevent re-renders const nodeTypes = { textUpdater : TextUpdaterNode , colorPicker : ColorPickerNode , } ; function Flow ( ) { const [ nodes , setNodes , onNodesChange ] = useNodesState ( [ { id : '1' , type : 'textUpdater' , position : { x : 0 , y : 0 } , data : { label : 'Hello' , onChange : ( value ) => console . log ( value ) , } , } , ] ) ; return ( < ReactFlow nodes = { nodes } nodeTypes = { nodeTypes } onNodesChange = { onNodesChange } /> ) ; } Styled Node with Tailwind import { memo } from 'react' ; import { Handle , Position , type NodeProps } from '@xyflow/react' ; type StatusNodeData = { label : string ; status : 'pending' | 'running' | 'completed' | 'error' ; } ; const statusColors = { pending : 'bg-yellow-100 border-yellow-400' , running : 'bg-blue-100 border-blue-400' , completed : 'bg-green-100 border-green-400' , error : 'bg-red-100 border-red-400' , } ; const statusIcons = { pending : '⏳' , running : '⚡' , completed : '✅' , error : '❌' , } ; function StatusNode ( { data } : NodeProps < Node < StatusNodeData
) { return ( < div className = {
px-4 py-2 rounded-lg border-2 shadow-sm ${ statusColors [ data . status ] }}< Handle type = " target " position = { Position . Top } className = " !bg-gray-400 " /> < div className = " flex items-center gap-2 "
< span className = " text-xl "
{ statusIcons [ data . status ] } </ span
< span className = " font-medium "
{ data . label } </ span
</ div
< Handle type = " source " position = { Position . Bottom } className = " !bg-gray-400 " /> </ div
) ; } export default memo ( StatusNode ) ; Node with Multiple Handles import { memo } from 'react' ; import { Handle , Position , type NodeProps } from '@xyflow/react' ; type SwitchNodeData = { label : string ; cases : string [ ] ; } ; function SwitchNode ( { data } : NodeProps < Node < SwitchNodeData
) { return ( < div className = " switch-node bg-white rounded-lg shadow-lg p-3 min-w-[150px] "
{ / Single input / } < Handle type = " target " position = { Position . Top } id = " input " /> < div className = " font-bold text-center border-b pb-2 mb-2 "
{ data . label } </ div
{ / Multiple outputs - one per case / } < div className = " space-y-2 "
{ data . cases . map ( ( caseLabel , index ) => ( < div key = { index } className = " relative text-sm text-right pr-4 "
{ caseLabel } < Handle type = " source " position = { Position . Right } id = {
case- ${ index }} style = { { top :${ 30 + index * 28 } px} } /> </ div) ) } </ div
</ div
) ; } export default memo ( SwitchNode ) ; Resizable Node import { memo } from 'react' ; import { Handle , Position , NodeResizer , type NodeProps } from '@xyflow/react' ; type ResizableNodeData = { label : string ; content : string ; } ; function ResizableNode ( { data , selected } : NodeProps < Node < ResizableNodeData
) { return ( <
< NodeResizer color = "
ff0071
" isVisible = { selected } minWidth = { 100 } minHeight = { 50 } handleStyle = { { width : 8 , height : 8 } } /> < Handle type = " target " position = { Position . Top } /> < div className = " p-4 h-full "
< div className = " font-bold "
{ data . label } </ div
< div className = " text-sm text-gray-600 "
{ data . content } </ div
</ div
< Handle type = " source " position = { Position . Bottom } /> </
) ; } export default memo ( ResizableNode ) ; Node Toolbar import { memo , useState } from 'react' ; import { Handle , Position , NodeToolbar , type NodeProps , useReactFlow , } from '@xyflow/react' ; type EditableNodeData = { label : string ; } ; function EditableNode ( { id , data , selected , } : NodeProps < Node < EditableNodeData
) { const { setNodes , deleteElements } = useReactFlow ( ) ; const [ isEditing , setIsEditing ] = useState ( false ) ; const [ label , setLabel ] = useState ( data . label ) ; const handleSave = ( ) => { setNodes ( ( nodes ) => nodes . map ( ( node ) => node . id === id ? { ... node , data : { ... node . data , label } } : node ) ) ; setIsEditing ( false ) ; } ; const handleDelete = ( ) => { deleteElements ( { nodes : [ { id } ] } ) ; } ; return ( <
< NodeToolbar isVisible = { selected } position = { Position . Top }
< button onClick = { ( ) => setIsEditing ( true ) } className = " toolbar-btn "
✏️ Edit </ button
< button onClick = { handleDelete } className = " toolbar-btn text-red-500 "
🗑️ Delete </ button
</ NodeToolbar
< Handle type = " target " position = { Position . Top } /> < div className = " px-4 py-2 bg-white rounded shadow "
{ isEditing ? ( < div className = " flex gap-2 "
< input value = { label } onChange = { ( e ) => setLabel ( e . target . value ) } className = " border rounded px-2 " autoFocus /> < button onClick = { handleSave }
Save </ button
</ div
) : ( < span
{ data . label } </ span
) } </ div
< Handle type = " source " position = { Position . Bottom } /> </
) ; } export default memo ( EditableNode ) ; Custom Edge import { memo } from 'react' ; import { BaseEdge , EdgeLabelRenderer , getBezierPath , useReactFlow , type EdgeProps , } from '@xyflow/react' ; function ButtonEdge ( { id , sourceX , sourceY , targetX , targetY , sourcePosition , targetPosition , style = { } , markerEnd , } : EdgeProps ) { const { setEdges } = useReactFlow ( ) ; const [ edgePath , labelX , labelY ] = getBezierPath ( { sourceX , sourceY , sourcePosition , targetX , targetY , targetPosition , } ) ; const onEdgeClick = ( ) => { setEdges ( ( edges ) => edges . filter ( ( edge ) => edge . id !== id ) ) ; } ; return ( <
< BaseEdge path = { edgePath } markerEnd = { markerEnd } style = { style } /> < EdgeLabelRenderer
< div style = { { position : 'absolute' , transform :
translate(-50%, -50%) translate( ${ labelX } px, ${ labelY } px), fontSize : 12 , pointerEvents : 'all' , } } className = " nodrag nopan "< button className = " w-5 h-5 bg-gray-200 rounded-full border border-gray-400 cursor-pointer hover:bg-red-200 " onClick = { onEdgeClick }
× </ button
</ div
</ EdgeLabelRenderer
</
) ; } export default memo ( ButtonEdge ) ; Edge with Custom Path import { memo } from 'react' ; import { BaseEdge , getStraightPath , type EdgeProps } from '@xyflow/react' ; function CustomPathEdge ( { sourceX , sourceY , targetX , targetY , } : EdgeProps ) { // Create a custom S-curve path const midY = ( sourceY + targetY ) / 2 ; const path =
M ${ sourceX } ${ sourceY } C ${ sourceX } ${ midY } , ${ targetX } ${ midY } , ${ targetX } ${ targetY }; return < BaseEdge path = { path } style = { { stroke : '#b1b1b7' , strokeWidth : 2 } } /> ; } export default memo ( CustomPathEdge ) ; Registering Custom Edges import { ReactFlow } from '@xyflow/react' ; import ButtonEdge from './ButtonEdge' ; import CustomPathEdge from './CustomPathEdge' ; const edgeTypes = { buttonEdge : ButtonEdge , customPath : CustomPathEdge , } ; function Flow ( ) { const [ edges , setEdges , onEdgesChange ] = useEdgesState ( [ { id : 'e1-2' , source : '1' , target : '2' , type : 'buttonEdge' , } , ] ) ; return ( < ReactFlow nodes = { nodes } edges = { edges } edgeTypes = { edgeTypes } onEdgesChange = { onEdgesChange } /> ) ; } Connection Validation import { useCallback } from 'react' ; import { ReactFlow , type IsValidConnection } from '@xyflow/react' ; function Flow ( ) { // Validate connections before they're made const isValidConnection : IsValidConnection = useCallback ( ( connection ) => { // Prevent self-connections if ( connection . source === connection . target ) { return false ; } // Only allow connections between specific handle types const sourceNode = nodes . find ( ( n ) => n . id === connection . source ) ; const targetNode = nodes . find ( ( n ) => n . id === connection . target ) ; // Example: input nodes can't receive connections if ( targetNode ?. type === 'input' ) { return false ; } // Example: output nodes can't send connections if ( sourceNode ?. type === 'output' ) { return false ; } return true ; } , [ nodes ] ) ; return ( < ReactFlow nodes = { nodes } edges = { edges } isValidConnection = { isValidConnection } /> ) ; } Group Node (Parent Container) import { memo } from 'react' ; import { type NodeProps } from '@xyflow/react' ; type GroupNodeData = { label : string ; } ; function GroupNode ( { data } : NodeProps < Node < GroupNodeData) { return ( < div className = " p-2 border-2 border-dashed border-gray-400 rounded-lg bg-gray-50/50 min-w-[200px] min-h-[150px] "
< div className = " text-xs text-gray-500 font-medium mb-2 "
{ data . label } </ div
</ div
) ; } export default memo ( GroupNode ) ; // Usage - child nodes reference parent const nodes = [ { id : 'group-1' , type : 'group' , data : { label : 'Group A' } , position : { x : 0 , y : 0 } , style : { width : 300 , height : 200 } , } , { id : 'child-1' , data : { label : 'Child Node' } , position : { x : 50 , y : 50 } , parentId : 'group-1' , extent : 'parent' , } , ] ; CSS Styling / node.css / .react-flow__node-custom { background : white ; border : 1 px solid
1a192b
; border-radius : 8 px ; padding : 10 px ; font-size : 12 px ; width : 150 px ; } .react-flow__node-custom .selected { border-color :
ff0071
; box-shadow : 0 0 0 2 px
ff0071
; } .react-flow__handle { width : 10 px ; height : 10 px ; border-radius : 50 % ; background-color :
555
; } .react-flow__handle-connecting { background-color :
ff0071
; } .react-flow__handle-valid { background-color :
55dd99
; } / Prevent drag on interactive elements / .nodrag { pointer-events : all ; } / Edge styling / .react-flow__edge-path { stroke :
b1b1b7
; stroke-width : 2 ; } .react-flow__edge .selected .react-flow__edge-path { stroke :
ff0071
; } .react-flow__edge .animated .react-flow__edge-path { stroke-dasharray : 5 ; animation : dashdraw 0.5 s linear infinite ; } @keyframes dashdraw { from { stroke-dashoffset : 10 ; } } When to Use This Skill Use reactflow-custom-nodes when you need to: Build nodes with interactive form elements Create visually distinct node types Add node toolbars and context menus Build resizable nodes Create group/container nodes Add custom edge interactions Validate connections between nodes Build complex workflow interfaces Best Practices Always memoize custom node components with memo() Define nodeTypes/edgeTypes outside the component Use the nodrag class on interactive elements Keep node components focused and reusable Use TypeScript for type-safe node data Test connection validation thoroughly Consider accessibility in custom nodes Use CSS modules or Tailwind for styling Resources Custom Nodes Guide Custom Edges Guide Node Toolbar Node Resizer