Custom nodes allow you to create specialized components that fit your application’s needs. This guide covers everything from basic custom nodes to advanced patterns with TypeScript.
Basic custom node
Every custom node needs three core elements: a node definition, a component, and registration with Vue Flow.
Define your node type
Create a node with a custom type in your graph data: const nodes = ref ([
{
id: '1' ,
type: 'custom' ,
position: { x: 50 , y: 50 },
data: { label: 'Custom Node' },
},
])
Create the component
Build your custom node component with handles: < script setup lang = "ts" >
import { Handle , Position } from '@vue-flow/core'
defineProps ([ 'data' ])
</ script >
< template >
< div >
< Handle type = "target" : position = " Position . Top " />
< div > {{ data . label }} </ div >
< Handle type = "source" : position = " Position . Bottom " />
</ div >
</ template >
Register the component
Register your custom node using a template slot: < script setup >
import { VueFlow } from '@vue-flow/core'
import CustomNode from './CustomNode.vue'
</ script >
< template >
< VueFlow : nodes = " nodes " >
< template # node-custom = " props " >
< CustomNode v-bind = " props " />
</ template >
</ VueFlow >
</ template >
Using node types object
For better organization, register nodes using the nodeTypes prop:
< script setup >
import { markRaw } from 'vue'
import CustomNode from './CustomNode.vue'
import SpecialNode from './SpecialNode.vue'
const nodeTypes = {
custom: markRaw ( CustomNode ),
special: markRaw ( SpecialNode ),
}
</ script >
< template >
< VueFlow : nodes = " nodes " : node-types = " nodeTypes " />
</ template >
Always wrap components with markRaw() to prevent Vue from converting them into reactive objects. This avoids performance issues and console warnings.
Working with node props
Custom nodes receive props from Vue Flow with node data and state:
< script setup lang = "ts" >
import type { NodeProps } from '@vue-flow/core'
import { Handle , Position } from '@vue-flow/core'
interface CustomData {
label : string
value : number
}
const props = defineProps < NodeProps < CustomData >>()
</ script >
< template >
< div : class = " { selected: props . selected } " >
< Handle type = "target" : position = " Position . Top " />
< div >
< strong > {{ props . data . label }} </ strong >
< p > Value: {{ props . data . value }} </ p >
</ div >
< Handle type = "source" : position = " Position . Bottom " />
</ div >
</ template >
Available node props
Prop Type Description idstring Unique node identifier typestring Node type dataobject Custom data passed to the node selectedboolean Whether node is selected positionXYPosition Node position on canvas dimensionsDimensions Width and height draggingboolean Whether node is being dragged connectableboolean Whether handles can connect targetPositionPosition Default target handle position sourcePositionPosition Default source handle position
Multiple handles
Create nodes with multiple connection points using handle IDs:
< script setup lang = "ts" >
import type { CSSProperties } from 'vue'
import { Handle , Position } from '@vue-flow/core'
const handleStyleA : CSSProperties = { top: '10px' }
const handleStyleB : CSSProperties = { bottom: '10px' , top: 'auto' }
</ script >
< template >
< div >
< Handle type = "target" : position = " Position . Left " />
< div > Color Selector </ div >
< Handle
id = "a"
type = "source"
: position = " Position . Right "
: style = " handleStyleA "
/>
< Handle
id = "b"
type = "source"
: position = " Position . Right "
: style = " handleStyleB "
/>
</ div >
</ template >
When connecting, specify which handle to use:
const edges = ref ([
{
id: 'e1-2a' ,
source: '1' ,
target: '2' ,
sourceHandle: 'a' , // Connect to handle 'a'
},
{
id: 'e1-2b' ,
source: '1' ,
target: '2' ,
sourceHandle: 'b' , // Connect to handle 'b'
},
])
Updating node data
Use the useNode composable to update node data directly from within the component:
< script setup lang = "ts" >
import { useNode } from '@vue-flow/core'
const { node } = useNode ()
function updateValue ( newValue : number ) {
node . data = {
... node . data ,
value: newValue ,
}
}
function toggleSelectable () {
node . selectable = ! node . selectable
}
</ script >
< template >
< div >
< input
class = "nodrag"
type = "number"
: value = " node . data . value "
@ input = " updateValue ( $event . target . value ) "
/>
< button @ click = " toggleSelectable " > Toggle Selectable </ button >
</ div >
</ template >
Add the nodrag class to interactive elements like inputs and buttons to prevent dragging when interacting with them.
Interactive elements
Prevent node dragging when interacting with specific elements:
< template >
< div class = "custom-node" >
<!-- This input won't trigger node dragging -->
< input class = "nodrag" type = "text" placeholder = "Type here..." />
<!-- This button won't trigger node dragging -->
< button class = "nodrag" @ click = " handleClick " > Click me </ button >
<!-- This area allows dragging -->
< div > Drag from here </ div >
</ div >
</ template >
Scrollable content
For nodes with scrollable content, use the nowheel class:
< template >
< div class = "custom-node" >
< div class = "header" > Scrollable Node </ div >
<!-- This area scrolls without zooming the canvas -->
< ul class = "nowheel" style = " max-height : 150 px ; overflow-y : auto ; " >
< li v-for = " item in items " : key = " item " > {{ item }} </ li >
</ ul >
</ div >
</ template >
Custom drag handle
Restrict dragging to specific areas using the dragHandle property:
< script setup >
const nodes = ref ([
{
id: '1' ,
type: 'custom' ,
position: { x: 0 , y: 0 },
dragHandle: '.drag-handle' , // Only drag from this selector
data: { label: 'Custom Node' },
},
])
</ script >
< template >
< div class = "custom-node" >
< div class = "drag-handle" style = " cursor : move ; padding : 10 px ; background : #ddd ; " >
Drag from here
</ div >
< div class = "nodrag" style = " padding : 10 px ; " >
< input type = "text" placeholder = "Type without dragging" />
</ div >
</ div >
</ template >
Styling custom nodes
Add styles to your custom nodes:
.vue-flow__node-custom {
background : #9ca8b3 ;
color : #fff ;
padding : 10 px ;
border-radius : 8 px ;
border : 2 px solid #555 ;
}
.vue-flow__node-custom.selected {
border-color : #ff0072 ;
box-shadow : 0 0 0 2 px #ff0072 ;
}
Or use inline styles in node definitions:
const nodes = ref ([
{
id: '1' ,
type: 'custom' ,
position: { x: 100 , y: 100 },
style: {
backgroundColor: 'purple' ,
border: '2px solid white' ,
borderRadius: '8px' ,
padding: '10px' ,
},
data: { label: 'Styled Node' },
},
])
Complete example
Here’s a full example with a color picker node:
ColorSelectorNode.vue
App.vue
< script setup lang = "ts" >
import type { CSSProperties } from 'vue'
import type { NodeProps } from '@vue-flow/core'
import { Handle , Position } from '@vue-flow/core'
interface Data {
color : string
onChange : ( event : InputEvent ) => void
}
interface ColorSelectorNodeProps extends NodeProps < Data > {
data : Data
}
const props = defineProps < ColorSelectorNodeProps >()
const targetHandleStyle : CSSProperties = { background: '#555' }
const sourceHandleStyleA : CSSProperties = { ... targetHandleStyle , top: '10px' }
const sourceHandleStyleB : CSSProperties = { ... targetHandleStyle , bottom: '10px' , top: 'auto' }
</ script >
< template >
< Handle type = "target" : position = " Position . Left " : style = " targetHandleStyle " />
< div >
< div > Custom Color Picker Node: < strong > {{ data . color }} </ strong ></ div >
< input class = "nodrag" type = "color" : value = " data . color " @ input = " props . data . onChange " />
</ div >
< Handle id = "a" type = "source" : position = " Position . Right " : style = " sourceHandleStyleA " />
< Handle id = "b" type = "source" : position = " Position . Right " : style = " sourceHandleStyleB " />
</ template >
Next steps
Node events Learn about node interaction events
TypeScript guide Type-safe custom nodes with TypeScript