Skip to contents

Sidebar layouts in web interfaces allow your users to easily access filters, settings and other inputs alongside the interactive features they control. In the Getting Started with dashboards article, we covered “page-level” sidebar layouts via the page_sidebar() and page_navbar() functions. In this article, we’ll explore the full range of sidebar layouts available in bslib.

Overview

There are three main types of sidebar layouts: floating, filling, and multi-page/tab.

Floating layout

Use layout_sidebar() to create a sidebar layout that can go anywhere on any page. This layout approach is great for visually grouping together semantically related inputs and output(s). It can also be paired with a card() to leverage full_screen expansion, add a header/footer, and more.

Show code
layout_sidebar(
  sidebar = sidebar("Sidebar"),
  "Main contents"
)
Main contents
Show code
card(
  full_screen = TRUE,
  card_header("Title"),
  layout_sidebar(
    sidebar = sidebar("Sidebar"),
    "Main contents"
  )
)
layout_sidebar() in card()
Title
Main contents

Filling layout

In the Getting Started with dashboards article, we saw how page_sidebar() yields a sidebar layout that fills the page. Underneath the hood, page_sidebar() is just a simple wrapper around page_fillable() and layout_sidebar(). Understanding this unlocks the potential to have (any number of) sidebar layouts within a filling layout.

Show code
page_fillable(
  layout_sidebar(
    sidebar = sidebar("Sidebar area"),
    "Main area"
  )
)
layout_sidebar() in page_fillable()
Main contents

Multi-page layout

For a multi-page (or multi-tab) layout, use the sidebar argument of page_navbar() (or navset_card_tab()). In this case, we get a sidebar that not only fills the page, but that same sidebar remains visible on every page/tab. Later on, we’ll explore how to put multiple, varied, layouts on different pages; but also keep in mind, if it is actually desirable to have the same sidebar on every page, it often helps to hide/show sidebar contents on certain pages via conditionalPanel().

Show code
page_navbar(
  sidebar = sidebar("Sidebar"),
  nav_panel("Page 1", "Page 1 content"),
  nav_panel("Page 2", "Page 2 content")
)
Show code
navset_card_tab(
  sidebar = sidebar("Sidebar"),
  nav_panel("Tab 1", "Tab 1 content"),
  nav_panel("Tab 2", "Tab 2 content")
)
Tab Card
Tab 1 content
Tab 2 content

A real example

Now that we’ve enumerated bslib’s sidebar layout options, lets use some real data1 to create some real inputs and outputs, and explore some additional features of sidebar layouts.

In a Shiny app2, you’ll probably want to use inputs like selectInput(), sliderInput(), etc., in the sidebar, but because you’re reading this article in a static website, we’ll use crosstalk input widgets.

Setup code

Throughout this section, we’ll make repeated use of the following widgets from plotly and leaflet. The details on how these widgets work alongside crosstalk to create linked views isn’t important for understanding sidebar layouts, but do keep in mind this will give us a list of filters and plots (views of the diamonds dataset), as well as map_filter and map_quakes (views of the quakes dataset).

Show code
library(bslib)
library(shiny)
library(crosstalk)
library(plotly)
library(leaflet)

# Creates the "filter link" between the controls and plots
dat <- SharedData$new(dplyr::sample_n(diamonds, 1000))

# Sidebar elements (e.g., filter controls)
filters <- list(
  filter_select("cut", "Cut", dat, ~cut),
  filter_select("color", "Color", dat, ~color),
  filter_select("clarity", "Clarity", dat, ~clarity)
)

# plotly visuals
plots <- list(
  plot_ly(dat) |> add_histogram(x = ~price),
  plot_ly(dat) |> add_histogram(x = ~carat),
  plot_ly(dat) |> add_histogram(x = ~cut, color = ~clarity)
)
plots <- lapply(plots, \(x) config(x, displayModeBar = FALSE))

# map filter and visual
quake_dat <- SharedData$new(quakes)
map_filter <- filter_slider("mag", "Magnitude", quake_dat, ~mag)
map_quakes <- leaflet(quake_dat) |>
  addTiles() |>
  addCircleMarkers()

Hello layout_sidebar()

layout_sidebar() behaves a lot like a card. For example, when used inside page_fillable() they’ll also grow/shrink to fit the page (because they default to fill = TRUE). They also default to fillable = TRUE which allows fill items in the main content area (e.g., plots[[1]]) to also grow/shrink to fit their container. They also behave a lot like a card_body() in that they can be put directly inside a card() (which is useful for adding a header/footer, full_screen = TRUE, etc.).

sidebar_diamonds <- layout_sidebar(
  sidebar = filters[[1]],
  plots[[1]]
)

sidebar_quakes <- layout_sidebar(
  sidebar = map_filter,
  map_quakes
)

page_fillable(
  sidebar_diamonds,
  card(
    card_header("Earthquakes"),
    sidebar_quakes
  )
)

Resizable example

The example above is resizable. Try using the handle in the lower-right corner to change the “window” size and notice how the plot grow/shrink to fit the window (because of fillable = TRUE).

Filling layouts

To learn more about how fillable containers and fill items work, see the article on filling layouts.

Multi-page varied layout

As we covered in Getting Started with dashboards, the sidebar argument of page_navbar() puts a sidebar on each page that fills the window. However, sometimes it’s better that only particular pages have such a sidebar layout. To acheive this, just provide a layout_sidebar() as a “root” element of a fillable page.

For example, let’s put a “page-level” sidebar on a page dedicated to Earthquakes, and then put multiple sidebar layouts on a page dedicated to Diamonds (one for each plot). In this case, we’ve only allowed the Earthquakes page to be fillable since there are multiple plots on the Diamonds page (you could also keep the Diamonds page fillable an put a min_height on the cards to prevent them from shrinking too much).

page_navbar(
  title = "Sidebar demo",
  fillable = "Earthquakes",
  nav_panel("Earthquakes", sidebar_quakes),
  nav_panel(
    "Diamonds",
    Map(
      function(filter, plot) {
        card(
          full_screen = TRUE,
          layout_sidebar(sidebar = filter, plot)
        )
      },
      filters, plots
    )
  )
)

Multiple tabs

Just like page_navbar(), navset_card_tab() also has a sidebar argument that puts the same sidebar on each tab. The same approach (i.e., putting a layout_sidebar() within each nav_panel()) can be used to put different sidebars on different tabs.

Restricting growth

Just like with cards, when a filling layout isn’t enforcing the size of the layout_sidebar(), it will allow it’s contents to decide how big it should be. Thus, if there a large amount of sidebar/main contents, consider specifying a height or max_height via card() (as well as full_screen = TRUE to reduce the need for scrolling).

page_fixed(
  h1("Sidebar demo", class = "lead mt-3"),
  card(
    height = 400,
    full_screen = TRUE,
    layout_sidebar(sidebar = filters, plots)
  ),
  card(
    full_screen = TRUE,
    layout_sidebar(sidebar = map_filter, map_quakes)
  )
)

Shiny

Although sidebars work just fine outside Shiny, using them in Shiny provides a few additional useful features.

Conditional contents

Sometimes in a multiple page/tab setting, it’s useful to have a sidebar on every page/tab, but changes it’s contents based on which page/tab is active.3 Thanks to conditionalPanel(), this can be done fairly easily in a Shiny app with page_navbar() (or in navset_card_tab()/navset_tab_pill()). The trick is to provide an id to the page_navbar() and then reference that id in the conditionalPanel():

shinyApp(
  page_navbar(
    title = "Conditional sidebar",
    id = "nav",
    sidebar = sidebar(
      conditionalPanel(
        "input.nav === 'Page 1'",
        "Page 1 sidebar"
      ),
      conditionalPanel(
        "input.nav === 'Page 2'",
        "Page 2 sidebar"
      )
    ),
    nav_panel("Page 1", "Page 1 contents"),
    nav_panel("Page 2", "Page 2 contents")
  ),
  server = function(...) {
    # no server logic required
  }
)

Reactive updates

To programmatically update (and/or re-actively read) the open/closed state of a sidebar(), provide an id and reference that id in your server code. Here we reference use the id to programmatically open the sidebar on the 2nd page.

library(shiny)

ui <- page_navbar(
  title = "Sidebar updates",
  id = "nav",
  sidebar = sidebar(
    id = "sidebar",
    open = FALSE,
    "Sidebar"
  ),
  nav_panel("Page 1", "Sidebar closed. Go to Page 2 to open."),
  nav_panel("Page 2", "Sidebar open. Go to Page 1 to close.")
)

server <- function(input, output) {
  observe({
    sidebar_toggle(
      id = "sidebar",
      open = input$nav == "Page 2"
    )
  })
}

shinyApp(ui, server)

Accordions

All sidebars have special treatment for accordions. When an accordion() appears directly within a sidebar() (as an immediate child of the sidebar), the accordion panels will render flush to the sidebar, providing a convenient way to group multiple related input controls under a collapsible section.

Setup code

This example depends on objects from the setup code section.

accordion_filters <- accordion(
  accordion_panel(
    "Dropdowns", icon = bsicons::bs_icon("menu-app"),
    !!!filters
  ),
  accordion_panel(
    "Numerical", icon = bsicons::bs_icon("sliders"),
    filter_slider("depth", "Depth", dat, ~depth),
    filter_slider("table", "Table", dat, ~table)
  )
)

card(
  card_header("Groups of diamond filters"),
  layout_sidebar(
    sidebar = accordion_filters,
    plots[[1]]
  )
)
Groups of diamond filters

Nested sidebars

It’s possible to nest sidebar layouts, which means you can effectively have any number of left and/or right sidebars in a given layout. When doing this, you’ll want the main content area of every layout_sidebar() that contains a layout_sidebar() to be fillable and have zero padding (class = "p-0").

page_fillable(
  h1("Left and right sidebar", class = "px-3 my-3"),
  layout_sidebar(
    sidebar = sidebar("Left sidebar"),
    layout_sidebar(
        sidebar = sidebar("Right sidebar", position = "right", open = FALSE),
        "Main contents",
        border = FALSE
    ),
    border_radius = FALSE,
    fillable = TRUE,
    class = "p-0"
  )
)

Styling and customization

In the above sections we’ve focused primarily on the variety of interface layouts where sidebars can be used. Along the way, we’ve touched on a few of the named arguments of sidebar() and layout_sidebar() that are helpful for customizing the styling and behavior of both the sidebar and main content areas. However, there are a handful of other arguments to further customize the look and feel if the sidebar layout.

Both sidebar() and layout_sidebar() allow for a specific background color (via bg), which is applied to the sidebar area and main content area respectively. When bg is provided, bslib automatically provides a high-contrast foreground color to ensure readability (but a fg color may also be provided). Both functions also include a class argument that works well with Bootstrap utility classes and a style argument for inline styles.

Be aware that in layout_sidebar(), bg, class and style attributes are applied to the main content area’s container and not the overall layout container. To add additional classes to the layout container, use htmltools::tagAppendAttributes(). Also note that layout_sidebar() derives some of it’s default style from Bootstrap CSS variables (e.g., --bs-card-border-color), which enables theming at the component-level (theming via bs_theme() works on the page-level).

The following example combines all of these concepts to create sidebar with a dark background. Utility classes are used to make the sidebar text monospace and bold, and we used tagAppendAttributes() to tweak the border color of the sidebar layout to match the sidebar background.

library(htmltools)
library(leaflet)

squake <- SharedData$new(quakes)

container <- layout_sidebar(
  class = "p-0",
  sidebar = sidebar(
    title = "Earthquakes off Fiji",
    bg = "#1E1E1E",
    width = "35%",
    class = "fw-bold font-monospace",
    filter_slider("mag", "Magnitude", squake, ~mag)
  ),
  leaflet(squake) |> addTiles() |> addCircleMarkers()
)

tagAppendAttributes(container, style = css("--bs-card-border-color" = "#1E1E1E"))