Skip to contents

This article focuses on using bslib to create the user interface (UI) for Shiny dashboards. In the next getting started article, we’ll also cover how these dashboard components, layout helpers, etc. be also be used in isolation inside of any Bootstrap project (e.g., R Markdown, pkgdown, etc), not just Shiny dashboards.

Running examples

To run the Shiny app examples in this article, consider copy/paste/run/edit the code in this RStudio Cloud project.

Hello dashboards

Quickly create a dashboard by providing a title, sidebar, and main area contents to page_sidebar().

library(shiny)
library(bslib)

ui <- page_sidebar(
  title = "My dashboard",
  sidebar = "Sidebar",
  "Main content area"
)

shinyApp(ui, function(input, output) {})

Both the sidebar and main content areas can hold any arbitrary collection of UI elements, but it’s good practice to keep inputs in the sidebar and outputs in the main content area. Also, consider wrapping outputs in a card() and sidebar contents in a sidebar() object to add a title and/or customize styling, positioning, etc.

Let’s start with a basic yet interesting example of a customizable histogram with numeric variables from the palmerpenguins dataset.

library(ggplot2)
data(penguins, package = "palmerpenguins")

ui <- page_sidebar(
  title = "Penguins dashboard",
  sidebar = sidebar(
    title = "Histogram controls",
    varSelectInput(
      "var", "Select variable",
      dplyr::select_if(penguins, is.numeric)
    ),
    numericInput("bins", "Number of bins", 30)
  ),
  card(
    card_header("Histogram"),
    plotOutput("p")
  )
)

server <- function(input, output) {
  output$p <- renderPlot({
    ggplot(penguins) +
      geom_histogram(aes(!!input$var), bins = input$bins) +
      theme_bw(base_size = 20)
  })
}

shinyApp(ui, server)

Throughout the rest of this article, we’ll cover how to layout multiple outputs, add multiple pages, and more. For that, it’ll be useful to have some reusable outputs and inputs. So, let’s create a list of cards, each with a plotOutput() container for a numeric variable of interest, plus a color_by input control to color the histograms by species, island, or sex.

cards <- list(
  card(
    full_screen = TRUE,
    card_header("Bill Length"),
    plotOutput("bill_length")
  ),
  card(
    full_screen = TRUE,
    card_header("Bill depth"),
    plotOutput("bill_depth")
  ),
  card(
    full_screen = TRUE,
    card_header("Body Mass"),
    plotOutput("body_mass")
  )
)

color_by <- varSelectInput(
  "color_by", "Color by",
  penguins[c("species", "island", "sex")],
  selected = "species"
)

Now, for a quick example, let’s pass color_by directly to sidebar and the cards to the main content area. By default, each card is placed in a new row, and the cards are allowed to grow/shrink to fit the browser window (if the browser window is wide enough). In the next section, we’ll cover how to customize the layout.

ui <- page_sidebar(
  title = "Penguins dashboard",
  sidebar = color_by,
  !!!cards
)

server <- function(input, output) {
  gg_plot <- reactive({
    ggplot(penguins) +
      geom_density(aes(fill = !!input$color_by), alpha = 0.2) +
      theme_bw(base_size = 16) +
      theme(axis.title = element_blank())
  })

  output$bill_length <- renderPlot(gg_plot() + aes(bill_length_mm))
  output$bill_depth <- renderPlot(gg_plot() + aes(bill_depth_mm))
  output$body_mass <- renderPlot(gg_plot() + aes(body_mass_g))
}

shinyApp(ui, server)

Layouts

Multi-column

Create multi-column layouts with layout_columns(). If no col_widths are specified, it divides space evenly among the UI elements in a row.

ui <- page_sidebar(
  title = "Penguins dashboard",
  sidebar = color_by,
  layout_columns(cards[[1]], cards[[2]]),
  cards[[3]]
)

shinyApp(ui, server)

A vector of column widths may also be supplied to allocate a given number of columns (out of 12) to each element. And, if elements go beyond the 12 column mark, they get wrapped onto a new row. By default, all row heights are equal, but this can be customized with the row_heights argument (numeric values are interpreted as fractional units, but fixed length units are also supported).

ui <- page_sidebar(
  title = "Penguins dashboard",
  sidebar = color_by,
  layout_columns(
    col_widths = c(4, 8, 12),
    row_heights = c(1, 2),
    cards[[1]],
    cards[[2]],
    cards[[3]]
  )
)

shinyApp(ui, server)

Negative col_widths may also be provided to easily create negative/empty space:

ui <- page_sidebar(
  title = "Penguins dashboard",
  sidebar = color_by,
  layout_columns(
    col_widths = c(4, 8, -2, 8, -2),
    cards[[1]],
    cards[[2]],
    cards[[3]]
  )
)

shinyApp(ui, server)

Although we don’t show it here, layout_columns() also supports different col_widths and row_heights at different breakpoints(). This is useful for creating responsive layouts that adapt to different screen sizes. To learn more, visit layout_columns().

Multi-page

To add multiple pages, change page_sidebar() to page_navbar() and use nav_panel() to supply the title and content for each page. Here you can also take advantage of bslib’s nav_spacer() and nav_item() to control the alignment and UI elements to the navbar (e.g., an external hyperlink).

ui <- page_navbar(
  title = "Penguins dashboard",
  sidebar = color_by,
  nav_spacer(),
  nav_panel("Bill Length", cards[[1]]),
  nav_panel("Bill Depth", cards[[2]]),
  nav_panel("Body Mass", cards[[3]]),
  nav_item(tags$a("Posit", href = "https://posit.co"))
)

shinyApp(ui, server)

Be wary that page_navbar()’s sidebar argument puts the same sidebar on every page, which may or may not be desirable. In the sidebar article, you’ll learn some strategies for keeping the sidebar’s contents close to the elements they affect. Specifically, that article covers how to: (1) hide/show sidebar contents on particular pages, (2) put a particular sidebar() with a given page, and even (3) put sidebar layouts inside of a card().

Multi-panel

Multiple nav_panel()s don’t necessarily need to go directly into page_navbar(). They can also be consumed by any navset_*() component, including navset_card_underline().

ui <- page_sidebar(
  title = "Penguins dashboard",
  sidebar = color_by,
  navset_card_underline(
    title = "Histograms by species",
    nav_panel("Bill Length", plotOutput("bill_length")),
    nav_panel("Bill Depth", plotOutput("bill_depth")),
    nav_panel("Body Mass", plotOutput("body_mass"))
  )
)

shinyApp(ui, server)

Multi-panel layouts

See here to learn more about multi-panel layout options.

Scrolling vs filling

Both page_sidebar() and page_navbar() default to a filling layout, meaning that outputs like plotOutput(), htmlwidgets, etc. are encouraged to grow/shrink to fit the browser window. As a result, when contents have an large intrinsic minimum height (e.g., a long textual description), filling layout may cause cards/outputs to grow/shrink beyond what is desirable. To combat this, consider setting a height on card()s that shouldn’t grow/shrink and min_height/max_height on card()s that shouldn’t shrink/grow beyond a certain point.

ui <- page_sidebar(
  title = "Penguins dashboard",
  sidebar = color_by,
  card(min_height = 200, plotOutput("bill_length")),
  card(height = 200, lorem::ipsum(15))
)

shinyApp(ui, server)

Filling layout may not be desirable when there are a lot of outputs to display on a single page. In this case, consider setting fillable = FALSE to prevent growing/shrinking altogether. When the page layout is not fillable, outputs will fall back to their intrinsic height, which is usually 400px. Users will be able to scroll the page if the outputs take up more vertical space than the height of the window.

ui <- page_sidebar(
  title = "Penguins dashboard",
  sidebar = color_by,
  fillable = FALSE,
  !!!cards
)

shinyApp(ui, server)

A note on page_navbar()

Like page_sidebar(), page_navbar() also has a fillable argument which also defaults to TRUE, which makes every page (i.e., nav_panel()) fillable. To make just a subset of pages fillable, pass a vector of page names to fillable (e.g., fillable = c("Bill Length", "Bill Depth")). To make all pages non-fillable, set fillable = FALSE.

Mobile layout

By default, filling layout is not enabled on mobile devices, meaning that fillable is effectively FALSE in that case. To enable filling layout on mobile devices, set fillable_mobile = TRUE. In this case, it’s good practice to set min_height on card()s to prevent them from growing/shrinking beyond what is desirable.

Also, sidebars are collapsed by default on mobile devices, but they can be opened via sidebar()’s open argument.

ui <- page_sidebar(
  title = "Penguins dashboard",
  sidebar = color_by,
  fillable_mobile = TRUE,
  card(min_height = 200, plotOutput("bill_length")),
  card(height = 200, lorem::ipsum(15))
)

shinyApp(ui, server)

Components

Value boxes

A value_box() is a special kind of card designed for highlighting a value along with a title and a showcase placeholder (typically a bsicons icon). They are covered in more detail in the value boxes article, but here’s a palmerpenguins example:

means <- colMeans(
  penguins[c("bill_length_mm", "bill_depth_mm", "body_mass_g")],
  na.rm = TRUE
)

ui <- page_sidebar(
  title = "Penguins dashboard",
  sidebar = color_by,
  layout_columns(
    fill = FALSE,
    value_box(
      title = "Average bill length",
      value = scales::unit_format(unit = "mm")(means[[1]]),
      showcase = bsicons::bs_icon("align-bottom")
    ),
    value_box(
      title = "Average bill depth",
      value = scales::unit_format(unit = "mm")(means[[2]]),
      showcase = bsicons::bs_icon("align-center")
    ),
    value_box(
      title = "Average body mass",
      value = scales::unit_format(unit = "g", big.mark = ",")(means[[3]]),
      showcase = bsicons::bs_icon("handbag")
    )
  ),
  layout_columns(
    cards[[1]], cards[[2]]
  ),
  cards[[3]]
)

shinyApp(ui, server)

Accordions

An accordion() uses vertical collapsing to provide scalable navigation through many sections of content. Accordions can be placed anywhere on the page, including to a sidebar, which is convenient for grouping sections of input controls. Learn more about accordions on their reference page.

ui <- page_sidebar(
  title = "Penguins dashboard",

  sidebar = sidebar(
    bg = "white",
    accordion(
      accordion_panel(
        "Primary controls",
        color_by
      ),
      accordion_panel(
        "Other controls",
        "Other controls go here"
      )
    )
  ),

  accordion(
    open = c("Bill Length", "About"),
    accordion_panel(
      "Bill Length",
      plotOutput("bill_length")
    ),
    accordion_panel(
      "Bill Depth",
      plotOutput("bill_depth")
    ),
    accordion_panel(
      "Body Mass",
      plotOutput("body_mass")
    )
  )
)

shinyApp(ui, server)

Cards with sidebars

We’ve already seen how the sidebar argument of page_sidebar() and page_navbar() yields a “page-level” sidebar layout, but they can also be made at the “component-level” via layout_sidebar(). This is especially useful for keeping input controls near the outputs they affect in more complex apps. layout_sidebar() also works inside of a card(), making it easy to add headers, footers, full_screen behavior, etc.

To learn more, visit the cards and sidebars articles.

ui <- page_sidebar(
  title = "Penguins dashboard",
  sidebar = sidebar(
    title = "Global controls",
    color_by
  ),
  card(
    full_screen = TRUE,
    card_header("Bill Length"),
    layout_sidebar(
      sidebar = sidebar(
        title = "Local controls",
        position = "right",
        "..."
      ),
      plotOutput("bill_length")
    )
  ),
  cards[[2]],
  cards[[3]],
)

shinyApp(ui, server)

Theming

In the getting started with theming article, you’ll learn some general theming strategies that also translate to dashboards. That is, by passing a bs_theme() object to the theme argument of page_sidebar() (or page_navbar()), we can add a Bootswatch theme, easily use Google Fonts, customize 100s of theming variables, and much more. Keep in mind, however, that bs_theme() only customizes CSS styles. So, if you want plotOutput()s to match the CSS, consider using the thematic package to translate the styles to ggplot2::theme() settings. When doing so, also be aware that “complete” ggplot2 themes (e.g., theme_bw()) only work sensibly when setting them globally (i.e., theme_set(theme_bw())).

ui <- page_sidebar(
  title = "Penguins dashboard",
  sidebar = color_by,
  theme = bs_theme(
    bootswatch = "darkly",
    base_font = font_google("Inter"),
    navbar_bg = "#25443B"
  ),
  !!!cards
)

# Enable thematic
thematic::thematic_shiny(font = "auto")

# Change ggplot2's default "gray" theme
theme_set(theme_bw(base_size = 16))

# New server logic (removes the `+ theme_bw()` part)
server <- function(input, output) {
  gg_plot <- reactive({
    ggplot(penguins) +
      geom_density(aes(fill = !!input$color_by), alpha = 0.2) +
      theme(axis.title = element_blank())
  })

  output$bill_length <- renderPlot(gg_plot() + aes(bill_length_mm))
  output$bill_depth <- renderPlot(gg_plot() + aes(bill_depth_mm))
  output$body_mass <- renderPlot(gg_plot() + aes(body_mass_g))
}

shinyApp(ui, server)

In production

Before deploying a dashboard to production, it’s wise to “hard-code” the version of Bootstrap used when it was developed. This reduces the chance of the dashboard breaking if and when bslib updates it’s Bootstrap dependency. To do so, call version_default() to get the current version of Bootstrap, then pass that value to bs_theme(version = ...) (and pass that theme object to the theme argument of the relevant page_*() function).

library(shiny)

ui <- page_sidebar(
  theme = bs_theme(version = 5),
  ...
)

shinyApp(ui, function(...) {})