Dash Bootstrap Templates V1.0.0 New: Theme Switch Components

Dash Bootstrap Templates V1.0.1

I’m please to announce the latest release of the dash-bootstrap-templates library :confetti_ball:

pip install dash-bootstrap-templates -U

See it live at Dash

The dash-bootstrap-templates library provides:

  • Bootstrap themed Plotly figure templates. You will find a Plotly template for each of the 26 Bootstrap/Bootswatch themes available in the Dash Bootstrap Components Library. These templates will automatically style your figures with Bootstrap theme colors and fonts.

    • :new: 4 themes added: “morph”, “quartz”, “vapor” “zephyr”
  • Two All-in-One components to change themes in a Dash app.

    • :new: ThemeSwitchAIO toggles between two themes.

    • :new: ThemeChangerAIO select from multiple themes.

  • :new: The dbc.css stylesheet which styles Dash Core Components and the Dash DataTable with a Bootstrap theme. (This is really cool - more examples coming soon)

Note: The ThemeSwitchAIO and ThemeChangerAIO components and the dbc.css stylesheet requires dash-bootstrap-components>=1.0.0. It will only work with the themes included in `dash-bootstrap-components>=1.0.0

Figure Template Quickstart


from dash import Dash, dcc, html
import plotly.express as px
import dash_bootstrap_components as dbc

from dash_bootstrap_templates import load_figure_template

app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
load_figure_template("bootstrap")


df = px.data.gapminder().query("continent != 'Asia'")  # remove Asia for visibility
fig = px.line(df, x="year", y="lifeExp", color="continent", line_group="country")


app.layout = dbc.Container(
    [
        html.H1("Dash Bootstrap Template Demo", className="bg-primary text-white p-2"),
        dbc.Row(dbc.Col(dcc.Graph(figure=fig))),
    ],
    fluid=True,
)


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

image

Demo App 2 - 4 Graphs Updated

This demo (code here), shows how the theme is applied to all 4 graphs.

figure_template2

Theme Switch Components

dash-bootstrap-templates has two All-in-One components to change themes.

  • The ThemeSwitchAIO is a switch with icons on the left and right, which is ideal for toggling between a light and a dark theme.

  • The ThemeChangerAIO has a button that opens an dbc.Offcanvas component which by default shows all the available themes.

Note the All-in-One component switches the Bootstrap stylesheet for the app and sets the default Plotly figure template for the theme, however, figures must be updated in a callback in order to render the figure with the new template. See the callback below for an example. The template_from_url is a helper function that returns the template name based on the theme url. For example template_from_url(dbc.themes.SLATE) returns "slate"

ThemeChangerAIO Quickstart


from dash import Dash, dcc, html, Input, Output
import pandas as pd
import plotly.express as px
import dash_bootstrap_components as dbc
from dash_bootstrap_templates import ThemeChangerAIO, template_from_url


dbc_css = (
    "https://cdn.jsdelivr.net/gh/AnnMarieW/dash-bootstrap-templates@V1.0.1/dbc.min.css"
)
app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP, dbc_css])


df = pd.DataFrame(
    {
        "Fruit": ["Apples", "Oranges", "Bananas", "Apples", "Oranges", "Bananas"],
        "Amount": [4, 1, 2, 2, 4, 5],
        "City": ["SF", "SF", "SF", "Montreal", "Montreal", "Montreal"],
    }
)
header = html.H4(
    "ThemeChangerAIO Demo", className="bg-primary text-white p-4 mb-2 text-center"
)
buttons = html.Div(
    [
        dbc.Button("Primary", color="primary"),
        dbc.Button("Secondary", color="secondary"),
        dbc.Button("Success", color="success"),
        dbc.Button("Warning", color="warning"),
        dbc.Button("Danger", color="danger"),
        dbc.Button("Info", color="info"),
        dbc.Button("Light", color="light"),
        dbc.Button("Dark", color="dark"),
        dbc.Button("Link", color="link"),
    ],
    className="m-4",
)
graph = html.Div(dcc.Graph(id="graph"), className="m-4")

app.layout = dbc.Container(
    [
        header,
        dbc.Row(
            [
                dbc.Col(ThemeChangerAIO(aio_id="theme", radio_props={"value":dbc.themes.FLATLY}), width=2,),
                dbc.Col([buttons, graph],width=10),
            ]
        ),
    ],
    className="m-4 dbc",
    fluid=True,
)


@app.callback(
    Output("graph", "figure"), Input(ThemeChangerAIO.ids.radio("theme"), "value"),
)
def update_graph_theme(theme):
    return px.bar(
        df, x="Fruit", y="Amount", color="City", barmode="group", template=template_from_url(theme)
    )


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

theme_changer



Here is the same app, but using a the ThemeSwitchAIO component to toggle between two themes.
See the (code here).

It’s also possible to change the icons. See an example of using Bootstrap icons instead of the default FontAwesome icons here.

theme_toggle

Background

Dash Labs is Plotly library that explores new features for future releases of Dash. In Dash Labs V0.4.0, there was a cool feature where Bootstrap themed figure templates were created “on the fly”. This was a part of the layout templates project that is no longer being developed.

Even though these Bootstrap themed figure templates will not be included in Dash, the dash-bootstrap-templates makes them available to you. The figure templates are created using the Dash Labs’ algorithms and saved in json format. When you use load_figure_template() in your app, it loads the json file, adds it to plotly.io and sets it as the default figure template for an app. See more information about Plotly figure templates here.

Available Themes

This library provides a figure template for the following Bootstrap/Bootswatch themes:

valid_themes = [
“bootstrap”,
“cerulean”,
“cosmo”,
“flatly”,
“journal”,
“litera”,
“lumen”,
“lux”,
“materia”,
“minty”,
“pulse”,
“sandstone”,
“simplex”,
“sketchy”,
“spacelab”,
“united”,
“yeti”,
“cyborg”,
“darkly”,
“slate”,
“solar”,
“superhero”,
“morph”,
“quartz”,
“vapor”
“zephyr”
]

ThemeChangerAIO Reference

ThemeChangerAIO is an All-in-One component composed of a parent html.Div with
the following components as children:

  • dbc.Button (“switch”) Opens the Offcanvas component for user to select a theme.
  • dbc.Offcanvas (“offcanvas”)
  • dbc.RadioItems (“radio”). The themes are displayed as RadioItems inside the dbc.Offcanvas component.
    The value is a url for the theme
  • html.Div is used as the Output of the clientside callbacks.

The ThemeChangerAIO component updates the stylesheet when the value of radio changes. (ie the user selects a new theme)

  • param: radio_props A dictionary of properties passed into the dbc.RadioItems component. The default value is dbc.themes.BOOTSTRAP
  • param: button_props A dictionary of properties passed into the dbc.Button component.
  • param: offcanvas_props. A dictionary of properties passed into the dbc.Offcanvas component
  • param: aio_id The All-in-One component ID used to generate components’ dictionary IDs.

The All-in-One component dictionary IDs are available as:

  • ThemeChangerAIO.ids.radio(aio_id)
  • ThemeChangerAIO.ids.offcanvas(aio_id)
  • ThemeChangerAIO.ids.button(aio_id)

ThemeSwitchAIO Reference

ThemeSwitchAIO is an All-in-One component composed of a parent html.Div with the following components as children:

  • dbc.Switch (“switch”) with icons to the left and right of the switch.
  • dcc.Store (“store”) The themes are stored in the data prop.
  • html.Div is used as the Output of the clientside callbacks.

The ThemeSwitchAIO component updates the stylesheet when triggered by changes to the value of switch or when
the themes are updated in the “store” component. The themes in the switch may be updated in a callback
by changing the theme urls in the “store” component.

  • param: themes A list of two urls for the external stylesheets. The default is [dbc.themes.CYBORG, dbc.themes.BOOTSTRAP].
  • param: icons A dict of the icons to the left and right of the switch. The default is
    {"left" :"fa fa-moon", "right" :"fa fa-sun"}.
  • param: aio_id The All-in-One component ID used to generate component’s dictionary IDs.

The All-in-One component dictionary IDs are available as

  • ThemeSwitchAIO.ids.switch(aio_id)
  • ThemeSwitchAIO.ids.store(aio_id)

Contributors

Special thanks to @tcbegley and @emilhe for their help with this project. Thanks also to @congnd91 for help with the dbc.css.

6 Likes

Awesome work @AnnMarieW!

2 Likes

@AnnMarieW This is great! Makes it super easy for the user to change themes!

2 Likes

Great work, very helpful.
However I encountered “Cannot set properties of undefined (setting ‘href’)”
on the browser during applying to my app with “debug=True” mode
It can work properly after that error , but I want to fix it.
I am now investigating this error but do you have any advice?

1 Like

Hi @takashi
Thanks for using dash-bootstrap-templates! The current version is now 1.0.4. (I’ll update the instructions)
If you still get that error after upgrading:

pip install dash-bootstrap-templates  -U

Could you please tell me when you you get the error - Is it with one of the theme switch components?

Also, could you check your version of dash-bootstrap-components? It requires verion 1.0.0 or greater

1 Like

Hello Ann

I found my mistake.
my script defined external_stylesheets=[[url_theme1, dbc_css], dbc.icons.FONT_AWESOME],
just copy and paste the demo script.and add font awesome
I deleted inner bracket, it worked correctly FANTASCTIC.
Thank you for your kind help.

2 Likes

Hello,
Is it possible to chose only from several themes in ThemeChangerAIO and not to have all of them?
Thank you!

1 Like

Hi @stefan_1803

That’s a great question - I had to read my docs :slight_smile:

Yes, the AIO Theme switch component uses a dbc.RadioItems to display the themes in the offcanvas component. You can use the radio_props to update the options. Here is a complete example where only three themes are displayed:


from dash import Dash, dcc, html, Input, Output
import pandas as pd
import plotly.express as px
import dash_bootstrap_components as dbc
from dash_bootstrap_templates import ThemeChangerAIO, template_from_url


dbc_css = (
    "https://cdn.jsdelivr.net/gh/AnnMarieW/dash-bootstrap-templates@V1.0.1/dbc.min.css"
)
app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP, dbc_css])


df = pd.DataFrame(
    {
        "Fruit": ["Apples", "Oranges", "Bananas", "Apples", "Oranges", "Bananas"],
        "Amount": [4, 1, 2, 2, 4, 5],
        "City": ["SF", "SF", "SF", "Montreal", "Montreal", "Montreal"],
    }
)
header = html.H4(
    "ThemeChangerAIO Demo", className="bg-primary text-white p-4 mb-2 text-center"
)
buttons = html.Div(
    [
        dbc.Button("Primary", color="primary"),
        dbc.Button("Secondary", color="secondary"),
        dbc.Button("Success", color="success"),
        dbc.Button("Warning", color="warning"),
        dbc.Button("Danger", color="danger"),
        dbc.Button("Info", color="info"),
        dbc.Button("Light", color="light"),
        dbc.Button("Dark", color="dark"),
        dbc.Button("Link", color="link"),
    ],
    className="m-4",
)
graph = html.Div(dcc.Graph(id="graph"), className="m-4")

themes_options = [
    {"label": "Flatly", "value": dbc.themes.FLATLY},
    {"label": "Darkly", "value": dbc.themes.DARKLY},
    {"label": "Minty", "value": dbc.themes.MINTY},
]


app.layout = dbc.Container(
    [
        header,
        dbc.Row(
            [
                dbc.Col(
                    ThemeChangerAIO(
                        aio_id="theme",
                        radio_props={
                            "value": dbc.themes.FLATLY,
                            "options": themes_options,
                        },
                    ),
                    width=2,
                ),
                dbc.Col([buttons, graph], width=10),
            ]
        ),
    ],
    className="m-4 dbc",
    fluid=True,
)


@app.callback(
    Output("graph", "figure"),
    Input(ThemeChangerAIO.ids.radio("theme"), "value"),
)
def update_graph_theme(theme):
    return px.bar(
        df,
        x="Fruit",
        y="Amount",
        color="City",
        barmode="group",
        template=template_from_url(theme),
    )


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


2 Likes

Awesome, thank you very much! May I kindly ask couple of questions more?

  1. dash_table.DataTable, dcc.Dropdown, html.Button are not affected by the theme change, is there a way to do it?
  2. Is there a way to have a custom theme by default and bootstrap ones as an options, or if you’re going to use them, you must have one loaded?
  3. Can a user save theme choice to be his/hers default?
  4. Is it possible to affect all pages of the app with the theme change?

Thank you very much in advance!

1 Like

Hi @stefan_1803

In response to your questions:

  1. I’d recommend using dbc.Button rather than html.Button so the theme gets updated automatically. For other dcc components, this post on styling the DataTable with a Bootstrap Theme might be helpful, and also see my site for more information:
  1. This theme switch component only handles Bootstrap 5 Themes. However, it would be possible to use a customized Bootstrap 5 theme.

  2. For a user default, you might try adding the persistance prop to the RadioItems.

  3. Yes, it’s possible to have the theme change affect all pages. You would just need to include the theme switch component in app.py . See an example GitHub - AnnMarieW/dash-multi-page-app-demos: Minimal examples of multi-page apps using the pages feature in dash>=2.5.1

1 Like

Hi Ann Marie,
Thank you very much for your time and responses, keep up with the great work ! :slight_smile:

3 Likes

@AnnMarieW

First: thanks a lot for that awesome peace of work!

2 questions:

  • Is it possible to automatically set button_props to the currently used bootstrap theme (in my case COSMO)?
  • Is it possible to put the the offcanvas content (the list of radiobuttons and according themes) inside the parent div instead into the offcanvas in the first place?
1 Like

Hi @luggie

Glad you like Dash Bootstrap Templates :slight_smile:

Yes, it’s possible to customize all the properties of the sub components of the ThemeChangerAIO

Here’s a minimal example:



from dash import Dash, dcc, html, Input, Output
import plotly.express as px
import dash_bootstrap_components as dbc
from dash_bootstrap_templates import ThemeChangerAIO, template_from_url

df = px.data.stocks()

app = Dash(__name__, external_stylesheets=[dbc.themes.COSMO])

theme_change = ThemeChangerAIO(
    aio_id="theme",
    radio_props={"value": dbc.themes.COSMO},
    button_props={"color": "primary", "outline": False, "size":None}

)
graph = html.Div(dcc.Graph(id="theme-change-graph"), className="m-4")

app.layout = dbc.Container([theme_change, graph, dbc.Button("my button")], className="m-4 dbc")


@app.callback(
    Output("theme-change-graph", "figure"),
    Input(ThemeChangerAIO.ids.radio("theme"), "value"),
)
def update_graph_theme(theme):
    return px.line(df, x="date", y="GOOG", template=template_from_url(theme))


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


You can find more info and examples here:

For the second question - it’s currently not possible to just have the RadioItems without the Offcanvas component. You could make your own component though - You can start with the current ThemeChangerAOI component and remove the Button, Offcanvas and the callback to open and close the Offcanvas. You can find the code here: dash-bootstrap-templates/src/aio/aio_theme_changer.py at main · AnnMarieW/dash-bootstrap-templates · GitHub

1 Like

Thx!

As for the second question, I think I did not make clear, what I meant.
When using ThemeChangerAIO like this:

ThemeChangerAIO(
    aio_id="theme",
     ...

, it is shown as a dbc.Offcanvas. Instead, I would want to have the ability, to put its content (the list of labelled dbc.RadioItems) somewhere else then in into that side bar.
Is this by any chance possible?

1 Like

Hi @luggie

You made the second question clear, but my answer must not have been :slight_smile:

To use the ThemeChangerAIO without the dbc.Offcanas you would have to modify the code in ThemeChangerAIO. It’s not possible by just changing the props. You would need to create your own AIO component. I could help you with that if you like. It shouldn’t be too hard. You could start with the code I included in the link and remove the code for the Offcanvas and the button.

1 Like

@luggie

Maybe I did misunderstand your question. Do you want to keep the dbc.Offcanvas and move it to a different position? Is so, note that you can pass any valid dbc.Offcanvas props to that component. For example, here is how to move the modal with the radio items to the bottom of the screen


ThemeChangerAIO(aio_id="theme", offcanvas_props={"placement":"bottom"})

1 Like

Well I just did fail to read what you originally posted to my second question…

I tried to copy your code and simply delete the offcanvas as well the the opening button but then the callbacks that work in your code, overlap in my copied one:

import plotly.express as px
from dash_bootstrap_templates import ThemeChangerAIO, template_from_url
from dash import (
    Dash, html, dcc, Input, Output, State, callback, clientside_callback, MATCH
)
import dash_bootstrap_components as dbc
import uuid

dbc_themes_url = {
    item: getattr(dbc.themes, item)
    for item in dir(dbc.themes)
    if not item.startswith(("_", "GRID"))
}
url_dbc_themes = dict(map(reversed, dbc_themes_url.items()))
dbc_themes_lowercase = [t.lower() for t in dbc_themes_url.keys()]
dbc_dark_themes = ["cyborg", "darkly", "slate", "solar", "superhero", "vapor"]


df = px.data.stocks()

app = Dash(__name__, external_stylesheets=[dbc.themes.COSMO])


class MyThemeChangerAIO(html.Div):
    class ids:
        button = lambda aio_id: {
            "component": "ThemeChangerAIO",
            "subcomponent": "button",
            "aio_id": aio_id,
        }
        offcanvas = lambda aio_id: {
            "component": "ThemeChangerAIO",
            "subcomponent": "offcanvas",
            "aio_id": aio_id,
        }
        radio = lambda aio_id: {
            "component": "ThemeChangerAIO",
            "subcomponent": "radio",
            "aio_id": aio_id,
        }
        dummy_div = lambda aio_id: {
            "component": "ThemeChangerAIO",
            "subcomponent": "dummy_div",
            "aio_id": aio_id,
        }

    ids = ids

    def __init__(
            self, radio_props={}, button_props={}, offcanvas_props={}, aio_id=None,
    ):

        """ThemeChangerAIO is an All-in-One component  composed  of a parent `html.Div` with
        the following components as children:

        - `dbc.Button` ("`switch`") Opens the Offcanvas component for user to select a theme.
        - `dbc.Offcanvas` ("`offcanvas`")
        - `dbc.RadioItems` ("`radio`").  The themes are displayed as RadioItems inside the `dbc.Offcanvas` component.
          The `value` is a url for the theme
        - `html.Div` is used as the `Output` of the clientside callbacks.

        The ThemeChangerAIO component updates the stylesheet  when the `value` of radio changes. (ie the user selects a new theme)

        - param: `radio_props` A dictionary of properties passed into the dbc.RadioItems component. The default `value` is `dbc.themes.BOOTSTRAP`
        - param: `button_props`  A dictionary of properties passed into the dbc.Button component.
        - param: `offcanvas_props`. A dictionary of properties passed into the dbc.Offcanvas component
        - param: `aio_id` The All-in-One component ID used to generate components' dictionary IDs.

        The All-in-One component dictionary IDs are available as:

        - ThemeChangerAIO.ids.radio(aio_id)
        - ThemeChangerAIO.ids.offcanvas(aio_id)
        - ThemeChangerAIO.ids.button(aio_id)
        """
        from dash_bootstrap_templates import load_figure_template

        load_figure_template("all")

        if aio_id is None:
            aio_id = str(uuid.uuid4())

        radio_props = radio_props.copy()
        if "value" not in radio_props:
            radio_props["value"] = dbc_themes_url["BOOTSTRAP"]
        if "options" not in radio_props:
            radio_props["options"] = [
                {
                    "label": str(i),
                    "label_id": "theme-switch-label",
                    "value": dbc_themes_url[i],
                }
                for i in dbc_themes_url
            ]
            # assign id to dark themes in order to apply css
            for option in radio_props["options"]:
                if option["label"].lower() in dbc_dark_themes:
                    option["label_id"] = "theme-switch-label-dark"

        button_props = button_props.copy()
        if "children" not in button_props:
            button_props["children"] = "Change Theme"
        if "color" not in button_props:
            button_props["color"] = "secondary"
        if "outline" not in button_props:
            button_props["outline"] = True
        if "size" not in button_props:
            button_props["size"] = "sm"

        offcanvas_props = offcanvas_props.copy()
        if "children" not in offcanvas_props:
            offcanvas_props["children"] = [
                dbc.RadioItems(id=self.ids.radio(aio_id), **radio_props),
            ]
        if "title" not in offcanvas_props:
            offcanvas_props["title"] = "Select a Theme"
        if "is_open" not in offcanvas_props:
            offcanvas_props["is_open"] = False
        if "style" not in offcanvas_props:
            offcanvas_props["style"] = {"width": 235}

        super().__init__(
            [
                dbc.Button(id=self.ids.button(aio_id), **button_props),
                dbc.Offcanvas(id=self.ids.offcanvas(aio_id), **offcanvas_props),
                html.Div(
                    id=self.ids.dummy_div(aio_id),
                    children=radio_props["value"],
                    hidden=True,
                ),
            ]
        )

    @callback(
        Output(ids.offcanvas(MATCH), "is_open"),
        Input(ids.button(MATCH), "n_clicks"),
        [State(ids.offcanvas(MATCH), "is_open")],
    )
    def toggle_theme_offcanvas(n1, is_open):
        if n1:
            return not is_open
        return is_open

    clientside_callback(
        """
        function switcher(url) {
          var stylesheets = document.querySelectorAll(
            `link[rel=stylesheet][href^="https://cdn.jsdelivr.net/npm/bootswatch@5"],
            link[rel=stylesheet][href^="https://cdn.jsdelivr.net/npm/bootstrap@5"]`
          );
          // The delay in updating the stylesheet reduces the flash when changing themes
          stylesheets[stylesheets.length - 1].href = url          
          setTimeout(function() {
            for (let i = 0; i < stylesheets.length -1; i++) {
              stylesheets[i].href = url;
            }
          }, 500);            
        }
        """,
        Output(ids.dummy_div(MATCH), "key"),
        Input(ids.radio(MATCH), "value"),
    )

    # This callback is used to bundle custom CSS with the AIO component
    # and to add a stylesheet so that the theme switcher will work even if there is a
    # Bootstrap stylesheet in the assets folder.
    # This only runs once when the app starts. The clientside function adds the css to a <style>
    # element and appends it to the <head>.  Dash requires callbacks to have an Output
    # even if there is nothing to update.
    #
    clientside_callback(
        """
        function(url) {
            var style = document.createElement('style')
            const aio_css = `
              #theme-switch-label-dark {
              background-color: black;
              color: white;
              width: 100px
            }            
            #theme-switch-label {
              background-color: white;
              color: black;
              width: 100px            
            `            
            style.innerText = aio_css            
            document.head.appendChild(style)

            // initialize theme
            var link = document.createElement("link");            
            link.type = "text/css";
            link.rel = "stylesheet";
            link.href = url;
            document.head.appendChild(link);
        }
        """,
        Output(ids.dummy_div(MATCH), "role"),
        Input(ids.dummy_div(MATCH), "children"),
    )


theme_change = MyThemeChangerAIO(
    aio_id="theme",
    radio_props={"value": dbc.themes.COSMO},
    button_props={"color": "primary", "outline": False, "size":None}

)
graph = html.Div(dcc.Graph(id="theme-change-graph"), className="m-4")

app.layout = dbc.Container([theme_change, graph, dbc.Button("my button")], className="m-4 dbc")


@app.callback(
    Output("theme-change-graph", "figure"),
    Input(ThemeChangerAIO.ids.radio("theme"), "value"),
)
def update_graph_theme(theme):
    return px.line(df, x="date", y="GOOG", template=template_from_url(theme))


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

Hi @luggie

Try this version:
(You will likely want to add additional style for how you want the radio items to appear in your app)


import plotly.express as px
from dash_bootstrap_templates import  template_from_url
from dash import (
    Dash, html, dcc, Input, Output, State, callback, clientside_callback, MATCH
)
import dash_bootstrap_components as dbc
import uuid

dbc_themes_url = {
    item: getattr(dbc.themes, item)
    for item in dir(dbc.themes)
    if not item.startswith(("_", "GRID"))
}
url_dbc_themes = dict(map(reversed, dbc_themes_url.items()))
dbc_themes_lowercase = [t.lower() for t in dbc_themes_url.keys()]
dbc_dark_themes = ["cyborg", "darkly", "slate", "solar", "superhero", "vapor"]


df = px.data.stocks()

app = Dash(__name__, external_stylesheets=[dbc.themes.COSMO])


class MyThemeChangerAIO(html.Div):
    class ids:

        radio = lambda aio_id: {
            "component": "MyThemeChangerAIO",
            "subcomponent": "radio",
            "aio_id": aio_id,
        }
        dummy_div = lambda aio_id: {
            "component": "MyThemeChangerAIO",
            "subcomponent": "dummy_div",
            "aio_id": aio_id,
        }

    ids = ids

    def __init__(
            self, radio_props={}, aio_id=None,
    ):

        """ThemeChangerAIO is an All-in-One component  composed  of a parent `html.Div` with
        the following components as children:


        - `dbc.RadioItems` ("`radio`").  The themes are displayed as RadioItems inside the `dbc.Offcanvas` component.
          The `value` is a url for the theme
        - `html.Div` is used as the `Output` of the clientside callbacks.

        The ThemeChangerAIO component updates the stylesheet  when the `value` of radio changes. (ie the user selects a new theme)

        - param: `radio_props` A dictionary of properties passed into the dbc.RadioItems component. The default `value` is `dbc.themes.BOOTSTRAP`
        - param: `aio_id` The All-in-One component ID used to generate components' dictionary IDs.

        The All-in-One component dictionary IDs are available as:

        - ThemeChangerAIO.ids.radio(aio_id)

        """
        from dash_bootstrap_templates import load_figure_template

        load_figure_template("all")

        if aio_id is None:
            aio_id = str(uuid.uuid4())

        radio_props = radio_props.copy()
        if "value" not in radio_props:
            radio_props["value"] = dbc_themes_url["BOOTSTRAP"]
        if "options" not in radio_props:
            radio_props["options"] = [
                {
                    "label": str(i),
                    "label_id": "theme-switch-label",
                    "value": dbc_themes_url[i],
                }
                for i in dbc_themes_url
            ]
            # assign id to dark themes in order to apply css
            for option in radio_props["options"]:
                if option["label"].lower() in dbc_dark_themes:
                    option["label_id"] = "theme-switch-label-dark"


        super().__init__(
            [
                html.Div(
                    id=self.ids.dummy_div(aio_id),
                    children=radio_props["value"],
                    hidden=True,
                ),
                dbc.RadioItems(id=self.ids.radio(aio_id), **radio_props),
            ]
        )


    clientside_callback(
        """
        function switcher(url) {
          var stylesheets = document.querySelectorAll(
            `link[rel=stylesheet][href^="https://cdn.jsdelivr.net/npm/bootswatch@5"],
            link[rel=stylesheet][href^="https://cdn.jsdelivr.net/npm/bootstrap@5"]`
          );
          // The delay in updating the stylesheet reduces the flash when changing themes
          stylesheets[stylesheets.length - 1].href = url          
          setTimeout(function() {
            for (let i = 0; i < stylesheets.length -1; i++) {
              stylesheets[i].href = url;
            }
          }, 500);            
        }
        """,
        Output(ids.dummy_div(MATCH), "key"),
        Input(ids.radio(MATCH), "value"),
    )

    # This callback is used to bundle custom CSS with the AIO component
    # and to add a stylesheet so that the theme switcher will work even if there is a
    # Bootstrap stylesheet in the assets folder.
    # This only runs once when the app starts. The clientside function adds the css to a <style>
    # element and appends it to the <head>.  Dash requires callbacks to have an Output
    # even if there is nothing to update.
    #
    clientside_callback(
        """
        function(url) {
            var style = document.createElement('style')
            const aio_css = `
              #theme-switch-label-dark {
              background-color: black;
              color: white;
              width: 100px
            }            
            #theme-switch-label {
              background-color: white;
              color: black;
              width: 100px            
            `            
            style.innerText = aio_css            
            document.head.appendChild(style)

            // initialize theme
            var link = document.createElement("link");            
            link.type = "text/css";
            link.rel = "stylesheet";
            link.href = url;
            document.head.appendChild(link);
        }
        """,
        Output(ids.dummy_div(MATCH), "role"),
        Input(ids.dummy_div(MATCH), "children"),
    )


theme_change = MyThemeChangerAIO(aio_id="theme")

graph = html.Div(dcc.Graph(id="theme-change-graph"), className="m-4")

app.layout = dbc.Container([theme_change, graph, dbc.Button("my button")], className="m-4 dbc")


@app.callback(
    Output("theme-change-graph", "figure"),
    Input(MyThemeChangerAIO.ids.radio("theme"), "value"),
)
def update_graph_theme(theme):
    return px.line(df, x="date", y="GOOG", template=template_from_url(theme))


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


2 Likes

Thanks! I boiled it down to what I need from your code and put everything into a dcc.Dropdown. However struggling now to apply the chosen dbc.theme to a dcc component. I stumbled across this issue and wondered, since you’ve been working on the matter, if you know, if there’s any news on applying dbc.themes onto dcc components

import plotly.express as px
from dash_bootstrap_templates import template_from_url, load_figure_template
from dash import (
    Dash, html, dcc, Input, Output, clientside_callback
)
import dash_bootstrap_components as dbc

df = px.data.stocks()

app = Dash(__name__, external_stylesheets=[dbc.themes.SLATE])


class MyThemeChangerAIO(html.Div):

    def __init__(self):
        load_figure_template("all")

        dbc_themes_url = {
            item: getattr(dbc.themes, item)
            for item in dir(dbc.themes)
            if not item.startswith(("_", "GRID"))
        }

        radio_props = {"value": dbc_themes_url["BOOTSTRAP"],
                       "options": [
                            {
                                "label": str(i),
                                "value": dbc_themes_url[i],
                            }
                            for i in dbc_themes_url
                        ]
                       }

        super().__init__(
            [
                html.Div(
                    id="dummy-div",
                    children=radio_props["value"],
                    hidden=True,
                ),
                html.Div(
                    children=dcc.Dropdown(id="dropdown-menu", **radio_props),
                    className="dash-bootstrap"
                ),
            ]
        )

    clientside_callback(
        """
        function switcher(url) {
          var stylesheets = document.querySelectorAll(
            `link[rel=stylesheet][href^="https://cdn.jsdelivr.net/npm/bootswatch@5"],
            link[rel=stylesheet][href^="https://cdn.jsdelivr.net/npm/bootstrap@5"]`
          );
          // The delay in updating the stylesheet reduces the flash when changing themes
          stylesheets[stylesheets.length - 1].href = url          
          setTimeout(function() {
            for (let i = 0; i < stylesheets.length -1; i++) {
              stylesheets[i].href = url;
            }
          }, 500);            
        }
        """,
        Output("dummy-div", "key"),
        Input("dropdown-menu", "value"),
    )

    clientside_callback(
        """
        function(url) {
            // initialize theme
            var link = document.createElement("link");            
            link.type = "text/css";
            link.rel = "stylesheet";
            link.href = url;
            document.head.appendChild(link);
        }
        """,
        Output("dummy-div", "role"),
        Input("dropdown-menu", "value"),
    )


theme_change = MyThemeChangerAIO()

graph = html.Div(dcc.Graph(id="theme-change-graph"))

app.layout = dbc.Container([theme_change, graph])


@app.callback(
    Output("theme-change-graph", "figure"),
    Input("dropdown-menu", "value"),
)
def update_graph_theme(theme):
    return px.line(df, x="date", y="GOOG", template=template_from_url(theme))


if __name__ == "__main__":
    app.run_server(debug=True, port=7000)
1 Like

Hi @luggie

I have a stylesheet that minimally styles dash-core-components, the DataTable and dash-ag-grid based on the Bootstrap theme. You can find more info here:

2 Likes