reactflow-custom-nodes

React Flow Custom Nodes and Edges

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "reactflow-custom-nodes" with this command: npx skills add thebushidocollective/han/thebushidocollective-han-reactflow-custom-nodes

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" />

  &#x3C;div className="flex items-center gap-2">
    &#x3C;span className="text-xl">{statusIcons[data.status]}&#x3C;/span>
    &#x3C;span className="font-medium">{data.label}&#x3C;/span>
  &#x3C;/div>

  &#x3C;Handle
    type="source"
    position={Position.Bottom}
    className="!bg-gray-400"
  />
&#x3C;/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" />

  &#x3C;div className="font-bold text-center border-b pb-2 mb-2">
    {data.label}
  &#x3C;/div>

  {/* Multiple outputs - one per case */}
  &#x3C;div className="space-y-2">
    {data.cases.map((caseLabel, index) => (
      &#x3C;div key={index} className="relative text-sm text-right pr-4">
        {caseLabel}
        &#x3C;Handle
          type="source"
          position={Position.Right}
          id={`case-${index}`}
          style={{ top: `${30 + index * 28}px` }}
        />
      &#x3C;/div>
    ))}
  &#x3C;/div>
&#x3C;/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>

  &#x3C;Handle type="target" position={Position.Top} />

  &#x3C;div className="px-4 py-2 bg-white rounded shadow">
    {isEditing ? (
      &#x3C;div className="flex gap-2">
        &#x3C;input
          value={label}
          onChange={(e) => setLabel(e.target.value)}
          className="border rounded px-2"
          autoFocus
        />
        &#x3C;button onClick={handleSave}>Save&#x3C;/button>
      &#x3C;/div>
    ) : (
      &#x3C;span>{data.label}&#x3C;/span>
    )}
  &#x3C;/div>

  &#x3C;Handle type="source" position={Position.Bottom} />
&#x3C;/>

); }

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: 1px solid #1a192b; border-radius: 8px; padding: 10px; font-size: 12px; width: 150px; }

.react-flow__node-custom.selected { border-color: #ff0071; box-shadow: 0 0 0 2px #ff0071; }

.react-flow__handle { width: 10px; height: 10px; 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.5s 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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

android-jetpack-compose

No summary provided by upstream source.

Repository SourceNeeds Review
General

fastapi-async-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

storybook-story-writing

No summary provided by upstream source.

Repository SourceNeeds Review