source( rprojroot::find_package_root_file("vignettes/_common.R") ) describe_layout_function <- function(name, description, element = NULL) { name <- knitr::combine_words(sprintf("<code>%s</code>", name), and = " or ") if (is.null(element)) { element <- card_body( class = "d-flex flex-column align-items-center", div(style = "width: 6em; height: 6em; background-color: gray") ) } card( card_header(class = "bg-info text-center", HTML(name)), element, card_footer(class = "bg-white text-center", description) ) }
```{scss, echo = FALSE} .plotly { height: 250px !important; .modebar-container { display: none; } }
.section { margin-top: 2rem; }
.card-header.bg-info code { background: none; color: hsl(209deg, 85%, 25%); }
::: lead 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](../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. :::: {.row .mt-3} ### 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. ::: {.col-sm-12 .col-lg-6} <details> <summary>Show code</summary> ```r layout_sidebar( sidebar = sidebar("Sidebar"), "Main contents" )
describe_layout_function( "layout_sidebar()", "A sidebar layout.", card_body( class = "p-2", height = 200, layout_sidebar( sidebar = sidebar("Sidebar", width = "33%"), "Main contents" ) |> tagAppendAttributes(class = "border rounded-top") ) )
:::
::: {.col-sm-12 .col-lg-6}
Show code
card( full_screen = TRUE, card_header("Title"), layout_sidebar( sidebar = sidebar("Sidebar"), "Main contents" ) )
describe_layout_function( "layout_sidebar() in card()", "A sidebar layout within a card.", card_body( class = "p-2", height = 200, card( full_screen = TRUE, card_header("Title"), layout_sidebar( sidebar = sidebar("Sidebar", width = "33%"), "Main contents" ) ) ) )
::: ::::
:::: {.row .mt-5}
::: {.col-sm-12 .col-lg-6}
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.
:::
::: {.col-sm-12 .col-lg-6}
Show code
page_fillable( layout_sidebar( sidebar = sidebar("Sidebar area"), "Main area" ) )
describe_layout_function( "layout_sidebar() in page_fillable()", "A sidebar that fills the page.", card_body( class = "p-0", height = 200, page_fillable( layout_sidebar( sidebar = sidebar("Sidebar", width = "33%"), "Main contents" ) ) ) |> tagAppendAttributes(class = "flex-row", .cssSelector = ".navbar-nav") |> tagAppendAttributes(class = "d-none", .cssSelector = ".navbar-header") |> tagAppendAttributes(class = "navbar-dark", .cssSelector = "nav") )
::: ::::
:::: {.row .mt-5}
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()
.
::: {.col-sm-12 .col-lg-6}
Show code
page_navbar( sidebar = sidebar("Sidebar"), nav_panel("Page 1", "Page 1 content"), nav_panel("Page 2", "Page 2 content") )
describe_layout_function( "page_navbar()", "A sidebar shared across multiple pages.", card_body( class = "p-0", style = "gap:0;", height = 200, id = "page_navbar_example_card", navset_bar( collapsible = FALSE, sidebar = sidebar("Shared sidebar", width = "33%"), nav_panel("Page 1", "Page 1 content"), nav_panel("Page 2", "Page 2 content") ), tags$script(HTML("$(document).ready(function() { // pkgdown activates headroom on the navset_bar() container and ends up // auto-hiding our navbar, so this destroys the unwanted headroom instance var demo_nav = $('#page_navbar_example_card nav.navbar'); setTimeout(function() { if (demo_nav.headroom) demo_nav.headroom('destroy') }, 100); });")) ) |> tagAppendAttributes(class = "flex-row", .cssSelector = ".navbar-nav") |> tagAppendAttributes(class = "d-none", .cssSelector = ".navbar-header") |> tagAppendAttributes(class = "navbar-dark", .cssSelector = "nav") )
:::
::: {.col-sm-12 .col-lg-6}
Show code
navset_card_tab( sidebar = sidebar("Sidebar"), nav_panel("Tab 1", "Tab 1 content"), nav_panel("Tab 2", "Tab 2 content") )
describe_layout_function( "navset_card_tab()", "A sidebar shared across multiple tabs.", card_body( class = "p-2", height = 200, navset_card_tab( title = "Tab Card", sidebar = sidebar("Shared sidebar", width = "33%"), nav_panel("Tab 1", "Tab 1 content"), nav_panel("Tab 2", "Tab 2 content") ) ) )
::: ::::
Now that we've enumerated bslib's sidebar layout options, lets use some real data[^real-data] to create some real inputs and outputs, and explore some additional features of sidebar layouts.
In a Shiny app[^shiny-id], 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.
[^real-data]: Our "real data" is just a 1,000 rows randomly sampled from {ggplot2}
's diamonds
data as well as {leaflet}
's quakes
.
[^shiny-id]: In a Shiny app, we also recommend you add an id
to the sidebar()
so that you can reactively read/update whether the sidebar is open/closed.
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()
layout_sidebar()
{#hello-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 ) )
:::: {.row .my-3} ::: {.col-sm-12 .col-lg-6} ::: {.callout .callout-note}
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
).
:::
:::
::: {.col-sm-12 .col-lg-6 .mt-auto .mb-auto} ::: {.callout .callout-note}
To learn more about how fillable containers and fill items work, see the article on filling layouts. ::: ::: ::::
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 ) ) )
::: {.callout .callout-note}
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.
:::
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) ) )
Although sidebars work just fine outside Shiny, using them in Shiny provides a few additional useful features.
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.[^4] 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()
:
[^4]: If the controls depend on some other application state, you'll need to use uiOutput()
to fill the contents of a sidebar()
).
:::: row ::: {.col-sm-12 .col-lg-6}
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 } )
:::
::: {.col-sm-12 .col-lg-6 .mt-auto .mb-auto}
page_navbar( title = "Conditional sidebar", id = "nav", collapsible = FALSE, sidebar = sidebar( open = TRUE, 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"), footer = htmltools::tags$script(htmltools::HTML(" var $page1,$page2; $(document).ready(function() { $page1 = $(`[data-display-if*='Page 1']`); $page2 = $(`[data-display-if*='Page 2']`); $page2.hide(); }); $('#nav').on('shown.bs.tab', function(ev) { const page = ev.target.dataset.value; page === 'Page 2' ? $page1.hide() : $page1.show(); page === 'Page 1' ? $page2.hide() : $page2.show(); }); " )) )
::: ::::
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.
:::: row ::: {.col-sm-12 .col-lg-6}
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)
:::
::: {.col-sm-12 .col-lg-6 .mt-auto .mb-auto}
page_navbar( title = "Sidebar updates", id = "nav", collapsible = FALSE, 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."), footer = htmltools::tags$script(htmltools::HTML(" $('#nav').on('shown.bs.tab', function(ev) { const page = ev.target.dataset.value; const layout = document.getElementById('sidebar').parentElement; const open = page === 'Page 2' ? 'open' : 'close'; bslib.Sidebar.getInstance(layout).toggle(open); });" )) )
::: ::::
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.
::: {.callout .callout-note}
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]] ) )
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" ) )
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 class
es 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"))
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.