NodeToolbar component creates a toolbar that appears next to one or more nodes, perfect for contextual actions and controls.
Installation
npm install @vue-flow/node-toolbar
Basic Usage
<script setup>
import { NodeToolbar } from '@vue-flow/node-toolbar'
import { Position } from '@vue-flow/core'
</script>
<template>
<div class="custom-node">
<NodeToolbar :position="Position.Top">
<button>Delete</button>
<button>Copy</button>
</NodeToolbar>
<div>Node Content</div>
</div>
</template>
<style>
@import '@vue-flow/node-toolbar/dist/style.css';
</style>
Props
ID(s) of the node(s) to attach the toolbar to. If not provided, uses the node ID from context (when used inside a custom node component).
<!-- Single node -->
<NodeToolbar node-id="node-1" />
<!-- Multiple nodes -->
<NodeToolbar :node-id="['node-1', 'node-2', 'node-3']" />
Control toolbar visibility. By default, the toolbar is visible when:
- Exactly one node is provided
- That node is selected
- Only that node is selected (no other selections)
<!-- Always visible -->
<NodeToolbar :is-visible="true" />
<!-- Conditionally visible -->
<NodeToolbar :is-visible="node.data.showToolbar" />
Position of the toolbar relative to the node(s):
Position.TopPosition.RightPosition.BottomPosition.Left
Distance in pixels between the toolbar and node(s).
Alignment of the toolbar relative to the node(s):
'center': Centered'start': Aligned to the start (left/top)'end': Aligned to the end (right/bottom)
Slots
default
Toolbar content.
Examples
Basic Toolbar
<script setup>
import { NodeToolbar } from '@vue-flow/node-toolbar'
import { Position } from '@vue-flow/core'
</script>
<template>
<div class="custom-node">
<NodeToolbar :position="Position.Top">
<div class="toolbar-content">
<button>Edit</button>
<button>Delete</button>
<button>Duplicate</button>
</div>
</NodeToolbar>
<div>My Node</div>
</div>
</template>
<style scoped>
.toolbar-content {
display: flex;
gap: 4px;
padding: 4px;
background: white;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
</style>
Different Positions
<script setup>
import { NodeToolbar } from '@vue-flow/node-toolbar'
import { Position } from '@vue-flow/core'
</script>
<template>
<div class="custom-node">
<!-- Top toolbar -->
<NodeToolbar :position="Position.Top">
<button>Top</button>
</NodeToolbar>
<!-- Right toolbar -->
<NodeToolbar :position="Position.Right" :offset="15">
<button>Right</button>
</NodeToolbar>
<div>Node with multiple toolbars</div>
</div>
</template>
Different Alignments
<script setup>
import { NodeToolbar } from '@vue-flow/node-toolbar'
import { Position } from '@vue-flow/core'
</script>
<template>
<div class="custom-node">
<!-- Aligned to start (left) -->
<NodeToolbar :position="Position.Top" align="start">
<button>Start</button>
</NodeToolbar>
<!-- Centered (default) -->
<NodeToolbar :position="Position.Bottom" align="center">
<button>Center</button>
</NodeToolbar>
<div>Wide Node Content</div>
</div>
</template>
Multi-Node Toolbar
<script setup>
import { NodeToolbar } from '@vue-flow/node-toolbar'
import { Position, useVueFlow } from '@vue-flow/core'
import { computed } from 'vue'
const { getSelectedNodes } = useVueFlow()
const selectedNodeIds = computed(() =>
getSelectedNodes.value.map(node => node.id)
)
function groupNodes() {
console.log('Grouping nodes:', selectedNodeIds.value)
}
</script>
<template>
<VueFlow>
<!-- Toolbar appears when multiple nodes are selected -->
<NodeToolbar
:node-id="selectedNodeIds"
:is-visible="selectedNodeIds.length > 1"
:position="Position.Top"
>
<button @click="groupNodes">
Group {{ selectedNodeIds.length }} nodes
</button>
</NodeToolbar>
</VueFlow>
</template>
Conditional Visibility
<script setup>
import { NodeToolbar } from '@vue-flow/node-toolbar'
import { Position } from '@vue-flow/core'
import { inject, ref } from 'vue'
const node = inject('node')
const isHovered = ref(false)
</script>
<template>
<div
class="custom-node"
@mouseenter="isHovered = true"
@mouseleave="isHovered = false"
>
<!-- Show only when hovered -->
<NodeToolbar
:position="Position.Top"
:is-visible="isHovered"
>
<button>Quick action</button>
</NodeToolbar>
<!-- Show only when selected -->
<NodeToolbar
:position="Position.Bottom"
:is-visible="node.selected"
>
<button>Selected action</button>
</NodeToolbar>
<div>Hover or select me</div>
</div>
</template>
Custom Offset
<template>
<div class="custom-node">
<NodeToolbar
:position="Position.Right"
:offset="30"
>
<button>Far away</button>
</NodeToolbar>
<div>Node</div>
</div>
</template>
Rich Toolbar Content
<script setup>
import { NodeToolbar } from '@vue-flow/node-toolbar'
import { Position } from '@vue-flow/core'
import { inject } from 'vue'
const node = inject('node')
function updateColor(color) {
node.data.color = color
}
function deleteNode() {
// Delete logic
}
</script>
<template>
<div class="custom-node">
<NodeToolbar :position="Position.Top">
<div class="toolbar">
<div class="toolbar-section">
<label>Color:</label>
<input
type="color"
:value="node.data.color"
@input="updateColor($event.target.value)"
/>
</div>
<div class="toolbar-section">
<button @click="deleteNode" class="danger">
Delete
</button>
</div>
</div>
</NodeToolbar>
<div :style="{ background: node.data.color }">
{{ node.label }}
</div>
</div>
</template>
<style scoped>
.toolbar {
display: flex;
gap: 12px;
padding: 8px;
background: white;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
align-items: center;
}
.toolbar-section {
display: flex;
gap: 4px;
align-items: center;
}
.danger {
color: #ef4444;
background: #fee2e2;
border: 1px solid #fca5a5;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
}
</style>
Icon Toolbar
<script setup>
import { NodeToolbar } from '@vue-flow/node-toolbar'
import { Position } from '@vue-flow/core'
</script>
<template>
<div class="custom-node">
<NodeToolbar :position="Position.Top">
<div class="icon-toolbar">
<button title="Edit">
<svg><!-- edit icon --></svg>
</button>
<button title="Copy">
<svg><!-- copy icon --></svg>
</button>
<button title="Delete">
<svg><!-- delete icon --></svg>
</button>
</div>
</NodeToolbar>
<div>Node</div>
</div>
</template>
<style scoped>
.icon-toolbar {
display: flex;
background: white;
border-radius: 6px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.icon-toolbar button {
padding: 8px;
border: none;
background: white;
cursor: pointer;
border-right: 1px solid #e5e7eb;
}
.icon-toolbar button:last-child {
border-right: none;
}
.icon-toolbar button:hover {
background: #f3f4f6;
}
</style>
Complete Example
<script setup>
import { NodeToolbar } from '@vue-flow/node-toolbar'
import { Position, Handle } from '@vue-flow/core'
import { inject } from 'vue'
import { useVueFlow } from '@vue-flow/core'
const node = inject('node')
const { removeNodes, updateNode } = useVueFlow()
function deleteNode() {
removeNodes([node.id])
}
function duplicateNode() {
const newNode = {
...node,
id: `${node.id}-copy`,
position: {
x: node.position.x + 50,
y: node.position.y + 50,
},
}
// Add new node logic
}
function toggleLock() {
updateNode(node.id, {
draggable: !node.draggable,
})
}
</script>
<template>
<div class="custom-node">
<NodeToolbar :position="Position.Top">
<div class="toolbar">
<button @click="duplicateNode" title="Duplicate">
Copy
</button>
<button @click="toggleLock" title="Lock/Unlock">
{{ node.draggable ? 'Lock' : 'Unlock' }}
</button>
<button @click="deleteNode" title="Delete" class="danger">
Delete
</button>
</div>
</NodeToolbar>
<Handle type="target" :position="Position.Left" />
<div class="node-content">
{{ node.label }}
</div>
<Handle type="source" :position="Position.Right" />
</div>
</template>
<style scoped>
.custom-node {
padding: 16px;
border: 2px solid #3b82f6;
border-radius: 8px;
background: white;
}
.toolbar {
display: flex;
gap: 4px;
padding: 6px;
background: white;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.toolbar button {
padding: 6px 12px;
border: 1px solid #e5e7eb;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.toolbar button:hover {
background: #f3f4f6;
}
.toolbar button.danger {
color: #ef4444;
border-color: #fca5a5;
}
</style>
Styling
/* Toolbar wrapper */
.vue-flow__node-toolbar {
position: absolute;
/* positioning is handled automatically */
}
/* Custom toolbar styles */
.my-toolbar {
background: white;
border-radius: 8px;
padding: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
The NodeToolbar uses CSS transforms to position itself relative to nodes. When used with multiple nodes, it positions itself relative to the bounding box of all specified nodes.