Drag and Drop
In these examples, we use a custom Sidebar component to create new nodes when dragging from the sidebar into the Vue Flow pane.
Drag and Drop with HTML Drag and Drop API
The HTML Drag and Drop API is a native API that is supported by all major browsers. It is a simple API that allows you to drag and drop elements between different elements on the page.
Note: HTML Drag and Drop API is not properly supported on touch devices. See the example below for an alternative approach using pointer events.
import type { InjectionKey, Ref } from 'vue';
import { inject, provide, shallowRef } from 'vue';
// the node type currently being dragged from the sidebar, shared between the sidebar and the flow
const DnDKey: InjectionKey<Ref<string | null>> = Symbol('dnd-type');
export function provideDnD() {
const type = shallowRef<string | null>(null);
provide(DnDKey, type);
return type;
}
export function useDnD() {
const type = inject(DnDKey);
if (!type) {
throw new Error('useDnD must be used within a component that called provideDnD');
}
return type;
}Drag and Drop with Pointer Events
Another way to implement drag and drop is to use pointer events.
The Pointer Events API is a modern API that is supported by all major browsers. It is slightly more complex to implement than the HTML Drag and Drop API, but by using pointer events we can make sure that the drag and drop behavior is consistent across all devices, both mouse and touch.
import type { InjectionKey, Ref } from 'vue';
import type { XYPosition } from '@xyflow/vue';
import { inject, onMounted, onUnmounted, provide, shallowRef, watch } from 'vue';
import { useVueFlow } from '@xyflow/vue';
export type OnDropAction = (params: { position: XYPosition }) => void;
interface DnDContext {
isDragging: Ref<boolean>;
dropAction: Ref<OnDropAction | null>;
}
const DnDKey: InjectionKey<DnDContext> = Symbol('dnd-custom');
// Wrap the flow subtree so the sidebar can start dragging a node without registering any callback on the
// flow itself.
export function provideDnD() {
provide(DnDKey, {
isDragging: shallowRef(false),
dropAction: shallowRef<OnDropAction | null>(null),
});
}
export function useDnD() {
const context = inject(DnDKey);
if (!context) {
throw new Error('useDnD must be used within a component tree that called provideDnD');
}
const { isDragging, dropAction } = context;
const { screenToFlowPosition } = useVueFlow();
// start dragging a new node into the flow (used by the sidebar entries)
function onDragStart(event: PointerEvent, onDrop: OnDropAction) {
event.preventDefault();
(event.target as HTMLElement).setPointerCapture(event.pointerId);
isDragging.value = true;
dropAction.value = onDrop;
}
function onDragEnd(event: PointerEvent) {
if (!isDragging.value) {
return;
}
(event.target as HTMLElement).releasePointerCapture(event.pointerId);
event.preventDefault();
// only drop when the pointer is released over the flow
const elementUnderPointer = document.elementFromPoint(event.clientX, event.clientY);
if (elementUnderPointer?.closest('.vue-flow')) {
dropAction.value?.({ position: screenToFlowPosition({ x: event.clientX, y: event.clientY }) });
}
isDragging.value = false;
}
watch(isDragging, (dragging) => {
if (dragging) {
document.addEventListener('pointerup', onDragEnd);
}
else {
document.removeEventListener('pointerup', onDragEnd);
}
});
onUnmounted(() => document.removeEventListener('pointerup', onDragEnd));
return { isDragging, onDragStart };
}
export function useDnDPosition() {
const position = shallowRef<XYPosition>();
function onDrag(event: PointerEvent) {
event.preventDefault();
position.value = { x: event.clientX, y: event.clientY };
}
onMounted(() => document.addEventListener('pointermove', onDrag));
onUnmounted(() => document.removeEventListener('pointermove', onDrag));
return { position };
}Drag and Drop with Neodrag
Neodrag is a library that provides a simple and easy to use drag and drop API. In a real-world application, you may want to use the Neodrag library to handle the drag and drop behavior in a cross-platform compatible way, compatible with both mouse and touch devices.
/* xyflow theme files. Delete these to start from our base */
/*
* `@xyflow/vue`'s `colorMode` sets `color-scheme` (light/dark/system) rather than toggling a `.dark`
* class, so these tokens use CSS `light-dark(<light>, <dark>)` to follow the resolved scheme instead of a
* `.vue-flow.dark` selector. Single values are scheme-independent.
*/
.vue-flow {
--xy-background-color: light-dark(#f7f9fb, #101012);
/* Custom Variables */
--xy-theme-selected: #16a34a;
--xy-theme-hover: light-dark(#c5c5c5, #7a7d86);
--xy-theme-edge-hover: light-dark(black, #f3f4f6);
--xy-theme-color-focus: light-dark(#e8e8e8, #4b4e57);
/* Built-in Variables see https://vueflow.dev/learn/customization/theming */
/* light-dark() is a <color> function — it must wrap only the color, not the whole border shorthand */
--xy-node-border-default: 1px solid light-dark(#ededed, #303238);
/* light-dark() can't split a comma-separated shadow list (wrapping each in a var() doesn't help — the
commas re-expand before light-dark parses its args), so the dark shadow is applied via the
prefers-color-scheme media query below, which matches `color-scheme: system` (the common case) */
--xy-node-boxshadow-default:
0px 3.54px 4.55px 0px #00000005, 0px 3.54px 4.55px 0px #0000000d,
0px 0.51px 1.01px 0px #0000001a;
--xy-node-border-radius-default: 8px;
--xy-handle-background-color-default: light-dark(#ffffff, #141519);
--xy-handle-border-color-default: light-dark(#aaaaaa, #5a5d66);
--xy-edge-label-color-default: light-dark(#505050, #e6e7ea);
--xy-node-background-color-default: light-dark(#ffffff, #1a1b1e);
--xy-theme-panel-bg: light-dark(#ffffff, #141519);
--xy-theme-panel-text: light-dark(#111827, #f3f4f6);
--xy-theme-resize-handle-bg: light-dark(#ffffff, #141519);
--xy-theme-focus-border: light-dark(#d9d9d9, #5f6470);
--xy-theme-muted-bg: light-dark(#f3f4f6, #24262c);
--xy-theme-subtle-border: light-dark(#d1d5db, #3f434c);
/* example UI tokens (consumed by the .xy-theme__* helpers below) */
--color-primary: #16a34a;
--color-background: light-dark(#fefefe, #1a1b1e);
--color-hover-bg: light-dark(#f6f6f6, #24262c);
--color-disabled: light-dark(#76797e, #8b9099);
}
.vue-flow {
background-color: var(--xy-background-color);
}
/* dark node shadow — the one token light-dark() can't express (see above) */
@media (prefers-color-scheme: dark) {
.vue-flow {
--xy-node-boxshadow-default:
0px 6px 18px 0px rgba(0, 0, 0, 0.35), 0px 2px 6px 0px rgba(0, 0, 0, 0.3),
0px 1px 2px 0px rgba(0, 0, 0, 0.35);
}
}
/* Customizing Default Theming */
.vue-flow__node {
box-shadow: var(--xy-node-boxshadow-default);
border-radius: var(--xy-node-border-radius-default);
background-color: var(--xy-node-background-color-default);
display: flex;
justify-content: center;
align-items: center;
text-align: center;
padding: 10px;
font-size: 12px;
flex-direction: column;
border: var(--xy-node-border-default);
color: var(--xy-node-color, var(--xy-node-color-default));
}
.vue-flow__node.selectable:focus {
box-shadow: 0px 0px 0px 4px var(--xy-theme-color-focus);
border-color: var(--xy-theme-focus-border);
}
.vue-flow__node.selectable:focus:active {
box-shadow: var(--xy-node-boxshadow-default);
}
.vue-flow__node.selectable:hover,
.vue-flow__node.draggable:hover {
border-color: var(--xy-theme-hover);
}
.vue-flow__node.selectable.selected {
border-color: var(--xy-theme-selected);
box-shadow: var(--xy-node-boxshadow-default);
}
.vue-flow__node-group {
background-color: light-dark(rgba(207, 182, 255, 0.4), rgba(123, 107, 148, 0.22));
border-color: light-dark(#9e86ed, #7b6b94);
}
.vue-flow__edge.selectable:hover .vue-flow__edge-path,
.vue-flow__edge.selectable.selected .vue-flow__edge-path {
stroke: var(--xy-theme-edge-hover);
}
.vue-flow__handle {
background-color: var(--xy-handle-background-color-default);
}
.vue-flow__handle.connectionindicator:hover {
pointer-events: all;
border-color: var(--xy-theme-edge-hover);
background-color: var(--xy-handle-background-color-default);
}
.vue-flow__handle.connectionindicator:focus,
.vue-flow__handle.connectingfrom,
.vue-flow__handle.connectingto {
border-color: var(--xy-theme-edge-hover);
}
.vue-flow__node-resizer {
border-radius: 0;
border: none;
}
.vue-flow__resize-control.handle {
background-color: var(--xy-theme-resize-handle-bg);
border-color: #9e86ed;
border-radius: 0;
width: 5px;
height: 5px;
}
/*
Custom Example CSS - This CSS is to improve the example experience.
You can remove it if you want to use the default styles.
New Theme Classes:
.xy-theme__button - Styles for buttons.
.xy-theme__input - Styles for text inputs.
.xy-theme__checkbox - Styles for checkboxes.
.xy-theme__select - Styles for dropdown selects.
.xy-theme__label - Styles for labels.
Use these classes to apply consistent theming across your components.
*/
.xy-theme__button-group {
display: flex;
align-items: center;
.xy-theme__button:first-child {
border-radius: 100px 0 0 100px;
}
.xy-theme__button:last-child {
border-radius: 0 100px 100px 0;
margin: 0;
}
}
/* Custom Button Styling */
.xy-theme__button {
display: inline-flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 1rem;
border-radius: 100px;
border: 1px solid var(--color-primary);
background-color: var(--color-background);
color: var(--color-primary);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
box-shadow: var(--xy-node-boxshadow-default);
cursor: pointer;
}
.xy-theme__button.active {
background-color: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.xy-theme__button.active:hover,
.xy-theme__button.active:active {
background-color: var(--color-primary);
opacity: 0.9;
}
.xy-theme__button:hover {
background-color: var(--xy-controls-button-background-color-hover-default);
}
.xy-theme__button:active {
background-color: var(--color-hover-bg);
}
.xy-theme__button:disabled {
color: var(--color-disabled);
opacity: 0.8;
cursor: not-allowed;
border: 1px solid var(--color-disabled);
}
.xy-theme__button > span {
margin-right: 0.2rem;
}
/* Add gap between adjacent buttons */
.xy-theme__button + .xy-theme__button {
margin-left: 0.3rem;
}
/* Example Input Styling */
.xy-theme__input {
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-primary);
border-radius: 7px;
background-color: var(--color-background);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
font-size: 1rem;
color: var(--xy-theme-panel-text);
}
.xy-theme__input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(22, 163, 74, 0.3);
}
/* Specific Checkbox Styling */
.xy-theme__checkbox {
appearance: none;
-webkit-appearance: none;
width: 1.25rem;
height: 1.25rem;
border-radius: 7px;
border: 2px solid var(--color-primary);
background-color: var(--color-background);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
cursor: pointer;
display: inline-block;
vertical-align: middle;
margin-right: 0.5rem;
}
.xy-theme__checkbox:checked {
background-color: var(--color-primary);
border-color: var(--color-primary);
}
.xy-theme__checkbox:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(22, 163, 74, 0.3);
}
/* Dropdown Styling */
.xy-theme__select {
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-primary);
border-radius: 50px;
background-color: var(--color-background);
transition:
background-color 0.2s ease,
border-color 0.2s ease;
font-size: 1rem;
color: var(--xy-theme-panel-text);
margin-right: 0.5rem;
box-shadow: var(--xy-node-boxshadow-default);
}
.xy-theme__select:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(22, 163, 74, 0.3);
}
.xy-theme__label {
margin-top: 10px;
margin-bottom: 3px;
display: inline-block;
color: var(--xy-theme-panel-text);
}