Drag & Drop
Drag nodes from a palette onto the canvas, and create nodes by dropping an edge.
Overview
Build a node palette and drop new nodes onto the canvas (droppedNode), or drag a connection into empty space to spawn a connected node with addNodeOnEdgeDrop (edgeDroppedNode).
Live demo
Drag the nodes, pan, and zoom — the canvas below is a real, running DashFlows component (no callbacks, just the rendered graph):
# File: docs/drag_drop/demo.py
"""Live, callback-free demo for the docs page. Rendered via `.. exec::docs.drag_drop.demo`."""
import dash_flows
nodes = [{'id': 'palette', 'type': 'input', 'data': {'label': 'Drag me'}, 'position': {'x': 60, 'y': 60}},
{'id': 'canvas',
'type': 'default',
'data': {'label': 'Drop target'},
'position': {'x': 240, 'y': 200}}]
edges = [{'id': 'pc', 'source': 'palette', 'target': 'canvas'}]
component = dash_flows.DashFlows(
id="drag_drop-demo",
nodes=nodes,
edges=edges,
style={'border': '1px solid var(--mantine-color-default-border)',
'borderRadius': '8px',
'height': '440px'},
showControls=True,
fitView=True,
colorMode='system',
)
:defaultExpanded: false :withExpandedButton: true
Examples
Each example below is a complete, runnable Dash app from the examples/ folder. Run any of them with python examples/<file>.
Drag and Drop
Drag node templates from a sidebar palette and drop them onto the flow canvas. Use this pattern when you want a component-library-style builder where users assemble a graph from pre-defined node kinds.
# File: examples/17_drag_and_drop.py
"""
Example 17: Drag and Drop from Sidebar
======================================
This example demonstrates how to:
- Create a sidebar with draggable node templates
- Drop nodes onto the flow canvas
- Create new nodes at the drop position
"""
import dash
from dash import html, callback, Input, Output, State
import dash_flows
import uuid
app = dash.Dash(__name__)
# Sidebar styles
sidebar_style = {
"width": "200px",
"padding": "15px",
"background": "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
"borderRadius": "12px",
"marginRight": "15px",
}
draggable_item_style = {
"padding": "12px",
"marginBottom": "10px",
"background": "rgba(255, 255, 255, 0.9)",
"borderRadius": "8px",
"cursor": "grab",
"textAlign": "center",
"fontWeight": "500",
"boxShadow": "0 2px 8px rgba(0,0,0,0.15)",
"transition": "transform 0.2s, box-shadow 0.2s",
}
# Initial nodes
initial_nodes = [
{
"id": "start",
"type": "input",
"data": {"label": "Start Here"},
"position": {"x": 250, "y": 50},
},
]
app.layout = html.Div([
html.H1("Drag and Drop Example"),
html.P("Drag node types from the sidebar and drop them onto the canvas."),
html.Div([
# Sidebar with draggable items
html.Div([
html.H4("Node Palette", style={"color": "white", "marginBottom": "15px"}),
# Input node
html.Div(
"Input Node",
id="drag-input",
draggable="true",
style=draggable_item_style,
**{"data-node-type": "input", "data-node-label": "Input"}
),
# Default node
html.Div(
"Process Node",
id="drag-default",
draggable="true",
style=draggable_item_style,
**{"data-node-type": "default", "data-node-label": "Process"}
),
# Output node
html.Div(
"Output Node",
id="drag-output",
draggable="true",
style=draggable_item_style,
**{"data-node-type": "output", "data-node-label": "Output"}
),
# Resizable node
html.Div(
"Resizable Node",
id="drag-resizable",
draggable="true",
style=draggable_item_style,
**{"data-node-type": "resizable", "data-node-label": "Resizable"}
),
], style=sidebar_style),
# Flow canvas
html.Div([
dash_flows.DashFlows(
id="flow",
nodes=initial_nodes,
edges=[],
style={"height": "500px", "border": "2px dashed #ccc", "borderRadius": "8px"},
fitView=True,
showControls=True,
showMiniMap=True,
showBackground=True,
),
], style={"flex": "1"}),
], style={"display": "flex", "gap": "15px"}),
# Status display
html.Div(id="drop-status", style={
"marginTop": "15px",
"padding": "10px",
"background": "#f0f0f0",
"borderRadius": "5px",
}),
# Hidden client-side callback script
html.Script('''
document.addEventListener('DOMContentLoaded', function() {
const draggables = document.querySelectorAll('[draggable="true"]');
draggables.forEach(item => {
item.addEventListener('dragstart', function(e) {
const nodeType = this.getAttribute('data-node-type');
const nodeLabel = this.getAttribute('data-node-label');
e.dataTransfer.setData('application/reactflow', JSON.stringify({
type: nodeType,
data: { label: nodeLabel }
}));
e.dataTransfer.effectAllowed = 'move';
});
});
});
'''),
])
# Client-side callback to handle drag start
app.clientside_callback(
"""
function(n) {
// Set up drag events for all draggable items
const draggables = document.querySelectorAll('[draggable="true"]');
draggables.forEach(item => {
item.ondragstart = function(e) {
const nodeType = this.getAttribute('data-node-type');
const nodeLabel = this.getAttribute('data-node-label');
e.dataTransfer.setData('application/reactflow', JSON.stringify({
type: nodeType,
data: { label: nodeLabel }
}));
e.dataTransfer.setData('text/plain', JSON.stringify({
type: nodeType,
data: { label: nodeLabel }
}));
e.dataTransfer.effectAllowed = 'move';
};
});
return window.dash_clientside.no_update;
}
""",
Output("drop-status", "data-init"),
Input("drop-status", "id"),
)
# Handle dropped node - create new node
@callback(
[Output("flow", "nodes"),
Output("drop-status", "children")],
Input("flow", "droppedNode"),
State("flow", "nodes"),
prevent_initial_call=True,
)
def handle_drop(dropped_node, current_nodes):
if not dropped_node:
return dash.no_update, dash.no_update
# Generate unique ID for new node
new_id = f"node-{str(uuid.uuid4())[:8]}"
# Create new node
new_node = {
"id": new_id,
"type": dropped_node.get("type", "default"),
"data": dropped_node.get("data", {"label": "New Node"}),
"position": dropped_node.get("position", {"x": 100, "y": 100}),
}
# Add resizable node specific properties
if new_node["type"] == "resizable":
new_node["data"]["handles"] = [
{"id": "top", "type": "target", "position": "top"},
{"id": "bottom", "type": "source", "position": "bottom"},
]
new_node["style"] = {"width": 200, "height": 100}
# Append to existing nodes
updated_nodes = current_nodes + [new_node] if current_nodes else [new_node]
status = f"Created new {new_node['type']} node '{new_node['data'].get('label', 'Node')}' at ({dropped_node['position']['x']:.0f}, {dropped_node['position']['y']:.0f})"
return updated_nodes, status
if __name__ == "__main__":
app.run(debug=True, port=8087)
:defaultExpanded: false :withExpandedButton: true
How it works
draggable="true"HTML divs in the sidebar carrydata-node-type/data-node-labelattributes describing what to create.- A
clientside_callbackwires upondragstarthandlers that pack that data intoe.dataTransferasapplication/reactflow(andtext/plain) JSON — this runs once on load via a dummyInput("...", "id")trigger. droppedNode(output prop) fires when the user releases the drag over the canvas, giving you the parsedtype,data, and dropposition.- The
droppedNodecallback appends a new node dict tonodes—type="resizable"nodes additionally get customhandlesand astylewith width/height. - The raw
droppedNodepayload is echoed as JSON below the canvas so you can see exactly what the drop event contains.
Add Node on Edge Drop
Drag a connection off a handle and release it on empty canvas to spawn a new, already-connected node — no palette required.
# File: examples/28_add_node_on_drop.py
"""
Example 28: Add Node on Edge Drop
===================================
Demonstrates creating new nodes by dragging a connection from a handle
and dropping it on empty canvas. A new default node is created at the
drop position and automatically connected.
"""
import dash
from dash import html, callback, Input, Output, State
import dash_flows
import json
app = dash.Dash(__name__)
initial_nodes = [
{
"id": "start",
"type": "input",
"data": {"label": "Start Here", "sublabel": "Drag from a handle"},
"position": {"x": 250, "y": 50},
},
]
initial_edges = []
app.layout = html.Div([
html.H1("Add Node on Edge Drop"),
html.P(
"Drag from any handle and release on empty canvas to create a new node. "
"The new node is automatically connected to the source."
),
dash_flows.DashFlows(
id="drop-flow",
nodes=initial_nodes,
edges=initial_edges,
addNodeOnEdgeDrop=True,
style={"height": "600px", "border": "1px solid #ddd"},
fitView=True,
showControls=True,
showBackground=True,
showMiniMap=True,
backgroundVariant="dots",
colorScheme="forest",
),
html.Div([
html.H3("Edge Drop Events", style={"marginTop": "16px"}),
html.Pre(
id="drop-info",
style={
"background": "#f5f5f5",
"padding": "12px",
"borderRadius": "6px",
"fontSize": "12px",
"maxHeight": "200px",
"overflow": "auto",
},
),
]),
html.Div([
html.H3("Node Connections"),
html.Pre(
id="connections-info",
style={
"background": "#f5f5f5",
"padding": "12px",
"borderRadius": "6px",
"fontSize": "12px",
"maxHeight": "200px",
"overflow": "auto",
},
),
]),
])
@callback(
Output("drop-info", "children"),
Input("drop-flow", "edgeDroppedNode"),
)
def show_drop_event(dropped):
if not dropped:
return "Drag from a handle and drop on empty canvas..."
return json.dumps(dropped, indent=2)
@callback(
Output("connections-info", "children"),
Input("drop-flow", "nodeConnections"),
)
def show_connections(connections):
if not connections:
return "No connections yet."
return json.dumps(connections, indent=2)
if __name__ == "__main__":
app.run(debug=True, port=8028)
:defaultExpanded: false :withExpandedButton: true
How it works
addNodeOnEdgeDrop=Truetells DashFlows to create a node automatically whenever a connection drag ends over empty canvas instead of over a handle.edgeDroppedNode(output prop) reports the new node/edge pair that was created, including the drop position and which node it was connected from.nodeConnections(output prop) is a live map of every node's incoming/outgoing edges, updated as connections are made — useful for building things like validation or dependency graphs.- Both outputs are dumped as formatted JSON under the canvas so you can watch the events fire as you drag.
backgroundVariant="dots"andcolorScheme="forest"are purely cosmetic and can be swapped for any of the supported presets.
Source: /drag-drop
Note for AI agents: This is the static, prerendered view of an interactive Dash application served because we detected a non-JS user agent. Full prose docs:
- /drag-drop/llms.txt — LLM-friendly documentation
- /sitemap.xml
- /robots.txt