From agent-almanac
Designs responsive Shiny app UIs with bslib theming, layout_columns grids, value boxes, cards, and custom CSS/SCSS. For new UIs, modernizing from fluidPage, brand theming, responsiveness, accessibility.
npx claudepluginhub pjt222/agent-almanacThis skill uses the workspace's default tool permissions.
---
Builds modern Shiny dashboards and apps using bslib (Bootstrap 5) layouts, grids, cards, value boxes, navigation, sidebars, theming. Modernizes legacy Shiny UIs like fluidPage or shinythemes.
Guides building SaaS dashboards, settings pages, data tables, and layouts using patterns for component selection (shadcn/ui + Tailwind), page composition, responsive design, dark mode, and UI states (loading, empty, error).
Share bugs, ideas, or general feedback.
Design responsive, accessible Shiny application interfaces using bslib theming, modern layout primitives, and custom CSS.
bslib provides several page constructors:
# Sidebar layout — most common for data apps
ui <- page_sidebar(
title = "My App",
sidebar = sidebar("Controls here"),
"Main content here"
)
# Navbar layout — for multi-page apps
ui <- page_navbar(
title = "My App",
nav_panel("Tab 1", "Content 1"),
nav_panel("Tab 2", "Content 2"),
nav_spacer(),
nav_item(actionButton("help", "Help"))
)
# Fillable layout — content fills available space
ui <- page_fillable(
card(
full_screen = TRUE,
plotOutput("plot")
)
)
# Dashboard layout — grid of value boxes and cards
ui <- page_sidebar(
title = "Dashboard",
sidebar = sidebar(open = "closed", "Filters"),
layout_columns(
fill = FALSE,
value_box("Revenue", "$1.2M", theme = "primary"),
value_box("Users", "4,521", theme = "success"),
value_box("Uptime", "99.9%", theme = "info")
),
layout_columns(
card(plotOutput("chart1")),
card(plotOutput("chart2"))
)
)
Expected: Page layout matches the application's navigation and content needs.
On failure: If the layout doesn't look right, check that you're using page_sidebar() / page_navbar() (bslib) not fluidPage() / navbarPage() (base shiny). The bslib versions have better defaults and theming support.
my_theme <- bslib::bs_theme(
version = 5, # Bootstrap 5
bootswatch = "flatly", # Optional preset theme
bg = "#ffffff", # Background color
fg = "#2c3e50", # Foreground (text) color
primary = "#2c3e50", # Primary brand color
secondary = "#95a5a6", # Secondary color
success = "#18bc9c",
info = "#3498db",
warning = "#f39c12",
danger = "#e74c3c",
base_font = bslib::font_google("Source Sans Pro"),
heading_font = bslib::font_google("Source Sans Pro", wght = 600),
code_font = bslib::font_google("Fira Code"),
"navbar-bg" = "#2c3e50"
)
ui <- page_sidebar(
theme = my_theme,
title = "Themed App",
# ...
)
Use the interactive theme editor during development:
bslib::bs_theme_preview(my_theme)
Expected: App renders with consistent brand colors, fonts, and Bootstrap 5 components.
On failure: If fonts don't load, check internet access (Google Fonts requires it) or switch to system fonts: font_collection("system-ui", "-apple-system", "Segoe UI"). If theme variables don't apply, check that you're passing theme to the page function.
ui <- page_sidebar(
theme = my_theme,
title = "Analysis Dashboard",
sidebar = sidebar(
width = 300,
title = "Filters",
selectInput("dataset", "Dataset", choices = c("iris", "mtcars")),
sliderInput("sample", "Sample %", 10, 100, 100, step = 10),
hr(),
actionButton("refresh", "Refresh", class = "btn-primary w-100")
),
# KPI row — non-filling
layout_columns(
fill = FALSE,
col_widths = c(4, 4, 4),
value_box(
title = "Observations",
value = textOutput("n_obs"),
showcase = bsicons::bs_icon("table"),
theme = "primary"
),
value_box(
title = "Variables",
value = textOutput("n_vars"),
showcase = bsicons::bs_icon("columns-gap"),
theme = "info"
),
value_box(
title = "Missing",
value = textOutput("n_missing"),
showcase = bsicons::bs_icon("exclamation-triangle"),
theme = "warning"
)
),
# Main content row
layout_columns(
col_widths = c(8, 4),
card(
card_header("Distribution"),
full_screen = TRUE,
plotOutput("main_plot")
),
card(
card_header("Summary"),
tableOutput("summary_table")
)
)
)
Key layout primitives:
layout_columns() — responsive grid with col_widthscard() — content container with optional header/footervalue_box() — KPI display with icon and themelayout_sidebar() — nested sidebar within cardsnavset_card_tab() — tabbed cardsExpected: Responsive grid layout that adapts to screen size.
On failure: If columns stack unexpectedly on wide screens, check col_widths sum equals 12 (Bootstrap grid). If cards overlap, ensure fill = FALSE on non-filling rows.
server <- function(input, output, session) {
output$dynamic_filters <- renderUI({
data <- current_data()
tagList(
selectInput("col", "Column", choices = names(data)),
if (is.numeric(data[[input$col]])) {
sliderInput("range", "Range",
min = min(data[[input$col]], na.rm = TRUE),
max = max(data[[input$col]], na.rm = TRUE),
value = range(data[[input$col]], na.rm = TRUE)
)
} else {
selectInput("values", "Values",
choices = unique(data[[input$col]]),
multiple = TRUE
)
}
)
})
# Conditional panels (no server round-trip)
# In UI:
# conditionalPanel(
# condition = "input.show_advanced == true",
# numericInput("alpha", "Alpha", 0.05)
# )
}
Expected: UI elements update dynamically based on user selections and data.
On failure: If dynamic UI flickers, use conditionalPanel() (CSS-based) instead of renderUI() where possible. If dynamic inputs lose their values on re-render, add session$sendInputMessage() to restore state.
For styles beyond bslib theme variables:
# Inline CSS
ui <- page_sidebar(
theme = my_theme,
tags$head(tags$style(HTML("
.sidebar { border-right: 2px solid var(--bs-primary); }
.card-header { font-weight: 600; }
.value-box .value { font-size: 2.5rem; }
"))),
# ...
)
# External CSS file (place in www/ directory)
ui <- page_sidebar(
theme = my_theme,
tags$head(tags$link(rel = "stylesheet", href = "custom.css")),
# ...
)
For SCSS integration with bslib:
my_theme <- bslib::bs_theme(version = 5) |>
bslib::bs_add_rules(sass::sass_file("www/custom.scss"))
Expected: Custom styles applied without breaking bslib theming.
On failure: If custom CSS conflicts with bslib, use Bootstrap CSS variables (var(--bs-primary)) instead of hardcoded colors. This ensures theme changes propagate to custom styles.
# Add ARIA labels to inputs
selectInput("category", "Category",
choices = c("A", "B", "C")
) |> tagAppendAttributes(`aria-describedby` = "category-help")
# Add alt text to plots
output$plot <- renderPlot({
plot(data(), main = "Distribution of Values")
}, alt = "Histogram showing the distribution of selected values")
# Ensure sufficient color contrast in theme
my_theme <- bslib::bs_theme(
version = 5,
bg = "#ffffff", # White background
fg = "#212529" # Dark text — 15.4:1 contrast ratio
)
# Use semantic HTML
tags$main(
role = "main",
tags$h1("Dashboard"),
tags$section(
`aria-label` = "Key Performance Indicators",
layout_columns(
# value boxes...
)
)
)
Expected: App meets WCAG 2.1 AA standards for color contrast, keyboard navigation, and screen reader compatibility.
On failure: Test with browser dev tools accessibility audit (Lighthouse). Check color contrast ratios with WebAIM's contrast checker. Ensure all interactive elements are keyboard-focusable.
fluidPage() with bslib components. Use page_sidebar(), page_navbar(), or page_fillable() exclusively.var(--bs-primary) instead of #2c3e50. Hardcoded colors break when the theme changes.fill = FALSE on non-filling rows: Value box rows and summary rows usually shouldn't stretch to fill available space. Set fill = FALSE.font_google().layout_columns automatically stacks on narrow screens, but custom CSS may not.scaffold-shiny-app — initial app setup including theme configurationbuild-shiny-module — create modular UI componentsoptimize-shiny-performance — performance-conscious renderingreview-web-design — visual design review for layout, typography, and colourreview-ux-ui — usability and accessibility review