Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

R Shiny light/dark mode switch

Tags:

r

shiny

I have a basic R shiny app that I would like to build a light/ dark mode switch for. I think if I can just get it working for the table tab it should be fine to do for the rest. I am aware that shinyjs is the best way to go about it but I can't seem to find the code anywhere.

library(dplyr)
library(shiny)
library(shinythemes)

ui <- fluidPage(theme = shinytheme("slate"),
                tags$head(tags$style(HTML(
                  "
                  .dataTables_length label,
                  .dataTables_filter label,
                  .dataTables_info {
                      color: white!important;
                      }

                  .paginate_button {
                      background: white!important;
                  }

                  thead {
                      color: white;
                      }

                  "))),
                mainPanel(tabsetPanel(
                  type = "tabs",
                  tabPanel(
                    title = "Table",
                    icon = icon("table"),
                    tags$br(),
                    DT::DTOutput("table")
                  )
                )))

server <- function(input, output) {
  output$table <- DT::renderDT({
    iris
  })
}

shinyApp(ui = ui, server = server)
like image 773
L10Yn Avatar asked Jun 24 '26 06:06

L10Yn


2 Answers

EDITED: see notes at the end

If you want to use bootstrap themes, it's possible to do this using a checkbox input and a javascript event that adds/removes <link> elements (i.e., the html element that loads the bootstrap css theme). I switched the shinytheme to darkly as there's a corresponding light theme (flatly). I removed the css that you defined in tags$head as that will be added/removed based on the theme toggle. (see full example below)

Even though this works, there are likely performance issues. Be aware that each time the theme is changed, the file is fetched and reloaded into the browser. There are also style differences between themes, this may cause content to be reorganized or moved slightly when new theme is applied (this may be disruptive for the user). If you were to choose this approach, I would recommend finding a well-designed light and dark theme combo.

Alternatively, you can select a basic bootstrap theme and define your own css themes. You can use a toggle (like this example) or the media query prefers-color-scheme. Then the shinyjs class functions, you can toggle themes from the R server. This approach is often recommended, but does take a bit longer to develop and validate.

Using the bootstrap approach, here's how you could switch themes.

app.R

In the ui, I created a checkbox input and placed it as the last element (for example purposes).

checkboxInput(
  inputId = "themeToggle",
  label = icon("sun")
)

JS

To switch the bootstrap themes, I defined the html dependency paths defined by the shinythemes package. You can find these in your R package library (library/shinythemes/).

const themes = {
    dark: 'shinythemes/css/darkly.min.css',
    light: 'shinythemes/css/flatly.min.css'
}

To load a new theme, the paths need to be rendered as an html element. We will also need a function that removes an existing css theme. The easiest way to do that is to select the element that has a matching href as defined in the themes variable.

// create new <link>
function newLink(theme) {
    let el = document.createElement('link');
    el.setAttribute('rel', 'stylesheet');
    el.setAttribute('text', 'text/css');
    el.setAttribute('href', theme);
    return el;
}

// remove <link> by matching the href attribute
function removeLink(theme) {
    let el = document.querySelector(`link[href='${theme}']`)
    return el.parentNode.removeChild(el);
}

I also removed the styles defined in the tags$head and created a new <style> element in js.

// css themes (originally defined in tags$head)
const extraDarkThemeCSS = ".dataTables_length label, .dataTables_filter label, .dataTables_info { color: white!important;} .paginate_button { background: white!important;} thead { color: white;}"

// create new <style> and append css
const extraDarkThemeElement = document.createElement("style");
extraDarkThemeElement.appendChild(document.createTextNode(extraDarkThemeCSS));

// add element to <head>
head.appendChild(extraDarkThemeElement);

Lastly, I created an event and attached it to the checkbox input. In this example, checked = 'light' and unchecked = 'dark'.

toggle.addEventListener('input', function(event) {
    // if checked, switch to light theme
    if (toggle.checked) {
        removeLink(themes.dark);
        head.removeChild(extraDarkThemeElement);
        head.appendChild(lightTheme);

    }  else {
        // else add darktheme
        removeLink(themes.light);
        head.appendChild(extraDarkThemeElement)
        head.appendChild(darkTheme);
    }
})

Here's the full app.R file.

library(dplyr)
library(shiny)
library(shinythemes)

ui <- fluidPage(
    theme = shinytheme("darkly"),
    mainPanel(
        tabsetPanel(
            type = "tabs",
            tabPanel(
                title = "Table",
                icon = icon("table"),
                tags$br(),
                DT::DTOutput("table")
            )
        ),
        checkboxInput(
            inputId = "themeToggle",
            label = icon("sun")
        )
    ),
    tags$script(
        "
        // define css theme filepaths
        const themes = {
            dark: 'shinythemes/css/darkly.min.css',
            light: 'shinythemes/css/flatly.min.css'
        }

        // function that creates a new link element
        function newLink(theme) {
            let el = document.createElement('link');
            el.setAttribute('rel', 'stylesheet');
            el.setAttribute('text', 'text/css');
            el.setAttribute('href', theme);
            return el;
        }

        // function that remove <link> of current theme by href
        function removeLink(theme) {
            let el = document.querySelector(`link[href='${theme}']`)
            return el.parentNode.removeChild(el);
        }

        // define vars
        const darkTheme = newLink(themes.dark);
        const lightTheme = newLink(themes.light);
        const head = document.getElementsByTagName('head')[0];
        const toggle = document.getElementById('themeToggle');

        // define extra css and add as default
        const extraDarkThemeCSS = '.dataTables_length label, .dataTables_filter label, .dataTables_info {       color: white!important;} .paginate_button { background: white!important;} thead { color: white;}'
        const extraDarkThemeElement = document.createElement('style');
        extraDarkThemeElement.appendChild(document.createTextNode(extraDarkThemeCSS));
        head.appendChild(extraDarkThemeElement);


        // define event - checked === 'light'
        toggle.addEventListener('input', function(event) {
            // if checked, switch to light theme
            if (toggle.checked) {
                removeLink(themes.dark);
                head.removeChild(extraDarkThemeElement);
                head.appendChild(lightTheme);
            }  else {
                // else add darktheme
                removeLink(themes.light);
                head.appendChild(extraDarkThemeElement)
                head.appendChild(darkTheme);
            }
        })
        "
    )
)

server <- function(input, output) {
    output$table <- DT::renderDT({
        iris
    })
}

shinyApp(ui, server)

EDITS

In this example, I used a checkBoxInput. You can "hide" the input using the following css class. I would recommend adding a visually hidden text element to make this element accessible. The UI would be changed to the following.

checkboxInput(
    inputId = "themeToggle",
    label = tagList(
        tags$span(class = "visually-hidden", "toggle theme"),
        tags$span(class = "fa fa-sun", `aria-hidden` = "true")
    )
)

Then add the css following css. You can also select and style the icon using #themeToggle + span .fa-sun


/* styles for toggle and visually hidden */
#themeToggle, .visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    clip: rect(0 0 0 0);
    clip: rect(0, 0, 0, 0);
    overflow: hidden;
}

/* styles for icon */
#themeToggle + span .fa-sun {
   font-size: 16pt;
}

Here's the updated ui. (I removed the js to make the example shorter)

ui <- fluidPage(
    theme = shinytheme("darkly"),
    tags$head(
        tags$style(
            "#themeToggle, 
            .visually-hidden {
                position: absolute;
                width: 1px;
                height: 1px;
                clip: rect(0 0 0 0);
                clip: rect(0, 0, 0, 0);
                overflow: hidden;
            }",
            "#themeToggle + span .fa-sun {
                font-size: 16pt;
            }"
        )
    ),
    mainPanel(
        tabsetPanel(
            type = "tabs",
            tabPanel(
                title = "Table",
                icon = icon("table"),
                tags$br(),
                DT::DTOutput("table")
            )
        ),
        checkboxInput(
            inputId = "themeToggle",
            label = tagList(
                tags$span(class = "visually-hidden", "toggle theme"),
                tags$span(class = "fa fa-sun", `aria-hidden` = "true")
            )
        )
    ),
    tags$script("...")
)
like image 68
dcruvolo Avatar answered Jun 26 '26 20:06

dcruvolo


You can dynamically switch between bootstrap themes by downloading their CSS files from here, putting them into a folder in your project and using includeCSS in a dynamically generated UI chunk:

library(dplyr)
library(shiny)
library(shinythemes)

ui <- fluidPage(
  theme = shinytheme("flatly"),
  uiOutput("style"),
  tags$head(
    tags$style(
      HTML(
        "
        .dataTables_length label,
        .dataTables_filter label,
        .dataTables_info {
            color: white!important;
            }

        .paginate_button {
            background: white!important;
        }

        thead {
            color: white;
            }

        "
      )
    )
  ),
  mainPanel(
    tabsetPanel(
      type = "tabs",
      tabPanel(
        title = "Table",
        icon = icon("table"),
        tags$br(),
        DT::DTOutput("table")
      )
    ),
    checkboxInput("style", "Dark theme")
  )
)

server <- function(input, output) {
  output$table <- DT::renderDT({
    iris
  })
  
  output$style <- renderUI({
    if (!is.null(input$style)) {
      if (input$style) {
        includeCSS("www/darkly.css")
      } else {
        includeCSS("www/flatly.css")
      }
    }
  })
}

shinyApp(ui = ui, server = server)

From what I understand, this will solve the problem.

The advantage of this approach is that if you remove the checkbox and then generate it again, it will still work. Personally, I was going to use dcruvolos helpful solution in my app until I realised that I can't use it with shiny.router because as soon as you temporarily remove the checkbox from the UI, the JS code stops working (if I understand correctly).

Here is a checkbox in the form of a uiOutput that you can add or remove and it will continue working:

library(dplyr)
library(shiny)
library(shinythemes)

ui <- fluidPage(
  theme = shinytheme("flatly"),
  uiOutput("style"),
  tags$head(
    tags$style(
      HTML(
        "
        .dataTables_length label,
        .dataTables_filter label,
        .dataTables_info {
            color: white!important;
            }

        .paginate_button {
            background: white!important;
        }

        thead {
            color: white;
            }

        "
      )
    )
  ),
  mainPanel(
    tabsetPanel(
      type = "tabs",
      tabPanel(
        title = "Table",
        icon = icon("table"),
        tags$br(),
        DT::DTOutput("table")
      )
    ),
    uiOutput("style_checkbox")
  )
)

server <- function(input, output) {
  
  output$table <- DT::renderDT({
    iris
  })
  
  current_theme <- reactiveVal(FALSE)
  
  output$style_checkbox <- renderUI({
    checkboxInput("style", "Dark theme", value = current_theme())
  })
  
  output$style <- renderUI({
    if (!is.null(input$style)) {
      current_theme(input$style)
      if (input$style) {
        includeCSS("www/darkly.css")
      } else {
        includeCSS("www/flatly.css")
      }
    }
  })
}

shinyApp(ui = ui, server = server)
like image 34
Maciej Pomykała Avatar answered Jun 26 '26 21:06

Maciej Pomykała