Changing button CSS class has unexpected results

Hi all,

I have a big interactive dashboard with lots of moving parts and I recently added a dark mode toggle, which toggles a dark mode CSS class using CustomJS. However, switching to dark mode doesn’t work exactly as expected.

Initial load (light mode)

After clicking dark mode toggle, you can see that

  1. The background changed colors (class now contains “dark-background”)
  2. The data toggle changed colors (class now contains “dark-button”)
  3. The dark mode toggle did not change colors (class doesn’t contain “dark-button”)

After clicking another button (data toggle in this example), you can see that

  1. The data toggle reverted back to the original color
  2. “dark-button” is no longer in the class list

Some other weird stuff happens if you keep clicking the buttons, but this is the gist of it. I’ve included all of the code for this example below. Let me know if you need any more information. If anyone could help with this I’d really appreciate it.

Folder structure
project /
__main.py
__templates /
____index.html
____styles.css

Python

    #!/usr/bin/env python3

    import os
    import numpy as np
    import pandas as pd
    from time import time

    from bokeh.io import curdoc
    from bokeh.plotting import figure
    from bokeh.models import ColumnDataSource, CustomJS
    from bokeh.models.widgets import Toggle

    pd.options.mode.chained_assignment = None


    def make_data():

        d = {
            'x': np.random.random(100),
            'y': np.random.random(100)
        }

        return ColumnDataSource(d)


    def make_plot(source):

        plot_height = 200
        plot_width = 600

        color = '#987987'

        p = figure(plot_width=plot_width, plot_height=plot_height, css_classes=['plot'], sizing_mode='stretch_both',
                   name='plot', tools='tap,hover', toolbar_location=None, x_range=(0, 1), y_range=(0, 1))

        p.circle('x', 'y', size=20, color=color, source=source)

        p.background_fill_color = None
        p.border_fill_color = None

        return p


    def change_data(attr, old, new):

        if new:
            d.data.update({'y': d.data['y'] / 2})
        else:
            d.data.update({'y': d.data['y'] * 2})


    d = make_data()

    p = make_plot(d)

    w0 = Toggle(label='data toggle', height=30, css_classes=['toggle'], width=300, width_policy='max', name='toggle_data')
    w0.on_change('active', change_data)

    w1 = Toggle(label='dark mode toggle', height=30, css_classes=['toggle'], width=300, width_policy='max', name='toggle_color')


    def change_colors_js():
        return CustomJS(args=dict(tgl=w1), code="""

            console.log(tgl.active);

            // body
            var el = document.body;
            el.classList.toggle("dark-background");

            // spinners
            var els = document.getElementsByClassName("bk-btn");
            for (el of els) {
                el.classList.toggle("dark-button");
            }

        """)


    w1.js_on_change('active', change_colors_js())

    curdoc().add_root(w0)
    curdoc().add_root(w1)
    curdoc().add_root(p)

    curdoc().title = 'Test'

HTML

    {% from macros import embed %}

    <!DOCTYPE html>
    <html>
    <head>
    <title>Waze Live Dashboard</title>
    <meta charset="UTF-8" />

    {{ bokeh_css }}
    {{ bokeh_js }}

    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato|Montserrat|Raleway|Oswald|Open+Sans"/>
    <style type="text/css"> {% include 'styles.css' %} </style>

    </head>
    <body>

    <div id="container">

      <div id="button-holder" class="row">

        <div class="button">
          {{ embed(roots.toggle_data) }}
        </div>
        <div class="button">
          {{ embed(roots.toggle_color) }}
        </div>

      </div>

      <div id="plot-holder">
        {{ embed(roots.plot) }}
      </div>

    </div>

    </body>

    <!-- PLOT SCRIPT -->

    {{ plot_script }}

    </html>

CSS

    /* GENERAL */

    .row {
      display: flex;
      justify-content: space-between;
      align-items: center;
      flex-direction: row;
    }

    html, body {
      margin: 0;
      padding: 0;
      overflow: hidden;
    }

    #container {
      max-height: 100vh;
      min-height: 100vh;
      max-width: 100vw;
      min-width: 100vw;
    }

    #button-holder {
      max-width: 100vw;
      min-width: 100vw;
    }

    #plot-holder {
      max-height: calc(100vh - 40px);
      min-height: calc(100vh - 40px);
      max-width: 100vw;
      min-width: 100vw;
    }

    .button {
      max-width: 49.5vw;
      min-width: 49.5vw;
    }

    /* DARK MODE */

    .dark-background {
      background: #262626;
      color: #fff;
    }

    .dark-button {
      background: #404040!important;
      color: #fff!important;
      border-color: #000!important;
    }

I expect for the Bokeh components that your manual changes are simply lost whenever Bokeh re-renders the component. If you want to change any “extra” CSS classes to a Bokeh component, you should generally update the Bokeh model property css_classes on that component.

Thanks for the quick replay Bryan. It won’t be ideal to update the css_classes of every object (I was hoping to update all at once), but if necessary that’s what I’ll do. I tested updating the css_classes and it works as expected, but I’ll provide updates if I find another method.

I ended up going with a CustomJS method to update all of the objects at once. Instead of updating the CSS of each individual object (using getElementByClassName), I am updating the CSS of the entire page.

def change_colors_js():
    return CustomJS(args=dict(tgl=w1), code="""
        
        var ss = document.styleSheets[document.styleSheets.length - 2];
        
        var first = document.styleSheets[document.styleSheets.length - 2].cssRules.length == 0;
        
        if (tgl.active) {
          
          // body
          first ? console.log("first") : ss.removeRule(0);
          ss.insertRule("body {background: #404040; color: #fff;}", 0);
          
          // buttons
          first ? console.log("first") : ss.removeRule(1);
          ss.insertRule(".bk-btn {background: #404040!important; color: #fff!important; border-color: #000!important;}", 1);
        
        } else {
        
          // body
          ss.removeRule(0);
          ss.insertRule("body {background: #fff; color: #000;}", 0);
          
          // buttons
          ss.removeRule(1);
          ss.insertRule(".bk-btn {background: #fff; color: #000; border-color: #ccc;}", 1);
        
        }

    """)
2 Likes