Light and Dark Color Modes with Dash Bootstrap Components>=1.5.0

Boostrap Color Modes

:partying_face: Now it’s easier than ever to switch between light and dark modes in your dash app.

Bootstrap now supports color modes, starting with dark mode. With dash-bootstrap-components v1.5.0 you can set the color mode globally or on specific components and elements, thanks to the data-bs-theme attribute.

For more information, see the Bootstrap docs: Bootstrap Color Modes.

Here’s a preview - this is the “minty” theme in either light or dark mode. See the code at the end of this post.

color-mode-templates

Setting Color Modes

Setting Dark mode globally

If you are not using a component to toggle the color mode, you can set the dark mode globally (the default is light).
Simply add the following to a .js file in the /assets folder:

document.documentElement.setAttribute('data-bs-theme', 'dark')

This allows you to have even more options for dark themes. It will set a dark mode for any theme – for example, you can have a dark mode for any of the 26 themes available in the dash-bootstrap-components library.

Color mode switch

You can change the global theme using a clientside callback.

For example, here is the theme switch component shown in the image above:


color_mode_switch =  html.Span(
    [
        dbc.Label(className="fa fa-moon", html_for="switch"),
        dbc.Switch( id="switch", value=True, className="d-inline-block ms-1", persistence=True),
        dbc.Label(className="fa fa-sun", html_for="switch"),
    ]
)

And here’s the callback to change the theme:


clientside_callback(
    """
    (switchOn) => {
       document.documentElement.setAttribute('data-bs-theme', switchOn ? 'light' : 'dark');
       return window.dash_clientside.no_update
    }
    """,
    Output("switch", "id"),
    Input("switch", "value"),
)

Setting dark or light mode on components

After setting a global theme, it’s possible to use a different color mode on individual components by setting the data-bs-theme attribute on a html.Div container.

Here is a dbc.Card in dark mode:

html.Div(
    [
        dbc.Card( # card content here)
    ]
    id="card",
    **{"data-bs-theme": "dark"}
)

Here is a card with a light mode


html.div(
    [
        dbc.Card( # card content here)
    ]
    id="card",
    **{"data-bs-theme": "light"}
)

You can even change the color mode in a callback:

app.callback(
    Output("card", "data-bs-theme").
    ...

Applying Bootstrap themes to other component libraries

Note that the Bootstrap theme is automatically applied only to dbc components. If you are using other libraries like dash-core-componets, DataTable, dash-ag-grid you need to style them yourself. To save some time, you can use the stylesheet from the dash-bootstrap-templates library. For more information, see:

Applying Bootstrap themes to figures

When you change the color mode, the figures do not update automatically. One option is to update the figure template in a callback. The built-in “plotly_white” and “plotly_dark” work well. If you would like your figures to match your bootstrap theme, you can use one of the 52 Bootstrap themed figure templates from the dash-bootstrap-templates library.

The latest release (V1.1.0) has each of the 26 themes in both light and dark mode. For example minty and minty_dark

For more information, see the Bootstrap Themed Figure Templates
(The update for the dark mode templates comming soon)

Color Mode Switch Example

Here is the app shown in the image above to toggle between light and dark mode for any Bootstrap theme.


"""
Example of light and dark color modes available in Bootstrap >= 5.3
"""
from dash import Dash, html, dcc, Input, Output, Patch, clientside_callback, callback
import plotly.express as px
import plotly.io as pio
import dash_bootstrap_components as dbc
from dash_bootstrap_templates import load_figure_template

# adds  templates to plotly.io
load_figure_template(["minty", "minty_dark"])


df = px.data.gapminder()

app = Dash(__name__, external_stylesheets=[dbc.themes.MINTY, dbc.icons.FONT_AWESOME])

color_mode_switch =  html.Span(
    [
        dbc.Label(className="fa fa-moon", html_for="switch"),
        dbc.Switch( id="switch", value=False, className="d-inline-block ms-1", persistence=True),
        dbc.Label(className="fa fa-sun", html_for="switch"),
    ]
)

fig = px.scatter(
        df.query("year==2007"),
        x="gdpPercap",
        y="lifeExp",
        size="pop",
        color="continent",
        log_x=True,
        size_max=60,
        template="minty",
    )

app.layout = dbc.Container(
    [
        html.Div(["Bootstrap Light Dark Color Modes Demo"], className="bg-primary text-white h3 p-2"),
        color_mode_switch,
        dcc.Graph(id="graph", figure= fig, className="border"),
    ]

)

@callback(
    Output("graph", "figure"),
    Input("switch", "value"),
)
def update_figure_template(switch_on):
    # When using Patch() to update the figure template, you must use the figure template dict
    # from plotly.io  and not just the template name
    template = pio.templates["minty"] if switch_on else pio.templates["minty_dark"]

    patched_figure = Patch()
    patched_figure["layout"]["template"] = template
    return patched_figure



clientside_callback(
    """
    (switchOn) => {
       document.documentElement.setAttribute('data-bs-theme', switchOn ? 'light' : 'dark');
       return window.dash_clientside.no_update
    }
    """,
    Output("switch", "id"),
    Input("switch", "value"),
)


if __name__ == "__main__":
    app.run_server(debug=True)

Color Mode Theme Explorer - See All 52 themes!

Here is a minimal app where you can see each of the 26 themes in both light and dark mode. It also applies Bootstrap themes to dash-core-components and dash-ag-grid. (The color mode switch will be coming soon to the Dash Bootstrap Theme Explorer site.

color-mode-all



from dash import Dash, dcc, html, Input, Output, callback, Patch, clientside_callback
import plotly.express as px
import plotly.io as pio
import dash_bootstrap_components as dbc
from dash_bootstrap_templates import ThemeChangerAIO, template_from_url
import dash_ag_grid as dag

df = px.data.gapminder()
years = df.year.unique()
continents = df.continent.unique()

# stylesheet with the .dbc class to style  dcc, DataTable and AG Grid components with a Bootstrap theme
dbc_css = "https://cdn.jsdelivr.net/gh/AnnMarieW/dash-bootstrap-templates/dbc.min.css"

app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP, dbc.icons.FONT_AWESOME, dbc_css])


color_mode_switch =  html.Span(
    [
        dbc.Label(className="fa fa-moon", html_for="switch"),
        dbc.Switch( id="switch", value=True, className="d-inline-block ms-1", persistence=True),
        dbc.Label(className="fa fa-sun", html_for="switch"),
    ]
)

# The ThemeChangerAIO loads all 52  Bootstrap themed figure templates to plotly.io
theme_controls = html.Div(
    [ThemeChangerAIO(aio_id="theme"), color_mode_switch],
    className="hstack gap-3 mt-2"
)

header = html.H4(
    "Theme Explorer Sample App", className="bg-primary text-white p-2 mb-2 text-center"
)

grid = dag.AgGrid(
    id="grid",
    columnDefs=[{"field": i} for i in df.columns],
    rowData=df.to_dict("records"),
    defaultColDef={"flex": 1, "minWidth": 120, "sortable": True, "resizable": True, "filter": True},
    dashGridOptions={"rowSelection":"multiple"},
)

dropdown = html.Div(
    [
        dbc.Label("Select indicator (y-axis)"),
        dcc.Dropdown(
            ["gdpPercap", "lifeExp", "pop"],
            "pop",
            id="indicator",
            clearable=False,
        ),
    ],
    className="mb-4",
)

checklist = html.Div(
    [
        dbc.Label("Select Continents"),
        dbc.Checklist(
            id="continents",
            options=continents,
            value=continents,
            inline=True,
        ),
    ],
    className="mb-4",
)

slider = html.Div(
    [
        dbc.Label("Select Years"),
        dcc.RangeSlider(
            years[0],
            years[-1],
            5,
            id="years",
            marks=None,
            tooltip={"placement": "bottom", "always_visible": True},
            value=[years[2], years[-2]],
            className="p-0",
        ),
    ],
    className="mb-4",
)
theme_colors = [
    "primary",
    "secondary",
    "success",
    "warning",
    "danger",
    "info",
    "light",
    "dark",
    "link",
]
colors = html.Div(
    [dbc.Button(f"{color}", color=f"{color}", size="sm") for color in theme_colors]
)
colors = html.Div(["Theme Colors:", colors], className="mt-2")


controls = dbc.Card(
    [dropdown, checklist, slider],
    body=True,
)

tab1 = dbc.Tab([dcc.Graph(id="line-chart", figure=px.line(template="bootstrap"))], label="Line Chart")
tab2 = dbc.Tab([dcc.Graph(id="scatter-chart", figure=px.scatter(template="bootstrap"))], label="Scatter Chart")
tab3 = dbc.Tab([grid], label="Grid", className="p-4")
tabs = dbc.Card(dbc.Tabs([tab1, tab2, tab3]))

app.layout = dbc.Container(
    [
        header,
        dbc.Row([
            dbc.Col([controls, theme_controls], width=4),
            dbc.Col([tabs, colors], width=8),
        ]),
    ],
    fluid=True,
    className="dbc dbc-ag-grid",
)



@callback(
    Output("line-chart", "figure" ),
    Output("scatter-chart", "figure"),
    Output("grid", "rowData"),
    Input("indicator", "value"),
    Input("continents", "value"),
    Input("years", "value"),
    Input(ThemeChangerAIO.ids.radio("theme"), "value"),
    Input("switch", "value"),
)
def update(indicator, continent, yrs, theme, color_mode_switch_on):

    if continent == [] or indicator is None:
        return {}, {}, []

    theme_name = template_from_url(theme)
    template_name = theme_name if color_mode_switch_on else theme_name + "_dark"

    dff = df[df.year.between(yrs[0], yrs[1])]
    dff = dff[dff.continent.isin(continent)]

    fig = px.line(
        dff,
        x="year",
        y=indicator,
        color="continent",
        line_group="country",
        template=template_name
    )

    fig_scatter = px.scatter(
        dff[dff.year == yrs[0]],
        x="gdpPercap",
        y="lifeExp",
        size="pop",
        color="continent",
        log_x=True,
        size_max=60,
        template=template_name,
        title="Gapminder %s: %s theme" % (yrs[1], template_name),
    )

    return fig, fig_scatter, dff.to_dict("records")


# updates the Bootstrap global light/dark color mode
clientside_callback(
    """
    switchOn => {       
       document.documentElement.setAttribute('data-bs-theme', switchOn ? 'light' : 'dark');
       return window.dash_clientside.no_update
    }
    """,
    Output("switch", "id"),
    Input("switch", "value"),
)


# This callback isn't necessary, but it makes updating figures with the new theme much faster
@callback(
    Output("line-chart", "figure", allow_duplicate=True ),
    Output("scatter-chart", "figure", allow_duplicate=True),
    Input(ThemeChangerAIO.ids.radio("theme"), "value"),
    Input("switch", "value"),
    prevent_initial_call=True
)
def update_template(theme, color_mode_switch_on):
    theme_name = template_from_url(theme)
    template_name = theme_name if color_mode_switch_on else theme_name + "_dark"

    patched_figure = Patch()
    # When using Patch() to update the figure template, you must use the figure template dict
    # from plotly.io  and not just the template name
    patched_figure["layout"]["template"] = pio.templates[template_name]
    return patched_figure, patched_figure



if __name__ == "__main__":
    app.run_server(debug=True)

5 Likes

This is a great example, @AnnMarieW . Thanks for sharing. The ThemeChangerAIO should be used by anyone interesting in offering app users a full gamut of theme options.