9 Beautify with {bslib}

bslib (Sievert and Cheng 2021) is a package developed by RStudio, built on top sass. Contrary to fresh, bslib does not support shinydashboard and bs4Dash. Again, you’ll see that in few lines of code, you may provide impressive design modifications to your apps, in a ridiculous amount of time.

9.1 Disclaimer

This feature requires {shiny} >= 1.6.0.

9.2 Plug and play customization

Like fresh, bslib offers a high level set of functions allowing to quickly customize shiny apps. For many users, this step will be enough. bs_theme() seamlessly modifies the main CSS properties like:

  • The background color, namely bg.
  • The foreground color, namely fg.
  • Change default theme colors, also called accents.
  • Modify the font. This leverages the font_google(), font_face() and font_link() functions. In practice, font_google() caches the font resources so that they are always available to the user.

Additionally, it gives the flexibility to choose Bootstrap version with version_default(). As shown in Chapter 3, Shiny primarily relies on Bootstrap 3. This function has three flavors:

  • Bootstrap 3, with "3".
  • Bootstrap 4 + compatibility with Bootstrap 3, with "4".

Advanced users appreciate a ... slot to add extra variables through bs_add_variables(), with literally hundreds of choices.

library(bslib)

bs_theme(
  version = version_default(),
  bootswatch = NULL,
  ...,
  bg = NULL,
  fg = NULL,
  primary = NULL,
  secondary = NULL,
  success = NULL,
  info = NULL,
  warning = NULL,
  danger = NULL,
  base_font = NULL,
  code_font = NULL,
  heading_font = NULL
)

bs_update_theme() is able to update a give theme object, generated with bs_theme().

At any time, developers may preview the resulting theme with bs_theme_preview(), for instance:

library(shiny)
neon_theme <- bs_theme(
  bg = "#000000", 
  fg = "#FFFFFF", 
  primary = "#9600FF", 
  secondary = "#1900A0",
  success = "#38FF12",
  info = "#00F5FB",
  warning = "#FFF100",
  danger = "#FF00E3",
  base_font = "Marker Felt",
  heading_font = "Marker Felt",
  code_font = "Chalkduster"
)
bs_theme_preview(neon_theme, with_themer = FALSE)

This gives the result shown on Figure 9.1.

Theme preview in action

FIGURE 9.1: Theme preview in action

Passing with_themer = TRUE will show a live theming tools to modify the current theme.

Let’s try with another example. We would like to bring a refreshment to the Bootstrap UI elements with one of the most modern UI kit to date, namely 98.css, a Windows 98 CSS kit. The CSS assets may be accessed from jsdelivr, as shown below:

win98_cdn <- "https://cdn.jsdelivr.net/npm/98.css@0.1.16/"
win98_css <- paste0(win98_cdn, "dist/98.min.css")

bslib exposes neat tools to import extra CSS in the current theme, such as bs_add_rules(), which calls sass::sass_file() under the hood:

theme %>% 
  bs_add_rules(
    sprintf('@import %s', win98_css)
  ) 

The three theme colors are #c0c0c0 for the background and all colors (except primary), #03158b for primary and #222222 for the text. We also disable the rounded option so that button borders stay squared.

windows_grey <- "#c0c0c0"
windows98_theme <- bs_theme(
  bg = windows_grey, 
  fg = "#222222", 
  primary = "#03158b",
  base_font = c("Times", "Arial"), 
  secondary = windows_grey, 
  success = windows_grey, 
  danger = windows_grey, 
  info = windows_grey, 
  light = windows_grey, 
  dark = windows_grey, 
  warning = windows_grey,
  "font-size-base" = "0.75rem", 
  "enable-rounded" = FALSE
) %>%
  bs_add_rules(
    sprintf('@import "%s"', win98_css)
  ) 

windows98_theme %>% bs_theme_preview()

When you run the above demonstration, you’ll notice that the slider input is not properly styled. It’s not surprising knowing that the slider is not shaped by Bootstrap but Ion.RangeSlider. Therefore, if you want a better appearance, like in 98.css, we would need extra work. The output is shown Figure 9.2.

Modern Windows 98 theme for Shiny

FIGURE 9.2: Modern Windows 98 theme for Shiny

9.3 Dynamic theming

Now, let’s say you design an app and want to give the choice between the vanilla shiny design and your new cyberpunk theme, created in the previous section. We leverage the new session$setCurrentTheme feature that allows to pass bslib generated themes to JavaScript through the session object. Our Shiny app contains a toggle, which value is either TRUE or FALSE. On the JavaScript side, we first create a custom shiny input with Shiny.setInputValue that gets the current mode value from the toggle. If TRUE, then the custom theme is applied by session$setCurrentTheme in an observeEvent().

Like for fresh, the bslib theme does not apply to static plots, as they are not HTML elements. Therefore we load thematic:

library(thematic)

theme_toggle <- function() {
  div(
    class = "custom-control custom-switch", 
    tags$input(
      id = "custom_mode", 
      type = "checkbox", 
      class = "custom-control-input",
      onclick = HTML(
        "Shiny.setInputValue(
          'dark_mode', 
          document.getElementById('custom_mode').value
        );"
      )
    ),
    tags$label(
      "Custom mode?", 
      `for` = "custom_mode", 
      class = "custom-control-label"
    )
  )
}

default <- bs_theme()
ui <- fluidPage(
  theme = default, 
  theme_toggle(),
  sliderInput("obs", "Number of observations:",
    min = 0, max = 1000, value = 500
  ),
  plotOutput("distPlot")
)
server <- function(input, output, session) {
  observeEvent(input$custom_mode, {
    session$setCurrentTheme(
      if (input$custom_mode) neon_theme else default
    )
  })
  
  output$distPlot <- renderPlot({
    hist(rnorm(input$obs))
  })
}
thematic_shiny()
shinyApp(ui, server)

An issue with bindCache() described here, with shiny 1.6.

9.4 Custom elements

For other elements than core shiny components like numericInput() or thematic compatible elements such as plotOutput(), bslib provides tools to design dynamically themeable custom components.

Let’s take the example of a simple card where the Sass code is defined below:

  • .supercard has a shadow, takes half of the page width and has a fixed height. Notice the background-color that takes the value of the $primary Sass variable, inherited from Bootstrap 4.
  • .supercard_body adds padding to the card body content.
.supercard {
    box-shadow: 0 4px 10px 0 rgb(0, 0, 0), 0 4px 20px 0 
    rgb(0, 0, 0);
    width: 50%;
    height: 200px;
    background-color: $primary;
    
    .supercard_body {
      padding: 0.01em 16px;
    }
}

Below, for convenience, we put that Sass code inside a R string, even though best practice would be to save it in a file and compile it with sass_file().

sass_str <- "
  .supercard {
    box-shadow: 0 4px 10px 0 rgb(0, 0, 0), 0 4px 20px 0 
    rgb(0, 0, 0);
    width: 50%;
    height: 200px;
    
    background-color: $primary;
    .supercard_body {
      padding: 0.01em 16px;
    }
}"

If you try to run sass(input = sass_str), it will fail, as $primary is not defined. Now, the goal is to link this custom Sass code to the main app theme, created with bs_theme(). We invoke the bs_dependency() function where:

  • input refers to a list of Sass rules, that is sass_str in our example.
  • theme is a theme generated with bs_theme().
  • name and version are metadata.

In case we are not in a bslib context, ie the app does not pass a bs_theme() element, we create a fallback containing the card CSS code:

library(htmltools)
super_card_dependency <- function(theme) {
  
  dep_name <- "supercard"
  dep_version <- "1.0.0"
  
  if (is_bs_theme(theme)) {
    bs_dependency(
      input = sass_str,
      theme = theme,
      name = dep_name,
      version = dep_version
    )
  } else {
    htmlDependency(
      name = dep_name,
      version = dep_version,
      src = "supercard-1.0.0/css",
      stylesheet = "super-card.css",
      package = "OSUICode"
    )
  }
}

As shown, in Chapter 4, we have to add this dependency to the card tag. Importantly, we wrap it inside bs_dependency_defer() that enables us to dynamically update the theme on the server side, each time session$setCurrentTheme is called:

super_card <- function(...) {
  div(
    class = "supercard",
    div(class = "supercard_body", ...),
    bs_dependency_defer(super_card_dependency)
  )
}

We then create two simple theme, namely white and dark and run the app:

white_theme <- bs_theme()
dark_theme <- white_theme %>% 
  bs_theme_update(
    bg = "black", 
    fg = "white", 
    primary = "orange"
  )


ui <- fluidPage(
  theme = white_theme,
  theme_toggle(),
  br(),
  super_card("Hello World!")
)

server <- function(input, output, session) {
  observeEvent(input$custom_mode, {
    session$setCurrentTheme(
      if (input$custom_mode) dark_theme else white_theme
    )
  })
}

shinyApp(ui, server)

Live theming requires to pass the session parameter to the server function. Don’t forget it!

The reader is invited to exploit the run_with_themer() capabilities, that allows to dynamically modify the current theme, as shown Figure 9.3.

Theme preview with custom component

FIGURE 9.3: Theme preview with custom component

Below, we try without passing any theme to fluidPage(), to test our CSS fall back strategy:

ui <- fluidPage(super_card("Hello World!"))
server <- function(input, output) {}
shinyApp(ui, server)

9.5 Further resources

The reader will refer to the bslib various vignettes.