Cards in bslib
Cards are the primary container component in modern bslib dashboards. They group related content with borders and padding, helping users digest, engage with, and navigate through information.
Table of Contents
- Core Concept
- Card Structure
- Card Components
- Height Control & Scrolling
- Full-Screen Expansion
- Filling Outputs
- Multiple card_body() Sections
- Multi-Column Layouts Within Cards
- Tabbed Cards
- Sidebar Integration
- Static Images
- Flexbox Behavior
- Shiny-Specific Features
- Best Practices
Core Concept
At their core, cards are "just an HTML div() with a special Bootstrap class." They serve as rectangular containers that visually group related information.
Basic card:
card(
card_header("My Card"),
"Card content goes here"
)
Card Structure
The card() function accepts "known" card items as unnamed arguments (children):
card_header()— top section, supports Bootstrap utility classescard_body()— main content area (often implicit)card_footer()— bottom sectioncard_image()— for embedding static imagescard_title()— styled title element
Implicit card_body()
Direct children of card() that aren't recognized card items automatically get wrapped in card_body(). These are equivalent:
# Explicit
card(
card_header("Title"),
card_body("Content")
)
# Implicit (recommended for simple cases)
card(
card_header("Title"),
"Content" # Automatically wrapped in card_body()
)
Card Components
card_header()
Top section of the card, useful for titles and controls. The header is a flex container by default, so child elements are laid out horizontally. Use Bootstrap utility classes (e.g., class = "bg-primary text-white") to style it, and the gap argument to control spacing between child elements.
With icons or buttons (flex layout):
card(
card_header(
gap = "0.5rem", # Space between header elements
"Settings",
tooltip(bsicons::bs_icon("info-circle"), "Configure options"),
popover(bsicons::bs_icon("gear"), title = "Options", selectInput("opt", "Option", ...))
),
...
)
card_body()
Main content area. Usually implicit, but useful for adding multiple body sections, controlling padding and styling, or setting min/max heights for filling behavior. Use class = "p-0" to remove padding for edge-to-edge content like maps.
card_footer()
Bottom section for metadata, actions, or links.
card(
card_header("Analysis Results"),
plotOutput("results"),
card_footer(
class = "text-muted",
"Last updated: ", textOutput("last_update", inline = TRUE)
)
)
card_title()
Styled title element that can be used within card_body() or card_header().
Height Control & Scrolling
Cards grow by default to fit their contents. Control sizing with height (fixed), min_height (floor), and max_height (ceiling) arguments. When content exceeds the card's height, scrolling is automatically enabled.
- Use
min_heightto prevent cards from becoming too small in filling layouts. - Use
max_heighton cards with potentially long scrollable content. - Use fixed
heightsparingly —min_heightis usually more flexible.
Full-Screen Expansion
Add full_screen = TRUE to enable an expand icon that shows the card in full browser window size. Enable this for all cards containing plots, maps, or detailed tables — it is highly valued by users. When expanded, max_height and height constraints are ignored.
Tracking full-screen state in Shiny: Provide an id to the card to observe its full-screen state:
card(
id = "my_card",
full_screen = TRUE,
card_header("Plot"),
plotOutput("plot")
)
# Server: input$my_card_full_screen is TRUE/FALSE
observe({
if (isTRUE(input$my_card_full_screen)) {
# Render higher-resolution plot when expanded
}
})
Combining with scrolling: max_height and full_screen = TRUE work well together — the card scrolls at normal size and expands to show all content at full screen.
Filling Outputs
Cards are optimized for filling layouts. When a fill item (like plotly, leaflet, or most htmlwidgets) is a direct child of card_body(), it resizes to match the card's specified height. Fill items by default include most htmlwidgets (plotly, leaflet, DT, etc.), plotOutput(), and imageOutput().
Example:
card(
height = 400,
full_screen = TRUE,
card_header("Interactive Map"),
card_body(
class = "p-0", # Remove padding for edge-to-edge display
leafletOutput("map")
)
)
Use min_height on card_body() to prevent excessive shrinking when multiple fill outputs share a body:
card_body(
min_height = 250,
plotlyOutput("plot1"),
plotlyOutput("plot2")
)
Multiple card_body() Sections
A single card can contain several card_body() elements, useful for combining resizable and fixed-size content. Set fill = FALSE on body sections that should maintain their natural size and not participate in filling behavior.
Example:
card(
full_screen = TRUE,
card_header("Sales Analysis"),
# Subtitle section - won't fill or scroll
card_body(
fill = FALSE,
gap = 0,
card_title("Q4 Results"),
p(class = "text-muted", "Preliminary data as of Dec 31")
),
# Plot section - fills available space
card_body(
min_height = 300,
plotlyOutput("sales_plot")
),
# Summary section - fixed size
card_body(
fill = FALSE,
verbatimTextOutput("summary_stats")
)
)
Multi-Column Layouts Within Cards
Use layout_column_wrap() for responsive multi-column arrangements inside cards:
card(
card_header("Quarterly Metrics"),
card_body(
min_height = 200,
layout_column_wrap(
width = 1/2,
plotOutput("q1"),
plotOutput("q2"),
plotOutput("q3"),
plotOutput("q4")
)
)
)
Tabbed Cards
Use navset_card_tab(), navset_card_pill(), or navset_card_underline() to create multi-tab cards:
navset_card_underline(
title = "Analysis",
full_screen = TRUE,
nav_panel("Plot", plotOutput("plot")),
nav_panel("Summary", verbatimTextOutput("summary")),
nav_panel("Data", tableOutput("data"))
)
Key features:
- The
titleargument adds a card header - Full-screen support works with tabbed cards
- Each
nav_panel()behaves like a card body — do not wrap panel content incard(); the navset already provides the card container
See navigation.md for more details on navset functions.
Sidebar Integration
layout_sidebar() works inside cards to create component-level sidebars. Set fillable = TRUE on layout_sidebar() to preserve fill behavior for outputs.
card(
full_screen = TRUE,
card_header("Customizable Plot"),
layout_sidebar(
fillable = TRUE, # Preserve fill behavior
sidebar = sidebar(
title = "Plot Options",
position = "right",
selectInput("color", "Color scheme", ...),
sliderInput("bins", "Bins", ...)
),
plotlyOutput("plot")
)
)
See sidebars.md for more sidebar patterns.
Static Images
card_image() embeds pre-generated images. Key arguments: file (path to image), alt (accessibility text), href (optional URL to make the image a clickable link), and border_radius (corner rounding).
card(
card_header("Project Logo"),
card_image(
file = "path/to/image.png",
alt = "Project logo",
href = "https://project-website.com" # Makes image clickable
),
card_body("Project description...")
)
Flexbox Behavior
Both card() and card_body() default to fillable = TRUE, making them CSS flexbox containers. This enables fill behavior but changes how inline elements render — inline tags like span() and a() appear on separate lines.
Solution for inline content: Set fillable = FALSE to restore normal inline flow:
card(
card_body(
fillable = FALSE,
"Text with ", tags$a("inline link", href = "#"), " and more text."
)
)
Flexbox Utilities
Use Bootstrap flex utility classes and the gap argument for precise layout control. For example, class = "d-flex justify-content-between" on card_header() spaces title and action elements to opposite ends, class = "d-flex align-items-center" on card_body() vertically centers content, and gap = 10 sets pixel spacing between children.
Shiny-Specific Features
Dynamic Content Based on Card Size
Use shiny::getCurrentOutputInfo() to render different content based on whether the card is expanded:
output$plot <- renderPlot({
info <- getCurrentOutputInfo()
if (info$height() > 500) {
# Full plot with labels when expanded
ggplot(data, aes(x, y)) +
geom_point() +
labs(title = "Detailed Analysis", subtitle = "With annotations")
} else {
# Simplified plot in normal view
ggplot(data, aes(x, y)) + geom_point()
}
})
This is particularly useful with full_screen = TRUE to show additional detail when the card is expanded.
Best Practices
Always Use Full-Screen for Visualizations
Enable full_screen = TRUE on cards containing:
- Plots (ggplot2, base R plots, plotly)
- Maps (leaflet, other mapping libraries)
- Tables with many rows
- Any content that benefits from more space
Use Appropriate Heights
- Set
min_heightto prevent cards from becoming too small in filling layouts - Set
max_heighton cards with potentially long scrollable content - Set fixed
heightsparingly — usuallymin_heightis more flexible
Remove Padding for Edge-to-Edge Content
Use class = "p-0" on card_body() for full-bleed maps and visualizations.
Organize Related Content
Use multiple card_body() sections to separate concerns:
card(
card_header("Analysis"),
card_body(fill = FALSE, "Introduction and context..."),
card_body(plotOutput("main_plot")),
card_body(fill = FALSE, "Key findings and conclusions...")
)
Leverage Tabbed Cards
When a card would contain multiple related outputs, use navset_card_*():
navset_card_underline(
title = "Sales Data",
full_screen = TRUE,
nav_panel("Overview", plotOutput("overview")),
nav_panel("By Region", plotOutput("by_region")),
nav_panel("By Product", plotOutput("by_product")),
nav_panel("Raw Data", tableOutput("raw_data"))
)
Test Filling Behavior
Always test your cards in:
- Different viewport sizes
- Full-screen mode
- With varying amounts of content
- On mobile devices (or use browser dev tools)