Nodes
Built-in node types, handle configuration, and Dash components or icons inside nodes.
Overview
dash-flows ships several node types: input (source only), output (target only), default (both), group (a container for child nodes via parentId), resizable, circle, and toolbar. You can also render arbitrary Dash components and DashIconify icons inside a node's data.
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/nodes/demo.py
"""Live, callback-free demo for the docs page. Rendered via `.. exec::docs.nodes.demo`."""
import dash_flows
nodes = [{'id': 'in', 'type': 'input', 'data': {'label': 'input'}, 'position': {'x': 40, 'y': 20}},
{'id': 'def', 'type': 'default', 'data': {'label': 'default'}, 'position': {'x': 40, 'y': 140}},
{'id': 'out', 'type': 'output', 'data': {'label': 'output'}, 'position': {'x': 40, 'y': 260}},
{'id': 'res',
'type': 'resizable',
'data': {'label': 'resizable'},
'position': {'x': 260, 'y': 40},
'style': {'width': 170, 'height': 90}},
{'id': 'circ', 'type': 'circle', 'data': {'label': 'circle'}, 'position': {'x': 300, 'y': 200}},
{'id': 'grp',
'type': 'group',
'data': {'label': 'group'},
'position': {'x': 480, 'y': 20},
'style': {'width': 220, 'height': 180}},
{'id': 'c1',
'type': 'default',
'data': {'label': 'child 1'},
'position': {'x': 20, 'y': 40},
'parentId': 'grp',
'extent': 'parent'},
{'id': 'c2',
'type': 'default',
'data': {'label': 'child 2'},
'position': {'x': 20, 'y': 110},
'parentId': 'grp',
'extent': 'parent'}]
edges = [{'id': 'n1', 'source': 'in', 'target': 'def'},
{'id': 'n2', 'source': 'def', 'target': 'out'},
{'id': 'n3', 'source': 'res', 'target': 'circ'}]
component = dash_flows.DashFlows(
id="nodes-demo",
nodes=nodes,
edges=edges,
style={'border': '1px solid var(--mantine-color-default-border)',
'borderRadius': '8px',
'height': '440px'},
showControls=True,
showMiniMap=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>.
All Node Types
Every built-in node type side by side: input, output, default, group, resizable, circle, and toolbar. Use this as a visual reference when picking a type for a new node.
# File: examples/02_all_node_types.py
"""
Example 02: All Node Types
==========================
This example showcases all available node types in DashFlows:
- default: Standard node with both source and target handles
- input: Node with only source handle (start nodes)
- output: Node with only target handle (end nodes)
- group: Container node that can hold other nodes
- toolbar: Node with a configurable toolbar
- resizable: Node that can be resized by the user
- circle: Animated circular node
"""
import dash
from dash import html
import dash_flows
app = dash.Dash(__name__)
nodes = [
# Input Node - Entry point (only source handle)
{
"id": "input-1",
"type": "input",
"data": {
"label": "Input Node",
"sublabel": "Entry point",
},
"position": {"x": 50, "y": 50},
},
# Default Node - Standard processing node
{
"id": "default-1",
"type": "default",
"data": {
"label": "Default Node",
"sublabel": "Both handles",
},
"position": {"x": 50, "y": 200},
},
# Output Node - Exit point (only target handle)
{
"id": "output-1",
"type": "output",
"data": {
"label": "Output Node",
"sublabel": "Exit point",
},
"position": {"x": 50, "y": 350},
},
# Toolbar Node - With action buttons
{
"id": "toolbar-1",
"type": "toolbar",
"data": {
"label": "Toolbar Node",
"sublabel": "Click to see toolbar",
"toolbarPosition": "top",
},
"position": {"x": 300, "y": 50},
},
# Resizable Node - Can be resized
{
"id": "resizable-1",
"type": "resizable",
"data": {
"label": html.Div([
html.Strong("Resizable Node"),
html.P("Drag corners to resize", style={"fontSize": "11px"}),
]),
"handles": [
{"type": "target", "position": "top", "id": "r-top"},
{"type": "source", "position": "bottom", "id": "r-bottom"},
],
},
"position": {"x": 300, "y": 200},
"style": {"width": 200, "height": 100},
},
# Circle Node - Animated circular node
{
"id": "circle-1",
"type": "circle",
"data": {"label": "A"},
"position": {"x": 340, "y": 360},
},
# Group Node - Container for other nodes
{
"id": "group-1",
"type": "group",
"data": {
"label": "Group Container",
},
"position": {"x": 550, "y": 50},
"style": {"width": 250, "height": 300},
},
# Child nodes inside the group
{
"id": "child-1",
"type": "default",
"data": {"label": "Child A"},
"position": {"x": 50, "y": 50}, # Position below the label badge
"parentId": "group-1",
"extent": "parent", # Keep within parent bounds
},
{
"id": "child-2",
"type": "default",
"data": {"label": "Child B"},
"position": {"x": 50, "y": 160},
"parentId": "group-1",
"extent": "parent",
},
]
edges = [
{"id": "e1", "source": "input-1", "target": "default-1"},
{"id": "e2", "source": "default-1", "target": "output-1"},
{"id": "e3", "source": "toolbar-1", "target": "resizable-1", "sourceHandle": None, "targetHandle": "r-top"},
{"id": "e4", "source": "resizable-1", "target": "circle-1", "sourceHandle": "r-bottom"},
{"id": "e5", "source": "child-1", "target": "child-2"},
]
app.layout = html.Div([
html.H1("All Node Types Example"),
html.P("Demonstrates every available node type in DashFlows."),
html.Ul([
html.Li("Input Node: Green accent, only outgoing connections"),
html.Li("Default Node: Standard node with both connection types"),
html.Li("Output Node: Purple accent, only incoming connections"),
html.Li("Toolbar Node: Click to reveal action toolbar"),
html.Li("Resizable Node: Drag corners to resize"),
html.Li("Circle Node: Animated circular indicator"),
html.Li("Group Node: Container that holds child nodes"),
]),
dash_flows.DashFlows(
id="node-types-flow",
nodes=nodes,
edges=edges,
style={"height": "600px", "border": "1px solid #ddd"},
fitView=True,
showControls=True,
showMiniMap=True,
),
])
if __name__ == "__main__":
app.run(debug=True, port=8071)
:defaultExpanded: false :withExpandedButton: true
How it works
type: "input"/"output"— single-handle nodes with a green (source-only) or purple (target-only) accent, good for entry/exit points.type: "toolbar"— reveals an action toolbar above the node on selection viadata.toolbarPosition.type: "resizable"withdata.handles— a resizable node with a custom list of handles (each withtype,position, andid), used here to expose a top target and bottom source handle.type: "group"plusparentId/extent: "parent"on child nodes — nests nodes inside a container that moves and resizes as a unit.type: "circle"— a compact animated circular node, handy for status dots or simple junctions.
Handle Configurations
Control where connection handles sit, how many a node has, and how they're styled — beyond the single default source/target pair.
# File: examples/06_handle_configurations.py
"""
Example 06: Handle Configurations
=================================
This example demonstrates various handle configurations:
- Multiple handles per node
- Handle positions (top, bottom, left, right)
- Custom handle IDs for specific connections
- Handle styling
"""
import dash
from dash import html
import dash_flows
app = dash.Dash(__name__)
nodes = [
# Node with multiple handles on different sides
{
"id": "multi-handle",
"type": "resizable",
"data": {
"label": html.Div([
html.Strong("Multi-Handle Node"),
html.P("4 handles on each side", style={"fontSize": "11px", "margin": 0}),
]),
"handles": [
{"type": "target", "position": "top", "id": "top-in", "style": {"background": "#10b981"}},
{"type": "target", "position": "left", "id": "left-in", "style": {"background": "#3b82f6"}},
{"type": "source", "position": "bottom", "id": "bottom-out", "style": {"background": "#8b5cf6"}},
{"type": "source", "position": "right", "id": "right-out", "style": {"background": "#f59e0b"}},
],
},
"position": {"x": 250, "y": 150},
"style": {"width": 180, "height": 80},
},
# Source nodes feeding into the multi-handle node
{
"id": "top-source",
"type": "input",
"data": {"label": "Top Source"},
"position": {"x": 270, "y": 20},
},
{
"id": "left-source",
"type": "input",
"data": {"label": "Left Source"},
"position": {"x": 50, "y": 160},
},
# Target nodes receiving from the multi-handle node
{
"id": "bottom-target",
"type": "output",
"data": {"label": "Bottom Target"},
"position": {"x": 270, "y": 310},
},
{
"id": "right-target",
"type": "output",
"data": {"label": "Right Target"},
"position": {"x": 480, "y": 160},
},
# Node with offset handles
{
"id": "offset-handles",
"type": "resizable",
"data": {
"label": html.Div([
html.Strong("Offset Handles"),
html.P("Handles at specific positions", style={"fontSize": "11px", "margin": 0}),
]),
"handles": [
{"type": "target", "position": "top", "id": "t1", "style": {"left": "25%", "background": "#ef4444"}},
{"type": "target", "position": "top", "id": "t2", "style": {"left": "75%", "background": "#ef4444"}},
{"type": "source", "position": "bottom", "id": "s1", "style": {"left": "25%", "background": "#22c55e"}},
{"type": "source", "position": "bottom", "id": "s2", "style": {"left": "75%", "background": "#22c55e"}},
],
},
"position": {"x": 250, "y": 420},
"style": {"width": 180, "height": 80},
},
# Nodes to connect to offset handles
{
"id": "offset-source-1",
"type": "input",
"data": {"label": "A"},
"position": {"x": 220, "y": 350},
},
{
"id": "offset-source-2",
"type": "input",
"data": {"label": "B"},
"position": {"x": 380, "y": 350},
},
{
"id": "offset-target-1",
"type": "output",
"data": {"label": "X"},
"position": {"x": 220, "y": 560},
},
{
"id": "offset-target-2",
"type": "output",
"data": {"label": "Y"},
"position": {"x": 380, "y": 560},
},
]
edges = [
# Connections to multi-handle node
{"id": "e1", "source": "top-source", "target": "multi-handle", "targetHandle": "top-in"},
{"id": "e2", "source": "left-source", "target": "multi-handle", "targetHandle": "left-in"},
{"id": "e3", "source": "multi-handle", "target": "bottom-target", "sourceHandle": "bottom-out"},
{"id": "e4", "source": "multi-handle", "target": "right-target", "sourceHandle": "right-out"},
# Connections to offset handles node
{"id": "e5", "source": "offset-source-1", "target": "offset-handles", "targetHandle": "t1"},
{"id": "e6", "source": "offset-source-2", "target": "offset-handles", "targetHandle": "t2"},
{"id": "e7", "source": "offset-handles", "target": "offset-target-1", "sourceHandle": "s1"},
{"id": "e8", "source": "offset-handles", "target": "offset-target-2", "sourceHandle": "s2"},
]
app.layout = html.Div([
html.H1("Handle Configurations Example"),
html.P("Demonstrates multiple handles, custom positions, and handle styling."),
html.Ul([
html.Li("Multi-Handle Node: 4 handles, one on each side with different colors"),
html.Li("Offset Handles: Multiple handles on the same side at specific positions"),
html.Li("Each handle has a unique ID for precise edge connections"),
]),
dash_flows.DashFlows(
id="handle-flow",
nodes=nodes,
edges=edges,
style={"height": "700px", "border": "1px solid #ddd"},
fitView=True,
showControls=True,
),
])
if __name__ == "__main__":
app.run(debug=True, port=8055)
:defaultExpanded: false :withExpandedButton: true
How it works
data.handles— a list of{type, position, id, style}dicts; each entry adds one handle to the node, so a node can mix any number of sources and targets on any side.position("top"/"bottom"/"left"/"right") picks which edge of the node the handle sits on.style.left(e.g."25%","75%") offsets multiple handles that share the same side so they don't overlap.sourceHandle/targetHandleon an edge reference a specific handleid, letting you route several distinct connections through one node.style.backgroundcolors each handle individually, which is useful for visually pairing a handle with the edges that use it.
Dash Components in Nodes
Render arbitrary Dash and Dash Mantine components — including live charts — as node content, and combine that with a "detail panel" pattern for content too heavy to keep inline.
# File: examples/14_dash_components_in_nodes.py
"""
Example 14: Dash Components in Resizable Nodes
===============================================
This example demonstrates embedding HTML content inside resizable flow nodes.
What works inside ResizableNode:
- html.* components (Div, Span, Button, Img, etc.)
- CSS-styled visualizations (progress bars, stat cards)
- Simple text and formatted content
What requires the "detail panel" pattern:
- dcc.Graph (Plotly charts)
- Complex interactive components
- Components with callbacks
This example shows both patterns working together.
"""
import dash
from dash import html, dcc, Input, Output, callback
import dash_flows
import dash_mantine_components as dmc
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
app = dash.Dash(__name__, suppress_callback_exceptions=True)
# Sample data for the detail panel charts
df = pd.DataFrame({
"Month": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"],
"Revenue": [12400, 15800, 14200, 18900, 21000, 19500],
"Users": [1200, 1450, 1380, 1720, 1950, 1840],
})
# ============================================
# Node Definitions
# ============================================
nodes = [
# Input Node - Data Source
{
"id": "data-source",
"type": "input",
"data": {"label": "Data Source", "sublabel": "API Feed"},
"position": {"x": 50, "y": 150},
},
# Resizable Node - Revenue Metrics Card
{
"id": "revenue-card",
"type": "resizable",
"data": {
"label": html.Div([
# Header
html.Div("Revenue", style={
"fontSize": "12px",
"color": "#6b7280",
"marginBottom": "4px",
}),
# Main value
html.Div("$19,500", style={
"fontSize": "28px",
"fontWeight": "bold",
"color": "#1a1b1e",
}),
# Trend indicator
html.Div([
html.Span("+8.2%", style={
"color": "#10b981",
"fontWeight": "600",
"fontSize": "13px",
}),
html.Span(" vs last month", style={
"color": "#9ca3af",
"fontSize": "11px",
}),
], style={"marginTop": "4px"}),
# Mini bar chart using CSS
html.Div([
html.Div(style={"flex": "1", "height": "20px", "background": "#dbeafe", "borderRadius": "2px", "margin": "1px"}),
html.Div(style={"flex": "1", "height": "28px", "background": "#93c5fd", "borderRadius": "2px", "margin": "1px"}),
html.Div(style={"flex": "1", "height": "24px", "background": "#60a5fa", "borderRadius": "2px", "margin": "1px"}),
html.Div(style={"flex": "1", "height": "35px", "background": "#3b82f6", "borderRadius": "2px", "margin": "1px"}),
html.Div(style={"flex": "1", "height": "42px", "background": "#2563eb", "borderRadius": "2px", "margin": "1px"}),
html.Div(style={"flex": "1", "height": "38px", "background": "#1d4ed8", "borderRadius": "2px", "margin": "1px"}),
], style={
"display": "flex",
"alignItems": "flex-end",
"marginTop": "12px",
"height": "45px",
}),
], style={
"padding": "12px",
"height": "100%",
"boxSizing": "border-box",
}),
"handles": [
{"type": "target", "position": "left", "id": "revenue-in"},
{"type": "source", "position": "right", "id": "revenue-out"},
],
"minWidth": 160,
"minHeight": 140,
"padding": 0,
},
"position": {"x": 250, "y": 50},
"style": {"width": 180, "height": 160},
},
# Resizable Node - User Metrics Card
{
"id": "users-card",
"type": "resizable",
"data": {
"label": html.Div([
# Header
html.Div("Active Users", style={
"fontSize": "12px",
"color": "#6b7280",
"marginBottom": "4px",
}),
# Main value
html.Div("1,840", style={
"fontSize": "28px",
"fontWeight": "bold",
"color": "#1a1b1e",
}),
# Trend indicator
html.Div([
html.Span("-5.6%", style={
"color": "#ef4444",
"fontWeight": "600",
"fontSize": "13px",
}),
html.Span(" vs last month", style={
"color": "#9ca3af",
"fontSize": "11px",
}),
], style={"marginTop": "4px"}),
# Progress ring simulation
html.Div([
html.Div(style={
"width": "50px",
"height": "50px",
"borderRadius": "50%",
"border": "4px solid #e5e7eb",
"borderTopColor": "#8b5cf6",
"borderRightColor": "#8b5cf6",
"transform": "rotate(45deg)",
}),
html.Div("73%", style={
"position": "absolute",
"fontSize": "11px",
"fontWeight": "bold",
"color": "#6b7280",
}),
], style={
"display": "flex",
"alignItems": "center",
"justifyContent": "center",
"marginTop": "8px",
"position": "relative",
}),
], style={
"padding": "12px",
"height": "100%",
"boxSizing": "border-box",
}),
"handles": [
{"type": "target", "position": "left", "id": "users-in"},
{"type": "source", "position": "right", "id": "users-out"},
],
"minWidth": 160,
"minHeight": 140,
"padding": 0,
},
"position": {"x": 250, "y": 240},
"style": {"width": 180, "height": 170},
},
# Resizable Node - Processing Status
{
"id": "status-card",
"type": "resizable",
"data": {
"label": html.Div([
html.Div("Pipeline Status", style={
"fontSize": "13px",
"fontWeight": "600",
"color": "#1a1b1e",
"marginBottom": "12px",
}),
# Stage 1 - Complete
html.Div([
html.Div([
html.Span("Extract", style={"fontSize": "11px", "color": "#374151"}),
html.Span("Done", style={"fontSize": "10px", "color": "#10b981", "marginLeft": "auto"}),
], style={"display": "flex", "marginBottom": "4px"}),
html.Div([
html.Div(style={
"width": "100%",
"height": "6px",
"background": "#10b981",
"borderRadius": "3px",
})
], style={"background": "#e5e7eb", "borderRadius": "3px"}),
], style={"marginBottom": "10px"}),
# Stage 2 - In Progress
html.Div([
html.Div([
html.Span("Transform", style={"fontSize": "11px", "color": "#374151"}),
html.Span("75%", style={"fontSize": "10px", "color": "#3b82f6", "marginLeft": "auto"}),
], style={"display": "flex", "marginBottom": "4px"}),
html.Div([
html.Div(style={
"width": "75%",
"height": "6px",
"background": "#3b82f6",
"borderRadius": "3px",
})
], style={"background": "#e5e7eb", "borderRadius": "3px"}),
], style={"marginBottom": "10px"}),
# Stage 3 - Pending
html.Div([
html.Div([
html.Span("Load", style={"fontSize": "11px", "color": "#374151"}),
html.Span("Pending", style={"fontSize": "10px", "color": "#9ca3af", "marginLeft": "auto"}),
], style={"display": "flex", "marginBottom": "4px"}),
html.Div([
html.Div(style={
"width": "0%",
"height": "6px",
"background": "#9ca3af",
"borderRadius": "3px",
})
], style={"background": "#e5e7eb", "borderRadius": "3px"}),
]),
], style={
"padding": "12px",
"height": "100%",
"boxSizing": "border-box",
}),
"handles": [
{"type": "target", "position": "top", "id": "status-in-1"},
{"type": "target", "position": "left", "id": "status-in-2"},
{"type": "source", "position": "right", "id": "status-out"},
],
"minWidth": 150,
"minHeight": 150,
"padding": 0,
},
"position": {"x": 500, "y": 130},
"style": {"width": 170, "height": 175},
},
# Output Node - Dashboard
{
"id": "dashboard",
"type": "output",
"data": {"label": "Dashboard", "sublabel": "Final Output"},
"position": {"x": 730, "y": 175},
},
]
# Edge connections
edges = [
{"id": "e1", "source": "data-source", "target": "revenue-card", "targetHandle": "revenue-in"},
{"id": "e2", "source": "data-source", "target": "users-card", "targetHandle": "users-in"},
{"id": "e3", "source": "revenue-card", "target": "status-card", "sourceHandle": "revenue-out", "targetHandle": "status-in-1"},
{"id": "e4", "source": "users-card", "target": "status-card", "sourceHandle": "users-out", "targetHandle": "status-in-2"},
{"id": "e5", "source": "status-card", "target": "dashboard", "sourceHandle": "status-out"},
]
# ============================================
# Chart Functions for Detail Panel
# ============================================
def create_revenue_chart():
"""Full Plotly chart shown in detail panel."""
fig = px.bar(
df, x="Month", y="Revenue",
color_discrete_sequence=["#3b82f6"]
)
fig.update_layout(
margin=dict(l=40, r=20, t=40, b=40),
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
title=dict(text="Monthly Revenue", font=dict(size=14)),
height=280,
)
return fig
def create_users_chart():
"""Full Plotly chart shown in detail panel."""
fig = go.Figure()
fig.add_trace(go.Scatter(
x=df["Month"],
y=df["Users"],
mode="lines+markers",
line=dict(color="#8b5cf6", width=3),
marker=dict(size=8),
fill="tozeroy",
fillcolor="rgba(139, 92, 246, 0.1)",
))
fig.update_layout(
margin=dict(l=40, r=20, t=40, b=40),
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
title=dict(text="User Growth", font=dict(size=14)),
height=280,
)
return fig
# ============================================
# App Layout
# ============================================
app.layout = dmc.MantineProvider([
html.H2("Dash Components in Resizable Nodes", style={"marginBottom": "8px"}),
html.P("Click on metric cards to see full charts in the detail panel.",
style={"color": "#6b7280", "marginBottom": "16px"}),
dmc.Grid([
# Flow canvas
dmc.GridCol([
dash_flows.DashFlows(
id="metrics-flow",
nodes=nodes,
edges=edges,
style={"height": "480px", "border": "1px solid #e5e7eb", "borderRadius": "8px"},
fitView=True,
showControls=True,
showMiniMap=False,
nodesDraggable=True,
elementsSelectable=True,
),
], span=8),
# Detail panel
dmc.GridCol([
dmc.Paper([
dmc.Text("Node Details", fw=600, size="sm", mb="sm"),
html.Div(id="node-info", children=[
dmc.Text("Click a node to see details", c="dimmed", size="sm"),
]),
], p="md", withBorder=True, mb="md"),
dmc.Paper([
dmc.Text("Chart Preview", fw=600, size="sm", mb="sm"),
html.Div(id="chart-panel", children=[
dmc.Text("Select a metric card to see the full chart", c="dimmed", size="sm"),
]),
], p="md", withBorder=True, style={"minHeight": "320px"}),
], span=4),
]),
])
# ============================================
# Callbacks
# ============================================
@callback(
Output("node-info", "children"),
Output("chart-panel", "children"),
Input("metrics-flow", "clickedNode"),
prevent_initial_call=True,
)
def show_node_details(clicked_node):
"""Show node info and corresponding chart when a node is clicked."""
if not clicked_node:
return (
dmc.Text("No node selected", c="dimmed", size="sm"),
dmc.Text("Click a metric card", c="dimmed", size="sm"),
)
node_id = clicked_node.get("id", "Unknown")
node_type = clicked_node.get("type", "unknown")
position = clicked_node.get("position", {})
# Node info display
info = dmc.Stack([
dmc.Group([
dmc.Text("ID:", size="sm", fw=500),
dmc.Badge(node_id, variant="light", color="blue"),
], gap="xs"),
dmc.Group([
dmc.Text("Type:", size="sm", fw=500),
dmc.Badge(node_type, variant="light", color="gray"),
], gap="xs"),
dmc.Group([
dmc.Text("Position:", size="sm", fw=500),
dmc.Code(f"({position.get('x', 0):.0f}, {position.get('y', 0):.0f})"),
], gap="xs"),
], gap="xs")
# Chart based on selected node
if node_id == "revenue-card":
chart = dcc.Graph(
figure=create_revenue_chart(),
config={"displayModeBar": False},
)
elif node_id == "users-card":
chart = dcc.Graph(
figure=create_users_chart(),
config={"displayModeBar": False},
)
elif node_id == "status-card":
chart = dmc.Stack([
dmc.Text("ETL Pipeline Status", fw=600, size="sm"),
dmc.Progress(value=100, color="green", size="lg", mb="xs"),
dmc.Text("Extract: Complete", size="xs", c="dimmed"),
dmc.Progress(value=75, color="blue", size="lg", mb="xs"),
dmc.Text("Transform: 75%", size="xs", c="dimmed"),
dmc.Progress(value=0, color="gray", size="lg", mb="xs"),
dmc.Text("Load: Pending", size="xs", c="dimmed"),
], gap="sm")
else:
chart = dmc.Text(f"No chart for: {node_id}", c="dimmed", size="sm")
return info, chart
if __name__ == "__main__":
app.run(debug=True, port=8074)
:defaultExpanded: false :withExpandedButton: true
How it works
data.labelaccepts any Dash component tree (here,html.Divstat cards with CSS-drawn mini bar charts and a progress ring) instead of a plain string.type: "resizable"nodes size themselves to their content viadata.minWidth/data.minHeight, so cards keep their layout as they're resized.clickedNode(an Input on the flow's id) fires with the clicked node's full dict, letting a callback branch onnode["id"]to render node-specific detail — fulldcc.Graphcharts live in a side panel rather than inside the node itself.- Multiple named handles (
revenue-in,status-out, etc.) route several cards into a single "Pipeline Status" node without ambiguity. - This mirrors the library's guidance: simple HTML/CSS content can live directly in a node, while
dcc.Graphand other heavy/interactive components are better shown in a panel driven by node click events.
Custom Icons
Use DashIconify icons inside node data, and combine them with the layout ("stacked" vs "horizontal") and showIcon options to control node composition — then update everything live from Dash Mantine form controls.
# File: examples/22_custom_icons.py
"""
Example 22: Custom Icons with DashIconify & Layout Options
===========================================================
This example demonstrates how to use DashIconify icons in nodes
and the new layout system for flexible node styling.
Features:
- Custom icons via DashIconify
- Dynamic icon updates via DMC form controls
- Title and body text customization
- Layout options: 'stacked' (vertical) or 'horizontal' (two-column)
- Content-aware sizing: icon-only, text-only, or full-content nodes
Requirements:
pip install dash-iconify dash-mantine-components
Icon Search: https://icon-sets.iconify.design/
"""
import dash
from dash import html, callback, Input, Output, State, ALL, ctx
import dash_mantine_components as dmc
from dash_iconify import DashIconify
import dash_flows
app = dash.Dash(
__name__,
external_stylesheets=[
"https://unpkg.com/@mantine/core@7.11.0/styles.css",
],
)
# Initial node configurations showcasing different layouts and content modes
initial_nodes = [
# Full content node with icon, title, and body (stacked layout)
{
"id": "input-1",
"type": "input",
"data": {
"icon": DashIconify(icon="mdi:database", width=20, color="white"),
"label": "Data Source",
"body": "PostgreSQL Database",
"layout": "stacked",
},
"position": {"x": 100, "y": 50},
},
# Icon-only node (compact sizing)
{
"id": "icon-only",
"type": "default",
"data": {
"icon": DashIconify(icon="mdi:lightning-bolt", width=24, color="white"),
"showIcon": True,
"iconColor": "#f59e0b",
},
"position": {"x": 300, "y": 50},
},
# Full content node with horizontal layout
{
"id": "process-1",
"type": "default",
"data": {
"icon": DashIconify(icon="mdi:cog", width=20, color="white"),
"label": "Transform",
"body": "Clean and normalize",
"layout": "horizontal",
"showIcon": True,
},
"position": {"x": 100, "y": 200},
},
# Text-only node (centered, no icon space reserved)
{
"id": "text-only",
"type": "default",
"data": {
"label": "Validate",
"sublabel": "Quality check",
"showIcon": False,
},
"position": {"x": 300, "y": 200},
},
# Output with horizontal layout
{
"id": "output-1",
"type": "output",
"data": {
"icon": DashIconify(icon="mdi:chart-bar", width=20, color="white"),
"label": "Dashboard",
"body": "Visualization",
"layout": "horizontal",
},
"position": {"x": 100, "y": 350},
},
# Icon-only output
{
"id": "output-icon",
"type": "output",
"data": {
"icon": DashIconify(icon="mdi:file-export", width=24, color="white"),
"showIcon": True,
},
"position": {"x": 300, "y": 350},
},
]
initial_edges = [
{"id": "e1-p1", "source": "input-1", "target": "process-1", "animated": True},
{"id": "e1-icon", "source": "icon-only", "target": "text-only", "animated": True},
{"id": "ep1-o1", "source": "process-1", "target": "output-1", "animated": True},
{"id": "et-oicon", "source": "text-only", "target": "output-icon", "animated": True},
]
# Popular icon suggestions for each node type
icon_suggestions = {
"input": [
"mdi:database",
"mdi:file-document",
"mdi:api",
"mdi:cloud-download",
"mdi:folder",
"mdi:web",
"mdi:server",
"mdi:import",
],
"process": [
"mdi:cog",
"mdi:function",
"mdi:filter",
"mdi:merge",
"mdi:swap-horizontal",
"mdi:code-braces",
"mdi:math-integral",
"mdi:lightning-bolt",
],
"output": [
"mdi:chart-bar",
"mdi:file-export",
"mdi:monitor-dashboard",
"mdi:email-send",
"mdi:cloud-upload",
"mdi:database-export",
"mdi:printer",
"mdi:share-variant",
],
}
def create_node_editor(
node_id: str,
node_type: str,
default_icon: str,
default_title: str,
default_body: str,
default_layout: str = "stacked",
show_icon: bool = True,
):
"""Create a form section for editing a node's icon, title, body, and layout."""
suggestions = icon_suggestions.get(node_type, icon_suggestions["process"])
# Color based on node type
accent_color = {
"input": "#10b981",
"process": "#3b82f6",
"output": "#8b5cf6",
}.get(node_type, "#3b82f6")
return dmc.Paper(
[
dmc.Group(
[
DashIconify(
icon=default_icon,
width=24,
color=accent_color,
),
dmc.Text(
f"{node_type.title()} Node",
size="lg",
fw=600,
),
],
gap="sm",
mb="md",
),
# Layout toggle
dmc.Group(
[
dmc.Text("Layout:", size="sm", fw=500),
dmc.SegmentedControl(
id={"type": "layout-toggle", "node": node_id},
data=[
{"value": "stacked", "label": "Stacked"},
{"value": "horizontal", "label": "Horizontal"},
],
value=default_layout,
size="xs",
),
],
gap="sm",
mb="sm",
),
# Show icon toggle
dmc.Switch(
id={"type": "show-icon-toggle", "node": node_id},
label="Show Icon",
checked=show_icon,
size="sm",
mb="sm",
),
dmc.TextInput(
id={"type": "icon-input", "node": node_id},
label="Icon",
description="Enter an icon name from iconify.design",
placeholder="mdi:database",
value=default_icon,
leftSection=DashIconify(icon="mdi:emoticon", width=16),
mb="sm",
disabled=not show_icon,
),
dmc.Group(
[
dmc.Text("Suggestions:", size="xs", c="dimmed"),
*[
dmc.Badge(
icon,
size="sm",
variant="light",
color="gray",
style={"cursor": "pointer"},
id={"type": "icon-suggestion", "node": node_id, "icon": icon},
)
for icon in suggestions[:4]
],
],
gap="xs",
mb="md",
),
dmc.TextInput(
id={"type": "title-input", "node": node_id},
label="Title",
placeholder="Node title",
value=default_title,
leftSection=DashIconify(icon="mdi:format-title", width=16),
mb="sm",
),
dmc.Textarea(
id={"type": "body-input", "node": node_id},
label="Body Text",
placeholder="Description text",
value=default_body,
autosize=True,
minRows=2,
maxRows=4,
),
],
p="md",
radius="md",
withBorder=True,
style={"borderLeft": f"4px solid {accent_color}"},
)
app.layout = dmc.MantineProvider(
[
dmc.Container(
[
dmc.Title("Custom Icons & Layout Options", order=1, mb="xs"),
dmc.Text(
"Customize node icons, layouts, and content dynamically.",
c="dimmed",
mb="md",
),
dmc.Group(
[
dmc.Anchor(
dmc.Group(
[
DashIconify(icon="mdi:magnify", width=16),
"Search icons at iconify.design",
],
gap="xs",
),
href="https://icon-sets.iconify.design/",
target="_blank",
),
dmc.Badge("Stacked = Vertical layout", color="blue", variant="light"),
dmc.Badge("Horizontal = Two-column layout", color="green", variant="light"),
],
gap="md",
mb="lg",
),
# Layout preview section
dmc.Paper(
[
dmc.Text("Content Mode Examples", size="sm", fw=500, mb="xs"),
dmc.Group(
[
dmc.Badge("Icon + Text = Full Content", color="violet", variant="outline"),
dmc.Badge("Icon Only = Compact", color="orange", variant="outline"),
dmc.Badge("Text Only = Centered", color="cyan", variant="outline"),
],
gap="xs",
),
],
p="sm",
radius="md",
bg="gray.0",
mb="md",
),
dmc.Grid(
[
# Left panel - Node editors
dmc.GridCol(
dmc.Stack(
[
create_node_editor(
"input-1",
"input",
"mdi:database",
"Data Source",
"PostgreSQL Database",
default_layout="stacked",
show_icon=True,
),
create_node_editor(
"process-1",
"process",
"mdi:cog",
"Transform",
"Clean and normalize",
default_layout="horizontal",
show_icon=True,
),
create_node_editor(
"output-1",
"output",
"mdi:chart-bar",
"Dashboard",
"Visualization",
default_layout="horizontal",
show_icon=True,
),
],
gap="md",
),
span=4,
),
# Right panel - Flow canvas
dmc.GridCol(
dmc.Paper(
dash_flows.DashFlows(
id="icon-flow",
nodes=initial_nodes,
edges=initial_edges,
fitView=True,
style={"height": "600px"},
showControls=True,
showMiniMap=True,
),
radius="md",
withBorder=True,
style={"overflow": "hidden"},
),
span=8,
),
],
gutter="lg",
),
],
size="xl",
py="xl",
),
],
)
@callback(
Output("icon-flow", "nodes"),
Input({"type": "icon-input", "node": ALL}, "value"),
Input({"type": "title-input", "node": ALL}, "value"),
Input({"type": "body-input", "node": ALL}, "value"),
Input({"type": "layout-toggle", "node": ALL}, "value"),
Input({"type": "show-icon-toggle", "node": ALL}, "checked"),
State("icon-flow", "nodes"),
prevent_initial_call=True,
)
def update_nodes(icons, titles, bodies, layouts, show_icons, current_nodes):
"""Update nodes when form values change."""
if not current_nodes:
return dash.no_update
# Map node IDs to their indices in the callback inputs
node_mapping = {
"input-1": 0,
"process-1": 1,
"output-1": 2,
}
# Update each node
updated_nodes = []
for node in current_nodes:
node_id = node["id"]
if node_id in node_mapping:
idx = node_mapping[node_id]
# Determine icon color based on node type
icon_color = {
"input": "white",
"default": "white",
"output": "white",
}.get(node.get("type", "default"), "white")
# Create updated node data
updated_data = {
**node.get("data", {}),
"label": titles[idx] if titles[idx] else node.get("data", {}).get("label", ""),
"body": bodies[idx] if bodies[idx] else "",
"layout": layouts[idx] if layouts[idx] else "stacked",
"showIcon": show_icons[idx],
}
# Add icon if provided and showIcon is enabled
if icons[idx] and show_icons[idx]:
updated_data["icon"] = DashIconify(
icon=icons[idx],
width=20,
color=icon_color,
)
updated_nodes.append({
**node,
"data": updated_data,
})
else:
updated_nodes.append(node)
return updated_nodes
@callback(
Output({"type": "icon-input", "node": ALL}, "value"),
Input({"type": "icon-suggestion", "node": ALL, "icon": ALL}, "n_clicks"),
State({"type": "icon-input", "node": ALL}, "value"),
prevent_initial_call=True,
)
def handle_icon_suggestion(clicks, current_values):
"""Handle clicking on icon suggestions."""
if not ctx.triggered_id or not any(clicks):
return dash.no_update
# Get the clicked suggestion
triggered = ctx.triggered_id
clicked_node = triggered["node"]
clicked_icon = triggered["icon"]
# Update the corresponding input
node_mapping = {"input-1": 0, "process-1": 1, "output-1": 2}
result = list(current_values)
if clicked_node in node_mapping:
result[node_mapping[clicked_node]] = clicked_icon
return result
@callback(
Output({"type": "icon-input", "node": ALL}, "disabled"),
Input({"type": "show-icon-toggle", "node": ALL}, "checked"),
)
def toggle_icon_input_disabled(show_icons):
"""Disable icon input when showIcon is toggled off."""
return [not checked for checked in show_icons]
if __name__ == "__main__":
app.run(debug=True, port=8091)
:defaultExpanded: false :withExpandedButton: true
How it works
data.icon— pass aDashIconify(icon="mdi:...")component directly; any name from icon-sets.iconify.design works.data.layout—"stacked"renders the icon above the text,"horizontal"renders a two-column icon/text layout.data.showIcon— toggles whether the icon renders at all, letting the same node type serve icon-only, text-only, or full-content variants.data.body— optional secondary text rendered below the label, for a short description under the title.- Editing a
dmc.TextInput/dmc.SegmentedControl/dmc.Switchwith a pattern-matching id ({"type": ..., "node": node_id}) triggers a callback that rebuilds the matching node'sdataand writes it back to the flow'snodesprop, showing that node content can be fully data-driven.
Source: /nodes
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:
- /nodes/llms.txt — LLM-friendly documentation
- /sitemap.xml
- /robots.txt