Below is a step by step introduction to the bs4Dash structure.

Create a basic page

This is the template to start with bs4Dash:

library(shiny)
library(bs4Dash)

shinyApp(
  ui = dashboardPage(
    title = "Basic Dashboard",
    header = dashboardHeader(),
    sidebar = dashboardSidebar(),
    controlbar = dashboardControlbar(),
    footer = dashboardFooter(),
    body = dashboardBody()
  ),
  server = function(input, output) {}
)
Basic page template

Basic page template

The dashboardPage() is the main wrapper:

dashboardPage(
  header,
  sidebar,
  body,
  controlbar = NULL,
  footer = NULL,
  title = NULL,
  freshTheme = NULL,
  preloader = NULL,
  options = NULL,
  fullscreen = FALSE,
  help = FALSE,
  dark = FALSE,
  scrollToTop = FALSE
)

has mandatory slots for the navbar (dashboardHeader()), sidebar (dashboardSidebar()) and (dashboardBody()). Note the dashboardControlbar() and dashboardFooter() are optional. The title parameter gives its name to the web browser tab. freshTheme, when provided, expects a fresh powered theme created with fresh::create_theme(). It allows deeper customization of colors to fit very specific needs like industry brand colors. preloader expects a loader tag built with waiter, see more here, for instance:

preloader <- list(html = tagList(spin_1(), "Loading ..."), color = "#343a40")

At the moment, options are not available, but the idea is to provide deeper customization of the AdminLTE3 template like changing the sidebars and cards animation speed, …

When fullscreen is TRUE, an icon is displayed in the navbar to switch to full screen mode. help automatically enable/disable all tooltips and popover that are present in the shiny app: this is an easier approach than using the server methods addPopover(), addTooltip(), … but less specific. dark allows to toggle the dark mode: if FALSE, the theme switch is hidden and the dashboard takes the light design. scrollToTop allows to toggle the scroll to top button shown in the bottom right corner.

Now, it is time to fill this template!

Below we quickly describe the dashboardSidebar() function:

dashboardSidebar(
  disable = FALSE,
  width = NULL,
  skin = "dark",
  status = "primary",
  elevation = 4,
  collapsed = FALSE,
  minified = TRUE,
  expandOnHover = TRUE,
  fixed = TRUE,
  id = "sidebar",
  customArea = NULL,
  ...
)

A lot of options are available:

  • disable to disable the sidebar.
  • width controls the sidebar width.
  • Two skins, namely “light” or “dark”.
  • elevation is a number between 0 and 5, which applies a shadow to the sidebar to add a shadow effect.
  • The sidebar status determins which color menuItem() have. There are 20 different colors listed in getAdminLTEColors().
  • collapsed , if TRUE the sidebar is collapsed at start.
  • minified, if TRUE the sidebar is minified at start. What is the difference between collapse and minified? Minified means a little part of the sidebar is still visible.
  • expandOnHover, when minified is TRUE, if this property is TRUE, the sidebar opens when hovering but re-collapses as soon as the focus is lost.
  • fixed, if TRUE, the sidebar has a vertical overflow, which allows to see all menus at once without scrolling up and down.
  • id is used by the updateSidebar() function to programmatically toggle the sidebar on the server. input$<id> indicates the state of the sidebar: TRUE means open and FALSE means collapsed/minified.
  • customArea is an area at the bottom of the sidebar to contain elements like buttons.

The skin switch feature allows to automatically toggle the sidebar skin.

Importantly, the sidebar contains sidebarMenu() as well as other items like sidebarUserPanel(), sidebarHeader():

sidebarUserPanel(
    image = "https://image.flaticon.com/icons/svg/1149/1149168.svg",
    name = "Welcome Onboard!"
)

sidebarMenu(
  id = "sidebarmenu",
  sidebarHeader("Header 1"),
  menuItem(
    "Item 1",
    tabName = "item1",
    icon = icon("sliders")
  ),
  menuItem(
    "Item 2",
    tabName = "item2",
    icon = icon("id-card")
  )
)

sidebarMenu() drives the navigation within your dashboard. It has an id parameter which allows to :

sidebarMenu() also offers 4 cosmetic parameters:

  • flat is a style parameter like in material design.
  • compact makes the sidebar content smaller.
  • childIndent shows an indentation between the parent item and nested subitems.
  • legacy allows to use the old AdminLTE2 style.

Interestingly, menuItem() can be more than a simple item and contain sub-items, namely menuSubItem():

menuItem(
    text = "Item List 1",
    icon = icon("bars"),
    startExpanded = TRUE,
    menuSubItem(
        text = "Item 3",
        tabName = "tab3",
        icon = icon("circle-thin")
    ),
    menuSubItem(
        text = "Item 4",
        tabName = "tab4",
        icon = icon("circle-thin")
    )
)

startExpanded defines whether the item container has to be opened when the app starts. When a menuItem() contains nested items, it is not necessary to give it a tabName. text may also contain more complex HTML tags like dashboardBadge(). If you want to use menuItem() to browse to an external website, use the href parameter as well as newTab to open a new web browser tab.

Like in shinydashboard, input$sidebarItemExpanded hosts the value of the currently expanded sidebarItem.

The dashboardHeader() function creates a navbar for bs4Dash:

dashboardHeader(
  title = NULL,
  titleWidth = NULL,
  disable = FALSE,
  .list = NULL,
  skin = "light",
  status = "white",
  border = TRUE,
  compact = FALSE,
  sidebarIcon = shiny::icon("bars"),
  controlbarIcon = shiny::icon("th"),
  fixed = FALSE,
  leftUi = NULL,
  rightUi = NULL
)

The title parameter can host simple text but more complex content like dashboardBrand():

title <- dashboardBrand(
    title = "My dashboard",
    color = "primary",
    href = "https://adminlte.io/themes/v3",
    image = "https://adminlte.io/themes/v3/dist/img/AdminLTELogo.png"
)

dashboardBrand() is an enhanced title which has a color status, points to an optional url and may contain a logo. The title width can be controlled by titleWidth, like in shinydashboard. Like dashboardSidebar(), dashboardHeader() offers a lot of theming options with skin and status, but also with border and compact. They respectively show a bottom border and smaller text. sidebarIcon and controlbarIcon control icons for sidebar and controlbar, respectively. The fixed parameter is useful when one wants to see the navbar even at the bottom of the dashboard, without having to scroll up.

leftUi, ... and rightUi are containers that can contains content from left to right. Ideally, we put dropdownMenu() as well as taskItem(), messageItem(), notificationItem(), dashboardUser()

dashboardControlbar() provides an extra sidebar, on the right side:

dashboardControlbar(
    id = NULL,
    disable = FALSE,
    width = 250,
    collapsed = TRUE,
    overlay = TRUE,
    skin = "dark",
    pinned = NULL
)

Like the dashboardSidebar(), dashboardControlbar() may be programmatically toggled on the server with updateControlbar(), provided that the id parameter has a value. In practice, if no id is passed by the user, bs4Dash assigns a specific id. One can control the dashboardControlbar() state at start with collapsed. If TRUE, the controlbar is collapsed and inversely. By default, overlay is TRUE, meaning that the controlbar opens on top of the body content. If FALSE, it pushes and the body content to the left. pinned allows the controlbar to remain open even after a click outside (clicking outside collapses the controlbar by default). This is useful to keep focus on important options whenever necessary. Finally, the controlbar is entirely themable, like dashboardSidebar() and dashboardHeader().

dashboardControlbar() contains controlbarMenu() that hosts controlbarItem(). This feature is built on top the shiny::tabsetPanel, that has been rebranded for Bootstrap 4 compatibility:

controlbarMenu(
    ...,
    id = NULL,
    selected = NULL,
    type = c("tabs", "pills"),
    position = NULL,
    vertical = FALSE,
    side = "left",
    .list = NULL
)

controlbarMenu() may be updated on the server with updateControlbarMenu() (which is no more than shiny::updateTabsetPanel). If you want to have a simple container without menu, you will have to add a specific class to account for padding, as shown below:

dashboardControlbar(
    div(
        class = "p-3",
        # any content
    )
)

We will use dashboardFooter():

dashboardFooter(
  left = a(
    href = "https://twitter.com/divadnojnarg",
    target = "_blank", "@DivadNojnarg"
  ),
  right = "2020"
)

Nothing special to add here!

Setting up the body content

dashboardBody() is the main dashboard container:

dashboardBody(
  tabItems(
    tabItem(
      tabName = "item1",
      fluidRow(
        lapply(1:3, FUN = function(i) {
          sortable(
            width = 4,
            p(class = "text-center", paste("Column", i)),
            lapply(1:2, FUN = function(j) {
              box(
                title = paste0("I am the ", j, "-th card of the ", i, "-th column"),
                width = 12,
                "Click on my header"
              )
            })
          )
        })
      )
    ),
    tabItem(
      tabName = "item2",
      box(
        title = "Card with messages",
        width = 9,
        userMessages(
          width = 12,
          status = "success",
          userMessage(
            author = "Alexander Pierce",
            date = "20 Jan 2:00 pm",
            image = "https://adminlte.io/themes/AdminLTE/dist/img/user1-128x128.jpg",
            type = "received",
            "Is this template really for free? That's unbelievable!"
          ),
          userMessage(
            author = "Dana Pierce",
            date = "21 Jan 4:00 pm",
            image = "https://adminlte.io/themes/AdminLTE/dist/img/user5-128x128.jpg",
            type = "sent",
            "Indeed, that's unbelievable!"
          )
        )
      )
    )
  )
)

The principle is pretty straightforward: all dashboardBody() elements must be embeded in a tabItems() list containing as may elements as the number of items. Each item is a tabItem(). Importantly, the tabName argument must be provide and unique. Moreover, it must be identical to the corresponding menuItem(), so that the navigation between tabs work. This is exactly the same principle as for shinydashboard. Therefore, users should not be lost.

In practice, if the sidebar is empty (without menu), it is still possible to get rid of tabItems() and tabItem().

Wrap Up

Below is the code for your first bs4Dash application:

Code
shinyApp(
  ui = dashboardPage(
    title = "Basic Dashboard",
    fullscreen = TRUE,
    header = dashboardHeader(
      title = dashboardBrand(
        title = "bs4Dash",
        color = "primary",
        href = "https://www.google.fr",
        image = "https://adminlte.io/themes/AdminLTE/dist/img/user2-160x160.jpg",
      ),
      skin = "light",
      status = "white",
      border = TRUE,
      sidebarIcon = icon("bars"),
      controlbarIcon = icon("th"),
      fixed = FALSE,
      leftUi = tagList(
        dropdownMenu(
          badgeStatus = "info",
          type = "notifications",
          notificationItem(
            inputId = "triggerAction2",
            text = "Error!",
            status = "danger"
          )
        ),
        dropdownMenu(
          badgeStatus = "info",
          type = "tasks",
          taskItem(
            inputId = "triggerAction3",
            text = "My progress",
            color = "orange",
            value = 10
          )
        )
      ),
      rightUi = dropdownMenu(
        badgeStatus = "danger",
        type = "messages",
        messageItem(
          inputId = "triggerAction1",
          message = "message 1",
          from = "Divad Nojnarg",
          image = "https://adminlte.io/themes/v3/dist/img/user3-128x128.jpg",
          time = "today",
          color = "lime"
        )
      )
    ),
    sidebar = dashboardSidebar(
      skin = "light",
      status = "primary",
      elevation = 3,
      sidebarUserPanel(
        image = "https://image.flaticon.com/icons/svg/1149/1149168.svg",
        name = "Welcome Onboard!"
      ),
      sidebarMenu(
        sidebarHeader("Header 1"),
        menuItem(
          "Item 1",
          tabName = "item1",
          icon = icon("sliders")
        ),
        menuItem(
          "Item 2",
          tabName = "item2",
          icon = icon("id-card")
        )
      )
    ),
    controlbar = dashboardControlbar(
      skin = "light",
      pinned = TRUE,
      collapsed = FALSE,
      overlay = FALSE,
      controlbarMenu(
        id = "controlbarmenu",
        controlbarItem(
          title = "Item 1",
          sliderInput(
            inputId = "obs",
            label = "Number of observations:",
            min = 0,
            max = 1000,
            value = 500
          ),
          column(
            width = 12,
            align = "center",
            radioButtons(
              inputId = "dist",
              label = "Distribution type:",
              c(
                "Normal" = "norm",
                "Uniform" = "unif",
                "Log-normal" = "lnorm",
                "Exponential" = "exp"
              )
            )
          )
        ),
        controlbarItem(
          "Item 2",
          "Simple text"
        )
      )
    ),
    footer = dashboardFooter(
      left = a(
        href = "https://twitter.com/divadnojnarg",
        target = "_blank", "@DivadNojnarg"
      ),
      right = "2018"
    ),
    body = dashboardBody(
      tabItems(
        tabItem(
          tabName = "item1",
          fluidRow(
            lapply(1:3, FUN = function(i) {
              sortable(
                width = 4,
                p(class = "text-center", paste("Column", i)),
                lapply(1:2, FUN = function(j) {
                  box(
                    title = paste0("I am the ", j, "-th card of the ", i, "-th column"),
                    width = 12,
                    "Click on my header"
                  )
                })
              )
            })
          )
        ),
        tabItem(
          tabName = "item2",
          box(
            title = "Card with messages",
            width = 9,
            userMessages(
              width = 12,
              status = "success",
              userMessage(
                author = "Alexander Pierce",
                date = "20 Jan 2:00 pm",
                image = "https://adminlte.io/themes/AdminLTE/dist/img/user1-128x128.jpg",
                type = "received",
                "Is this template really for free? That's unbelievable!"
              ),
              userMessage(
                author = "Dana Pierce",
                date = "21 Jan 4:00 pm",
                image = "https://adminlte.io/themes/AdminLTE/dist/img/user5-128x128.jpg",
                type = "sent",
                "Indeed, that's unbelievable!"
              )
            )
          )
        )
      )
    )
  ),
  server = function(input, output) {}
)


Original ImageLight

Modified ImageDark


* All credits go to https://codyhouse.co/gem/css-jquery-image-comparison-slider/ for the nice image slider widget!

Advanced shiny user would probably design shiny modules to generate this page, which I really encourage. However, how to deal with modules is not the purpose of this article.