Offline dbc.themes

I’ve altered ThemeChangerAIO to work offline, so that no ressources need to be downloaded.

I did not change a lot but hard coding the theme names and downloading them into assets/dbc_bootstrap_themes with this script:

import requests
import os
import dash_bootstrap_components as dbc

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

for theme, url in dbc_themes_url.items():
    response = requests.get(url)
    with open(os.path.join(f'{theme}.css'), 'w') as f:
        f.write(response.text)

I needed

@app.server.before_request
def before_request():
    if '.css' in request.path and "dbc_bootstrap_themes" in request.path:
        response = make_response(request.get_data())
        response.headers["Content-Type"] = "text/css"
        return response

because otherwise the offline served themes were of MIME type text/html even though they are appended as text/css in the clientside_callback

Here’s the full code of what I tried so far. The stylesheet is correctly appended as I can see, but the theme is not applied.

from dash import Dash, html, dcc, clientside_callback, Output, Input
from dataclasses import dataclass
import os
from flask import request, make_response
import dash_bootstrap_components as dbc

app = Dash(__name__)


@app.server.before_request
def before_request():
    if '.css' in request.path and "dbc_bootstrap_themes" in request.path:
        response = make_response(request.get_data())
        response.headers["Content-Type"] = "text/css"
        return response


@dataclass(frozen=True)
class ThemeChanger:

    element_id: str = "theme-changer"
    dropdown_id: str = "theme-changer-dropdown"
    default_theme: str = "SANDSTONE"

    dbc_boostrap_themes = [
        "BOOTSTRAP",
        "CERULEAN",
        "COSMO",
        "CYBORG",
        "DARKLY",
        "FLATLY",
        "JOURNAL",
        "LITERA",
        "LUMEN",
        "LUX",
        "MATERIA",
        "MINTY",
        "MORPH",
        "PULSE",
        "QUARTZ",
        "SANDSTONE",
        "SIMPLEX",
        "SKETCHY",
        "SLATE",
        "SOLAR",
        "SPACELAB",
        "SUPERHERO",
        "UNITED",
        "VAPOR",
        "YETI",
        "ZEPHYR",
    ]

    def get_dropdown_probs(self) -> dict[str, list[dict[str, str]]]:

        return {
            "value": os.path.join(
                os.getcwd(),
                "assets",
                "dbc_bootstrap_themes",
                f"{self.default_theme}.css",
            ),
            "options": [
                {
                    "label": theme,
                    "value": os.path.join(
                        os.getcwd(),
                        "assets",
                        "dbc_bootstrap_themes",
                        f"{theme}.css",
                    ),
                }
                for theme in self.dbc_boostrap_themes
            ],
        }

    def gen_component(self) -> html.Div:
        dropdown_probs: dict = self.get_dropdown_probs()

        return html.Div(
            id=self.element_id,
            style={"display": "inline"},
            children=[
                dcc.Dropdown(id=self.dropdown_id, clearable=False, **dropdown_probs),
            ],
        )

    def __post_init__(self):
        clientside_callback(
            """
            function switcher(path) {
              var stylesheets = document.querySelectorAll(
                `link[rel=stylesheet][href*="dbc_bootstrap_themes"]`
              );
              console.log(stylesheets);
              // The delay in updating the stylesheet reduces the flash when changing themes
              stylesheets[stylesheets.length - 1].href = path          
              setTimeout(function() {
                for (let i = 0; i < stylesheets.length -1; i++) {
                  stylesheets[i].href = path;
                }
              }, 500);            
            }
            """,
            Output(self.element_id, "key"),
            Input(self.dropdown_id, "value"),
        )

        clientside_callback(
            """
            function(path) {
                // initialize theme
                var link = document.createElement("link");            
                link.type = "text/css";
                link.rel = "stylesheet";
                link.href = path;
                console.log(link);
                document.head.appendChild(link);
            }
            """,
            Output(self.element_id, "role"),
            Input(self.dropdown_id, "value"),
        )


theme_changer = ThemeChanger()

tabs = dbc.Tabs([
    dbc.Tab("Hello"),
    dbc.Tab("World")
]
)

app.layout = html.Div([
    theme_changer.gen_component(),
    tabs
])


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

Maybe @AnnMarieW could have a quick look over it? :smiley:

Hi @luggie

What version are you using? The current version supports using a local pathname instead of a url in the theme switch components. Just be sure to put the stylesheets in a folder other than /assets because Dash will load all of those stylesheets when the app starts and they would conflict with eachother.

Hey @AnnMarieW
I’m using the newest version 1.1.2.
Do you mean it natively supports offline path? If so, how? I’d say at least in the first clientside_callback one would need to make some changes right?

I guess it has something to do with the fact that the links are absolute rather then relative to the server …

Hi @luggie

Actually this is only supported for the ThemeSwitchAIO. I opened an issue to add the same thing to the ThemeChangerAIO.

It should be similar to: Updating ThemeSwithAIO for custom css by BSd3v · Pull Request #14 · AnnMarieW/dash-bootstrap-templates · GitHub

1 Like

I’ve tried to use offline Themes with ThemeSwitcherAIO but got the same ...dbc_bootstrap_themes/CYBORG.css was not loaded because its MIME type, “text/html”, is not “text/css”. error.

import os
from dash import Dash, html
from dash_bootstrap_templates import ThemeSwitchAIO

app = Dash(__name__)

themes = [
        os.getcwd() + "/dbc_bootstrap_themes/CYBORG.css",
        os.getcwd() + "/dbc_bootstrap_themes/LUX.css"
    ]

app.layout = html.Div([
    ThemeSwitchAIO(themes=themes),
    html.Div("Hello World")
])


if __name__ == '__main__':
    app.run_server(debug=True, port=8020)

Hi @luggie

Could you try running the example here:

1 Like