Skip to main content
This example demonstrates how to create custom node components in Vue Flow. You’ll learn how to define custom node types, use handles, and create interactive nodes.

Live Demo

View the live demo on vueflow.dev.

Complete Example

<script lang="ts" setup>
import type { Elements, Node, SnapGrid } from '@vue-flow/core'
import { Position, VueFlow, isEdge, useVueFlow } from '@vue-flow/core'
import { Controls } from '@vue-flow/controls'
import { MiniMap } from '@vue-flow/minimap'
import ColorSelectorNode from './ColorSelectorNode.vue'

const bgColor = ref('#1A192B')
const connectionLineStyle = { stroke: '#fff' }
const snapGrid: SnapGrid = [16, 16]

const elements = ref<Elements>([
  {
    id: '1',
    type: 'input',
    label: 'An input node',
    position: { x: 0, y: 50 },
    sourcePosition: Position.Right,
  },
  {
    id: '2',
    type: 'selectorNode',
    data: { onChange, color: bgColor },
    style: { border: '1px solid #777', padding: '10px' },
    position: { x: 250, y: 50 },
  },
  {
    id: '3',
    type: 'output',
    label: 'Output A',
    position: { x: 650, y: 25 },
    targetPosition: Position.Left,
  },
  {
    id: '4',
    type: 'output',
    label: 'Output B',
    position: { x: 650, y: 100 },
    targetPosition: Position.Left,
  },
  { id: 'e1-2', source: '1', target: '2', animated: true, style: { stroke: '#fff' } },
  { id: 'e2a-3', source: '2', sourceHandle: 'a', target: '3', animated: true, style: { stroke: '#fff' } },
  { id: 'e2b-4', source: '2', sourceHandle: 'b', target: '4', animated: true, style: { stroke: '#fff' } },
])

useVueFlow({
  connectionLineOptions: {
    style: connectionLineStyle,
  },
  snapToGrid: true,
  snapGrid,
})

function nodeStroke(n: Node) {
  switch (n.type) {
    case 'selectorNode':
      return bgColor.value
    case 'input':
      return '#0041d0'
    case 'output':
      return '#ff0072'
    default:
      return '#eee'
  }
}

function nodeColor(n: Node) {
  if (n.type === 'selectorNode') {
    return bgColor.value
  }
  return '#fff'
}

function onChange(event: InputEvent) {
  elements.value.forEach((e) => {
    if (isEdge(e) || e.id !== '2') {
      return e
    }
    bgColor.value = (event.target as HTMLInputElement).value
  })
}
</script>

<template>
  <VueFlow v-model="elements" fit-view-on-init :style="{ backgroundColor: bgColor }">
    <template #node-selectorNode="props">
      <ColorSelectorNode :data="props.data" />
    </template>
    <MiniMap :node-stroke-color="nodeStroke" :node-color="nodeColor" />
    <Controls />
  </VueFlow>
</template>

Key Concepts

Registering Custom Nodes

Use the #node-{type} template slot to register custom node components:
<VueFlow v-model="elements">
  <template #node-selectorNode="props">
    <ColorSelectorNode :data="props.data" />
  </template>
</VueFlow>
The slot name must match the type property of your node.

Node Props

Custom node components receive these props:
interface NodeProps<T = any> {
  id: string
  type: string
  data: T
  selected: boolean
  connectable: boolean
  position: { x: number; y: number }
  dimensions: { width: number; height: number }
  // ... and more
}

Adding Handles

Handles are connection points on nodes. Use the Handle component:
<template>
  <!-- Target handle (input) -->
  <Handle type="target" :position="Position.Left" />
  
  <!-- Source handles (outputs) -->
  <Handle id="a" type="source" :position="Position.Right" />
  <Handle id="b" type="source" :position="Position.Right" />
</template>
When using multiple handles of the same type, you must provide unique id props.

Handle Positions

Handles can be positioned on any side of the node:
import { Position } from '@vue-flow/core'

Position.Top
Position.Right
Position.Bottom
Position.Left

Node Data

Pass custom data to nodes through the data property:
const elements = ref([
  {
    id: '2',
    type: 'selectorNode',
    data: { 
      onChange: handleChange,
      color: bgColor 
    },
    position: { x: 250, y: 50 },
  },
])
Access it in your component:
<script setup>
const props = defineProps<{
  data: {
    color: string
    onChange: (event: InputEvent) => void
  }
}>()
</script>

<template>
  <div>Color: {{ data.color }}</div>
  <input @input="data.onChange" />
</template>

Advanced Features

Preventing Drag on Interactive Elements

Add the nodrag class to prevent dragging when interacting with inputs:
<input class="nodrag" type="color" :value="data.color" @input="onChange" />

Snap to Grid

Make nodes snap to a grid while dragging:
const snapGrid: SnapGrid = [16, 16] // [x, y]

useVueFlow({
  snapToGrid: true,
  snapGrid,
})

Custom MiniMap Colors

Customize node colors in the minimap:
function nodeColor(n: Node) {
  if (n.type === 'selectorNode') {
    return bgColor.value
  }
  return '#fff'
}
<MiniMap :node-color="nodeColor" />

TypeScript Support

Define types for your custom nodes:
import type { NodeProps } from '@vue-flow/core'

interface CustomNodeData {
  label: string
  value: number
  onChange: (value: number) => void
}

interface CustomNodeProps extends NodeProps<CustomNodeData> {
  data: CustomNodeData
}

const props = defineProps<CustomNodeProps>()

Best Practices

Always add inheritAttrs: false to custom node components to prevent attribute conflicts:
export default {
  inheritAttrs: false,
}

Next Steps

Custom Edges

Learn how to create custom edge components

Validation

Validate connections between nodes

Node API

Complete node type reference

Handle Component

Handle component documentation

Build docs developers (and LLMs) love