Drag and Drop
In these examples, we use a custom Sidebar component to create new nodes when dragging from the sidebar into the React Flow pane.
A drag and drop user interface is very common for node-based workflow editors. The drag and drop behavior outside of the React Flow pane is not built in but can be implemented with the native HTML Drag and Drop API , Pointer Events , or a third party library like react-draggable .
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.
/* xyflow theme files. Delete these to start from our base */
.react-flow {
--xy-background-color: #f7f9fb;
/* Custom Variables */
--xy-theme-selected: #f57dbd;
--xy-theme-hover: #c5c5c5;
--xy-theme-edge-hover: black;
--xy-theme-color-focus: #e8e8e8;
/* Built-in Variables see https://reactflow.dev/learn/customization/theming */
--xy-node-border-default: 1px solid #ededed;
--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: #ffffff;
--xy-handle-border-color-default: #aaaaaa;
--xy-edge-label-color-default: #505050;
--xy-node-background-color-default: #ffffff;
--xy-theme-panel-bg: #ffffff;
--xy-theme-panel-text: #111827;
--xy-theme-resize-handle-bg: #ffffff;
--xy-theme-focus-border: #d9d9d9;
--xy-theme-muted-bg: #f3f4f6;
--xy-theme-subtle-border: #d1d5db;
}
.react-flow.dark {
--xy-background-color: #101012;
--xy-node-background-color-default: #1a1b1e;
--xy-node-border-default: 1px solid #303238;
--xy-handle-background-color-default: #141519;
--xy-handle-border-color-default: #5a5d66;
--xy-edge-label-color-default: #e6e7ea;
--xy-theme-hover: #7a7d86;
--xy-theme-edge-hover: #f3f4f6;
--xy-theme-panel-bg: #141519;
--xy-theme-panel-text: #f3f4f6;
--xy-theme-resize-handle-bg: #141519;
--xy-theme-focus-border: #5f6470;
--xy-theme-muted-bg: #24262c;
--xy-theme-subtle-border: #3f434c;
--xy-node-boxshadow-default: none;
--xy-theme-color-focus: #4b4e57;
--color-background: #1a1b1e;
--color-hover-bg: #24262c;
--color-disabled: #8b9099;
}
.react-flow {
background-color: var(--xy-background-color);
}
/* Customizing Default Theming */
.react-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));
}
.react-flow__node.selectable:focus {
box-shadow: 0px 0px 0px 4px var(--xy-theme-color-focus);
border-color: var(--xy-theme-focus-border);
}
.react-flow__node.selectable:focus:active {
box-shadow: var(--xy-node-boxshadow-default);
}
.react-flow__node.selectable:hover,
.react-flow__node.draggable:hover {
border-color: var(--xy-theme-hover);
}
.react-flow__node.selectable.selected {
border-color: var(--xy-theme-selected);
box-shadow: var(--xy-node-boxshadow-default);
}
.react-flow__node-group {
background-color: rgba(207, 182, 255, 0.4);
border-color: #9e86ed;
}
.react-flow.dark .react-flow__node-group {
background-color: rgba(123, 107, 148, 0.22);
border-color: #7b6b94;
}
.react-flow__edge.selectable:hover .react-flow__edge-path,
.react-flow__edge.selectable.selected .react-flow__edge-path {
stroke: var(--xy-theme-edge-hover);
}
.react-flow__handle {
background-color: var(--xy-handle-background-color-default);
}
.react-flow__handle.connectionindicator:hover {
pointer-events: all;
border-color: var(--xy-theme-edge-hover);
background-color: var(--xy-handle-background-color-default);
}
.react-flow__handle.connectionindicator:focus,
.react-flow__handle.connectingfrom,
.react-flow__handle.connectingto {
border-color: var(--xy-theme-edge-hover);
}
.react-flow__node-resizer {
border-radius: 0;
border: none;
}
.react-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.
*/
:root {
--color-primary: #ff0073;
--color-background: #fefefe;
--color-hover-bg: #f6f6f6;
--color-disabled: #76797e;
}
.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(255, 0, 115, 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(255, 0, 115, 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(255, 0, 115, 0.3);
}
.xy-theme__label {
margin-top: 10px;
margin-bottom: 3px;
display: inline-block;
color: var(--xy-theme-panel-text);
}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 { useReactFlow, XYPosition } from '@xyflow/react';
import {
createContext,
Dispatch,
SetStateAction,
useCallback,
useContext,
useEffect,
useState,
} from 'react';
export type OnDropAction = ({ position }: { position: XYPosition }) => void;
interface DnDContextType {
// If a node is being dragged.
isDragging: boolean;
setIsDragging: Dispatch<SetStateAction<boolean>>;
// The action to be performed when something is dropped on the flow.
dropAction: OnDropAction | null;
setDropAction: Dispatch<SetStateAction<OnDropAction | null>>;
}
const DnDContext = createContext<DnDContextType | null>(null);
// The DnDProvider is used to provide the context for the DnD functionality.
// This allows you to wrap your `ReactFlow` component instance in the `DnDProvider`,
// so you do not need to register any callback in `App.tsx`.
// You can just use the `useDnD` hook in your components that need to start dragging a new node into the flow.
// In our case, it will be the `Sidebar` component.
export function DnDProvider({ children }: { children: React.ReactNode }) {
const [isDragging, setIsDragging] = useState(false);
const [dropAction, setDropAction] = useState<OnDropAction | null>(null);
return (
<DnDContext.Provider
value={{
isDragging,
setIsDragging,
dropAction,
// This is a workaround to ensure that the drop action is not treated as a lazy function.
setDropAction: (action) => setDropAction(() => action),
}}
>
{children}
</DnDContext.Provider>
);
}
export default DnDContext;
export const useDnD = () => {
const { screenToFlowPosition } = useReactFlow();
const context = useContext(DnDContext);
if (!context) {
throw new Error('useDnD must be used within a DnDProvider');
}
const { isDragging, setIsDragging, setDropAction, dropAction } = context;
// This callback will be returned by the `useDnD` hook, and can be used in your UI,
// when you want to start dragging a node into the flow.
// For example, this is used in the `Sidebar` component.
const onDragStart = useCallback(
(event: React.PointerEvent<HTMLDivElement>, onDrop: OnDropAction) => {
event.preventDefault();
(event.target as HTMLElement).setPointerCapture(event.pointerId);
setIsDragging(true);
setDropAction(onDrop);
},
[setIsDragging, setDropAction],
);
const onDragEnd = useCallback(
(event: PointerEvent) => {
if (!isDragging) {
setIsDragging(false);
return;
}
(event.target as HTMLElement).releasePointerCapture(event.pointerId);
// Use elementFromPoint to get the actual element under the pointer
const elementUnderPointer = document.elementFromPoint(event.clientX, event.clientY);
const isDroppingOnFlow = elementUnderPointer?.closest('.react-flow');
event.preventDefault();
// Only allow dropping on the flow area
if (isDroppingOnFlow) {
const flowPosition = screenToFlowPosition({ x: event.clientX, y: event.clientY });
dropAction?.({ position: flowPosition });
}
setIsDragging(false);
},
[screenToFlowPosition, setIsDragging, dropAction],
);
// Add global touch event listeners
useEffect(() => {
if (!isDragging) return;
document.addEventListener('pointerup', onDragEnd);
return () => {
document.removeEventListener('pointerup', onDragEnd);
};
}, [onDragEnd, isDragging]);
return {
isDragging,
onDragStart,
};
};
export const useDnDPosition = () => {
const [position, setPosition] = useState<XYPosition | undefined>(undefined);
// By default, the pointer move event sets the position of the dragged element in the context.
// This will be used to display the `DragGhost` component.
const onDrag = useCallback((event: PointerEvent) => {
event.preventDefault();
setPosition({ x: event.clientX, y: event.clientY });
}, []);
useEffect(() => {
document.addEventListener('pointermove', onDrag);
return () => {
document.removeEventListener('pointermove', onDrag);
};
}, [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.
import { ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}