# Nodes

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

**Site index:** [https://flows.2plot.dev/llms.txt](https://flows.2plot.dev/llms.txt) — every page on this site, as Markdown.  
**Network index:** [https://2plot.dev/llms.txt](https://2plot.dev/llms.txt) — The 2plot network; start here to discover sibling sites.  
**Sibling sites:** 13 more in The 2plot network — listed in the site index above.  
**Sitemap:** https://flows.2plot.dev/sitemap.xml  


---



### 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):



```python
# 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/`](https://github.com/pip-install-python/dash-flows/tree/main/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.



```python
# 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 via `data.toolbarPosition`.
- `type: "resizable"` with `data.handles` — a resizable node with a custom list of handles (each with `type`, `position`, and `id`), used here to expose a top target and bottom source handle.
- `type: "group"` plus `parentId` / `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.



```python
# 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` / `targetHandle` on an edge reference a specific handle `id`, letting you route several distinct connections through one node.
- `style.background` colors 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.



```python
# 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.label` accepts any Dash component tree (here, `html.Div` stat cards with CSS-drawn mini bar charts and a progress ring) instead of a plain string.
- `type: "resizable"` nodes size themselves to their content via `data.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 on `node["id"]` to render node-specific detail — full `dcc.Graph` charts 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.Graph` and 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.



```python
# 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 a `DashIconify(icon="mdi:...")` component directly; any name from [icon-sets.iconify.design](https://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.Switch` with a pattern-matching id (`{"type": ..., "node": node_id}`) triggers a callback that rebuilds the matching node's `data` and writes it back to the flow's `nodes` prop, showing that node content can be fully data-driven.


---

*Source: /nodes*
