Built-in node types, handle configuration, and Dash components or icons inside nodes.

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

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

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

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


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: