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("...")
)