Intermediate

You have a pandas DataFrame full of sales data, website metrics, or sensor readings, and you want to share it as an interactive dashboard — not a static chart, but something your team can filter, zoom, and explore. Dash is the Python framework for exactly this: it lets you build fully interactive web dashboards entirely in Python, no JavaScript required, and run them with a single command. The charts come from Plotly, the interactivity from Dash callbacks, and the layout from Dash’s HTML component library.

Install Dash with pip install dash, which also installs Plotly and the required Flask server. That is the only dependency. Every example in this tutorial runs with python app.py and opens in your browser at http://127.0.0.1:8050.

This tutorial covers Dash layouts with HTML and core components, building Plotly figures, connecting controls to charts with callbacks, chained callbacks for dependent dropdowns, and deploying a complete interactive sales dashboard. By the end you will have a working dashboard you can adapt to any dataset.

Your First Dash Dashboard: Quick Example

Here is the minimum Dash app — a bar chart with a dropdown to switch between datasets:

# quick_dash.py
import dash
from dash import dcc, html
from dash.dependencies import Input, Output
import plotly.express as px
import pandas as pd

# Sample data
data = {
    'Month': ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
    'Sales': [120, 145, 132, 178, 156, 194],
    'Expenses': [90, 105, 98, 120, 115, 135],
}
df = pd.DataFrame(data)

app = dash.Dash(__name__)

app.layout = html.Div([
    html.H1("Monthly Metrics", style={'textAlign': 'center'}),
    dcc.Dropdown(
        id='metric-dropdown',
        options=[{'label': col, 'value': col} for col in ['Sales', 'Expenses']],
        value='Sales',
        clearable=False,
        style={'width': '300px', 'margin': '0 auto 20px'}
    ),
    dcc.Graph(id='metric-chart'),
])

@app.callback(Output('metric-chart', 'figure'), Input('metric-dropdown', 'value'))
def update_chart(metric):
    fig = px.bar(df, x='Month', y=metric, title=f'Monthly {metric}', color_discrete_sequence=['#636EFA'])
    fig.update_layout(plot_bgcolor='white', paper_bgcolor='white')
    return fig

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

Run with python quick_dash.py then open http://127.0.0.1:8050. Output: A bar chart appears. Selecting “Expenses” in the dropdown immediately updates the chart with no page reload.

Every Dash app has the same three-part structure: app.layout defines the HTML structure using component objects, @app.callback decorators wire inputs to outputs, and app.run() starts the Flask development server. The callback receives the current dropdown value and returns a Plotly figure — Dash handles the JavaScript communication automatically.

Dash Architecture: Layout and Callbacks

Understanding how Dash separates layout from logic makes building complex dashboards straightforward:

LayerWhat It DoesKey Components
LayoutDefines the HTML structurehtml.Div, html.H1, html.P, html.Table
Core ComponentsInteractive input/output widgetsdcc.Graph, dcc.Dropdown, dcc.Slider, dcc.DatePickerRange
CallbacksPython functions that update components@app.callback with Input(), Output(), State()
FiguresPlotly chart objects returned by callbackspx.bar, px.line, px.scatter, go.Figure

The key design rule: every interactive element has an id, and callbacks reference those IDs. When an Input component’s value changes, Dash automatically calls the decorated function with the new value and updates the Output component with the return value. This pattern scales from simple single-input charts to complex dashboards with dozens of linked components.

Cache Katie at Dash dashboard control panel managing live metrics
Cache Katie at the controls — real-time metrics at a glance.

Building Layouts

Dash layouts use Python objects that mirror HTML elements. Wrap multiple components in rows and columns for a grid layout:

# layout_demo.py
import dash
from dash import dcc, html
import plotly.express as px

app = dash.Dash(__name__)

app.layout = html.Div(style={'fontFamily': 'Arial, sans-serif', 'maxWidth': '1200px', 'margin': '0 auto'}, children=[
    # Header row
    html.Div(style={'background': '#1a1a2e', 'color': 'white', 'padding': '20px', 'marginBottom': '20px'}, children=[
        html.H1("Sales Dashboard", style={'margin': 0}),
        html.P("Q2 2026 Performance Overview", style={'margin': 0, 'opacity': 0.7}),
    ]),

    # KPI row -- three summary cards
    html.Div(style={'display': 'flex', 'gap': '20px', 'marginBottom': '20px'}, children=[
        html.Div(style={'flex': 1, 'background': '#f0f8ff', 'padding': '20px', 'borderRadius': '8px', 'border': '1px solid #cce5ff'}, children=[
            html.H2("$284,500", style={'margin': 0, 'color': '#0066cc'}),
            html.P("Total Revenue", style={'margin': 0, 'color': '#666'}),
        ]),
        html.Div(style={'flex': 1, 'background': '#f0fff0', 'padding': '20px', 'borderRadius': '8px', 'border': '1px solid #b3ffb3'}, children=[
            html.H2("1,842", style={'margin': 0, 'color': '#009900'}),
            html.P("Total Orders", style={'margin': 0, 'color': '#666'}),
        ]),
        html.Div(style={'flex': 1, 'background': '#fff8f0', 'padding': '20px', 'borderRadius': '8px', 'border': '1px solid #ffd9b3'}, children=[
            html.H2("$154.45", style={'margin': 0, 'color': '#cc6600'}),
            html.P("Avg Order Value", style={'margin': 0, 'color': '#666'}),
        ]),
    ]),

    # Chart area
    dcc.Graph(id='main-chart', figure=px.bar(
        x=['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
        y=[42000, 48000, 51000, 49000, 46000, 52000],
        labels={'x': 'Month', 'y': 'Revenue ($)'},
        title='Monthly Revenue Q2 2026',
    )),
])

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

Inline styles use Python dicts instead of CSS strings — style={'fontSize': '14px', 'color': '#333'} — because Dash components are Python objects. For production dashboards, move styles to an external CSS file placed in the assets/ folder next to your script; Dash automatically serves files from that folder.

Multi-Input Callbacks and Filtering

Callbacks can take multiple inputs and update multiple outputs simultaneously. Here is a dashboard with a date range filter and category selector that both affect the same chart:

# multi_input_dash.py
import dash
from dash import dcc, html
from dash.dependencies import Input, Output
import plotly.express as px
import pandas as pd
import numpy as np

np.random.seed(42)
dates = pd.date_range('2026-01-01', periods=180, freq='D')
df = pd.DataFrame({
    'date': dates,
    'revenue': np.random.randint(1000, 5000, 180),
    'category': np.random.choice(['Electronics', 'Clothing', 'Books'], 180),
    'region': np.random.choice(['North', 'South', 'East', 'West'], 180),
})

app = dash.Dash(__name__)

app.layout = html.Div([
    html.H2("Sales Explorer"),
    html.Div(style={'display': 'flex', 'gap': '20px', 'marginBottom': '20px'}, children=[
        html.Div([
            html.Label("Category"),
            dcc.Dropdown(
                id='category-filter',
                options=[{'label': 'All', 'value': 'All'}] + [{'label': c, 'value': c} for c in df['category'].unique()],
                value='All', clearable=False,
            )
        ], style={'flex': 1}),
        html.Div([
            html.Label("Region"),
            dcc.Dropdown(
                id='region-filter',
                options=[{'label': 'All', 'value': 'All'}] + [{'label': r, 'value': r} for r in df['region'].unique()],
                value='All', clearable=False,
            )
        ], style={'flex': 1}),
    ]),
    dcc.Graph(id='revenue-chart'),
    html.Div(id='stats-box', style={'padding': '10px', 'background': '#f9f9f9', 'borderRadius': '4px'}),
])

@app.callback(
    Output('revenue-chart', 'figure'),
    Output('stats-box', 'children'),
    Input('category-filter', 'value'),
    Input('region-filter', 'value'),
)
def update_dashboard(category, region):
    filtered = df.copy()
    if category != 'All':
        filtered = filtered[filtered['category'] == category]
    if region != 'All':
        filtered = filtered[filtered['region'] == region]

    daily = filtered.groupby('date')['revenue'].sum().reset_index()
    fig = px.line(daily, x='date', y='revenue', title=f'Daily Revenue -- {category} / {region}')
    fig.update_traces(fill='tozeroy', line_color='#636EFA')

    stats = f"Total: ${filtered['revenue'].sum():,.0f} | Orders: {len(filtered)} | Avg/day: ${daily['revenue'].mean():,.0f}"
    return fig, stats

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

When multiple Input components are listed, the callback receives all their current values as arguments in the same order. The callback fires whenever any input changes. Returning a tuple from the callback populates the multiple Output components in matching order — the figure goes to revenue-chart and the stats string goes to stats-box.

Pyro Pete triggering Dash callbacks by pulling interactive levers
Pete pulls the levers — callbacks fire and charts update live.

Real-Life Example: Interactive Sales Dashboard

Here is a complete sales dashboard with linked charts, KPI cards that update with filters, and a data table — production-ready in under 100 lines:

# sales_dashboard.py
import dash
from dash import dcc, html, dash_table
from dash.dependencies import Input, Output
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
import numpy as np

np.random.seed(0)
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
categories = ['Electronics', 'Clothing', 'Books', 'Toys']
rows = []
for m in months:
    for c in categories:
        rows.append({'Month': m, 'Category': c, 'Revenue': np.random.randint(5000, 20000), 'Units': np.random.randint(50, 300)})
df = pd.DataFrame(rows)

app = dash.Dash(__name__)

app.layout = html.Div(style={'fontFamily': 'Arial', 'padding': '20px'}, children=[
    html.H1("Sales Dashboard 2026", style={'textAlign': 'center'}),
    dcc.Dropdown(id='cat-filter', options=[{'label': 'All Categories', 'value': 'All'}] +
        [{'label': c, 'value': c} for c in categories], value='All', clearable=False,
        style={'width': '300px', 'marginBottom': '20px'}),
    html.Div(id='kpi-row', style={'display': 'flex', 'gap': '15px', 'marginBottom': '20px'}),
    html.Div(style={'display': 'flex', 'gap': '20px'}, children=[
        dcc.Graph(id='revenue-bar', style={'flex': 1}),
        dcc.Graph(id='units-pie', style={'flex': 1}),
    ]),
    dash_table.DataTable(id='data-table', page_size=8,
        style_header={'backgroundColor': '#1a1a2e', 'color': 'white'},
        style_data_conditional=[{'if': {'row_index': 'odd'}, 'backgroundColor': '#f9f9f9'}]),
])

@app.callback(
    Output('kpi-row', 'children'), Output('revenue-bar', 'figure'),
    Output('units-pie', 'figure'), Output('data-table', 'data'),
    Output('data-table', 'columns'), Input('cat-filter', 'value')
)
def update_all(category):
    d = df if category == 'All' else df[df['Category'] == category]
    total_rev = d['Revenue'].sum()
    total_units = d['Units'].sum()
    avg_rev = d.groupby('Month')['Revenue'].sum().mean()

    kpi_style = {'flex': 1, 'background': '#eef2ff', 'padding': '15px', 'borderRadius': '8px', 'textAlign': 'center'}
    kpis = [
        html.Div([html.H3(f"${total_rev:,.0f}"), html.P("Total Revenue")], style=kpi_style),
        html.Div([html.H3(f"{total_units:,}"), html.P("Total Units")], style=kpi_style),
        html.Div([html.H3(f"${avg_rev:,.0f}"), html.P("Avg Monthly Revenue")], style=kpi_style),
    ]

    monthly = d.groupby('Month')['Revenue'].sum().reindex(months).reset_index()
    bar_fig = px.bar(monthly, x='Month', y='Revenue', title='Revenue by Month', color_discrete_sequence=['#636EFA'])

    cat_totals = d.groupby('Category')['Units'].sum().reset_index()
    pie_fig = px.pie(cat_totals, names='Category', values='Units', title='Units by Category')

    cols = [{'name': c, 'id': c} for c in d.columns]
    return kpis, bar_fig, pie_fig, d.to_dict('records'), cols

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

This dashboard updates all five outputs (KPIs, bar chart, pie chart, table data, table columns) from a single callback triggered by the category dropdown. The dash_table.DataTable component provides sortable, paginated tabular data with no extra code. Run it, select “Electronics”, and every component immediately filters to show only electronics data.

Frequently Asked Questions

How do I deploy a Dash app to production?

Dash runs on Flask, so any Flask deployment method works: Gunicorn (gunicorn app:server), Docker, or platform-as-a-service hosts like Heroku, Render, or Railway. Export the Flask server with server = app.server and point Gunicorn at it. For production, set debug=False and use environment variables for any credentials. Dash Enterprise (commercial) provides managed hosting with authentication built in.

How do I read an input value without triggering a callback?

Use State instead of Input for components you want to read without triggering updates: from dash.dependencies import State. A State value is passed to the callback only when an Input changes, not when the State component itself changes. This is useful for “submit” button patterns where you want to read several fields only when the button is clicked.

How do I update a chart with live data?

Use dcc.Interval as a callback input: dcc.Interval(id='timer', interval=5000) fires an event every 5 seconds. Add it to your layout, then use Input('timer', 'n_intervals') in your callback. The n_intervals value increments each time the interval fires, triggering your data refresh callback. This pattern works for polling a database, an API endpoint, or reading a log file.

How do I build a multi-page Dash app?

Use Dash Pages (built into Dash 2.5+): create a pages/ folder and add one Python file per page, each calling dash.register_page(__name__, path='/my-page'). Set app = dash.Dash(__name__, use_pages=True) in your main file and include dash.page_container in the layout. Dash automatically generates routing for all registered pages without any URL configuration.

How do I apply a consistent theme across my dashboard?

Use the dash-bootstrap-components library (pip install dash-bootstrap-components) for Bootstrap-based grid layouts and pre-styled components: app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]). Alternatively, put a CSS file in the assets/ folder — Dash serves everything in that folder automatically. For consistent Plotly chart theming, set plotly.io.templates.default = 'plotly_white' once at the top of your script.

Conclusion

Dash lets you build fully interactive data dashboards in pure Python. In this tutorial you built a dropdown-driven bar chart, applied multi-input filtering to a line chart with stats, and assembled a complete sales dashboard with KPI cards, linked charts, and a data table — all updated by a single callback. The entire dashboard works without writing a single line of JavaScript.

The sales dashboard is ready to extend — add a date range picker with dcc.DatePickerRange, a live refresh interval, an export-to-CSV button, or user authentication with Dash’s built-in dash-auth library. Each extension adds one new component and one new callback, following the same Input/Output pattern you learned here.

Official documentation: Dash — Python framework for building analytical web apps.