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
colorMode— set to"light","dark", or"system"onDashFlows; the canvas repaints its glass-morphism surfaces, node colors, and edge/handle styling to match.- A Mantine
SegmentedControldrives a callback that writes directly to thecolorModeprop, so the flow can be tied to any Dash-driven theme toggle. - In the standalone example, a clientside callback instead flips
forceColorSchemeon a page-leveldmc.MantineProvider, andDashFlowspicks up the change automatically via thedata-mantine-color-schemeattribute — nocolorModeprop needed when the whole page shares one theme. - CSS variables in
glass-theme.cssprovide both light and dark styling for nodes, edges, Controls, and the MiniMap.
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
data.status—"initial"/"loading"/"success"/"error"on a node'sdatarenders the built-in status indicator; a button callback cycles the top-row nodes through each state.type: "data"edges — read adata.keyfield (with an optionalprefix) straight off the source node and render it as an edge label, so the label updates whenever the source node's data changes.type: "animatedSvg"edges — animate ashape(circle,arrow,pulse,rect) along the edge path;duration,size,color, andcountcontrol the animation, and aSegmentedControlrescales every edge'sdurationat once.viewportAction: {'action': 'focusNode', ...}— used by both the "Fit View" button and the search-result buttons to pan/zoom the canvas to a specific node.- A pattern-matching
Input({'type': 'ex21-search-result', 'id': dash.ALL}, 'n_clicks')callback readsctx.triggered_idto know which search result was clicked.
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:
- /theming/llms.txt — LLM-friendly documentation
- /sitemap.xml
- /robots.txt