Author

Dianyi Yang

Published

September 27, 2024

Github Thread

This tutorial aims to show you how to build a shiny app like the one below, the dark mode of which is synchronized with the Quarto page.

#| standalone: true
#| components: [editor, viewer]
## file: app.py
from shiny import *
from pathlib import Path

app_ui = ui.page_fluid(
    "This is a Shinylive app with dark theme synced with Quarto", # other ui inputs that you like
    ui.head_content(ui.include_js(Path(__file__).parent / "light-dark.js")),
)

app = App(app_ui, None)

## file: light-dark.js
window.parent.addEventListener("quarto-color-mode", function(event) {
    document.documentElement.dataset.bsTheme = event.detail.mode;
    if (event.detail.mode === "dark") {
        document.documentElement.style.setProperty('--bs-body-bg', "#16242f"); // custom dark background color
    }  else if (event.detail.mode === "light") {
        document.documentElement.style.setProperty('--bs-body-bg', "#f9fffe"); // custom light background color
    }
})
window.parent.postMessage('ShinyColorQuery', '*'); // request Quarto theme color when Shinyapp is loaded

First, in the Quarto Website Page, we need to add a JavaScript (script?) to communicate with the ShinyLive app, which is in iframe. The adding is done by:

include-after-body:
  text: |
    <script type="application/javascript" src="js/light-dark.js"></script>

The actual light-dark.js file is inspired by @mcanouil ’s blog post and needs to contain:

const DarkEvent = new CustomEvent("quarto-color-mode", { detail: { mode: "dark" }}); // add new events
const LightEvent = new CustomEvent("quarto-color-mode", { detail: { mode: "light" }});

function updateAppTheme() { // dispatch events when theme needs updating
    var bodyClass = window.document.body.classList;
    if (bodyClass.contains('quarto-light')) {
        window.dispatchEvent(LightEvent);
    } else if (bodyClass.contains('quarto-dark')) {
        window.dispatchEvent(DarkEvent);
    }
  }
  
var observer = new MutationObserver(function(mutations) { // listen for theme changes
    mutations.forEach(function(mutation) {
      if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
        updateAppTheme();
      }
    });
  });
  
observer.observe(window.document.body, { // enable observer
    attributes: true
});

window.onmessage = function(e) { // update theme when receives querry
  if (e.data == 'ShinyColorQuery') {
    updateAppTheme();
  }
};

Back to the ShinyLive app, it also needs to add a JS script to communicate with the parent page. A minimal working ShinyLive app code looks like this:

#| standalone: true
#| components: [editor, viewer]
## file: app.py
from shiny import *
from pathlib import Path

app_ui = ui.page_fluid(
    "This is a Shinylive app with dark theme synced with Quarto", # other ui inputs that you like
    ui.head_content(ui.include_js(Path(__file__).parent / "light-dark.js")),
)

app = App(app_ui, None)

## file: light-dark.js
window.parent.addEventListener("quarto-color-mode", function(event) {
    document.documentElement.dataset.bsTheme = event.detail.mode;
    if (event.detail.mode === "dark") {
        document.documentElement.style.setProperty('--bs-body-bg', "#16242f"); // custom dark background color
    }  else if (event.detail.mode === "light") {
        document.documentElement.style.setProperty('--bs-body-bg', "#f9fffe"); // custom light background color
    }
})
window.parent.postMessage('ShinyColorQuery', '*'); // request Quarto theme color when Shinyapp is loaded

Reuse

Citation

BibTeX citation:
@online{yang2024,
  author = {Yang, Dianyi},
  title = {How to Sync {Shinylive} Dark Mode with {Quarto}},
  date = {2024-09-27},
  url = {https://rubuky.com/blog/2024-09-27-AutoShinyLiveTheme/},
  langid = {en}
}
For attribution, please cite this work as:
Yang, D. (2024, September 27). How to sync Shinylive dark mode with Quarto. https://rubuky.com/blog/2024-09-27-AutoShinyLiveTheme/