shiny-bslib
Build modern Shiny dashboards and applications using bslib (Bootstrap 5). Use when creating new Shiny apps, modernizing legacy apps (fluidPage, fluidRow/column, tabsetPanel, wellPanel, shinythemes), or working with bslib page layouts, grid systems, cards, value boxes, navigation,
Install
npx skills add https://github.com/posit-dev/skills/tree/main/shiny/shiny-bslib
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install posit-dev-skills@llmmart
git clone https://github.com/posit-dev/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole posit-dev/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Modern Shiny Apps with bslib
Build professional Shiny dashboards using bslib's Bootstrap 5 components and layouts. This skill focuses on modern UI/UX patterns that replace legacy Shiny approaches.
Quick Start
Single-page dashboard:
library(shiny)
library(bslib)
ui <- page_sidebar(
title = "My Dashboard",
theme = bs_theme(version = 5), # "shiny" preset by default
sidebar = sidebar(
selectInput("variable", "Variable", choices = names(mtcars))
),
layout_column_wrap(
width = 1/3,
fill = FALSE,
value_box(title = "Users", value = "1,234", theme = "primary"),
value_box(title = "Revenue", value = "$56K", theme = "success"),
value_box(title = "Growth", value = "+18%", theme = "info")
),
card(
full_screen = TRUE,
card_header("Plot"),
plotOutput("plot")
)
)
server <- function(input, output, session) {
output$plot <- renderPlot({
hist(mtcars[[input$variable]], main = input$variable)
})
}
shinyApp(ui, server)
Multi-page dashboard:
ui <- page_navbar(
title = "Analytics Platform",
theme = bs_theme(version = 5),
nav_panel("Overview", overview_ui),
nav_panel("Analysis", analysis_ui),
nav_panel("Reports", reports_ui)
)
Core Concepts
Page Layouts
page_sidebar()-- Single-page dashboard with sidebar (most common)page_navbar()-- Multi-page app with top navigation barpage_fillable()-- Viewport-filling layout for custom arrangementspage_fluid()-- Scrolling layout for long-form content
See page-layouts.md for detailed guidance.
Grid Systems
layout_column_wrap()-- Uniform grid with auto-wrapping (recommended for most cases)layout_columns()-- 12-column Bootstrap grid with precise control
See grid-layouts.md for detailed guidance.
Cards
Primary container for dashboard content. Support headers, footers, multiple body sections, and full-screen expansion.
See cards.md for detailed guidance.
Value Boxes
Display key metrics and KPIs with optional icons, sparklines, and built-in theming.
See value-boxes.md for detailed guidance.
Navigation
- Page-level:
page_navbar()for multi-page apps - Component-level:
navset_card_underline(),navset_tab(),navset_pill()for tabbed content
See navigation.md for detailed guidance.
Sidebars
- Page-level:
page_sidebar()orpage_navbar(sidebar = ...) - Component-level:
layout_sidebar()within cards - Supports conditional content, dynamic open/close, accordions
resizable = TRUEby default — users can drag the edge to resize on desktop
See sidebars.md for detailed guidance.
Filling Layouts
The fill system controls how components resize to fill available space. Key concepts: fillable containers, fill items, fill carriers. Fill activates when containers have defined heights.
See filling.md for detailed guidance.
Theming
bs_theme()with Bootswatch themes for quick styling- Custom colors:
bg,fg,primaryaffect hundreds of CSS rules - Fonts:
font_google()for typography - Dynamic theming:
input_dark_mode()+session$setCurrentTheme()
See theming.md for detailed guidance.
UI Components
- Accordions -- Collapsible sections, especially useful in sidebars
- Tooltips -- Hover-triggered help text
- Popovers -- Click-triggered containers for secondary UI/inputs
- Toasts -- Temporary notification messages
- Toolbars -- Compact horizontal strips of buttons, selects, and dividers for card headers and footers
See accordions.md, tooltips-popovers.md, toasts.md, and toolbars.md.
Icons
Recommended: bsicons package (Bootstrap Icons, designed for bslib):
bsicons::bs_icon("graph-up")
bsicons::bs_icon("people", size = "2em")
Browse icons: https://icons.getbootstrap.com/
Alternative: fontawesome package:
fontawesome::fa("envelope")
Accessibility for icon-only triggers: When an icon is used as the sole trigger for a tooltip, popover, or similar interactive element (no accompanying text), it must be accessible to screen readers. By default, icon packages mark icons as decorative (aria-hidden="true"), which hides them from assistive technology.
bsicons::bs_icon(): Providetitle— this automatically setsa11y = "sem"tooltip( bs_icon("info-circle", title = "More information"), "Tooltip content here" )fontawesome::fa(): Seta11y = "sem"and providetitletooltip( fa("circle-info", a11y = "sem", title = "More information"), "Tooltip content here" )
The title should describe the purpose of the trigger (e.g., "More information", "Settings"), not the icon itself (e.g., not "info circle icon").
Special Inputs
input_switch()-- Toggle switch (modern checkbox alternative)input_dark_mode()-- Dark mode toggleinput_task_button()-- Button for long-running operationsinput_code_editor()-- Code editor with syntax highlightinginput_submit_textarea()-- Textarea with explicit submission
See inputs.md for detailed guidance.
Common Workflows
Building a Dashboard
- Choose page layout:
page_sidebar()(single-page) orpage_navbar()(multi-page) - Add theme with
bs_theme()(consider Bootswatch for quick start) - Create sidebar with inputs for filtering/controls
- Add value boxes at top for key metrics (set
fill = FALSEon container) - Arrange cards with
layout_column_wrap()orlayout_columns() - Enable
full_screen = TRUEon all visualization cards - Add
thematic::thematic_shiny()for plot theming
Modernizing an Existing App
See migration.md for a complete mapping of legacy patterns to modern equivalents. Key steps:
- Replace
fluidPage()withpage_sidebar()orpage_navbar() - Replace
fluidRow()/column()withlayout_columns() - Wrap outputs in
card(full_screen = TRUE) - Add
theme = bs_theme(version = 5) - Convert key metrics to
value_box()components - Replace
tabsetPanel()withnavset_card_underline()
Guidelines
- Prefer bslib page functions (
page_sidebar(),page_navbar(),page_fillable(),page_fluid()) over legacy equivalents (fluidPage(),navbarPage()) - Use
layout_column_wrap()orlayout_columns()for grid layouts instead offluidRow()/column(), which don't support filling layouts - Wrap outputs in
card(full_screen = TRUE)when building dashboards -- full-screen expansion is a high-value feature - Set
fill = FALSEonlayout_column_wrap()containers holding value boxes (they shouldn't stretch to fill height) - Pin Bootstrap version: include
theme = bs_theme(version = 5)or a preset theme - Use
thematic::thematic_shiny()in the server so base R and ggplot2 plots match the app theme - Use responsive widths like
width = "250px"inlayout_column_wrap()for auto-adjusting columns - Group sidebar inputs with
accordion()when sidebars have many controls - See migration.md for mapping legacy Shiny patterns to modern bslib equivalents
Avoid Common Errors
- Avoid directly nesting
card()containers.navset_card_*()functions are already cards;nav_panel()content goes directly inside them without wrapping incard() - Only use
layout_columns()andlayout_column_wrap()for laying out multiple elements. Single children should be passed directly to their container functions. - Never nest
page_*()functions. Only use one top-level page function per app.
Reference Files
- migration.md -- Legacy Shiny to modern bslib migration guide
- page-layouts.md -- Page-level layout functions and patterns
- grid-layouts.md -- Multi-column grid systems
- cards.md -- Card components and features
- value-boxes.md -- Value boxes for metrics and KPIs
- navigation.md -- Navigation containers and patterns
- sidebars.md -- Sidebar layouts and organization
- filling.md -- Fillable containers and fill items
- theming.md -- Basic theming (colors, fonts, Bootswatch). See shiny-bslib-theming skill for advanced theming
- accordions.md -- Collapsible sections and sidebar organization
- tooltips-popovers.md -- Hover tooltips and click-triggered popovers
- toasts.md -- Temporary notification messages
- toolbars.md -- Toolbar components for card headers and footers
- inputs.md -- Special bslib input widgets
- best-practices.md -- bslib-specific patterns and common gotchas
Files (skills)
-
references
-
accordions.md 3.4 KB
# Accordions in bslib Accordions provide collapsible sections for organizing content vertically. Especially useful for grouping inputs in sidebars and providing progressive disclosure. ## Table of Contents - [Basic Usage](#basic-usage) - [In Sidebars](#accordions-in-sidebars) - [Dynamic Control](#dynamic-accordion-control) - [Best Practices](#best-practices) ## Basic Usage Use `accordion()` and `accordion_panel()` to create collapsible sections. Pass `icon` to `accordion_panel()` to add a leading icon. Use `open` to specify which panels start open (by title), and `multiple = TRUE` to allow more than one panel open at a time (default is `FALSE`). ```r accordion( id = "acc", open = c("Visualizations"), multiple = TRUE, accordion_panel( icon = bsicons::bs_icon("graph-up"), title = "Visualizations", plotOutput("plot") ), accordion_panel( icon = bsicons::bs_icon("table"), title = "Data Table", tableOutput("table") ) ) ``` ## Accordions in Sidebars When an `accordion()` appears as an immediate child of `sidebar()`, panels render flush to the sidebar for clean organization: ```r page_sidebar( sidebar = sidebar( title = "Controls", accordion( accordion_panel( "Data Filters", selectInput("species", "Species", ...), selectInput("island", "Island", ...), dateRangeInput("dates", "Date range", ...) ), accordion_panel( "Plot Options", selectInput("color", "Color by", ...), checkboxInput("facet", "Facet by species"), sliderInput("alpha", "Transparency", ...) ), accordion_panel( "Advanced Settings", checkboxInput("show_outliers", "Show outliers"), numericInput("smooth_span", "Smoothing span", ...), selectInput("theme", "ggplot2 theme", ...) ) ) ), card(plotOutput("plot")) ) ``` **Benefits:** - Groups related inputs - Reduces sidebar scrolling - Helps users focus on relevant controls - Provides clear organizational structure **Gotcha:** Accordion must be an immediate child of `sidebar()` for flush rendering. Wrapping it in another element adds extra padding. ## Dynamic Accordion Control Programmatically control accordion state (requires `id` on the accordion): - `accordion_panel_open("acc", "Panel Title")` — opens a panel - `accordion_panel_close("acc", "Panel Title")` — closes a panel - `accordion_panel_set("acc", c("Panel 1"))` — sets exactly which panels are open - `accordion_panel_remove("acc", "Panel Title")` — removes a panel - `accordion_panel_update("acc", "Panel Title", "New content")` — replaces panel body content **Insert a new panel** at a specific position: ```r observeEvent(input$add_panel, { accordion_panel_insert( "acc", accordion_panel("New Panel", "Dynamic content"), target = "Panel 2", position = "after" ) }) ``` ## Best Practices **Group logically:** - Related inputs in same panel - Order by importance or workflow - 3-6 panels is ideal; more than 8 becomes unwieldy **Set appropriate initial state:** Open the most important panel by default, and leave secondary or advanced panels closed: ```r accordion( open = "Essential Filters", accordion_panel("Essential Filters", ...), accordion_panel("Advanced Filters", ...), accordion_panel("Export Options", ...) ) ``` **Accessibility:** Keyboard navigation (arrow keys, Enter) and ARIA attributes are automatic. -
best-practices.md 11.1 KB
# bslib Best Practices and Common Gotchas This reference covers bslib-specific layout patterns, UX tips, and common pitfalls. For general Shiny best practices (reactive expressions, modules, deployment), refer to standard Shiny documentation. ## Table of Contents - [Layout Patterns](#layout-patterns) - [Mobile and Responsive Design](#mobile-and-responsive-design) - [User Experience with bslib Components](#user-experience-with-bslib-components) - [Add Contextual Help](#add-contextual-help) - [Card Header Controls with Toolbars](#card-header-controls-with-toolbars) - [bslib Module Patterns](#bslib-module-patterns) - [Common Gotchas](#common-gotchas) ## Layout Patterns ### Dashboard Header with KPIs Value boxes at top, detailed content below: ```r page_sidebar( class = "bslib-page-dashboard", # Light gray background; looks best with cards sidebar = sidebar(...), # KPIs at top - don't fill layout_column_wrap( width = 1/4, fill = FALSE, value_box(title = "Revenue", value = "$125K", theme = "success"), value_box(title = "Users", value = "1,234", theme = "primary"), value_box(title = "Growth", value = "+18%", theme = "info"), value_box(title = "Churn", value = "2.3%", theme = "warning") ), # Main content fills remaining space layout_columns( col_widths = c(8, 4), card(full_screen = TRUE, card_header("Trend"), plotOutput("trend")), card(card_header("Breakdown"), plotOutput("breakdown")) ) ) ``` ### Component-Level Controls When controls are specific to one visualization, use `layout_sidebar()` within the card: ```r card( full_screen = TRUE, card_header("Customizable Plot"), layout_sidebar( fillable = TRUE, sidebar = sidebar( position = "right", width = "200px", selectInput("color_by", "Color by", ...), sliderInput("alpha", "Transparency", ...) ), plotOutput("plot") ) ) ``` ### Tabbed Content Organization Use navset cards to organize related outputs: ```r navset_card_underline( title = "Sales Analysis", 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")) ) ``` ### Multi-Page with Page-Specific Sidebars Use `layout_sidebar()` within individual pages instead of `page_navbar(sidebar = ...)`: ```r page_navbar( title = "App", nav_panel( "Analysis", layout_sidebar( sidebar = sidebar("Analysis controls"), card(plotOutput("analysis_plot")) ) ), nav_panel( "Comparison", layout_sidebar( sidebar = sidebar("Comparison controls"), card(plotOutput("comparison_plot")) ) ) ) ``` ### Scrolling Page with Card Heights When content requires scrolling rather than filling: ```r page_sidebar( fillable = FALSE, # Enable scrolling sidebar = sidebar(...), card( height = 400, full_screen = TRUE, card_header("Plot 1"), plotOutput("plot1") ), card( height = 400, full_screen = TRUE, card_header("Plot 2"), plotOutput("plot2") ) ) ``` ## Mobile and Responsive Design ### Responsive Column Widths ```r # Auto-adjusting columns based on viewport layout_column_wrap( width = "250px", card(...), card(...), card(...) ) # Explicit breakpoints layout_columns( col_widths = breakpoints( sm = 12, # Stack on mobile md = c(6, 6), # Two columns on tablet lg = c(4, 4, 4) # Three columns on desktop ), card(...), card(...), card(...) ) ``` ### Mobile-Friendly Designs * Set `min_height` on cards (e.g., `min_height = 300`) to prevent them from becoming too small on narrow viewports. * By default, filling is disabled on mobile. Enable `fillable_mobile = TRUE` on the page function only after thorough testing on actual mobile devices. ## User Experience with bslib Components ### Add Contextual Help Place a `tooltip()` or `popover()` in `card_header()` to provide inline help. Use `tooltip()` for a brief one-line description and `popover()` when secondary controls or longer content are needed: ```r card_header( "Revenue", tooltip( bsicons::bs_icon("info-circle"), "Total revenue from all sources" ) ) card_header( "Plot", popover( bsicons::bs_icon("gear"), title = "Advanced Options", selectInput("option1", "Option 1", ...), checkboxInput("option2", "Option 2") ) ) ``` ### Card Header Controls with Toolbars Use `toolbar()` in `card_header()` when a card needs more than one control, or when controls should look compact rather than full-width. The toolbar sits flush with the header's right edge by default. **Multiple controls on one card:** ```r card( card_header( "Sales Trend", toolbar( toolbar_input_select("period", "Period", choices = c("Daily", "Weekly", "Monthly"), selected = "Monthly" ), toolbar_divider(), toolbar_input_button("download", "Download", icon = bsicons::bs_icon("download") ) ) ), plotOutput("trend_plot") ) ``` **Adding an info icon to an input label** — wrap the label text and a `tooltip()` together in a `toolbar()`: ```r selectInput( "metric", label = toolbar( align = "left", gap = "0.25rem", "Metric", tooltip( bsicons::bs_icon("info-circle", title = "About this metric"), "Revenue includes all recognized sales, net of returns and discounts." ) ), choices = c("Revenue", "Units", "Margin") ) ``` This pattern works with any Shiny input that takes an HTML `label`. The `title` on `bs_icon()` provides accessible text for screen readers (see the Icons section in the main skill). **When to use toolbar vs. popover in card headers:** - Use `toolbar()` when you have **multiple controls** (select + button, or multiple buttons) — toolbars keep them properly spaced and aligned - Use a single `tooltip()` or `popover()` directly in `card_header()` for a **single info/settings icon** — no toolbar needed for one element See [toolbars.md](toolbars.md) for the full toolbar API. ### Show Loading States ```r input_task_button("process", "Process Data") # Automatically shows loading state and prevents duplicate clicks ``` ### Toast Notifications for Feedback ```r observeEvent(input$save, { save_data(data()) show_toast( toast("Data saved successfully", header = "Success", type = "success") ) }) ``` ### Handle Empty States with bslib Components ```r output$empty_message <- renderUI({ if (nrow(filtered_data()) == 0) { card( card_body( class = "text-center text-muted", bsicons::bs_icon("inbox", size = "3em"), tags$p("No data matches the selected filters"), tags$p("Try adjusting your filter criteria") ) ) } }) ``` ## bslib Module Patterns When creating Shiny modules with bslib, wrap module UI in cards: ```r plot_module_ui <- function(id) { ns <- NS(id) card( full_screen = TRUE, card_header("Plot"), layout_sidebar( sidebar = sidebar( selectInput(ns("color"), "Color by", ...) ), plotOutput(ns("plot")) ) ) } ``` Extract theme configuration into a helper: ```r # theme.R app_theme <- function() { bs_theme( version = 5, preset = "flatly", primary = "#2c3e50", base_font = font_google("Lato") ) |> bs_add_rules(sass::sass_file("www/custom.scss")) } ``` Create reusable metric helpers: ```r metric_card <- function(title, value, theme = "primary") { value_box( title = title, value = value, theme = theme, showcase = bsicons::bs_icon("graph-up") ) } ``` ## Common Gotchas ### Unnecessary Card Nesting **Problem:** Wrapping content in `card()` inside a context that already provides a card container. **Cause:** `navset_card_*()` functions (`navset_card_underline()`, `navset_card_tab()`, `navset_card_pill()`) are already cards. Each `nav_panel()` inside them behaves like a card body. Adding `card()` inside a `nav_panel()` creates a card-within-a-card. ```r # Wrong: double-nested card navset_card_underline( nav_panel("Plot", card(plotOutput("plot"))) ) # Right: content goes directly in nav_panel navset_card_underline( nav_panel("Plot", plotOutput("plot")) ) ``` More broadly, not every piece of content needs a card. Cards are for grouping and visually separating content at the dashboard level — content that's already inside a card context doesn't need another one. ### Fill Chain Breaks **Problem:** Output doesn't fill despite being in a filling layout. **Cause:** A non-fill-carrier element (like `div()`) breaks the fill chain. ```r # Broken card_body( div(plotOutput("plot")) # div breaks fill chain ) # Fixed card_body( as_fill_carrier( div(plotOutput("plot")) ) ) ``` ### Value Boxes Expanding Too Much **Problem:** Value boxes take up too much vertical space in filling layouts. **Solution:** Set `fill = FALSE` on layout container. ```r layout_column_wrap( width = 1/3, fill = FALSE, # Important! value_box(...), value_box(...), value_box(...) ) ``` ### Sidebar on Every Page **Problem:** Used `page_navbar(sidebar = ...)` but need different sidebars per page. **Solution:** Use `layout_sidebar()` within individual `nav_panel()` elements. ### Accordion in Sidebar Not Flush **Problem:** Accordion has extra padding in sidebar. **Cause:** Accordion is not an immediate child of `sidebar()`. **Solution:** Place accordion directly in sidebar (not nested in another wrapper). ### fluidRow/column Doesn't Fill **Problem:** Used `fluidRow()`/`column()` in filling layout. **Cause:** These legacy functions are incompatible with the bslib fill system. **Solution:** Use `layout_columns()` instead. See [migration.md](migration.md). ### Plotly Doesn't Resize **Problem:** Plotly plot doesn't resize in card. **Solution:** Ensure card has a defined height (from page filling or explicit `height`): ```r card( height = 400, card_body( plotlyOutput("plot") # Already a fill item by default ) ) ``` ### Dark Mode Doesn't Affect Plots **Problem:** Switched to dark mode but plots still use light colors. **Cause:** `bs_theme()` only controls CSS; `renderPlot()` generates images server-side. **Solution:** Use the `thematic` package: ```r # At top of server function thematic::thematic_shiny() ``` ### Custom CSS Overriding Theme **Problem:** Custom CSS uses hardcoded colors that don't adapt to theme changes. **Solution:** Use `bs_add_rules()` with Sass variables instead of raw CSS: ```r theme <- bs_theme(...) |> bs_add_rules(" .custom-element { background: $bg; color: $fg; border-color: $primary; } ") ``` ### uiOutput Breaks Fill **Problem:** Dynamic UI via `uiOutput()` breaks filling behavior. **Cause:** `uiOutput()` wraps content in an extra element that isn't a fill carrier. **Solution:** Wrap it as a fill carrier: ```r card_body( as_fill_carrier( uiOutput("dynamic_plot") ) ) ``` ### DT DataTable Doesn't Fill **Problem:** DataTable doesn't resize to fill card. **Solution:** Set `fillContainer = TRUE` in `datatable()`: ```r output$table <- DT::renderDataTable({ DT::datatable( data, fillContainer = TRUE, options = list(scrollY = "300px") ) }) ``` -
cards.md 10.7 KB
# 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](#core-concept) - [Card Structure](#card-structure) - [Card Components](#card-components) - [Height Control & Scrolling](#height-control--scrolling) - [Full-Screen Expansion](#full-screen-expansion) - [Filling Outputs](#filling-outputs) - [Multiple card_body() Sections](#multiple-card_body-sections) - [Multi-Column Layouts Within Cards](#multi-column-layouts-within-cards) - [Tabbed Cards](#tabbed-cards) - [Sidebar Integration](#sidebar-integration) - [Static Images](#static-images) - [Flexbox Behavior](#flexbox-behavior) - [Shiny-Specific Features](#shiny-specific-features) - [Best Practices](#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:** ```r 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 classes - **`card_body()`** — main content area (often implicit) - **`card_footer()`** — bottom section - **`card_image()`** — for embedding static images - **`card_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: ```r # 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):** ```r 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. ```r 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_height` to prevent cards from becoming too small in filling layouts. - Use `max_height` on cards with potentially long scrollable content. - Use fixed `height` sparingly — `min_height` is 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: ```r 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:** ```r 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: ```r 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:** ```r 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: ```r 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: ```r 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 `title` argument adds a card header - Full-screen support works with tabbed cards - Each `nav_panel()` behaves like a card body — do **not** wrap panel content in `card()`; the navset already provides the card container See [navigation.md](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. ```r 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](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). ```r 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: ```r 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: ```r 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_height` to prevent cards from becoming too small in filling layouts - Set `max_height` on cards with potentially long scrollable content - Set fixed `height` sparingly — usually `min_height` is 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: ```r 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_*()`: ```r 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) -
filling.md 9.8 KB
# Filling Layouts in bslib Understanding fillable containers and fill items is crucial for creating modern, responsive bslib dashboards. This reference explains how the fill system works and when to use it. ## Table of Contents - [Core Concepts](#core-concepts) - [How Fill Activation Works](#how-fill-activation-works) - [Fill Carriers](#fill-carriers) - [Key Components and Their Fill Behavior](#key-components-and-their-fill-behavior) - [When Filling May Not Be Desired](#when-filling-may-not-be-desired) - [Scrolling vs Filling](#scrolling-vs-filling) - [Special Cases](#special-cases) - [Troubleshooting Fill Issues](#troubleshooting-fill-issues) - [Best Practices](#best-practices) ## Core Concepts **Fillable container:** A CSS flexbox container (`flex-direction: column`) that can make its children grow or shrink. **Fill item:** A child element with `flex: 1` that can grow or shrink to match its parent's height. **Fill carrier:** An element that is both a fill item AND a fillable container, allowing fill behavior to propagate through the UI hierarchy. ### Technical Implementation Technically, a fillable container is a `div()` with: ```css display: flex; flex-direction: column; ``` A fill item has: ```css flex: 1; ``` This CSS flexbox system enables dynamic resizing. ## How Fill Activation Works ### The Key Rule **Fill only activates when the container has a defined height.** By default, a fillable container's height depends on its children's heights (normal HTML behavior). Fill behavior activates when you constrain the container's height. **Example without height constraint:** ```r page_fluid( # No height constraint plotOutput("plot") # Uses default 400px height ) ``` **Example with height constraint:** ```r page_fillable( # Height set to viewport plotOutput("plot") # Fills available space ) ``` ### Multiple Fill Items When multiple fill items share a fillable container, they divide the available space equally. Non-fill items keep their natural size; fill items divide whatever space remains. **Warning:** If non-fill items are larger than the container, fill items won't be visible: ```r card( height = 300, card_body(fill = FALSE, lorem::ipsum(paragraphs = 10)), # 500px content card_body(plotOutput("plot")) # Not visible! ) ``` ## Fill Carriers ### Parent-Child Relationship Fill items require their **immediate parent** to be a fillable container. Non-fill elements between a fillable container and fill item break the chain: ```r card( height = 400, card_body( # This div() is not a fill carrier div( plotOutput("plot") # Won't fill because parent div isn't fillable ) ) ) ``` ### The Solution A **fill carrier** is both a fill item and a fillable container, preserving the fill chain. Use `as_fill_carrier()` to promote any element: ```r card( height = 400, card_body( as_fill_carrier( div( plotOutput("plot") # Now fills properly ) ) ) ) ``` `card_body()` is a fill carrier by default (both `fillable = TRUE` and `fill = TRUE`). ## Key Components and Their Fill Behavior | Component | Fillable | Fill item | Notes | |---|---|---|---| | `page_fillable()` | Yes | N/A | Sets height to viewport; `fillable_mobile = FALSE` by default | | `card()` | Yes | Yes | Fill carrier by default | | `card_body()` | Yes | Yes | Fill carrier by default | | `layout_columns()` | Yes | Yes | Each column wrapped in a fillable container | | `layout_column_wrap()` | Context-dependent | Yes | Children can be fill items | | `layout_sidebar()` | Main area yes | Yes | Set `fillable = TRUE` to ensure main area is fillable | | `value_box()` | Context-dependent | Yes | Maintains equal height in multi-column layouts | ## When Filling May Not Be Desired ### Flexbox Side Effects Fillable containers use CSS flexbox, which changes child rendering: - Inline elements appear on separate lines - Normal flow is disrupted Use `fillable = FALSE` when needed: ```r card_body( fillable = FALSE, "Text with ", tags$a("inline link"), " and more text." ) ``` ### Value Boxes in Filling Layouts Value boxes shouldn't expand to fill the entire page. Use `fill = FALSE` on the wrapping layout container so value boxes keep a natural height and the remaining space goes to other fill items: ```r page_fillable( layout_column_wrap( width = 1/3, fill = FALSE, # Important! value_box(title = "KPI 1", value = "123"), value_box(title = "KPI 2", value = "456"), value_box(title = "KPI 3", value = "789") ), card(plotOutput("main_plot")) # Fills remaining space ) ``` ### Disabling Filling for Scrolling Switch from `page_fillable()` to `page_fluid()` or `page_fixed()` to get a scrolling page with natural content heights. Even without page-level filling, cards with `full_screen = TRUE` still fill when expanded. ## Scrolling vs Filling **Filling layout** (`page_fillable()`): content adapts to viewport size with no page scroll — professional dashboard feel, but requires careful height management. **Scrolling layout** (`page_fluid()` / `page_fixed()`): content uses natural heights and the page scrolls — simpler to implement and better for long-form content. ### Hybrid Approach Use `fillable = FALSE` on the page but explicit heights on individual cards so each card can still use `full_screen = TRUE` filling: ```r page_sidebar( fillable = FALSE, # Page scrolls sidebar = sidebar("Controls"), card( height = 400, full_screen = TRUE, card_header("Plot 1"), plotlyOutput("plot1") ), card( height = 400, full_screen = TRUE, card_header("Plot 2"), plotlyOutput("plot2") ) ) ``` ## Special Cases ### Dynamic UI (uiOutput) `uiOutput()` wraps content in an extra element, breaking the fill chain. Mark it as a fill carrier: ```r card_body( as_fill_carrier( uiOutput("dynamic_plot") ) ) # Server output$dynamic_plot <- renderUI({ plotOutput("plot", height = "100%") }) ``` ### DT DataTables DataTables require explicit configuration to participate in filling: ```r output$table <- DT::renderDataTable({ DT::datatable( data, fillContainer = TRUE, # Required! options = list(scrollY = "300px") ) }) ``` ### htmlwidgets Most htmlwidgets are fill items by default. Use `remove_all_fill()` to opt a widget out, or `as_fill_item()` to explicitly opt a custom widget in. ### fluidRow() and column() The traditional Shiny grid system is mostly incompatible with filling layout due to Bootstrap's flexbox grid. Prefer `layout_columns()` instead: ```r # Avoid page_fillable( fluidRow( column(6, plotOutput("plot1")), column(6, plotOutput("plot2")) ) ) # Prefer page_fillable( layout_columns( col_widths = c(6, 6), plotOutput("plot1"), plotOutput("plot2") ) ) ``` ## Troubleshooting Fill Issues ### Output Not Filling **Symptoms:** Output stays at default height despite being in a filling layout. **Common causes and solutions:** 1. **Container has no defined height** — add an explicit `height` to the card or use `page_fillable()`. 2. **Broken fill chain** — wrap the intermediate element with `as_fill_carrier()`. 3. **Output isn't a fill item** — mark it with `as_fill_item()`. ### Output Too Small **Symptoms:** Fill item shrinks below usable size. Set `min_height` on the containing `card_body()` to prevent shrinking too small. Similarly, `max_height` enables scrolling when content exceeds a threshold. ### Multiple Outputs Not Dividing Space **Symptoms:** Only one output visible or unequal spacing. Ensure all outputs are fill items inside the same fillable container (e.g., directly inside one `card_body()`). Multiple `plotOutput()` calls in a single `card_body()` will divide space equally. ### Full-Screen Mode Not Working **Symptoms:** Full-screen button doesn't appear or content doesn't fill the expanded card. Ensure the card contains fill items (e.g., `plotlyOutput()`, `plotOutput()`) and that `full_screen = TRUE` is set on the `card()`. ## Best Practices ### Use Filling for Dashboards `page_fillable()` creates professional dashboards where content adapts to the viewport. A typical pattern combines a fixed-height KPI row with filling plot cards: ```r page_fillable( layout_columns( col_widths = c(12, 4, 8), layout_column_wrap( width = 1/3, fill = FALSE, value_box(...), value_box(...), value_box(...) ), card(...), card(plotlyOutput("main")) ) ) ``` ### Set Appropriate Heights Use `height` for a fixed size, `min_height` to prevent shrinking too small, and `max_height` to cap growth and enable scrolling beyond that point. ### Use page_fillable() for Single-Page Apps Best for dashboards, data exploration apps, and apps where all content should be visible without scrolling. ### Use page_fluid() for Long-Form Content Best for reports, documentation, apps with extensive text, or when natural vertical scrolling is preferred. ### Combine Approaches Use `page_navbar()` with a `fillable` vector to enable filling on specific tabs only: ```r page_navbar( title = "App", fillable = c("Dashboard"), # Only "Dashboard" page fills nav_panel("Dashboard", layout_columns(...) ), nav_panel("Details", card(...), card(...), card(...) # Scrolling layout ) ) ``` ### Preserve Fill with layout_sidebar() When using sidebars inside fillable containers, set `fillable = TRUE`: ```r card( height = 400, layout_sidebar( fillable = TRUE, # Important! sidebar = sidebar(...), plotOutput("plot") ) ) ``` ### Be Mindful of Fill Carriers When wrapping outputs in custom `div()` elements, use `as_fill_carrier()` on the wrapper; otherwise the fill chain is broken and the output won't resize. ### Document Fill Behavior When creating custom components, document whether they are fillable containers, fill items, fill carriers, or none of the above, so other developers can use them correctly in filling layouts. -
grid-layouts.md 7.6 KB
# Grid Layouts in bslib This reference covers the multi-column layout systems in bslib for arranging cards, value boxes, and other UI elements in responsive grid patterns. ## Table of Contents - [layout_column_wrap()](#layout_column_wrap) - [Fixed Number of Columns](#fixed-number-of-columns) - [Responsive Columns](#responsive-columns) - [Height Control](#height-control) - [Varying Column Widths](#varying-column-widths) - [Nested Layouts](#nested-layouts) - [layout_columns()](#layout_columns) - [Basic Grid System](#basic-grid-system) - [Row Heights](#row-heights) - [Negative Space](#negative-space) - [Responsive Layouts](#responsive-layouts) - [Choosing Between layout_column_wrap() and layout_columns()](#choosing-between-layout_column_wrap-and-layout_columns) ## layout_column_wrap() **Purpose:** Creates grid-based layouts optimized for displaying multiple UI elements with uniform sizing. Provides a simplified interface to CSS Grid. **Recommended use:** Most common for arranging cards and value boxes with consistent sizing. Easier and provides a cleaner uniform look than `layout_columns()`. ### Fixed Number of Columns Use `width = 1/n` where n is the desired column count. **Example - 2 columns:** ```r layout_column_wrap( width = 1/2, height = 300, card(...), card(...), card(...) # Wraps to new row ) ``` **Example - 3 columns:** ```r layout_column_wrap( width = 1/3, value_box(title = "Users", value = "1,234"), value_box(title = "Revenue", value = "$56K"), value_box(title = "Growth", value = "+12%") ) ``` **Important:** Do NOT use percent-based widths like `"50%"` instead of `1/2`. Percentages won't produce expected results. **Drawback:** On medium-sized screens, card width may become too small with fixed column counts. ### Responsive Columns Provide a CSS unit (e.g., `"200px"`, `"300px"`) to make column count adapt to viewport size. Cards equally distribute free space on wider screens and wrap when space is insufficient. **Example:** ```r layout_column_wrap( width = "250px", height = 300, card(...), card(...), card(...), card(...) ) ``` This creates as many columns as will fit given the minimum 250px width, automatically adjusting from 4 columns on wide screens → 3 → 2 → 1 as the viewport narrows. **Fixed column width:** Set `fixed_width = TRUE` to prevent cards from growing beyond the specified width: ```r layout_column_wrap( width = "200px", height = 300, fixed_width = TRUE, card(...), card(...), card(...) ) ``` ### Height Control By default, all rows are given equal height — all cards in all rows match the height of the tallest card. Set `heights_equal = "row"` to allow each row to have its own independent height instead. Since cards are fill items by default (`fill = TRUE`), they stretch to fill row height. Set `fill = FALSE` on individual cards to prevent stretching: ```r layout_column_wrap( width = 1/2, card(fill = FALSE, card_header("Short card"), "This won't stretch" ), card( card_header("Tall card"), lorem::ipsum(paragraphs = 5) ) ) ``` ### Varying Column Widths For unequal column sizing, set `width = NULL` and provide a custom `grid-template-columns` CSS property: ```r layout_column_wrap( width = NULL, height = 300, fill = FALSE, style = css(grid_template_columns = "2fr 1fr 2fr"), card(...), # 2x width card(...), # 1x width card(...) # 2x width ) ``` ### Nested Layouts Create complex arrangements by nesting `layout_column_wrap()`: ```r layout_column_wrap( width = 1/2, height = 300, card(...), # Left column layout_column_wrap( # Right column contains 2 stacked cards width = 1, heights_equal = "row", card(...), card(...) ) ) ``` ### Responsive/Mobile Behavior At small window widths, all layouts collapse into a mobile-friendly "show each card at maximum width" approach, stacking cards vertically. ## layout_columns() **Purpose:** More flexible grid system based on Bootstrap's 12-column grid. Better for complex layouts requiring precise control. ### Basic Grid System Without specifying `col_widths`, space is divided evenly among children. Supply `col_widths` to allocate columns out of a 12-column grid — common patterns include `c(6, 6)` for two equal columns, `c(4, 8)` for a sidebar/main split, `c(4, 4, 4)` for three equal columns, or `c(12, 4, 4, 4)` for a full-width top row followed by three equal columns below. Elements whose widths exceed 12 columns wrap to a new row. ```r layout_columns( col_widths = c(4, 8, 12), card(...), # 4/12 width (33%) card(...), # 8/12 width (67%) card(...) # 12/12 width (100%, new row) ) ``` **Common patterns:** ```r # Two equal columns col_widths = c(6, 6) # Sidebar + main (1:2 ratio) col_widths = c(4, 8) # Three equal columns col_widths = c(4, 4, 4) # Full width top, three equal below col_widths = c(12, 4, 4, 4) ``` ### Row Heights Customize with `row_heights` (numeric values are fractional units): ```r layout_columns( col_widths = c(4, 8, 12), row_heights = c(2, 3), card(card_header("Sidebar"), height = "100%"), card(card_header("Main plot"), plotOutput("main")), card(card_header("Full width table"), tableOutput("table")) ) ``` ### Negative Space Negative `col_widths` create empty space, useful for gutters or visual separation: ```r layout_columns( col_widths = c(4, 8, -2, 8, -2), card(...), # 4 cols card(...), # 8 cols # 2 cols empty space card(...), # 8 cols (new row) # 2 cols empty space (new row) ) ``` ### Responsive Layouts Use `breakpoints()` to specify different widths at different screen sizes: ```r layout_columns( col_widths = breakpoints( sm = c(12, 12), # Small screens: stack vertically md = c(6, 6), # Medium screens: two columns lg = c(4, 8) # Large screens: sidebar + main ), card(...), card(...) ) ``` **Bootstrap breakpoints:** - `xs`: < 576px (extra small - phones) - `sm`: ≥ 576px (small - phones landscape) - `md`: ≥ 768px (medium - tablets) - `lg`: ≥ 992px (large - desktops) - `xl`: ≥ 1200px (extra large - large desktops) - `xxl`: ≥ 1400px (extra extra large) ## Choosing Between layout_column_wrap() and layout_columns() ### Use layout_column_wrap() when: - You want uniform sizing across cards/value boxes - You want simple responsive behavior (auto-wrapping) - You're building a dashboard with consistent card sizes - You want cleaner, more readable code **Example use case:** Dashboard with multiple value boxes or cards showing metrics. ### Use layout_columns() when: - You need precise control over column widths - You want different column proportions (e.g., 4-8 sidebar-main split) - You need negative space or complex grid patterns - You want fine-grained responsive behavior with different layouts per breakpoint **Example use case:** Complex dashboard layout with sidebar, main content, and multiple regions with specific proportions. ### Common Pattern: Combine Both Use `layout_columns()` for overall page structure and `layout_column_wrap()` for uniform content sections: ```r page_fillable( layout_columns( col_widths = c(12, 4, 8), # Full-width header with value boxes layout_column_wrap( width = 1/3, value_box(...), value_box(...), value_box(...) ), # Left sidebar card(...), # Main content with multiple plots layout_column_wrap( width = 1/2, card(...), card(...), card(...), card(...) ) ) ) ``` ## Beyond These Functions For layouts exceeding what these functions can handle, consider: - The `{gridlayout}` package for more complex CSS Grid configurations - The Shiny UI editor for visual layout design - Custom CSS Grid properties via `style` parameter -
inputs.md 7.2 KB
# Special Inputs in bslib bslib provides specialized input widgets that enhance standard Shiny inputs with modern features. ## Table of Contents - [input_switch()](#input_switch) - [input_dark_mode()](#input_dark_mode) - [input_task_button()](#input_task_button) - [input_code_editor()](#input_code_editor) - [input_submit_textarea()](#input_submit_textarea) - [Choosing the Right Input](#choosing-the-right-input) ## input_switch() A modern toggle switch, alternative to `checkboxInput()` for on/off states with immediate effect. ```r input_switch("enable_feature", "Enable advanced features") input_switch("notifications", "Enable notifications", value = TRUE) ``` Use `input_switch()` freely in sidebars, card bodies, or toolbars wherever a boolean toggle fits. Update from the server with `update_switch("id", value = FALSE)` or flip the current state with `toggle_switch("id")`. ## input_dark_mode() Toggle between Bootstrap 5.3 light/dark color modes. Automatically switches the `data-bs-theme` attribute on the page. ```r input_dark_mode(id = "mode") # Follow OS preference input_dark_mode(id = "mode", mode = "dark") # Start in dark mode ``` **Placing in a navbar:** wrap in `nav_item()` and use `nav_spacer()` before it to push it to the right edge: ```r page_navbar( title = "My App", nav_panel("Dashboard", ...), nav_spacer(), nav_item(input_dark_mode(id = "mode")) ) ``` **Hidden mode (OS-aware without a toggle button):** use `style = css(display = "none")` to activate Bootstrap's color mode system without rendering a UI control. The app follows the user's OS `prefers-color-scheme` by default and can still be controlled from the server: ```r nav_item(input_dark_mode(id = "mode", style = css(display = "none"))) ``` Omit `id` if you don't need the server to read or change the mode. **Server access:** `input$mode` returns `"light"` or `"dark"`. **Programmatic toggle:** `toggle_dark_mode()`, `toggle_dark_mode("light")`, `toggle_dark_mode("dark")`. For full custom theme switching (different color palettes beyond light/dark), combine with `session$setCurrentTheme()`. See the **shiny-bslib-theming** skill for details. ## input_task_button() Action button for long-running operations with built-in loading state. Auto-disables while running. ```r # UI input_task_button("run_analysis", "Run Analysis") # Server observeEvent(input$run_analysis, { result <- expensive_computation() output$result <- renderText(result) }) ``` ### With ExtendedTask For truly long-running tasks, combine with `ExtendedTask` and `bind_task_button()`: ```r library(future) plan(multisession) server <- function(input, output, session) { long_task <- ExtendedTask$new(function() { future({ Sys.sleep(10); "Task complete!" }, seed = TRUE) }) |> bind_task_button("run") observeEvent(input$run, { long_task$invoke() }) output$result <- renderText({ long_task$result() }) } ``` **`bind_task_button(target_task, button_id)`** keeps the button in "busy" state while the task runs. Does NOT auto-trigger on click — you still need `observeEvent()`. **Update button:** `update_task_button("run", label = "Done", icon = bsicons::bs_icon("check"))` ## input_code_editor() Lightweight code editor with syntax highlighting, powered by [prism-code-editor](https://prism-code-editor.netlify.app/). Auto-switches themes with dark mode. Try `shiny::runExample("code-editor", package = "bslib")`. **Value updates reach the server** when the user moves focus away or presses `Ctrl/Cmd+Enter` (not on every keystroke). ```r input_code_editor( id = "code", language = "r", value = "# Enter R code here\n" ) ``` **Languages:** r, python, julia, sql, ggsql, javascript, typescript, html, css, scss, sass, json, markdown, yaml, xml, toml, ini, bash, docker, latex, cpp, rust, diff, plain. ### Configuration | Parameter | Default | Description | |---|---|---| | `height` | `"auto"` | CSS height | | `theme_light` | `"github-light"` | Light mode theme | | `theme_dark` | `"github-dark"` | Dark mode theme | | `read_only` | `FALSE` | Disable editing | | `line_numbers` | `TRUE` | Show line numbers | | `word_wrap` | | Enable word wrapping | | `tab_size` | `2` | Tab width | | `indentation` | `"space"` | `"space"` or `"tab"` | | `fill` | `TRUE` | Fill container | **Available themes:** `"atom-one-dark"`, `"dracula"`, `"github-dark-dimmed"`, `"github-dark"`, `"github-light"`, `"night-owl-light"`, `"night-owl"`, `"prism-okaidia"`, `"prism-solarized-light"`, `"prism-tomorrow"`, `"prism-twilight"`, `"prism"`, `"vs-code-dark"`, `"vs-code-light"`. **Keyboard shortcuts:** `Ctrl/Cmd+Enter` (submit), `Ctrl/Cmd+Z` (undo), `Tab`/`Shift+Tab` (indent/dedent). **Dynamic language switching:** ```r observeEvent(input$language, { update_code_editor("code", language = input$language) }) ``` ## input_submit_textarea() Textarea with explicit submission — prevents reactive updates on every keystroke. Auto-grows as user types. Ideal for chat boxes, comments, or inputs where users compose before submitting. **Important:** Initial server value is always `""`. Updates only on explicit submit. ```r input_submit_textarea( id = "user_input", label = "Enter text:", placeholder = "Type here...", rows = 4 ) ``` ### Submission Behavior - **Default (`submit_key = "enter+modifier"`):** `Ctrl/Cmd+Enter` to submit - **Enter-only:** `submit_key = "enter"` — submit with Enter, Shift+Enter for new lines ### Custom Button and Toolbar The `button` parameter accepts any HTML element. `input_task_button()` recommended for built-in busy state: ```r input_submit_textarea( id = "query", placeholder = "Ask a question...", button = input_task_button("submit", "Send", icon = bsicons::bs_icon("send")), toolbar = list( actionLink("attach", bsicons::bs_icon("paperclip")) ) ) ``` ### Update `update_submit_textarea()` accepts `value` to change the text, `submit = TRUE` to trigger submission programmatically, and `focus = TRUE` to move keyboard focus to the textarea. ### Chat Interface Pattern ```r # UI card( card_header("Chat"), card_body(uiOutput("chat_messages"), fillable = FALSE, fill = TRUE), card_footer( input_submit_textarea("chat_input", placeholder = "Type a message...", submit_key = "enter") ) ) # Server observeEvent(input$chat_input, { req(nchar(input$chat_input) > 0) add_message(input$chat_input) update_submit_textarea("chat_input", value = "") }) ``` ## Choosing the Right Input | Need | Use | Instead of | |---|---|---| | On/off toggle, immediate effect | `input_switch()` | `checkboxInput()` | | Selection/agreement, form submit | `checkboxInput()` | `input_switch()` | | Long operation (>2s), prevent duplicates | `input_task_button()` | `actionButton()` | | Quick action, custom loading | `actionButton()` | `input_task_button()` | | Code with syntax highlighting | `input_code_editor()` | `textAreaInput()` | | Expensive downstream, compose then submit | `input_submit_textarea()` | `textAreaInput()` | | Live preview, cheap updates | `textAreaInput()` | `input_submit_textarea()` | | Light/dark mode toggle | `input_dark_mode()` | Custom toggle | ### Feedback for Task Buttons Show completion feedback after long operations: ```r observeEvent(input$process, { result <- process_data() show_toast(toast("Processing complete", header = "Done", type = "success")) }) ``` -
migration.md 8 KB
# Migrating from Legacy Shiny to Modern bslib This reference maps legacy Shiny UI patterns to their modern bslib equivalents. Use this when modernizing existing apps or when tempted to use outdated patterns. ## Table of Contents - [Legacy to Modern Mapping](#legacy-to-modern-mapping) - [Page Functions](#page-functions) - [Layout Functions](#layout-functions) - [Containers and Outputs](#containers-and-outputs) - [Navigation](#navigation) - [Theming](#theming) - [Inputs](#inputs) - [Complete Migration Example](#complete-migration-example) ## Legacy to Modern Mapping | Legacy Pattern | Modern bslib Replacement | Why | |---|---|---| | `fluidPage()` | `page_sidebar()` or `page_navbar()` | Built-in sidebar, filling, theming | | `navbarPage()` | `page_navbar()` | Same concept, Bootstrap 5 | | `fluidRow(column(...))` | `layout_columns()` or `layout_column_wrap()` | Works with filling layouts | | `tabsetPanel(tabPanel(...))` | `navset_card_underline(nav_panel(...))` | Card integration, full-screen | | `wellPanel()` | `card()` | Full-screen, headers, filling | | `shinythemes::shinytheme()` | `bs_theme(preset = ...)` | Bootstrap 5, Sass variables | | `plotOutput("plot")` (bare) | `card(full_screen = TRUE, plotOutput("plot"))` | Full-screen expansion | | `conditionalPanel()` in sidebar | `accordion()` in `sidebar()` | Better organization, native styling | | `checkboxInput()` for toggles | `input_switch()` | Modern toggle UI | | `actionButton()` for slow tasks | `input_task_button()` | Built-in loading state | | `sidebarLayout(sidebarPanel(), mainPanel())` | `page_sidebar(sidebar = sidebar(), ...)` | Filling, collapsible | ## Page Functions ### Do Not Use - **`fluidPage()`** -- No sidebar, no filling, no theming integration - **`navbarPage()`** -- Bootstrap 3 era, use `page_navbar()` instead - **`fixedPage()`** -- Use `page_fixed()` if scrolling layout needed - **`sidebarLayout()` + `sidebarPanel()` + `mainPanel()`** -- Replaced entirely by `page_sidebar()` ### Use Instead ```r # Single-page dashboard page_sidebar( title = "Dashboard", theme = bs_theme(version = 5), sidebar = sidebar(...), card(...) ) # Multi-page app page_navbar( title = "App", theme = bs_theme(version = 5), nav_panel("Page 1", ...), nav_panel("Page 2", ...) ) # Scrolling layout (rare) page_fluid( theme = bs_theme(version = 5), card(...), card(...) ) ``` ## Layout Functions ### Do Not Use - **`fluidRow()`** -- Incompatible with filling layouts - **`column()`** -- Does not participate in fill system - **`fluidRow(column(4, ...), column(8, ...))`** -- Use `layout_columns(col_widths = c(4, 8))` ### Use Instead Use `layout_columns()` when columns have unequal or explicit widths, and `layout_column_wrap()` when all columns should be equal or auto-sized. **Legacy:** ```r fluidRow( column(4, plotOutput("plot1")), column(8, plotOutput("plot2")) ) ``` **Modern:** ```r layout_columns( col_widths = c(4, 8), card(full_screen = TRUE, card_header("Plot 1"), plotOutput("plot1")), card(full_screen = TRUE, card_header("Plot 2"), plotOutput("plot2")) ) ``` **Legacy:** ```r fluidRow( column(4, plotOutput("a")), column(4, plotOutput("b")), column(4, plotOutput("c")) ) ``` **Modern:** ```r layout_column_wrap( width = 1/3, card(full_screen = TRUE, card_header("A"), plotOutput("a")), card(full_screen = TRUE, card_header("B"), plotOutput("b")), card(full_screen = TRUE, card_header("C"), plotOutput("c")) ) ``` ## Containers and Outputs ### Do Not Use - **`wellPanel()`** -- Use `card()` for all content grouping - **Bare outputs** (`plotOutput("x")` without a container) -- Always wrap in `card()` ### Use Instead **Legacy:** ```r wellPanel( h3("Results"), plotOutput("plot"), verbatimTextOutput("summary") ) ``` **Modern:** ```r card( full_screen = TRUE, card_header("Results"), plotOutput("plot"), card_footer(verbatimTextOutput("summary")) ) ``` ## Navigation ### Do Not Use - **`tabsetPanel(tabPanel(...))`** -- Use `navset_*()` functions - **`navlistPanel()`** -- Use `navset_pill_list()` - **`navbarMenu()`** -- Use `nav_menu()` inside `page_navbar()` ### Use Instead Replace `tabsetPanel()` with a `navset_card_*()` variant (e.g., `navset_card_underline()`), and replace each `tabPanel()` with `nav_panel()`. The navset card variant accepts `title` and `full_screen` arguments directly. For top-level navigation, `navbarPage()` with `tabPanel()` and `navbarMenu()` maps directly to `page_navbar()` with `nav_panel()` and `nav_menu()`: **Legacy:** ```r tabsetPanel( tabPanel("Plot", plotOutput("plot")), tabPanel("Summary", verbatimTextOutput("summary")), tabPanel("Data", tableOutput("data")) ) ``` **Modern:** ```r navset_card_underline( title = "Analysis", full_screen = TRUE, nav_panel("Plot", plotOutput("plot")), nav_panel("Summary", verbatimTextOutput("summary")), nav_panel("Data", tableOutput("data")) ) ``` **Legacy multi-page:** ```r navbarPage( "My App", tabPanel("Home", ...), tabPanel("Analysis", ...), navbarMenu("More", tabPanel("Settings", ...), tabPanel("About", ...) ) ) ``` **Modern:** ```r page_navbar( title = "My App", theme = bs_theme(version = 5), nav_panel("Home", ...), nav_panel("Analysis", ...), nav_menu("More", nav_panel("Settings", ...), nav_panel("About", ...) ) ) ``` ## Theming ### Do Not Use - **`shinythemes::shinytheme()`** -- Bootstrap 3 only - **Custom CSS files for basic styling** -- Use `bs_theme()` variables instead - **Hardcoded colors in CSS** -- Use Sass variables via `bs_add_rules()` ### Use Instead **Legacy:** ```r fluidPage( theme = shinythemes::shinytheme("cerulean"), ... ) ``` **Modern:** ```r page_sidebar( theme = bs_theme( version = 5, preset = "cerulean", base_font = font_google("Roboto") ), ... ) ``` ## Inputs ### Prefer Modern Equivalents | Legacy | Modern | When to prefer modern | |---|---|---| | `checkboxInput()` | `input_switch()` | On/off toggles with immediate effect | | `actionButton()` | `input_task_button()` | Operations taking >2 seconds | | `textAreaInput()` | `input_submit_textarea()` | Expensive downstream computations | Standard Shiny inputs (`selectInput`, `sliderInput`, `dateInput`, etc.) remain correct and do not need replacement. ## Complete Migration Example ### Before (Legacy) ```r library(shiny) library(shinythemes) ui <- navbarPage( "Sales Dashboard", theme = shinytheme("flatly"), tabPanel("Overview", sidebarLayout( sidebarPanel( selectInput("region", "Region", choices = regions), dateRangeInput("dates", "Date range"), checkboxInput("show_trend", "Show trend") ), mainPanel( fluidRow( column(6, plotOutput("sales_plot")), column(6, plotOutput("growth_plot")) ), fluidRow( column(12, tableOutput("data_table")) ) ) ) ), tabPanel("Details", tabsetPanel( tabPanel("By Region", plotOutput("region_plot")), tabPanel("By Product", plotOutput("product_plot")) ) ) ) ``` ### After (Modern bslib) ```r library(shiny) library(bslib) ui <- page_navbar( title = "Sales Dashboard", theme = bs_theme(version = 5, preset = "flatly"), nav_panel("Overview", layout_sidebar( sidebar = sidebar( selectInput("region", "Region", choices = regions), dateRangeInput("dates", "Date range"), input_switch("show_trend", "Show trend") ), layout_columns( col_widths = c(6, 6, 12), card(full_screen = TRUE, card_header("Sales"), plotOutput("sales_plot")), card(full_screen = TRUE, card_header("Growth"), plotOutput("growth_plot")), card(full_screen = TRUE, card_header("Data"), tableOutput("data_table")) ) ) ), nav_panel("Details", navset_card_underline( title = "Detailed Analysis", full_screen = TRUE, nav_panel("By Region", plotOutput("region_plot")), nav_panel("By Product", plotOutput("product_plot")) ) ) ) server <- function(input, output, session) { thematic::thematic_shiny() # ... server logic unchanged } ``` -
navigation.md 11.3 KB
# Navigation in bslib This reference covers navigation patterns in bslib, including tabsets, multi-page apps, and navigation containers. Navigation helps organize content into logical sections and pages. ## Table of Contents - [Core Concept](#core-concept) - [Navigation Containers](#navigation-containers) - [navset_underline()](#navset_underline) - [navset_tab()](#navset_tab) - [navset_pill()](#navset_pill) - [navset_pill_list()](#navset_pill_list) - [navset_bar()](#navset_bar) - [navset_hidden()](#navset_hidden) - [Card Navigation](#card-navigation) - [Navigation Items](#navigation-items) - [Multi-Page Apps](#multi-page-apps) - [Accessing Active Tab](#accessing-active-tab) - [Dynamic Navigation](#dynamic-navigation) - [Best Practices](#best-practices) ## Core Concept Tabsets and navigation are created by combining `nav_panel()`s in a navigation container. Each `nav_panel()` has a label (shown in the navigation) and content (shown when selected). **Basic pattern:** ```r navset_*( nav_panel("Tab 1", "Content 1"), nav_panel("Tab 2", "Content 2"), nav_panel("Tab 3", "Content 3") ) ``` ## Navigation Containers All `navset_*()` functions accept any number of `nav_panel()` items as their primary arguments. Choose the variant based on visual style and layout needs: - **`navset_underline()`** — Modern underline-style; recommended for most use cases. Use for clean, modern interfaces with 2–5 items. - **`navset_tab()`** — Traditional filled-background tabs. Use when users expect classic Bootstrap tab styling or need stronger visual separation. - **`navset_pill()`** — Horizontal rounded pill buttons. Use when you want a button-like appearance for navigation items. - **`navset_pill_list()`** — Vertical sidebar-style pill list. Use when you have many items (5+) or when vertical layout fits the design. ### navset_bar() Navigation bar style, similar to `page_navbar()` but without being a full page layout. Supports a `title` parameter and `nav_menu()` dropdowns. ```r navset_bar( title = "App Section", nav_panel("Home", "Home content"), nav_panel("About", "About content"), nav_menu( "More", nav_panel("Option 1", "Content 1"), nav_panel("Option 2", "Content 2") ) ) ``` **Use when:** You want navbar-style navigation within a section of your app rather than at the page level. ### navset_hidden() Navigation container without visible navigation controls. Useful for programmatic tab switching. ```r # UI navset_hidden( id = "wizard", nav_panel("step1", "Step 1 content", actionButton("next1", "Next")), nav_panel("step2", "Step 2 content", actionButton("next2", "Next")), nav_panel("step3", "Step 3 content", actionButton("submit", "Submit")) ) # Server observeEvent(input$next1, { nav_select("wizard", "step2") }) observeEvent(input$next2, { nav_select("wizard", "step3") }) ``` **Use when:** You need programmatic control over navigation (wizards, workflows, complex state machines). ## Card Navigation Functions with `card` in their name wrap the navigation in a card container: `navset_card_underline()`, `navset_card_tab()`, and `navset_card_pill()`. They mirror their non-card counterparts but add card styling and accept `title` and `full_screen` arguments. ```r navset_card_underline( title = "Analysis Results", full_screen = TRUE, nav_panel("Plot", plotOutput("plot")), nav_panel("Summary", verbatimTextOutput("summary")), nav_panel("Table", tableOutput("table")) ) ``` **Key features:** - The `title` parameter adds a card header above the navigation - Supports `full_screen = TRUE` for expandable content - Each `nav_panel()` behaves like a card body — do **not** wrap panel content in `card()` **Don't nest cards inside navset_card_* panels.** The navset already provides the card container. Wrapping content in `card()` inside `nav_panel()` creates a card-within-a-card: ```r # Wrong: double-nested card navset_card_underline( nav_panel("Plot", card(plotOutput("plot"))) # card() is redundant ) # Right: output directly in nav_panel navset_card_underline( title = "Analysis Results", full_screen = TRUE, nav_panel("Plot", plotOutput("plot")) ) ``` **Best practice:** Always enable `full_screen = TRUE` when panels contain visualizations. ## Navigation Items Beyond `nav_panel()`, several helper functions control navigation appearance: ### nav_panel() The primary content container. Key parameters: - `title` — label shown in the navigation - `value` — optional ID string for programmatic control (defaults to `title`) - `icon` — optional icon, e.g. `bsicons::bs_icon("graph-up")` Content can be any outputs, text, or layout functions. Do not wrap content in `card()` when inside a `navset_card_*` container. ### nav_spacer() Adds flexible space, pushing subsequent items to the right (horizontal) or bottom (vertical). Commonly used to align utility links to the far right of a navbar: ```r page_navbar( title = "My App", nav_panel("Home", "..."), nav_panel("About", "..."), nav_spacer(), # Everything after goes to the right nav_item(tags$a("Help", href = "/help")), nav_item(tags$a("Login", href = "/login")) ) ``` ### nav_menu() Creates a dropdown menu of nav panels: ```r navset_tab( nav_panel("Overview", "..."), nav_menu( "Analysis", nav_panel("Trends", plotOutput("trends")), nav_panel("Comparisons", plotOutput("comparisons")), nav_panel("Forecasts", plotOutput("forecasts")) ) ) ``` ### nav_item() Adds arbitrary HTML to navigation without creating a panel. Useful for links, buttons, or custom elements: ```r nav_item(tags$a("Docs", href = "https://docs.example.com", target = "_blank")) nav_item(actionLink("refresh", "Refresh Data")) ``` ### nav_panel_hidden() A panel that exists but isn't shown in navigation. Useful for programmatically accessible panels: ```r navset_tab( id = "tabs", nav_panel("Public 1", "..."), nav_panel("Public 2", "..."), nav_panel_hidden("admin", "Admin-only content") ) # Server: show admin panel based on user role observe({ if (user_is_admin()) { nav_show("tabs", "admin") } }) ``` ## Multi-Page Apps Use `page_navbar()` to create full multi-page applications. This is covered in detail in [page-layouts.md](page-layouts.md), but here's the navigation pattern: ```r page_navbar( title = "My Application", nav_panel("Home", homepage_ui), nav_panel("Analysis", analysis_ui), nav_panel("Reports", reports_ui), nav_menu( "More", nav_panel("Settings", settings_ui), nav_panel("About", about_ui) ) ) ``` **Relationship to tabsets:** `page_navbar()` uses the same `nav_panel()` system as tabsets. The main difference is that `page_navbar()` creates a full-page layout, while `navset_*()` creates a component-level navigation container. ## Accessing Active Tab Provide an `id` argument to track which panel is selected: ```r # UI navset_card_underline( id = "selected_tab", nav_panel("Plot", plotOutput("plot")), nav_panel("Summary", verbatimTextOutput("summary")), nav_panel("Data", tableOutput("data")) ) # Server observe({ current_tab <- input$selected_tab if (current_tab == "Plot") { # Trigger plot-specific actions } }) ``` **Use cases:** - Conditional logic based on active panel - Analytics tracking - Loading data only when needed - Updating other UI elements based on navigation state ## Dynamic Navigation Programmatically control navigation with these functions. All take the navset's `id` as the first argument and a panel's `value` (or `title`) as the second. - **`nav_select("id", "panel")`** — Switch to the specified panel. - **`nav_show("id", "panel")`** / **`nav_hide("id", "panel")`** — Show or hide a panel in the navigation. - **`nav_remove("id", "panel")`** — Remove a panel entirely. - **`nav_insert("id", panel, position, target)`** — Insert a new `nav_panel()` before or after a target panel. ```r # Switch to a panel on button click observeEvent(input$show_plot, { nav_select("tabs", "Plot") }) # Insert a new panel after "Home" observeEvent(input$add_panel, { nav_insert( "tabs", nav_panel("New Panel", "Dynamic content"), position = "after", target = "Home" ) }) ``` ## Best Practices ### Choose Appropriate Navigation Style **Use `navset_underline()` for:** - Modern, clean interfaces - 2-5 top-level navigation items - When you want subtle visual emphasis **Use `navset_tab()` for:** - Traditional interfaces - When users expect classic tab styling - Stronger visual separation needed **Use `navset_pill_list()` for:** - Many navigation items (5+) - When vertical space is available - Sidebar-style navigation within a section **Use card variants for:** - Organizing related outputs in a dashboard - When the navigation is part of a specific content section - Always with `full_screen = TRUE` for viz-heavy content ### Organize Content Logically **Group related panels:** ```r navset_card_underline( title = "User Analysis", nav_panel("Demographics", plotOutput("demographics")), nav_panel("Behavior", plotOutput("behavior")), nav_panel("Segments", plotOutput("segments")) ) ``` **Use menus for secondary content:** ```r page_navbar( title = "Dashboard", nav_panel("Overview", overview_ui), # Primary nav_panel("Analysis", analysis_ui), # Primary nav_menu( # Secondary "More", nav_panel("Settings", settings_ui), nav_panel("Help", help_ui), nav_panel("About", about_ui) ) ) ``` ### Prevent Redundant Computation When multiple tabs share data, use reactive expressions: ```r # Server # Single reactive for shared data filtered_data <- reactive({ data |> filter(species == input$species) |> filter(island == input$island) }) # Each tab uses the shared reactive output$plot <- renderPlot({ ggplot(filtered_data(), aes(x, y)) + geom_point() }) output$summary <- renderPrint({ summary(filtered_data()) }) output$table <- renderTable({ filtered_data() }) ``` This prevents recalculating the same filtered data for each tab. ### Use IDs Consistently When providing `id` to navigation containers: - Use descriptive, semantic IDs (`id = "main_tabs"`, not `id = "t1"`) - Document which IDs are used for programmatic control - Keep track of panel `value` parameters when using dynamic navigation ### Lazy Loading for Performance For tabs with expensive computations, use `bindEvent()` or `req()` to load content only when the tab is viewed: ```r # Server output$expensive_plot <- renderPlot({ req(input$tabs == "Analysis") # Only render when Analysis tab is active # Expensive computation run_complex_analysis(data) }) |> bindEvent(input$tabs) ``` ### Test Navigation Flow Always test: - Switching between all tabs - Navigation on mobile (especially dropdowns) - Programmatic navigation if implemented - Deep linking if using bookmarking ### Consider Bookmarking For apps with important navigation state, enable bookmark support: ```r shinyApp( ui = page_navbar( id = "nav", nav_panel("Home", "..."), nav_panel("Analysis", "...") ), server = function(input, output, session) { # Navigation state is automatically bookmarkable }, enableBookmarking = "url" ) ``` This allows users to share links to specific tabs. ### Accessibility - Use clear, descriptive panel titles - Avoid relying solely on icons for navigation - Ensure keyboard navigation works (test with Tab key) - Test with screen readers for public-facing apps - Consider ARIA labels for complex navigation patterns -
page-layouts.md 7.8 KB
# Page Layouts in bslib This reference covers the page-level layout functions in bslib that structure entire Shiny applications. These are the top-level containers that determine the overall architecture of your app. ## Table of Contents - [Dashboard Layouts](#dashboard-layouts) - [page_sidebar()](#page_sidebar) - [page_navbar()](#page_navbar) - [page_fillable()](#page_fillable) - [Basic Page Layouts](#basic-page-layouts) - [Filling vs Scrolling Behavior](#filling-vs-scrolling-behavior) - [Mobile Considerations](#mobile-considerations) - [Production Best Practices](#production-best-practices) ## Dashboard Layouts ### page_sidebar() The primary function for creating single-page dashboards with a sidebar and main content area. **Basic structure:** ```r ui <- page_sidebar( title = "My dashboard", sidebar = sidebar("Sidebar content"), "Main content area" ) ``` **Best practice:** Keep inputs in the `sidebar` and outputs in the main content area. Wrap outputs in `card()` and sidebar contents in `sidebar()` for titles and custom styling. **Example with cards:** ```r ui <- page_sidebar( title = "Penguins Dashboard", sidebar = sidebar( selectInput("species", "Species", choices = unique(penguins$species)) ), card( full_screen = TRUE, card_header("Bill Length"), plotOutput("bill_length") ), card( card_header("Summary Statistics"), verbatimTextOutput("summary") ) ) ``` **Key parameters:** - `title`: App title displayed at the top - `sidebar`: A `sidebar()` object with inputs/controls - `theme`: Optional `bs_theme()` object for styling - `fillable`: Whether the page should fill the viewport height (default TRUE) - `fillable_mobile`: Whether fillable behavior applies on mobile (default FALSE) - `class = "bslib-page-dashboard"`: Adds a light gray background behind the main area, which looks best when cards are used as the primary content containers ### page_navbar() Use `page_navbar()` for multi-page dashboards with a top navigation bar. Each page is defined with `nav_panel()`. **Basic structure:** ```r ui <- page_navbar( title = "Multi-Page Dashboard", nav_panel("Page 1", "Content for page 1"), nav_panel("Page 2", "Content for page 2"), nav_panel("Page 3", "Content for page 3") ) ``` **With sidebar:** ```r ui <- page_navbar( title = "Penguins Dashboard", sidebar = sidebar( selectInput("color_by", "Color by", choices = c("species", "island")) ), nav_spacer(), nav_panel("Bill Length", card(...)), nav_panel("Bill Depth", card(...)), nav_panel("Body Mass", card(...)), nav_item(tags$a("Documentation", href = "https://example.com")) ) ``` **Important caveat:** `page_navbar()`'s `sidebar` argument puts the same sidebar on every page. If you need different sidebars per page or conditional sidebar contents, see the [sidebars reference](sidebars.md) for strategies. **Key parameters:** - `title`: App title in the navbar - `sidebar`: Optional `sidebar()` shown on all pages - `id`: ID for tracking the active page (accessible as `input$<id>`) - `fillable`: Can be TRUE (all pages), FALSE (no pages), or a vector of page names - `theme`: Optional `bs_theme()` object **Dashboard appearance:** Add `class = "bslib-page-dashboard"` to individual `nav_panel()` containers (not the `page_navbar()` itself) to get a light gray background on specific pages: ```r page_navbar( title = "My App", nav_panel("Dashboard", class = "bslib-page-dashboard", card(...), card(...) ), nav_panel("About", "Plain white background here") ) ``` **Navigation helpers:** - `nav_spacer()`: Adds spacing/pushes subsequent items right - `nav_item()`: Adds arbitrary HTML (e.g., links) to navbar - `nav_menu()`: Creates dropdown menus ### page_fillable() A screen-filling page layout where content grows/shrinks to fit the browser window. This is the foundation for filling layouts in bslib. **Key behavior:** Direct children of `page_fillable()` become fill items, meaning they'll resize to fill available space. This is ideal for dashboards where you want outputs to adapt to the viewport size. Typical usage wraps `layout_columns()` (or similar layout containers) and `card()` components as direct children. **When to use:** - Dashboards with plots/maps that should expand to fill the screen - Single-page apps where all content should be visible without scrolling - Apps with dynamic layouts that adapt to window size Note: `page_sidebar()` and `page_navbar()` are built on top of `page_fillable()` and inherit its filling behavior by default. ## Basic Page Layouts Beyond dashboard layouts, bslib provides traditional page functions: - **`page_fluid()`**: Full-width page that resizes horizontally but scrolls vertically - **`page_fixed()`**: Fixed-width page (940px default) that scrolls vertically - **`page()`**: Most flexible option with manual control These are useful when you don't want filling behavior and prefer traditional scrolling layouts. ## Filling vs Scrolling Behavior ### Filling Layouts (Default for Dashboard Pages) Both `page_sidebar()` and `page_navbar()` default to `fillable = TRUE`, where outputs are encouraged to grow/shrink to fit the browser window. **Benefits:** - Content adapts to available screen space - Professional dashboard appearance - No scrolling needed when content fits **Considerations:** When content has large intrinsic minimum heights: - Set `height` on cards that shouldn't resize - Set `min_height` on cards that need a minimum size - Set `max_height` on cards that shouldn't grow too large **Example:** ```r layout_columns( card(min_height = 200, max_height = 400, plotOutput("plot1")), card(height = 300, lorem::ipsum(10)) ) ``` ### Scrolling Layouts For pages with many outputs or long content, set `fillable = FALSE` to disable filling behavior. This causes outputs to fall back to their default heights (~400px for plots) with page scrolling enabled. Pass `fillable = FALSE` directly to `page_sidebar()` or `page_navbar()`. **For page_navbar(), use selective filling:** ```r ui <- page_navbar( title = "Mixed Layout", fillable = c("Overview", "Analysis"), # Only these pages fill nav_panel("Overview", ...), # Fills viewport nav_panel("Analysis", ...), # Fills viewport nav_panel("Details", ...) # Scrolls normally ) ``` ## Mobile Considerations By default, filling layout is disabled on mobile devices to prevent awkward resizing on small screens. Set `fillable_mobile = TRUE` on `page_sidebar()` or `page_navbar()` to enable filling on mobile. **Best practices for mobile:** - Use `min_height` on cards to prevent excessive shrinking - Sidebars collapse by default on mobile (configurable via `sidebar(open = ...)`) - Test responsive breakpoints using browser dev tools - Consider using `layout_column_wrap()` for responsive multi-column layouts ## Production Best Practices ### Pin Bootstrap Version Before deploying to production, hard-code the Bootstrap version to prevent breakage on updates. Pass `theme = bs_theme(version = 5)` to any page function. This ensures your app uses Bootstrap 5 (recommended for modern features) and won't break if bslib's default version changes. ### Theming Pass a `bs_theme()` object to the `theme` parameter of any page function to customize appearance. See [theming.md](theming.md) for comprehensive theming guidance. ### Plot Styling Use the `thematic` package to ensure plots match your theme. Call `thematic::thematic_shiny()` in your server function or `app.R` to automatically style `plotOutput()` to match your CSS theme colors. ### Performance Tips - For apps with many outputs, consider using `fillable = FALSE` and letting users scroll - Use `card(full_screen = TRUE)` to allow expanding individual visualizations - Consider using `navset_card_tab()` to organize related outputs within a single card - Profile your app with `profvis` to identify rendering bottlenecks -
sidebars.md 8.6 KB
# Sidebars in bslib Sidebars organize inputs and controls in Shiny dashboards. bslib provides flexible sidebar layouts at multiple levels: page-level, component-level, and within cards. ## Table of Contents - [Basic Sidebar Usage](#basic-sidebar-usage) - [Page-Level Sidebars](#page-level-sidebars) - [Component-Level Sidebars](#component-level-sidebars) - [Varied Sidebars Across Pages](#varied-sidebars-across-pages) - [Conditional Sidebar Contents](#conditional-sidebar-contents) - [Reactive Open/Close](#reactive-openclose) - [Accordions in Sidebars](#accordions-in-sidebars) - [Nested Sidebars](#nested-sidebars) - [Styling](#styling) - [Best Practices](#best-practices) ## Basic Sidebar Usage ```r sidebar( title = "Controls", position = "left", selectInput("var", "Variable", choices = names(data)), sliderInput("bins", "Bins", min = 1, max = 50, value = 30) ) ``` **Key parameters:** | Parameter | Default | Description | |---|---|---| | `title` | `NULL` | Title at top | | `open` | `"desktop"` | Initial open/closed state — see below | | `position` | `"left"` | `"left"` or `"right"` | | `width` | `"250px"` | CSS width | | `resizable` | `TRUE` | Whether users can drag to resize the sidebar on desktop | | `id` | `NULL` | For programmatic control via `sidebar_toggle()` | | `bg` | | Background color (auto-contrasts `fg`) | | `fg` | | Foreground color | | `fillable` | `FALSE` | Whether contents fill vertically | | `gap` | | CSS spacing between children | | `padding` | | CSS padding within sidebar | **`open` values:** - `"desktop"` *(default)* — open on desktop, closed on mobile - `"open"` / `TRUE` — starts open on all screen sizes - `"closed"` / `FALSE` — starts closed on all screen sizes - `"always"` / `NA` — always open, no collapse button shown For independent desktop/mobile control, pass a named list: `open = list(desktop = "open", mobile = "always-above")`. The `"always-above"` mobile option places the sidebar above the main content rather than as an overlay. The `"desktop"` shorthand is equivalent to `list(desktop = "open", mobile = "closed")`. Note: `sidebar_toggle()` only supports `"open"`/`TRUE` and `"closed"`/`FALSE`; its default `open = NULL` toggles the current state. ## Page-Level Sidebars ### page_sidebar() Most common pattern for single-page dashboards: ```r page_sidebar( title = "My Dashboard", sidebar = sidebar( title = "Filters", selectInput("species", "Species", choices = unique(penguins$species)), selectInput("island", "Island", choices = unique(penguins$island)) ), card(full_screen = TRUE, card_header("Plot"), plotOutput("scatter")), card(card_header("Summary"), verbatimTextOutput("summary")) ) ``` ### page_navbar() with Sidebar Sidebar visible on **all** pages: ```r page_navbar( title = "Multi-Page App", sidebar = sidebar( title = "Global Filters", selectInput("region", "Region", choices = regions), dateRangeInput("dates", "Date range") ), nav_panel("Overview", overview_ui), nav_panel("Details", details_ui) ) ``` **Caveat:** `page_navbar(sidebar = ...)` puts the same sidebar on every page. See [Varied Sidebars Across Pages](#varied-sidebars-across-pages) for per-page alternatives. ## Component-Level Sidebars ### layout_sidebar() in Cards Keep controls close to the outputs they affect: ```r card( full_screen = TRUE, card_header("Customizable Plot"), layout_sidebar( fillable = TRUE, # Important for fill behavior sidebar = sidebar( position = "right", width = "200px", selectInput("color", "Color scheme", ...), sliderInput("alpha", "Transparency", ...) ), plotlyOutput("plot") ) ) ``` **Key insight:** Set `fillable = TRUE` on `layout_sidebar()` to preserve fill behavior for outputs like plotly, leaflet, etc. ### layout_sidebar() in Filling Pages `page_sidebar()` is a convenience wrapper around `page_fillable()` + `layout_sidebar()`. Use this directly for more control: ```r page_fillable( layout_sidebar( sidebar = sidebar("Sidebar content"), layout_columns(card(...), card(...)) ) ) ``` ## Varied Sidebars Across Pages When different pages need different sidebars, place `layout_sidebar()` within individual pages instead of using `page_navbar(sidebar = ...)`. **Some pages with sidebars, some without:** ```r page_navbar( title = "App", fillable = c("Analysis", "Comparison"), nav_panel( "Analysis", layout_sidebar( sidebar = sidebar(title = "Analysis Controls", selectInput("metric", "Metric", ...)), card(plotOutput("analysis_plot")) ) ), nav_panel( "Comparison", layout_sidebar( sidebar = sidebar(title = "Comparison Controls", selectInput("compare_by", "Compare by", ...)), card(plotOutput("comparison_plot")) ) ), nav_panel("About", "No sidebar on this page") ) ``` ## Conditional Sidebar Contents Change sidebar contents based on the active page using `conditionalPanel()`: ```r page_navbar( title = "App", id = "nav", # Required: enables tracking active page sidebar = sidebar( conditionalPanel( "input.nav === 'Scatter'", selectInput("x_var", "X variable", ...), selectInput("y_var", "Y variable", ...) ), conditionalPanel( "input.nav === 'Histogram'", selectInput("hist_var", "Variable", ...), sliderInput("bins", "Bins", ...) ) ), nav_panel("Scatter", plotOutput("scatter")), nav_panel("Histogram", plotOutput("histogram")) ) ``` **Key:** Navigation container must have an `id`. JavaScript conditions use `===` and string values matching panel titles exactly. ## Reactive Open/Close Programmatically toggle sidebar visibility with `toggle_sidebar()` (requires `id` on the sidebar): ```r ui <- page_navbar( title = "App", id = "nav", sidebar = sidebar(id = "main_sidebar", open = FALSE, "Content"), nav_panel("Page 1", "Sidebar starts closed"), nav_panel("Page 2", "Sidebar opens automatically") ) server <- function(input, output, session) { observe({ toggle_sidebar("main_sidebar", open = input$nav == "Page 2") }) } ``` ## Accordions in Sidebars When `accordion()` is an immediate child of `sidebar()`, panels render flush for clean organization: ```r sidebar( title = "Controls", accordion( accordion_panel( "Data Filters", selectInput("species", "Species", ...), dateRangeInput("dates", "Date range", ...) ), accordion_panel( "Plot Options", selectInput("color", "Color by", ...), sliderInput("alpha", "Transparency", ...) ), accordion_panel( "Advanced", checkboxInput("show_outliers", "Show outliers"), numericInput("threshold", "Threshold", ...) ) ) ) ``` **Gotcha:** Accordion must be an immediate child of `sidebar()` for flush rendering. Wrapping in another element adds extra padding. See [accordions.md](accordions.md) for more. ## Nested Sidebars Create dual left/right sidebars by nesting `layout_sidebar()`: ```r page_fillable( layout_sidebar( sidebar = sidebar(title = "Left Sidebar", "Primary controls"), layout_sidebar( sidebar = sidebar(title = "Right Sidebar", position = "right", open = FALSE, "Secondary controls"), card(plotOutput("main_plot")), border = FALSE ), border_radius = FALSE, fillable = TRUE, class = "p-0" ) ) ``` Use `fillable = TRUE`, `class = "p-0"`, and `border = FALSE` for seamless nesting. ## Styling Set `bg` to a CSS color or theme color name (e.g., `"#f8f9fa"`, `"primary"`); foreground color auto-contrasts when `fg` is also set. Set `width` to a fixed value (`"300px"`) or proportional value (`"20%"`). Apply Bootstrap utility classes via `class` (e.g., `class = "border-start border-3 border-primary"`). ## Best Practices **Organize many inputs with accordions:** wrap inputs in `accordion()` with `accordion_panel()` groups (e.g., "Essential", "Advanced"). See [Accordions in Sidebars](#accordions-in-sidebars) above. **Handle sidebar state responsively:** the default `open = "desktop"` suits most dashboards. Override when needed: `open = FALSE` for secondary sidebars that start collapsed, `open = "always"` when you never want a collapse button. **Use right sidebars** for secondary/optional controls, keeping content focus on the left. **When the sidebar gets crowded:** 1. Use accordions to group inputs 2. Move less important controls into card header popovers 3. Split into multiple pages with page-specific sidebars **Card header popover for advanced options:** ```r card( card_header( "Plot", popover( bsicons::bs_icon("gear"), title = "Advanced Options", sliderInput("param", "Parameter", ...) ) ), plotOutput("plot") ) ``` -
theming.md 2.8 KB
# Theming in bslib Basic theming for Shiny apps using `bs_theme()`. For comprehensive theming (Sass variables, custom rules, dark mode, dynamic theming), see the **shiny-bslib-theming** skill. ## Table of Contents - [Quick Start](#quick-start) - [Preset Themes](#preset-themes) - [Main Colors](#main-colors) - [Typography](#typography) - [Brand YAML](#brand-yaml) - [Theming R Plots](#theming-r-plots) ## Quick Start `bs_theme(version = 5)` uses `preset = "shiny"` by default — a polished theme designed to look good for most Shiny apps. Start here, especially when modernizing a stock Shiny app: ```r page_sidebar( theme = bs_theme(version = 5), # "shiny" preset by default ... ) ``` ## Preset Themes ### The "shiny" preset (default) `bs_theme(version = 5)` defaults to `preset = "shiny"`, which is specifically designed for Shiny apps and looks professional without any extra configuration. Recommend this as the starting point. ### Bootswatch presets For a different visual style, use a Bootswatch preset via `bs_theme(version = 5, preset = "<name>")`. Popular options include `"minty"` (soft green), `"cosmo"` (clean and modern), `"darkly"` (dark background), and `"zephyr"` (light, airy). List all options with `bootswatch_themes()`. Choose one that fits the app's purpose and audience — don't apply one by default. The `bootswatch` argument is an alias for `preset`. ## Main Colors The most influential color parameters — changing these affects hundreds of CSS rules: | Parameter | Description | |---|---| | `bg` | Background color | | `fg` | Foreground (text) color | | `primary` | Primary brand color (links, nav active, input focus) | | `secondary` | Default for action buttons | | `success` | Positive/success states | | `info` | Informational content | | `warning` | Warnings | | `danger` | Errors/destructive actions | ```r bs_theme( bg = "#FFFFFF", fg = "#212529", primary = "#2c3e50", success = "#27ae60", danger = "#e74c3c" ) ``` **Tips:** - `bg`/`fg`: similar hue, large luminance difference - `primary`: should contrast well with both `bg` and `fg` ## Typography Three font arguments: `base_font`, `heading_font`, `code_font`. Each accepts `font_google("Name")` (most common), `font_link()` (custom URL), `font_face()` (local files), or `font_collection()` (fallback stacks). ## Brand YAML bslib auto-discovers `_brand.yml` in your app directory. No code changes needed. ```r bs_theme(brand = FALSE) # Disable auto-discovery ``` Requires the `brand.yml` R package. See the **brand-yml** skill for creating `_brand.yml` files. ## Theming R Plots `bs_theme()` only affects CSS. Use the `thematic` package to auto-match R plots: ```r library(thematic) thematic_shiny(font = "auto") # Call before shinyApp() shinyApp(ui, server) ``` Works with base R, ggplot2, and lattice. -
toasts.md 4.3 KB
# Toast Notifications in bslib Toasts are lightweight, temporary notification messages that appear in a corner of the screen. Based on [Bootstrap 5.3's toast component](https://getbootstrap.com/docs/5.3/components/toasts/). Try `shiny::runExample("toast", package = "bslib")` for a complete demo. ## Table of Contents - [Basic Usage](#basic-usage) - [Key Parameters](#key-parameters) - [Showing and Hiding Toasts](#showing-and-hiding-toasts) - [Common Patterns](#common-patterns) - [Best Practices](#best-practices) ## Basic Usage Pass a string directly to `show_toast()` for a plain notification, or pass a `toast()` object for full control. The `type` argument (`"success"`, `"danger"`, `"warning"`, `"info"`, `"primary"`, etc.) sets the background color automatically. ```r # String shorthand show_toast("Operation completed!") # Full control with type and header show_toast( toast( "Your results are ready to view.", header = "Analysis Complete", type = "success" ) ) ``` Use `toast_header()` to add an icon and status text to the header: ```r toast( "Your settings have been saved.", header = toast_header( title = "Settings Updated", icon = bsicons::bs_icon("gear"), status = "just now" ), type = "success" ) ``` ## Key Parameters ### toast() | Parameter | Default | Description | |---|---|---| | `header` | `NULL` | String or `toast_header()` object | | `icon` | `NULL` | Icon element (when not using `toast_header()`) | | `id` | auto | Stable ID for `hide_toast()` or replacing visible toasts | | `type` | `NULL` | `"success"`, `"danger"`, `"warning"`, `"info"`, `"primary"`, etc. | | `duration_s` | `5` | Seconds before auto-hide. Use `0` or `NA` to disable auto-hide | | `position` | `"top-right"` | e.g. `"bottom-right"`, `"top-center"`, `"middle-center"` | | `closable` | `TRUE` | Show close button | ### toast_header() | Parameter | Description | |---|---| | `title` | Header text (required) | | `icon` | Optional icon element | | `status` | Optional small muted text on right side (e.g. "just now") | ## Showing and Hiding Toasts `show_toast()` displays a toast and returns its ID. `hide_toast()` dismisses a toast by ID. Use a stable `id` when you need to hide a toast programmatically — for example, to replace a "Processing..." toast with a completion message: ```r observeEvent(input$start, { show_toast( toast( "Processing...", id = "progress_toast", duration_s = NA, closable = FALSE ) ) result <- expensive_computation() hide_toast("progress_toast") }) ``` **Replacing toasts:** If a toast with the same `id` is already visible, showing a new one with that `id` automatically hides the old one first. ## Common Patterns ### Success/Error Feedback ```r observeEvent(input$save, { tryCatch({ save_data(data()) show_toast( toast("Data saved successfully.", header = "Saved", type = "success") ) }, error = function(e) { show_toast( toast( paste("Failed to save:", e$message), header = "Error", type = "danger", duration_s = NA # Don't auto-hide errors ) ) }) }) ``` ### Progress then Completion ```r observeEvent(input$export, { show_toast( toast("Exporting data...", id = "export", duration_s = NA, closable = FALSE) ) export_data() hide_toast("export") show_toast( toast("File downloaded.", header = "Export Complete", type = "success") ) }) ``` ### Toast with Interactive Content ```r show_toast( toast( actionLink("undo_delete", "Undo"), header = "Item Deleted", id = "undo_toast", duration_s = 10, closable = FALSE ) ) observeEvent(input$undo_delete, { restore_item() hide_toast("undo_toast") }) ``` ## Best Practices **Be specific:** ```r # Good show_toast(toast("Results saved to output.csv", header = "Analysis Complete", type = "success")) # Too vague show_toast("Done") ``` **Set appropriate durations:** - Success messages: 3-5 seconds (default `duration_s = 5`) - Error messages: `duration_s = NA` (let user read and dismiss) - Progress updates: `duration_s = NA` + `closable = FALSE` until complete **Use sparingly:** Don't toast every minor action. Combine related events. Avoid overload. **Position consistently:** Use the same `position` throughout your app (default: `"top-right"`). **Accessibility:** ARIA live regions are automatic. Don't rely solely on color for meaning. -
toolbars.md 6.4 KB
# Toolbars in bslib Toolbars are compact horizontal strips of controls — buttons, selects, and dividers — designed for card headers and footers. Toolbar inputs are visually compact and integrate with the card's header/footer styling, unlike full-width sidebar controls. ## Table of Contents - [Basic Usage](#basic-usage) - [toolbar()](#toolbar) - [toolbar_input_button()](#toolbar_input_button) - [toolbar_input_select()](#toolbar_input_select) - [toolbar_divider()](#toolbar_divider) - [Server-Side Updates](#server-side-updates) - [Placement Patterns](#placement-patterns) - [Info Icon Labels](#info-icon-labels) ## Basic Usage A toolbar in a card header with a select and a button: ```r card( full_screen = TRUE, card_header( "Sales Trend", toolbar( toolbar_input_select("period", "Period", choices = c("Daily", "Weekly", "Monthly"), selected = "Monthly" ), toolbar_divider(), toolbar_input_button("download", "Download", icon = bsicons::bs_icon("download") ) ) ), plotOutput("trend_plot") ) ``` ## toolbar() ```r toolbar(..., align = c("right", "left"), gap = NULL, width = NULL) ``` Container for toolbar elements. Defaults to right-aligned within its parent (e.g., pushed to the right end of a card header). | Parameter | Default | Description | |---|---|---| | `...` | | Toolbar elements: buttons, selects, dividers, or arbitrary HTML | | `align` | `"right"` | Alignment within the parent: `"right"` or `"left"` | | `gap` | `NULL` | CSS length unit for spacing between elements (e.g., `"0.5rem"`) | | `width` | `NULL` | CSS width; defaults to `100%` | ## toolbar_input_button() ```r toolbar_input_button( id, label, icon = NULL, show_label = is.null(icon), tooltip = !show_label, ..., disabled = FALSE, border = FALSE ) ``` A compact button for use inside `toolbar()`. When an icon is provided, the label is hidden by default and shown as a tooltip instead — keeping the toolbar visually tight while remaining accessible. | Parameter | Default | Description | |---|---|---| | `id` | | Input ID. Behaves like `actionButton()` — increments on click | | `label` | | Button label (shown or used as tooltip text) | | `icon` | `NULL` | Icon element, e.g. `bsicons::bs_icon("download")` | | `show_label` | `is.null(icon)` | Show label text; defaults to `TRUE` when no icon | | `tooltip` | `!show_label` | Show label as hover tooltip when label is hidden | | `disabled` | `FALSE` | Prevent clicks | | `border` | `FALSE` | Show a border around the button | **Reading the value:** `input$id` increments like `actionButton()`. Use `observeEvent(input$id, ...)`. **Accessibility:** When `show_label = FALSE`, the label is still used as tooltip text (`tooltip = TRUE` by default) so screen reader and keyboard users can identify the button. ## toolbar_input_select() ```r toolbar_input_select( id, label, choices, ..., selected = NULL, icon = NULL, show_label = FALSE, tooltip = !show_label ) ``` A compact select input for use inside `toolbar()`. The label is hidden by default (`show_label = FALSE`) and shown as a tooltip, keeping the toolbar uncluttered when context makes the purpose obvious. | Parameter | Default | Description | |---|---|---| | `id` | | Input ID. `input$id` returns the selected value | | `label` | | Label (used as tooltip when `show_label = FALSE`) | | `choices` | | Character vector or named list of choices | | `selected` | `NULL` | Initially selected value (defaults to first choice) | | `icon` | `NULL` | Optional icon shown alongside the select | | `show_label` | `FALSE` | Show label text inline | | `tooltip` | `!show_label` | Show label as tooltip when hidden | **Reading the value:** `input$id` returns the selected value as a string. ## toolbar_divider() ```r toolbar_divider(..., width = NULL, gap = NULL) ``` A thin vertical rule for visually grouping toolbar elements. | Parameter | Default | Description | |---|---|---| | `width` | `"2px"` | CSS width of the divider line | | `gap` | `"1rem"` | CSS spacing on either side of the divider | ## Server-Side Updates ### update_toolbar_input_button() ```r update_toolbar_input_button( id, label = NULL, show_label = NULL, icon = NULL, disabled = NULL, session = get_current_session() ) ``` Update button appearance or state from the server. Pass only the parameters you want to change; `NULL` leaves them unchanged. **Example — toggle icon on click:** ```r chart_type <- reactiveVal("bar") observeEvent(input$toggle_type, { new_type <- if (chart_type() == "bar") "line" else "bar" chart_type(new_type) update_toolbar_input_button("toggle_type", icon = bsicons::bs_icon(if (new_type == "bar") "bar-chart" else "graph-up") ) }) ``` **Example — conditionally disable:** ```r observe({ update_toolbar_input_button("export", disabled = nrow(filtered_data()) == 0 ) }) ``` ### update_toolbar_input_select() ```r update_toolbar_input_select( id, label = NULL, show_label = NULL, choices = NULL, selected = NULL, icon = NULL, session = get_current_session() ) ``` Update select choices, selection, or label from the server. **Example — cascading selects:** ```r observeEvent(input$region, { update_toolbar_input_select("store", choices = stores_for_region(input$region) ) }) ``` ## Placement Patterns ### Card Footer ```r card( card_header("Plot"), plotOutput("plot"), card_footer( toolbar( toolbar_input_button("save", "Save", icon = bsicons::bs_icon("floppy") ), toolbar_input_button("share", "Share", icon = bsicons::bs_icon("share") ), toolbar_divider(), toolbar_input_button("fullscreen", "Expand", icon = bsicons::bs_icon("arrows-fullscreen") ) ) ) ) ``` ## Info Icon Labels A toolbar is the cleanest way to add a help tooltip to an input label. Wrap the label text and an info icon together in a `toolbar()` as the `label` argument: ```r selectInput( "metric", label = toolbar( align = "left", gap = "0.25rem", "Metric", tooltip( bsicons::bs_icon("info-circle", title = "About this metric"), "Revenue includes all recognized sales net of returns and discounts." ) ), choices = c("Revenue", "Units", "Margin") ) ``` This works with any Shiny input that takes an HTML `label` argument. The `title` on `bs_icon()` provides accessible text for screen readers — see the main skill's Icons section for accessibility guidance on icon-only triggers. -
tooltips-popovers.md 4.5 KB
# Tooltips and Popovers in bslib Tooltips and popovers add contextual information and secondary controls to your UI. Tooltips are hover-triggered read-only messages; popovers are click-triggered containers that can hold interactive content. ## Table of Contents - [Tooltips](#tooltips) - [Popovers](#popovers) - [Choosing Between Tooltips and Popovers](#choosing-between-tooltips-and-popovers) - [Best Practices](#best-practices) ## Tooltips ### Basic Usage Wrap any UI element in `tooltip()` to add a hover message: ```r tooltip( actionButton("analyze", "Analyze"), "Run the analysis on the selected data" ) ``` **Key insight:** `tooltip()` uses the **last HTML element** in its first argument as the trigger. This means you can place an icon next to text using `span()` or `tagList()`, and only the icon becomes the trigger. Common placements: wrap an `bsicons::bs_icon("info-circle")` inside `card_header()`, use `span("Label", bsicons::bs_icon("info-circle"))` as an input label, or nest inside a `value_box()` `title`. ### Dynamic Tooltips **`toggle_tooltip()`** shows or hides a tooltip programmatically. **`update_tooltip()`** changes its content: ```r # UI tooltip(id = "help_tip", actionButton("analyze", "Analyze"), "Click to run analysis") # Server observe({ toggle_tooltip("help_tip", show = TRUE) }) |> bindEvent(once = TRUE) observeEvent(input$update_status, { update_tooltip("status_tip", paste("Last updated:", Sys.time())) }) ``` ## Popovers ### Basic Usage `popover()` accepts any content as additional arguments and an optional `title`: ```r popover( actionButton("help", "Help"), title = "Getting Started", tags$ul( tags$li("Step 1: Select data"), tags$li("Step 2: Choose parameters"), tags$li("Step 3: Run analysis") ) ) ``` ### Common Patterns **Input toolbars in card headers** -- secondary controls that don't warrant sidebar space: ```r card( full_screen = TRUE, card_header( "Sales Analysis", popover( bsicons::bs_icon("gear"), title = "Plot Options", selectInput("color_scheme", "Colors", c("default", "viridis", "plasma")), checkboxInput("show_trend", "Show trend line"), sliderInput("alpha", "Transparency", min = 0, max = 1, value = 0.8) ) ), plotOutput("sales_plot") ) ``` **Editable card titles:** ```r card( card_header( uiOutput("card_title"), popover( bsicons::bs_icon("pencil"), title = "Edit Title", textInput("new_title", "Title", value = "My Plot"), actionButton("save_title", "Save") ) ), plotOutput("plot") ) ``` ### Dynamic Popovers **`toggle_popover()`** and **`update_popover()`** work like their tooltip counterparts: ```r # UI popover( id = "welcome_pop", actionButton("start", "Start"), title = "Welcome!", "Click Start to begin the analysis." ) # Server observe({ toggle_popover("welcome_pop", show = TRUE) }) |> bindEvent(once = TRUE) observeEvent(input$start, { toggle_popover("welcome_pop", show = FALSE) }) observeEvent(input$run, { update_popover("progress_pop", "Running analysis...") }) ``` ## Choosing Between Tooltips and Popovers | Feature | Tooltip | Popover | |---------|---------|---------| | **Trigger** | Hover/focus | Click | | **Persistence** | Disappears quickly | Remains until dismissed | | **Content** | Text only (read-only) | Rich content (interactive) | | **Use case** | Quick help | Secondary UI | | **User effort** | Passive | Active | **Rule of thumb:** Use tooltips for small read-only messages, and popovers when the user should interact with the content. ### Popovers vs Modals - **Popovers:** Non-blocking -- users can interact with other UI while open - **Modals:** Blocking -- users must address modal before continuing - Use modals when users must complete an action (confirm deletion, submit form) ## Best Practices ### Tooltips - Keep concise: 1-2 sentences maximum - Use consistent icon placement (prefer info-circle next to label) - Test on mobile (consider popovers as mobile alternative) ### Popovers - Limit to 2-4 inputs; use modal for complex forms - Always provide clear titles - Don't use hyperlinks as triggers (conflicts with click behavior): ```r # Bad popover(tags$a("Link"), "Content") # Good - icon next to link tagList( tags$a("Link", href = "#"), popover(bsicons::bs_icon("info-circle"), "Context about link") ) ``` ### Accessibility - **Tooltips:** Keyboard accessible (built-in), provide alt text for icon triggers - **Popovers:** Keyboard dismissible (Esc key), focus management automatic -
value-boxes.md 10.3 KB
# Value Boxes in bslib Value boxes are specialized card-like components designed for displaying key metrics, KPIs, and statistics in dashboards. They provide a focused, scannable way to communicate important numbers. ## Table of Contents - [Core Components](#core-components) - [Basic Usage](#basic-usage) - [Showcase Options](#showcase-options) - [Theming](#theming) - [Dashboard Layouts](#dashboard-layouts) - [Dynamic Rendering in Shiny](#dynamic-rendering-in-shiny) - [Expandable Sparklines](#expandable-sparklines) - [Best Practices](#best-practices) ## Core Components A `value_box()` has five main parts: 1. **`title`** — descriptive label (e.g., "Total Users", "Revenue", "Growth Rate") 2. **`value`** — the primary metric displayed prominently (e.g., "1,234", "$56K", "+12%") 3. **`showcase`** — optional icon or plot displayed alongside the value 4. **`theme`** — optional appearance customization 5. **`...`** (additional arguments) — extra text/UI elements rendered below the value ## Basic Usage **Simple value box:** ```r value_box( title = "Total Users", value = "1,234" ) ``` **With additional context:** ```r value_box( title = "Monthly Revenue", value = "$56,789", "Up 12% from last month" ) ``` **Multiple value boxes in a layout:** ```r layout_column_wrap( width = 1/3, value_box(title = "Users", value = "1,234", theme = "primary"), value_box(title = "Sessions", value = "5,678", theme = "info"), value_box(title = "Conversion", value = "4.5%", theme = "success") ) ``` ## Showcase Options The `showcase` parameter accepts icons or small plots. Three layout functions control positioning: ### Showcase Layouts Pass `showcase_layout` one of three options: `showcase_left_center()` (default), `showcase_top_right()`, or `showcase_bottom()`. String shorthands `"left center"`, `"top right"`, and `"bottom"` also work. Each function accepts optional `width` and `max_height` parameters for fine-grained control over the showcase area dimensions. ```r value_box( title = "New Users", value = "487", showcase = bsicons::bs_icon("person-plus"), showcase_layout = showcase_left_center() ) ``` ### Icons Use `bsicons::bs_icon()` (designed for Bootstrap) or `fontawesome::fa()` as the `showcase` value. Common dashboard icons: - Users: `bs_icon("people")`, `bs_icon("person")` - Money: `bs_icon("currency-dollar")`, `bs_icon("cash")` - Trends: `bs_icon("graph-up")`, `bs_icon("graph-down")`, `bs_icon("arrow-up")` - Status: `bs_icon("check-circle")`, `bs_icon("x-circle")`, `bs_icon("exclamation-triangle")` - Activity: `bs_icon("activity")`, `bs_icon("clock")`, `bs_icon("calendar")` ### Plots as Showcase Small sparkline plots work well in the showcase: ```r library(sparkline) value_box( title = "Daily Users", value = "1,234", showcase = sparkline::sparkline(daily_users_vector), showcase_layout = "left center" ) ``` See [Expandable Sparklines](#expandable-sparklines) for advanced patterns. ## Theming The `theme` argument accepts a string class name or `value_box_theme()` for custom colors. **Background themes** set a solid colored background. Strings without a `bg-` or `text-` prefix get `bg-` prepended automatically, so `"success"` and `"bg-success"` are equivalent. Semantic options: `"primary"`, `"secondary"`, `"success"`, `"danger"`, `"warning"`, `"info"`. Named color options: `"blue"`, `"indigo"`, `"purple"`, `"pink"`, `"red"`, `"orange"`, `"yellow"`, `"green"`, `"teal"`, `"cyan"`. **Foreground themes** (`text-*`) use a white/light background with colored text — less visually dominant than solid backgrounds. Examples: `"text-success"`, `"text-primary"`, `"text-purple"`, `"text-danger"`. **Gradient themes** use Bootstrap 5's named colors. Every non-identical pair is available as `"bg-gradient-{from}-{to}"`, e.g. `"bg-gradient-blue-purple"`, `"bg-gradient-orange-red"`, `"bg-gradient-teal-cyan"`. Gradient themes require Bootstrap 5 and are not available with Bootstrap 3/4 themes. **Custom theme** with `value_box_theme()`: ```r theme = value_box_theme(bg = "#1f77b4", fg = "#ffffff") ``` Omit `fg` to auto-compute a contrasting foreground color. **Interactive builder:** Visit bslib.shinyapps.io/build-a-box to explore all theme options and copy generated code. ## Dashboard Layouts ### With layout_column_wrap() **Fixed columns:** ```r layout_column_wrap( width = 1/4, # 4 columns value_box(title = "Metric 1", value = "1,234"), value_box(title = "Metric 2", value = "5,678"), value_box(title = "Metric 3", value = "9,012"), value_box(title = "Metric 4", value = "3,456") ) ``` **Responsive columns:** ```r layout_column_wrap( width = "250px", # Auto-wraps based on screen size value_box(title = "Users", value = "1,234"), value_box(title = "Revenue", value = "$56K"), value_box(title = "Growth", value = "+12%") ) ``` ### With layout_columns() **Custom proportions:** ```r layout_columns( col_widths = c(6, 3, 3), card( card_header("Main Content"), plotOutput("main_plot") ), value_box(title = "KPI 1", value = "123"), value_box(title = "KPI 2", value = "456") ) ``` ### In Filling Layouts When embedding value boxes within a larger filling layout, set `fill = FALSE` on the layout container to prevent boxes from consuming excess vertical space: ```r page_fillable( # Value boxes at top - don't fill layout_column_wrap( width = 1/3, fill = FALSE, # Important! value_box(title = "Users", value = "1,234"), value_box(title = "Sessions", value = "5,678"), value_box(title = "Revenue", value = "$90K") ), # Main content fills remaining space card( card_header("Detailed Analysis"), plotlyOutput("analysis") ) ) ``` This allows the card below to fill the remaining vertical space. ## Dynamic Rendering in Shiny **Best practice:** Wrap dynamic content in `textOutput()` as a placeholder to reduce layout shift: **Good:** ```r # UI value_box( title = "Active Users", value = textOutput("user_count"), showcase = bs_icon("people") ) # Server output$user_count <- renderText({ # Calculate value nrow(filtered_data()) }) ``` **With additional dynamic content:** ```r # UI value_box( title = "Revenue", value = textOutput("revenue_value"), textOutput("revenue_change"), showcase = bs_icon("currency-dollar"), theme = "success" ) # Server output$revenue_value <- renderText({ paste0("$", format(sum(data$revenue), big.mark = ",")) }) output$revenue_change <- renderText({ change <- calculate_change() paste0(ifelse(change > 0, "+", ""), round(change * 100, 1), "% vs last month") }) ``` ## Expandable Sparklines Since `value_box()` is implemented using `card()`, it inherits `full_screen` capabilities. This is particularly useful for sparklines: **Basic expandable sparkline:** ```r value_box( title = "Daily Traffic", value = textOutput("current_traffic"), showcase = plotOutput("traffic_sparkline", height = "100%"), showcase_layout = "left center", full_screen = TRUE ) ``` ### Responsive Sparkline Rendering **With Shiny - different plots for different sizes:** ```r # Server output$traffic_sparkline <- renderPlot({ info <- getCurrentOutputInfo() if (info$height() > 200) { # Expanded view: full chart with axes and labels ggplot(traffic_data, aes(date, visits)) + geom_line(color = "steelblue", linewidth = 1) + geom_area(fill = "steelblue", alpha = 0.2) + labs(x = "Date", y = "Visits", title = "Traffic Over Time") + theme_minimal() } else { # Compact view: minimal sparkline ggplot(traffic_data, aes(date, visits)) + geom_line(color = "steelblue") + theme_void() } }) ``` **Without Shiny - JavaScript approach:** ```r library(htmlwidgets) sparkline_widget <- htmlwidgets::createWidget(...) sparkline_widget |> htmlwidgets::onRender( "function(el) { el.closest('.bslib-value-box') .addEventListener('bslib.card', function(ev) { if (ev.detail.fullScreen) { // modify plot for full screen appearance } else { // trim plot for small style in value box } }) }" ) ``` ## Best Practices ### Keep Values Concise Value boxes work best with short, scannable values: **Good:** - "1.2K", "$56K", "+18%", "98.5%" **Avoid:** - "1,234 active users in the last 30 days" - Very long numbers without abbreviation ### Use Appropriate Number Formatting ```r # In server output$revenue <- renderText({ scales::dollar(sum(data$revenue), scale = 1e-3, suffix = "K") }) output$users <- renderText({ scales::comma(nrow(users_data)) }) output$rate <- renderText({ scales::percent(success_rate, accuracy = 1) }) ``` ### Choose Meaningful Themes Match theme to context: - `"success"` for positive metrics (growth, success rate) - `"danger"` for alerts or problems - `"warning"` for cautionary metrics - `"primary"` for neutral key metrics - `"info"` for informational metrics ### Group Related Metrics Use `layout_column_wrap()` to group related value boxes: ```r layout_column_wrap( width = 1/3, value_box(title = "New Users", value = "487", theme = "primary"), value_box(title = "Returning Users", value = "1,234", theme = "info"), value_box(title = "Total Sessions", value = "5,678", theme = "secondary") ) ``` ### Position Value Boxes Appropriately **Top of dashboard:** Most common placement for KPIs ```r page_sidebar( title = "Dashboard", sidebar = sidebar(...), # Value boxes at top layout_column_wrap(width = 1/4, fill = FALSE, ...), # Detailed content below card(...) ) ``` **Within sections:** Group with related content ```r card( card_header("Sales Performance"), layout_column_wrap( width = 1/3, value_box(title = "Revenue", value = "$125K"), value_box(title = "Orders", value = "487"), value_box(title = "AOV", value = "$256") ), plotOutput("sales_trend") ) ``` ### Use Showcase Strategically **Icons:** Use for quick visual identification **Sparklines:** Use when trends matter as much as current values **No showcase:** Valid choice for clean, minimal design ### Test Responsive Behavior Always check how value boxes look at different screen widths: - Desktop (4+ columns) - Tablet (2-3 columns) - Mobile (1 column) ### Consider Accessibility - Use clear, descriptive titles - Ensure sufficient color contrast (handled automatically by themes) - Don't rely solely on color to convey meaning - Test with screen readers if building public-facing apps
-
-
SKILL.md 9.7 KB
--- name: shiny-bslib description: Build modern Shiny dashboards and applications using bslib (Bootstrap 5). Use when creating new Shiny apps, modernizing legacy apps (fluidPage, fluidRow/column, tabsetPanel, wellPanel, shinythemes), or working with bslib page layouts, grid systems, cards, value boxes, navigation, sidebars, filling layouts, theming, accordions, tooltips, popovers, toasts, or bslib inputs. Assumes familiarity with basic Shiny. metadata: author: Garrick Aden-Buie (@gadenbuie) version: "1.0" license: MIT --- # Modern Shiny Apps with bslib Build professional Shiny dashboards using bslib's Bootstrap 5 components and layouts. This skill focuses on modern UI/UX patterns that replace legacy Shiny approaches. ## Quick Start **Single-page dashboard:** ```r library(shiny) library(bslib) ui <- page_sidebar( title = "My Dashboard", theme = bs_theme(version = 5), # "shiny" preset by default sidebar = sidebar( selectInput("variable", "Variable", choices = names(mtcars)) ), layout_column_wrap( width = 1/3, fill = FALSE, value_box(title = "Users", value = "1,234", theme = "primary"), value_box(title = "Revenue", value = "$56K", theme = "success"), value_box(title = "Growth", value = "+18%", theme = "info") ), card( full_screen = TRUE, card_header("Plot"), plotOutput("plot") ) ) server <- function(input, output, session) { output$plot <- renderPlot({ hist(mtcars[[input$variable]], main = input$variable) }) } shinyApp(ui, server) ``` **Multi-page dashboard:** ```r ui <- page_navbar( title = "Analytics Platform", theme = bs_theme(version = 5), nav_panel("Overview", overview_ui), nav_panel("Analysis", analysis_ui), nav_panel("Reports", reports_ui) ) ``` ## Core Concepts ### Page Layouts - **`page_sidebar()`** -- Single-page dashboard with sidebar (most common) - **`page_navbar()`** -- Multi-page app with top navigation bar - **`page_fillable()`** -- Viewport-filling layout for custom arrangements - **`page_fluid()`** -- Scrolling layout for long-form content See [page-layouts.md](references/page-layouts.md) for detailed guidance. ### Grid Systems - **`layout_column_wrap()`** -- Uniform grid with auto-wrapping (recommended for most cases) - **`layout_columns()`** -- 12-column Bootstrap grid with precise control See [grid-layouts.md](references/grid-layouts.md) for detailed guidance. ### Cards Primary container for dashboard content. Support headers, footers, multiple body sections, and full-screen expansion. See [cards.md](references/cards.md) for detailed guidance. ### Value Boxes Display key metrics and KPIs with optional icons, sparklines, and built-in theming. See [value-boxes.md](references/value-boxes.md) for detailed guidance. ### Navigation - **Page-level**: `page_navbar()` for multi-page apps - **Component-level**: `navset_card_underline()`, `navset_tab()`, `navset_pill()` for tabbed content See [navigation.md](references/navigation.md) for detailed guidance. ### Sidebars - **Page-level**: `page_sidebar()` or `page_navbar(sidebar = ...)` - **Component-level**: `layout_sidebar()` within cards - Supports conditional content, dynamic open/close, accordions - `resizable = TRUE` by default — users can drag the edge to resize on desktop See [sidebars.md](references/sidebars.md) for detailed guidance. ### Filling Layouts The fill system controls how components resize to fill available space. Key concepts: fillable containers, fill items, fill carriers. Fill activates when containers have defined heights. See [filling.md](references/filling.md) for detailed guidance. ### Theming - **`bs_theme()`** with Bootswatch themes for quick styling - **Custom colors**: `bg`, `fg`, `primary` affect hundreds of CSS rules - **Fonts**: `font_google()` for typography - **Dynamic theming**: `input_dark_mode()` + `session$setCurrentTheme()` See [theming.md](references/theming.md) for detailed guidance. ### UI Components - **Accordions** -- Collapsible sections, especially useful in sidebars - **Tooltips** -- Hover-triggered help text - **Popovers** -- Click-triggered containers for secondary UI/inputs - **Toasts** -- Temporary notification messages - **Toolbars** -- Compact horizontal strips of buttons, selects, and dividers for card headers and footers See [accordions.md](references/accordions.md), [tooltips-popovers.md](references/tooltips-popovers.md), [toasts.md](references/toasts.md), and [toolbars.md](references/toolbars.md). ### Icons **Recommended: `bsicons` package** (Bootstrap Icons, designed for bslib): ```r bsicons::bs_icon("graph-up") bsicons::bs_icon("people", size = "2em") ``` Browse icons: https://icons.getbootstrap.com/ **Alternative: `fontawesome` package:** ```r fontawesome::fa("envelope") ``` **Accessibility for icon-only triggers:** When an icon is used as the sole trigger for a tooltip, popover, or similar interactive element (no accompanying text), it must be accessible to screen readers. By default, icon packages mark icons as decorative (`aria-hidden="true"`), which hides them from assistive technology. - **`bsicons::bs_icon()`**: Provide `title` — this automatically sets `a11y = "sem"` ```r tooltip( bs_icon("info-circle", title = "More information"), "Tooltip content here" ) ``` - **`fontawesome::fa()`**: Set `a11y = "sem"` and provide `title` ```r tooltip( fa("circle-info", a11y = "sem", title = "More information"), "Tooltip content here" ) ``` The `title` should describe the purpose of the trigger (e.g., "More information", "Settings"), not the icon itself (e.g., not "info circle icon"). ### Special Inputs - **`input_switch()`** -- Toggle switch (modern checkbox alternative) - **`input_dark_mode()`** -- Dark mode toggle - **`input_task_button()`** -- Button for long-running operations - **`input_code_editor()`** -- Code editor with syntax highlighting - **`input_submit_textarea()`** -- Textarea with explicit submission See [inputs.md](references/inputs.md) for detailed guidance. ## Common Workflows ### Building a Dashboard 1. Choose page layout: `page_sidebar()` (single-page) or `page_navbar()` (multi-page) 2. Add theme with `bs_theme()` (consider Bootswatch for quick start) 3. Create sidebar with inputs for filtering/controls 4. Add value boxes at top for key metrics (set `fill = FALSE` on container) 5. Arrange cards with `layout_column_wrap()` or `layout_columns()` 6. Enable `full_screen = TRUE` on all visualization cards 7. Add `thematic::thematic_shiny()` for plot theming ### Modernizing an Existing App See [migration.md](references/migration.md) for a complete mapping of legacy patterns to modern equivalents. Key steps: 1. Replace `fluidPage()` with `page_sidebar()` or `page_navbar()` 2. Replace `fluidRow()`/`column()` with `layout_columns()` 3. Wrap outputs in `card(full_screen = TRUE)` 4. Add `theme = bs_theme(version = 5)` 5. Convert key metrics to `value_box()` components 6. Replace `tabsetPanel()` with `navset_card_underline()` ## Guidelines 1. **Prefer bslib page functions** (`page_sidebar()`, `page_navbar()`, `page_fillable()`, `page_fluid()`) over legacy equivalents (`fluidPage()`, `navbarPage()`) 2. **Use `layout_column_wrap()` or `layout_columns()`** for grid layouts instead of `fluidRow()`/`column()`, which don't support filling layouts 3. **Wrap outputs in `card(full_screen = TRUE)`** when building dashboards -- full-screen expansion is a high-value feature 4. **Set `fill = FALSE`** on `layout_column_wrap()` containers holding value boxes (they shouldn't stretch to fill height) 5. **Pin Bootstrap version**: include `theme = bs_theme(version = 5)` or a preset theme 6. **Use `thematic::thematic_shiny()`** in the server so base R and ggplot2 plots match the app theme 7. **Use responsive widths** like `width = "250px"` in `layout_column_wrap()` for auto-adjusting columns 8. **Group sidebar inputs** with `accordion()` when sidebars have many controls 9. **See [migration.md](references/migration.md)** for mapping legacy Shiny patterns to modern bslib equivalents ## Avoid Common Errors 1. Avoid directly nesting `card()` containers. `navset_card_*()` functions are already cards; `nav_panel()` content goes directly inside them without wrapping in `card()` 2. Only use `layout_columns()` and `layout_column_wrap()` for laying out multiple elements. Single children should be passed directly to their container functions. 3. Never nest `page_*()` functions. Only use one top-level page function per app. ## Reference Files - **[migration.md](references/migration.md)** -- Legacy Shiny to modern bslib migration guide - **[page-layouts.md](references/page-layouts.md)** -- Page-level layout functions and patterns - **[grid-layouts.md](references/grid-layouts.md)** -- Multi-column grid systems - **[cards.md](references/cards.md)** -- Card components and features - **[value-boxes.md](references/value-boxes.md)** -- Value boxes for metrics and KPIs - **[navigation.md](references/navigation.md)** -- Navigation containers and patterns - **[sidebars.md](references/sidebars.md)** -- Sidebar layouts and organization - **[filling.md](references/filling.md)** -- Fillable containers and fill items - **[theming.md](references/theming.md)** -- Basic theming (colors, fonts, Bootswatch). See **shiny-bslib-theming** skill for advanced theming - **[accordions.md](references/accordions.md)** -- Collapsible sections and sidebar organization - **[tooltips-popovers.md](references/tooltips-popovers.md)** -- Hover tooltips and click-triggered popovers - **[toasts.md](references/toasts.md)** -- Temporary notification messages - **[toolbars.md](references/toolbars.md)** -- Toolbar components for card headers and footers - **[inputs.md](references/inputs.md)** -- Special bslib input widgets - **[best-practices.md](references/best-practices.md)** -- bslib-specific patterns and common gotchas
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.