Color modes, glass/solid/minimal presets, color schemes, and UI building blocks.

Theming & UI

Color modes, glass/solid/minimal presets, color schemes, and UI building blocks.


Overview

Switch light/dark with colorMode (light / dark / system), choose a themePreset (glass / solid / minimal), and pick a colorScheme (default, ocean, forest, sunset, midnight, rose). dash-flows pairs naturally with Dash Mantine Components.

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/theming/demo.py

"""Live, callback-free demo for the docs page. Rendered via `.. exec::docs.theming.demo`."""
import dash_flows

nodes = [{'id': 't1', 'type': 'input', 'data': {'label': 'Ocean'}, 'position': {'x': 120, 'y': 40}},
 {'id': 't2', 'type': 'default', 'data': {'label': 'Scheme'}, 'position': {'x': 120, 'y': 180}},
 {'id': 't3', 'type': 'output', 'data': {'label': 'Preset'}, 'position': {'x': 120, 'y': 320}}]

edges = [{'id': 't12', 'source': 't1', 'target': 't2', 'animated': True},
 {'id': 't23', 'source': 't2', 'target': 't3'}]

component = dash_flows.DashFlows(
    id="theming-demo",
    nodes=nodes,
    edges=edges,
    style={'border': '1px solid var(--mantine-color-default-border)',
 'borderRadius': '8px',
 'height': '440px'},
    showControls=True,
    showMiniMap=True,
    colorScheme='ocean',
    themePreset='glass',
    fitView=True,
    colorMode='dark',
)

: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>.

Dark Mode with Mantine

Sync dash-flows color mode with a Dash Mantine Components theme. Use the segmented control below to switch the canvas between light, dark, and system — the standalone example additionally flips the whole page's Mantine theme via forceColorScheme, which this embedded demo intentionally does not do since the docs page owns its own theme.

# File: examples/11_dark_mode_mantine.py

"""
Example 11: Dark Mode with Mantine Theme Integration
====================================================
This example demonstrates:
- Light/dark mode toggle using Mantine
- CSS variable integration with dash-mantine-components
- Dynamic theme switching
- Persisting theme preference
"""

import dash
from dash import html, Input, Output, callback, clientside_callback
import dash_flows
import dash_mantine_components as dmc
from dash_iconify import DashIconify

app = dash.Dash(__name__)

nodes = [
    {"id": "input-1", "type": "input", "data": {"label": "Data Source", "sublabel": "API"}, "position": {"x": 100, "y": 50}},
    {"id": "default-1", "type": "default", "data": {"label": "Transform", "sublabel": "ETL Process"}, "position": {"x": 100, "y": 180}},
    {"id": "default-2", "type": "default", "data": {"label": "Validate", "sublabel": "Schema Check"}, "position": {"x": 300, "y": 180}},
    {"id": "toolbar-1", "type": "toolbar", "data": {"label": "Review", "sublabel": "Manual Check"}, "position": {"x": 200, "y": 310}},
    {"id": "output-1", "type": "output", "data": {"label": "Database", "sublabel": "PostgreSQL"}, "position": {"x": 200, "y": 440}},
]

edges = [
    {"id": "e1", "source": "input-1", "target": "default-1", "animated": True},
    {"id": "e2", "source": "default-1", "target": "default-2", "type": "smoothstep"},
    {"id": "e3", "source": "default-2", "target": "toolbar-1"},
    {"id": "e4", "source": "toolbar-1", "target": "output-1"},
]

# Theme toggle button in header
header = dmc.Group([
    dmc.Title("DashFlows Dark Mode Demo", order=3),
    dmc.ActionIcon(
        id="theme-toggle",
        variant="outline",
        size="lg",
        radius="md",
        color='yellow',
        children=[
        dmc.Paper(DashIconify(icon="radix-icons:sun", width=25), darkHidden=True),
        dmc.Paper(DashIconify(icon="radix-icons:moon", width=25), lightHidden=True),
    ],
    ),
], justify="space-between", style={"padding": "10px 20px", "borderBottom": "1px solid var(--mantine-color-gray-3)"})

flow_component = dash_flows.DashFlows(
    id="themed-flow",
    nodes=nodes,
    edges=edges,
    style={"height": "500px"},
    fitView=True,
    showControls=True,
    showMiniMap=True,
    backgroundVariant="dots",
)

theme_info = dmc.Paper([
    dmc.Text("Theme Integration Details:", fw=600, mb="xs"),
    dmc.Text([
        "The DashFlows component automatically responds to the ",
        dmc.Code("data-mantine-color-scheme"),
        " attribute set by MantineProvider."
    ], size="sm", mb="xs"),
    dmc.Text([
        "CSS variables defined in ",
        dmc.Code("glass-theme.css"),
        " provide both light and dark mode styling."
    ], size="sm", mb="xs"),
    dmc.List([
        dmc.ListItem("Glass morphism backgrounds adapt to theme"),
        dmc.ListItem("Node colors and shadows change automatically"),
        dmc.ListItem("Edge and handle colors update"),
        dmc.ListItem("Controls and MiniMap match the theme"),
    ], size="sm"),
], p="md", withBorder=True, mt="md")

# Main layout with MantineProvider for theming
app.layout = dmc.MantineProvider(
    id="mantine-provider",
    forceColorScheme="light",
    children=[
        header,
        html.Div([
            flow_component,
            theme_info,
        ], style={"padding": "20px"}),
    ]
)

# Clientside callback for theme toggle using the modern Mantine pattern
clientside_callback(
    """
    (n) => {
        if (!n) return window.dash_clientside.no_update;
        const currentScheme = document.documentElement.getAttribute('data-mantine-color-scheme') || 'light';
        const newScheme = currentScheme === 'light' ? 'dark' : 'light';
        document.documentElement.setAttribute('data-mantine-color-scheme', newScheme);
        return newScheme;
    }
    """,
    Output("mantine-provider", "forceColorScheme"),
    Input("theme-toggle", "n_clicks"),
    prevent_initial_call=True,
)

if __name__ == "__main__":
    app.run(debug=True, port=8030)

:defaultExpanded: false :withExpandedButton: true

How it works

UI Components

Compose toolbars, panels, and controls around the flow canvas: status-aware nodes, data-carrying edges, animated SVG edges, and a lightweight node search that focuses the viewport. The full app also wires this into a shared control bar; the embedded version below keeps the same nodes, edges, and callbacks with panel spacing trimmed to fit the page.

# File: examples/21_ui_components.py

"""
Example 21: UI Components Demo
Demonstrates the new UI components:
- NodeStatusIndicator (loading/success/error states)
- NodeTooltip (hover tooltips)
- ButtonHandle (clickable handles)
- NodeSearch (find and focus nodes)
- DataEdge (display data on edges)
- AnimatedSvgEdge (animated shapes along edges)
"""

import dash
from dash import html, dcc, callback, Input, Output, State, ctx
from dash.exceptions import PreventUpdate
import dash_flows as df
import dash_mantine_components as dmc
import time
import random

app = dash.Dash(__name__, suppress_callback_exceptions=True)

# Initial nodes demonstrating various features
initial_nodes = [
    # Status indicator demo nodes
    {
        'id': 'status-initial',
        'type': 'default',
        'position': {'x': 50, 'y': 50},
        'data': {
            'label': 'Initial State',
            'status': 'initial',
            'value': 0
        },
    },
    {
        'id': 'status-loading',
        'type': 'default',
        'position': {'x': 250, 'y': 50},
        'data': {
            'label': 'Loading State',
            'status': 'loading',
            'value': 42
        },
    },
    {
        'id': 'status-success',
        'type': 'default',
        'position': {'x': 450, 'y': 50},
        'data': {
            'label': 'Success State',
            'status': 'success',
            'value': 100
        },
    },
    {
        'id': 'status-error',
        'type': 'default',
        'position': {'x': 650, 'y': 50},
        'data': {
            'label': 'Error State',
            'status': 'error',
            'value': -1
        },
    },
    # Data flow demo nodes
    {
        'id': 'data-source',
        'type': 'input',
        'position': {'x': 100, 'y': 250},
        'data': {
            'label': 'Data Source',
            'price': 1299.99,
            'count': 42,
            'status': 'active'
        },
    },
    {
        'id': 'data-processor',
        'type': 'default',
        'position': {'x': 350, 'y': 250},
        'data': {
            'label': 'Processor',
            'multiplier': 2,
            'output': 2599.98
        },
    },
    {
        'id': 'data-output',
        'type': 'output',
        'position': {'x': 600, 'y': 250},
        'data': {
            'label': 'Output',
            'result': 'Complete'
        },
    },
    # Animated edge demo nodes
    {
        'id': 'anim-start',
        'type': 'input',
        'position': {'x': 50, 'y': 450},
        'data': {'label': 'Start'},
    },
    {
        'id': 'anim-circle',
        'type': 'default',
        'position': {'x': 250, 'y': 400},
        'data': {'label': 'Circle Animation'},
    },
    {
        'id': 'anim-arrow',
        'type': 'default',
        'position': {'x': 250, 'y': 500},
        'data': {'label': 'Arrow Animation'},
    },
    {
        'id': 'anim-pulse',
        'type': 'default',
        'position': {'x': 450, 'y': 400},
        'data': {'label': 'Pulse Animation'},
    },
    {
        'id': 'anim-rect',
        'type': 'default',
        'position': {'x': 450, 'y': 500},
        'data': {'label': 'Rect Animation'},
    },
    {
        'id': 'anim-end',
        'type': 'output',
        'position': {'x': 650, 'y': 450},
        'data': {'label': 'End'},
    },
]

# Initial edges demonstrating DataEdge and AnimatedSvgEdge
initial_edges = [
    # Data edges showing values from source nodes
    {
        'id': 'e-data-1',
        'source': 'data-source',
        'target': 'data-processor',
        'type': 'data',
        'data': {
            'key': 'price',
            'prefix': '$',
        },
    },
    {
        'id': 'e-data-2',
        'source': 'data-processor',
        'target': 'data-output',
        'type': 'data',
        'data': {
            'key': 'output',
            'prefix': 'Total: $',
        },
    },
    # Animated SVG edges with different shapes
    {
        'id': 'e-anim-circle',
        'source': 'anim-start',
        'target': 'anim-circle',
        'type': 'animatedSvg',
        'data': {
            'shape': 'circle',
            'duration': 2,
            'size': 5,
            'color': '#3b82f6',
            'count': 2,
        },
    },
    {
        'id': 'e-anim-arrow',
        'source': 'anim-start',
        'target': 'anim-arrow',
        'type': 'animatedSvg',
        'data': {
            'shape': 'arrow',
            'duration': 1.5,
            'size': 6,
            'color': '#10b981',
            'count': 3,
        },
    },
    {
        'id': 'e-anim-pulse',
        'source': 'anim-circle',
        'target': 'anim-pulse',
        'type': 'animatedSvg',
        'data': {
            'shape': 'pulse',
            'duration': 2.5,
            'size': 4,
            'color': '#f59e0b',
            'count': 2,
        },
    },
    {
        'id': 'e-anim-rect',
        'source': 'anim-arrow',
        'target': 'anim-rect',
        'type': 'animatedSvg',
        'data': {
            'shape': 'rect',
            'duration': 2,
            'size': 4,
            'color': '#8b5cf6',
            'count': 2,
        },
    },
    {
        'id': 'e-anim-end-1',
        'source': 'anim-pulse',
        'target': 'anim-end',
        'type': 'animatedSvg',
        'data': {
            'shape': 'circle',
            'duration': 1.5,
            'size': 6,
            'color': '#ef4444',
            'count': 1,
            'reverse': False,
        },
    },
    {
        'id': 'e-anim-end-2',
        'source': 'anim-rect',
        'target': 'anim-end',
        'type': 'animatedSvg',
        'data': {
            'shape': 'arrow',
            'duration': 1.5,
            'size': 5,
            'color': '#ec4899',
            'count': 2,
        },
    },
    # Status indicator connections
    {
        'id': 'e-status-1',
        'source': 'status-initial',
        'target': 'status-loading',
        'type': 'animatedSvg',
        'data': {
            'shape': 'circle',
            'duration': 1,
            'size': 4,
            'color': '#64748b',
            'count': 1,
        },
    },
    {
        'id': 'e-status-2',
        'source': 'status-loading',
        'target': 'status-success',
        'type': 'animatedSvg',
        'data': {
            'shape': 'pulse',
            'duration': 1,
            'size': 4,
            'color': '#3b82f6',
            'count': 2,
        },
    },
    {
        'id': 'e-status-3',
        'source': 'status-loading',
        'target': 'status-error',
        'type': 'animatedSvg',
        'data': {
            'shape': 'arrow',
            'duration': 1,
            'size': 4,
            'color': '#ef4444',
            'count': 1,
        },
    },
]

app.layout = dmc.MantineProvider(
    html.Div([
        # Header
        html.Div([
            html.H2("UI Components Demo", style={'margin': 0}),
            html.P("Testing NodeStatusIndicator, DataEdge, AnimatedSvgEdge, and more",
                   style={'margin': '5px 0 0 0', 'color': '#666'}),
        ], style={'padding': '15px 20px', 'borderBottom': '1px solid #e2e8f0'}),

        # Control Panel
        html.Div([
            # Node Search Toggle
            dmc.Button(
                "Toggle Search (Ctrl+F)",
                id="toggle-search",
                variant="light",
                color="blue",
                size="sm",
            ),

            # Status simulation controls
            dmc.Button(
                "Simulate Loading",
                id="btn-simulate-loading",
                variant="light",
                color="orange",
                size="sm",
                style={'marginLeft': '10px'},
            ),

            # Data update controls
            dmc.Button(
                "Update Data Values",
                id="btn-update-data",
                variant="light",
                color="green",
                size="sm",
                style={'marginLeft': '10px'},
            ),

            # Fit view
            dmc.Button(
                "Fit View",
                id="btn-fit-view",
                variant="light",
                color="gray",
                size="sm",
                style={'marginLeft': '10px'},
            ),

            # Animation speed control
            html.Div([
                html.Label("Animation Speed:", style={'marginRight': '10px', 'fontSize': '14px'}),
                dmc.SegmentedControl(
                    id="animation-speed",
                    value="normal",
                    data=[
                        {"value": "slow", "label": "Slow"},
                        {"value": "normal", "label": "Normal"},
                        {"value": "fast", "label": "Fast"},
                    ],
                    size="xs",
                ),
            ], style={'display': 'flex', 'alignItems': 'center', 'marginLeft': '20px'}),

        ], style={
            'padding': '10px 20px',
            'display': 'flex',
            'alignItems': 'center',
            'borderBottom': '1px solid #e2e8f0',
            'backgroundColor': '#f8fafc',
        }),

        # Node Search Panel (hidden by default)
        html.Div(
            id="search-panel",
            children=[
                dmc.TextInput(
                    id="node-search-input",
                    placeholder="Search nodes by label or ID...",
                    style={'width': '300px'},
                    leftSection=html.Span("🔍"),
                ),
                html.Div(id="search-results", style={'marginTop': '10px'}),
            ],
            style={
                'display': 'none',
                'position': 'absolute',
                'top': '130px',
                'left': '50%',
                'transform': 'translateX(-50%)',
                'zIndex': 1000,
                'padding': '15px',
                'backgroundColor': 'rgba(255, 255, 255, 0.95)',
                'backdropFilter': 'blur(10px)',
                'borderRadius': '12px',
                'boxShadow': '0 8px 32px rgba(0, 0, 0, 0.15)',
                'border': '1px solid rgba(255, 255, 255, 0.5)',
            },
        ),

        # Flow Container
        html.Div([
            df.DashFlows(
                id='flow',
                nodes=initial_nodes,
                edges=initial_edges,
                style={'height': '600px'},
                fitView=True,
                showMiniMap=True,
                showControls=True,
                showBackground=True,
                colorMode='light',
                themePreset='glass',
            ),
        ], style={'position': 'relative'}),

        # Info Panel
        html.Div([
            html.H4("Component Features:", style={'margin': '0 0 10px 0'}),
            dmc.Grid([
                dmc.GridCol([
                    html.Div([
                        html.Strong("DataEdge"),
                        html.P("Displays data from source node on the edge. "
                               "See 'price' and 'output' values on the middle row edges.",
                               style={'fontSize': '13px', 'margin': '5px 0 0 0', 'color': '#666'}),
                    ]),
                ], span=4),
                dmc.GridCol([
                    html.Div([
                        html.Strong("AnimatedSvgEdge"),
                        html.P("Animated shapes traveling along edges: circle, arrow, pulse, rect. "
                               "Watch the bottom section for all animation types.",
                               style={'fontSize': '13px', 'margin': '5px 0 0 0', 'color': '#666'}),
                    ]),
                ], span=4),
                dmc.GridCol([
                    html.Div([
                        html.Strong("Node Status"),
                        html.P("Top row shows different states. Click 'Simulate Loading' to see "
                               "the status transition through states.",
                               style={'fontSize': '13px', 'margin': '5px 0 0 0', 'color': '#666'}),
                    ]),
                ], span=4),
            ]),
        ], style={
            'padding': '15px 20px',
            'borderTop': '1px solid #e2e8f0',
            'backgroundColor': '#f8fafc',
        }),

        # Store for search state
        dcc.Store(id='search-open', data=False),

    ], style={'fontFamily': 'system-ui, -apple-system, sans-serif'}),
)


# Toggle search panel
@callback(
    [Output('search-panel', 'style'),
     Output('search-open', 'data')],
    Input('toggle-search', 'n_clicks'),
    State('search-open', 'data'),
    State('search-panel', 'style'),
    prevent_initial_call=True,
)
def toggle_search(n_clicks, is_open, current_style):
    if not n_clicks:
        raise PreventUpdate

    new_style = {**current_style}
    new_style['display'] = 'none' if is_open else 'block'
    return new_style, not is_open


# Search nodes
@callback(
    [Output('search-results', 'children'),
     Output('flow', 'viewportAction', allow_duplicate=True)],
    Input('node-search-input', 'value'),
    State('flow', 'nodes'),
    prevent_initial_call=True,
)
def search_nodes(search_term, nodes):
    if not search_term or len(search_term) < 2:
        return [], dash.no_update

    term = search_term.lower()
    matches = []

    for node in nodes:
        node_id = node['id'].lower()
        label = node.get('data', {}).get('label', '').lower()

        if term in node_id or term in label:
            matches.append(node)

    if not matches:
        return html.Div("No nodes found", style={'color': '#666', 'padding': '10px'}), dash.no_update

    results = []
    for node in matches[:5]:  # Limit to 5 results
        results.append(
            dmc.Button(
                f"{node['data'].get('label', node['id'])} ({node['id']})",
                id={'type': 'search-result', 'id': node['id']},
                variant="subtle",
                fullWidth=True,
                size="sm",
                style={'marginBottom': '5px', 'justifyContent': 'flex-start'},
            )
        )

    return results, dash.no_update


# Focus on searched node
@callback(
    Output('flow', 'viewportAction'),
    Input({'type': 'search-result', 'id': dash.ALL}, 'n_clicks'),
    prevent_initial_call=True,
)
def focus_search_result(n_clicks):
    if not any(n_clicks):
        raise PreventUpdate

    triggered = ctx.triggered_id
    if triggered and isinstance(triggered, dict):
        node_id = triggered['id']
        return {
            'action': 'focusNode',
            'nodeId': node_id,
            'zoom': 1.5,
            'duration': 500,
        }

    raise PreventUpdate


# Fit view button
@callback(
    Output('flow', 'viewportAction', allow_duplicate=True),
    Input('btn-fit-view', 'n_clicks'),
    prevent_initial_call=True,
)
def fit_view(n_clicks):
    if not n_clicks:
        raise PreventUpdate
    return {'action': 'fitView', 'options': {'padding': 0.2, 'duration': 500}}


# Simulate loading states
@callback(
    Output('flow', 'nodes', allow_duplicate=True),
    Input('btn-simulate-loading', 'n_clicks'),
    State('flow', 'nodes'),
    prevent_initial_call=True,
)
def simulate_loading(n_clicks, nodes):
    if not n_clicks:
        raise PreventUpdate

    # Cycle through states
    states = ['initial', 'loading', 'success', 'error']

    updated_nodes = []
    for node in nodes:
        if node['id'].startswith('status-'):
            current_status = node['data'].get('status', 'initial')
            current_idx = states.index(current_status) if current_status in states else 0
            next_idx = (current_idx + 1) % len(states)

            updated_node = {
                **node,
                'data': {
                    **node['data'],
                    'status': states[next_idx],
                }
            }
            updated_nodes.append(updated_node)
        else:
            updated_nodes.append(node)

    return updated_nodes


# Update data values (to show DataEdge updates)
@callback(
    Output('flow', 'nodes', allow_duplicate=True),
    Input('btn-update-data', 'n_clicks'),
    State('flow', 'nodes'),
    prevent_initial_call=True,
)
def update_data_values(n_clicks, nodes):
    if not n_clicks:
        raise PreventUpdate

    updated_nodes = []
    for node in nodes:
        if node['id'] == 'data-source':
            new_price = round(random.uniform(500, 2000), 2)
            updated_node = {
                **node,
                'data': {
                    **node['data'],
                    'price': new_price,
                    'count': random.randint(10, 100),
                }
            }
            updated_nodes.append(updated_node)
        elif node['id'] == 'data-processor':
            # Find source price
            source_price = 1299.99
            for n in nodes:
                if n['id'] == 'data-source':
                    source_price = n['data'].get('price', 1299.99)
                    break

            multiplier = random.choice([1.5, 2, 2.5, 3])
            updated_node = {
                **node,
                'data': {
                    **node['data'],
                    'multiplier': multiplier,
                    'output': round(source_price * multiplier, 2),
                }
            }
            updated_nodes.append(updated_node)
        else:
            updated_nodes.append(node)

    return updated_nodes


# Update animation speed
@callback(
    Output('flow', 'edges'),
    Input('animation-speed', 'value'),
    State('flow', 'edges'),
    prevent_initial_call=True,
)
def update_animation_speed(speed, edges):
    if not speed:
        raise PreventUpdate

    speed_multipliers = {
        'slow': 2.0,
        'normal': 1.0,
        'fast': 0.5,
    }

    multiplier = speed_multipliers.get(speed, 1.0)
    base_durations = {
        'e-anim-circle': 2,
        'e-anim-arrow': 1.5,
        'e-anim-pulse': 2.5,
        'e-anim-rect': 2,
        'e-anim-end-1': 1.5,
        'e-anim-end-2': 1.5,
        'e-status-1': 1,
        'e-status-2': 1,
        'e-status-3': 1,
    }

    updated_edges = []
    for edge in edges:
        if edge['type'] == 'animatedSvg' and edge['id'] in base_durations:
            updated_edge = {
                **edge,
                'data': {
                    **edge.get('data', {}),
                    'duration': base_durations[edge['id']] * multiplier,
                }
            }
            updated_edges.append(updated_edge)
        else:
            updated_edges.append(edge)

    return updated_edges


if __name__ == '__main__':
    app.run(debug=True, port=8095)

:defaultExpanded: false :withExpandedButton: true

How it works


Source: /theming

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: