Author: David Granjon
Date: 2025-08-25

# How to start?

#### David Granjon

#### 2025-08-25

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

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!

## Sidebar Setup

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(
      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 :

- Get the name of the currently selected tab, with `input$<id>`.
- Update the currently selected tab with `updateTabItems()`, which is actually `shiny::updateTabsetPanel`.

`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.

## Navbar Setup

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()`…

## Right Sidebar Setup

`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
        )
    )

## Footer Setup

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(
            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) {}
    )

Light

Dark

* 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.