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"
)
Show code
card(
full_screen = TRUE,
card_header("Title"),
layout_sidebar(
sidebar = sidebar("Sidebar"),
"Main contents"
)
)
layout_sidebar() in card()
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()
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")
)
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]]
)
)
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 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"))