User Guide

Layouts

Register layouts with the decorator:

@dash_prism.register_layout(
    id='dashboard',
    name='Dashboard',
    description='Main analytics dashboard',
    keywords=['analytics', 'charts', 'metrics'],
)
def dashboard_layout():
    return html.Div([...])

Or register static content directly:

dash_prism.register_layout(
    id='about',
    name='About',
    layout=html.Div('About page'),
)

Parameterized Layouts

Layouts can accept parameters. Prism automatically inspects function signatures:

@dash_prism.register_layout(id='user-profile', name='User Profile')
def user_profile(user_id: str):
    return html.Div(f'Profile: {user_id}')

For pre-defined parameter options, use param_options:

@dash_prism.register_layout(
    id='chart',
    name='Chart View',
    param_options={
        'bar': ('Bar Chart', {'chart_type': 'bar'}),
        'line': ('Line Chart', {'chart_type': 'line'}),
    }
)
def chart_layout(chart_type: str = 'bar'):
    return dcc.Graph(...)

Note

Parameters passed from the Prism UI are serialized as strings. If your layout expects richer types (e.g., numbers or booleans), convert them inside the layout callback.

Async Layouts

@dash_prism.register_layout(id='async-data', name='Data')
async def async_layout():
    data = await fetch_data()
    return html.Div(str(data))

Multiple Instances (allow_multiple)

By default, each layout can only be open in one tab at a time. Set allow_multiple=True to allow the same layout in multiple tabs:

@dash_prism.register_layout(
    id='chat',
    name='Chat Room',
    allow_multiple=True,  # Can open multiple chat tabs
)
def chat_layout():
    return html.Div([
        html.Div(id='chat-messages'),
        dcc.Input(id='chat-input'),
        html.Button('Send', id='chat-send'),
    ])

Warning

Pattern-Matching Callbacks Required

When allow_multiple=True, Prism automatically transforms all string component IDs into pattern-matching dicts to isolate each tab instance.

Your ID 'chat-input' becomes {'type': 'chat-input', 'index': '<tab-id>'}

You must use pattern-matching callbacks with MATCH:

from dash import MATCH

# CORRECT - uses pattern matching
@app.callback(
    Output({'type': 'chat-messages', 'index': MATCH}, 'children'),
    Input({'type': 'chat-send', 'index': MATCH}, 'n_clicks'),
    State({'type': 'chat-input', 'index': MATCH}, 'value'),
)
def send_message(n_clicks, text):
    return f'You said: {text}'

# WRONG - will not work with allow_multiple=True
@app.callback(
    Output('chat-messages', 'children'),  # String ID won't match
    Input('chat-send', 'n_clicks'),
)
def send_message(n_clicks): ...

If your layout uses allow_multiple=False (default), you can use regular string IDs in callbacks since only one instance exists.

Actions

Add buttons to the status bar:

dash_prism.Prism(
    id='workspace',
    actions=[
        dash_prism.Action(
            id='save-btn',
            label='Save',
            icon='Rocket',
            tooltip='Save workspace',
        ),
    ],
)

Handle clicks with callbacks:

@app.callback(
    Output('save-btn', 'loading'),
    Input('save-btn', 'n_clicks'),
    prevent_initial_call=True
)
def handle_save(n_clicks):
    # Do work...
    return False  # Stop loading spinner

Variant options (variant parameter): 'default', 'primary', 'secondary', 'success', 'warning', 'danger', or a hex color like '#FF5500'.

Icons

Use get_available_icons() to see all available icon names:

import dash_prism

for icon in dash_prism.get_available_icons():
    print(icon)

# Check if an icon exists
if 'Rocket' in dash_prism.AVAILABLE_ICONS:
    print('Available!')

Persistence

Enable workspace persistence:

dash_prism.Prism(
    id='workspace',
    persistence=True,
    persistence_type='local',  # 'local', 'session', or 'memory'
)
  • 'local' — Persists across browser sessions (localStorage)

  • 'session' — Persists for current tab only (sessionStorage)

  • 'memory' — No persistence

Initial Layout

Set a default layout to display in the first tab on initial page load:

dash_prism.Prism(
    id='workspace',
    initialLayout='dashboard',  # Must match a registered layout ID
)

The specified layout will load automatically when users first visit the page.

Note

If persistence is enabled and a saved workspace exists, the persisted state takes precedence over initialLayout. The initial layout only applies on the very first visit before any workspace is saved.

Example with persistence:

dash_prism.Prism(
    id='workspace',
    initialLayout='dashboard',
    persistence=True,
    persistence_type='local',
)
# First visit: shows 'dashboard' layout
# Subsequent visits: restores user's saved workspace

Workspace State

Prism exposes two properties for reading and writing workspace state:

  • readWorkspace — Read-only property containing the current workspace state (use as Input or State in callbacks)

  • updateWorkspace — Write-only property for programmatically updating the workspace (use as Output in callbacks)

Reading State with Actions

Use an Action to trigger reading the workspace. The Action provides the Input while readWorkspace is accessed via State:

dash_prism.Prism(
    id='workspace',
    actions=[
        dash_prism.Action(
            id='export-btn',
            label='Export',
            icon='Download',
            tooltip='Export workspace layout',
        ),
    ],
)

@app.callback(
    Output('export-btn', 'loading'),
    Input('export-btn', 'n_clicks'),
    State('workspace', 'readWorkspace'),
    prevent_initial_call=True
)
def export_workspace(n_clicks, workspace):
    if workspace:
        # Save to database, file, or external storage
        save_to_storage(workspace)
    return False

Writing State with Actions

Use updateWorkspace to restore a previously saved workspace:

dash_prism.Prism(
    id='workspace',
    actions=[
        dash_prism.Action(
            id='import-btn',
            label='Import',
            icon='Upload',
            tooltip='Import workspace layout',
        ),
    ],
)

@app.callback(
    Output('workspace', 'updateWorkspace'),
    Input('import-btn', 'n_clicks'),
    prevent_initial_call=True
)
def import_workspace(n_clicks):
    # Load from database, file, or external storage
    saved_workspace = load_from_storage()
    return saved_workspace

updateWorkspace accepts partial updates. You can update specific fields without replacing the entire workspace:

# Only update the theme
return {'theme': 'dark'}

# Only update favorite layouts
return {'favoriteLayouts': ['dashboard', 'analytics']}

Server-Side Persistence Pattern

Combine readWorkspace and updateWorkspace with Actions for server-side persistence (useful when users access their workspace from multiple devices):

dash_prism.Prism(
    id='workspace',
    actions=[
        dash_prism.Action(id='save-btn', label='Save', icon='Save'),
        dash_prism.Action(id='load-btn', label='Load', icon='FolderOpen'),
    ],
)

@app.callback(
    Output('save-btn', 'loading'),
    Input('save-btn', 'n_clicks'),
    State('workspace', 'readWorkspace'),
    prevent_initial_call=True
)
def save_to_server(n_clicks, workspace):
    # Save workspace to database keyed by user ID
    db.save_workspace(current_user.id, workspace)
    return False

@app.callback(
    Output('workspace', 'updateWorkspace'),
    Input('load-btn', 'n_clicks'),
    prevent_initial_call=True
)
def load_from_server(n_clicks):
    # Restore workspace from database
    return db.load_workspace(current_user.id)

Validating Workspace Data

Use validate_workspace to check workspace data before writing:

from dash_prism.utils import validate_workspace, InvalidWorkspace

@app.callback(
    Output('workspace', 'updateWorkspace'),
    Output('error-msg', 'children'),
    Input('import-btn', 'n_clicks'),
    prevent_initial_call=True
)
def import_with_validation(n_clicks):
    workspace = load_from_storage()
    try:
        validate_workspace(workspace)
        return workspace, ''
    except InvalidWorkspace as e:
        return dash.no_update, f'Invalid workspace: {e.errors}'

Configuration

dash_prism.Prism(
    id='workspace',
    theme='light',           # or 'dark'
    size='md',               # 'sm', 'md', 'lg'
    maxTabs=16,              # Max tabs globally (< 1 = unlimited)
    layoutTimeout=30,        # Seconds before timeout
    statusBarPosition='bottom',  # or 'top'
)

Tab Limits

The maxTabs parameter controls the maximum number of tabs allowed in the entire workspace. This is a global limit, not per-panel.

  • Default: 16

  • Set to 0 (or any value less than 1) for unlimited tabs

  • When the limit is reached, new tab actions are blocked with a console warning

Caveats

Callback Exceptions

If your layouts contain components with callbacks, you should set suppress_callback_exceptions=True on your Dash app:

app = Dash(__name__, suppress_callback_exceptions=True)

Why? Components inside tab layouts don’t exist in app.layout at startup — they are created dynamically when a user opens a tab. Dash validates callback IDs against the layout at registration time, so callbacks referencing those components will raise an exception.

# This layout has a Dropdown and Graph inside a tab:
@dash_prism.register_layout(id='analytics', name='Analytics')
def analytics():
    return html.Div([
        dcc.Dropdown(id='metric', options=['revenue', 'users']),
        dcc.Graph(id='chart'),
    ])

# This callback will FAIL at startup without suppress_callback_exceptions
# because 'metric' and 'chart' don't exist in app.layout yet.
@app.callback(Output('chart', 'figure'), Input('metric', 'value'))
def update_chart(metric):
    return px.line(df, y=metric)

When is it not needed? If all your layouts are purely static (no callbacks targeting components inside tabs), or if you exclusively use pattern-matching IDs (dict-style) for those components, you can skip it.

Note

init() logs a warning if suppress_callback_exceptions is False to help catch this early.

ID Transformation (allow_multiple only)

When a layout is registered with allow_multiple=True, Prism transforms string component IDs into pattern-matching dicts to isolate each tab instance:

  • 'my-button' -> {'type': 'my-button', 'index': 'tab-xyz'}

This transformation happens automatically via inject_tab_id() and only applies to layouts that allow multiple instances. Components that already have dict IDs are left unchanged.

Layouts with ``allow_multiple=False`` (the default) are rendered as-is. You can use regular string IDs in callbacks without any transformation.

Callback Patterns for allow_multiple

For layouts with allow_multiple=True, use pattern-matching callbacks:

from dash import MATCH, ALL, ctx

# Match specific tab instance
@app.callback(
    Output({'type': 'output', 'index': MATCH}, 'children'),
    Input({'type': 'button', 'index': MATCH}, 'n_clicks'),
)
def handle(n): ...

# Match all instances (use carefully)
@app.callback(
    Output('global-state', 'data'),
    Input({'type': 'button', 'index': ALL}, 'n_clicks'),
)
def handle_all(clicks): ...

Registration Order

Layouts must be registered before calling init():

# 1. Register first
@dash_prism.register_layout(...)
def my_layout(): ...

# 2. Then app.layout
app.layout = html.Div([dash_prism.Prism(...)])

# 3. Finally init
dash_prism.init('workspace', app)

Initialization (dash_prism.init)

The init() function connects Prism to your Dash application and is required for Prism to function. It performs three critical tasks:

  1. Injects layout metadata into the Prism component so the UI knows which layouts are available

  2. Injects server session ID to invalidate stale persisted workspaces after server restarts

  3. Creates callbacks to render tab contents dynamically

dash_prism.init('workspace', app)

Static vs Callable Layouts

Prism supports both static and callable app.layout configurations:

Static Layout (Recommended)

# Define layout as a static component tree
app.layout = html.Div([
    dash_prism.Prism(id='workspace')
])

dash_prism.init('workspace', app)

This is the recommended approach because:

  • Better performance (no function call overhead on every page load)

  • Simpler to reason about

  • Metadata is injected once during initialization

Callable Layout (Supported)

# Define layout as a function that returns a component tree
def layout():
    return html.Div([
        dash_prism.Prism(id='workspace')
    ])

app.layout = layout
dash_prism.init('workspace', app)

Callable layouts are useful for:

  • User-specific layouts (reading request context)

  • Conditional layout generation

  • Compatibility with existing Dash apps

Note

When using callable layouts, init() automatically wraps your layout function to inject Prism metadata on every render. This adds minimal overhead (~1ms) but ensures metadata stays synchronized even when the layout is regenerated.

Warning

Async callable layouts are supported but require app = Dash(__name__, use_async=True). The wrapping behavior is the same, but the wrapper preserves the async nature of your function.

Background Callbacks

If your layout callbacks are expensive (e.g. loading large datasets or running computations), you can offload them to a background process using Dash’s Background Callback Manager.

Pass background=True to init() to enable this:

from dash import Dash, DiskcacheManager
import diskcache

cache = diskcache.Cache("./cache")
background_callback_manager = DiskcacheManager(cache)

app = Dash(
    __name__,
    background_callback_manager=background_callback_manager,
)

# Register layouts as usual ...

dash_prism.init('workspace', app, background=True)

This registers Prism’s tab rendering callback with background=True, so Dash executes it in a separate worker process (DiskCache) or on a task queue (Celery) instead of blocking the main server thread.