From r-dev
Build Shiny web applications in R. Use when creating or editing Shiny apps, modules, reactive logic, bslib layouts, dashboards, or LLM-powered chat apps.
How this skill is triggered — by the user, by Claude, or both
Slash command
/r-dev:shiny-devThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Shiny is an R framework for building interactive web applications. This skill covers modern Shiny (1.7+) with bslib layouts, modules, and current best practices.
Shiny is an R framework for building interactive web applications. This skill covers modern Shiny (1.7+) with bslib layouts, modules, and current best practices.
Detailed guides on specific topics (read via btw MCP or WebFetch when relevant):
Single-file app (app.R): simplest structure, everything in one file.
library(shiny)
library(bslib)
ui <- page_sidebar(
title = "My App",
sidebar = sidebar(
sliderInput("n", "Sample size", 10, 1000, 100)
),
card(
card_header("Histogram"),
plotOutput("plot")
)
)
server <- function(input, output, session) {
output$plot <- renderPlot({
hist(rnorm(input$n), main = NULL)
})
}
shinyApp(ui, server)
Multi-file app (ui.R + server.R): for larger apps. Files in R/ are auto-sourced.
myapp/
├── R/
│ ├── mod_sidebar.R
│ └── mod_plot.R
├── ui.R
├── server.R
└── global.R
Module pattern: the building block of scalable Shiny apps.
# R/mod_counter.R
counterUI <- function(id, label = "Counter") {
ns <- NS(id)
tagList(
actionButton(ns("button"), label = label),
verbatimTextOutput(ns("out"))
)
}
counterServer <- function(id) {
moduleServer(id, function(input, output, session) {
count <- reactiveVal(0)
observeEvent(input$button, {
count(count() + 1)
})
output$out <- renderText(count())
count
})
}
Using the module:
ui <- page_fluid(
counterUI("counter1", "Click me"),
counterUI("counter2", "Me too")
)
server <- function(input, output, session) {
counterServer("counter1")
counterServer("counter2")
}
Always use bslib for layouts in modern Shiny apps. Do NOT use fluidPage(), sidebarLayout(), or navbarPage() unless the project already uses them.
page_sidebar(..., sidebar, title, fillable, theme)Dashboard with sidebar. The default choice for most apps.
page_sidebar(
title = "Dashboard",
sidebar = sidebar(
title = "Controls",
selectInput("var", "Variable", choices = names(mtcars)),
checkboxInput("smooth", "Add smoother", TRUE)
),
layout_columns(
col_widths = c(6, 6),
card(card_header("Plot"), plotOutput("plot")),
card(card_header("Summary"), verbatimTextOutput("summary"))
)
)
page_fillable(..., title, theme, padding, gap, fillable_mobile)Full-height page that fills the browser. Good for maps, large visualizations.
page_fluid(..., title, theme)Standard Bootstrap fluid page. Good for document-style layouts.
page_navbar(..., title, sidebar, header, footer, theme)Multi-page app with a top navigation bar.
page_navbar(
title = "Multi-Page App",
nav_panel("Tab 1", plotOutput("plot1")),
nav_panel("Tab 2", tableOutput("table1")),
nav_spacer(),
nav_item(actionButton("about", "About"))
)
layout_columns(..., col_widths, row_heights, fill, fillable, gap, class)Grid layout. col_widths takes a vector summing to 12 (Bootstrap grid).
layout_columns(
col_widths = c(4, 8),
card("Narrow column"),
card("Wide column")
)
layout_column_wrap(..., width, fixed_width, heights_equal, fill, fillable, height, gap, class)Auto-wrapping grid. width sets the minimum column width.
layout_column_wrap(
width = "250px",
value_box("Metric 1", 42, showcase = bsicons::bs_icon("bar-chart")),
value_box("Metric 2", 87, showcase = bsicons::bs_icon("graph-up")),
value_box("Metric 3", 15, showcase = bsicons::bs_icon("clock"))
)
sidebar(title, ..., open, width, position, id, bg, fg)Sidebar panel. Use inside page_sidebar() or layout_sidebar().
card(..., full_screen, height, max_height, min_height, fill, class, wrapper, id)General-purpose container.
card(
full_screen = TRUE,
card_header("Interactive Plot"),
card_body(plotOutput("plot")),
card_footer("Source: mtcars dataset")
)
value_box(title, value, ..., showcase, showcase_layout, full_screen, theme, height)Metric display box with optional icon.
value_box(
title = "Total Sales",
value = textOutput("total"),
showcase = bsicons::bs_icon("currency-dollar"),
theme = "primary"
)
accordion(..., id, open, multiple, class)Collapsible sections.
accordion(
id = "filters",
accordion_panel("Date Range", dateRangeInput("dates", NULL)),
accordion_panel("Categories", checkboxGroupInput("cats", NULL, choices = letters[1:5]))
)
navset_card_tab(..., id, selected, title, sidebar, header, footer)Tabbed card. Also: navset_card_pill(), navset_card_underline().
navset_card_tab(
title = "Results",
nav_panel("Plot", plotOutput("plot")),
nav_panel("Table", tableOutput("table")),
nav_panel("Code", verbatimTextOutput("code"))
)
textInput(inputId, label, value, width, placeholder)textAreaInput(inputId, label, value, width, height, rows, placeholder, resize)numericInput(inputId, label, value, min, max, step, width)passwordInput(inputId, label, value, width, placeholder)selectInput(inputId, label, choices, selected, multiple, selectize, width, size) — dropdownselectizeInput(inputId, label, choices, selected, multiple, options, width) — enhanced dropdownradioButtons(inputId, label, choices, selected, inline, width, choiceNames, choiceValues)checkboxInput(inputId, label, value, width) — single checkboxcheckboxGroupInput(inputId, label, choices, selected, inline, width, choiceNames, choiceValues)sliderInput(inputId, label, min, max, value, step, round, ticks, animate, width, sep, pre, post, timeFormat, timezone, dragRange)dateInput(inputId, label, value, min, max, format, startview, weekstart, language, width, autoclose, datesdisabled, daysofweekdisabled)dateRangeInput(inputId, label, start, end, min, max, format, startview, weekstart, language, separator, width, autoclose)actionButton(inputId, label, icon, width, ...)actionLink(inputId, label, icon, ...)fileInput(inputId, label, multiple, accept, width, buttonLabel, placeholder, capture)downloadButton(outputId, label, class, icon, ...)uiOutput(outputId, container, fill, ...) / renderUI({ ... })insertUI(selector, where, ui, multiple, immediate, session)removeUI(selector, multiple, immediate, session)plotOutput(outputId, width, height, click, dblclick, hover, brush, fill) / renderPlot({ ... })tableOutput(outputId) / renderTable({ ... })dataTableOutput(outputId) / renderDataTable({ ... }) — use DT::DTOutput for full featurestextOutput(outputId, container, inline) / renderText({ ... })verbatimTextOutput(outputId, placeholder) / renderPrint({ ... })imageOutput(outputId, width, height, click, dblclick, hover, brush, fill) / renderImage({ ... })htmlOutput(outputId, container, fill, ...) / renderUI({ ... })server <- function(input, output, session) {
# Reactive expression: cached, lazy, re-runs when dependencies change
data <- reactive({
mtcars |> dplyr::filter(cyl == input$cyl)
})
# Reactive value: manually set, like a variable
count <- reactiveVal(0)
# Observer: side effects, runs eagerly when dependencies change
observe({
message("Data has ", nrow(data()), " rows")
})
# observeEvent: side effects triggered by specific events
observeEvent(input$button, {
count(count() + 1)
})
# bindEvent: make any reactive respond only to specific events
filtered <- reactive({ expensive_filter(data()) }) |>
bindEvent(input$go)
# Output
output$plot <- renderPlot({
plot(data()$mpg, data()$wt)
})
}
(): data(), not dataval(new_value), read with val()rv <- reactiveValues(x = 1); rv$xreq(input$file), req(nrow(data()) > 0)Forgetting parentheses on reactive expressions:
# WRONG: passes the reactive object, not its value
output$text <- renderText(data)
# RIGHT: calls the reactive to get current value
output$text <- renderText(data())
Creating reactives inside observers (leaks memory):
# WRONG: creates a new reactive every time button is clicked
observeEvent(input$button, {
x <- reactive({ input$n + 1 }) # memory leak!
})
# RIGHT: define reactives at the top level
x <- reactive({ input$n + 1 })
observeEvent(input$button, {
message("x is ", x())
})
Using <<- to modify global state:
# WRONG: global assignment breaks reactivity
observeEvent(input$button, {
result <<- compute() # not reactive!
})
# RIGHT: use reactiveVal
result <- reactiveVal()
observeEvent(input$button, {
result(compute())
})
For long-running operations that should not block the UI.
library(shiny)
library(bslib)
library(promises)
library(future)
future::plan(multisession)
ui <- page_fluid(
input_task_button("go", "Run Analysis"),
textOutput("result")
)
server <- function(input, output, session) {
task <- ExtendedTask$new(function(n) {
future({
Sys.sleep(5) # simulate long computation
mean(rnorm(n))
})
})
observeEvent(input$go, {
task$invoke(1e6)
})
output$result <- renderText({
task$result()
})
}
shinyApp(ui, server)
input_task_button() automatically disables while the task is running.
library(shiny)
library(bslib)
library(shinychat)
library(ellmer)
ui <- page_fillable(
chat_ui("chat", fill = TRUE)
)
server <- function(input, output, session) {
chat <- chat_openai(
model = "gpt-4o",
system_prompt = "You are a helpful assistant."
)
observeEvent(input$chat_user_input, {
stream <- chat$stream(input$chat_user_input)
chat_append("chat", stream)
})
}
shinyApp(ui, server)
For Claude:
chat <- chat_claude(
model = "claude-sonnet-4-6-20250514",
system_prompt = "You are a helpful assistant."
)
{name}UI(id, ...) or {name}_ui(id, ...){name}Server(id, ...) or {name}_server(id, ...)R/mod_{name}.RfilterServer <- function(id, data) {
moduleServer(id, function(input, output, session) {
filtered <- reactive({
data() |> dplyr::filter(species == input$species)
})
# Return reactive for parent to use
filtered
})
}
# In parent server:
server <- function(input, output, session) {
raw_data <- reactive({ palmerpenguins::penguins })
filtered <- filterServer("filter1", raw_data)
output$table <- renderTable({
filtered() # use returned reactive
})
}
Always pass reactives as functions (without calling them):
# RIGHT: pass the reactive itself
filterServer("filter1", data = raw_data)
# WRONG: pass the current value (won't update)
filterServer("filter1", data = raw_data())
Modules communicate through their return values and arguments — never through global state.
# Parent orchestrates module communication
server <- function(input, output, session) {
selected <- selectorServer("selector") # returns reactive
details <- detailServer("detail", selected) # receives reactive
summaryServer("summary", selected, details) # receives both
}
library(shinytest2)
test_that("app works", {
app <- AppDriver$new(app_dir = ".", name = "myapp")
# Set input and wait
app$set_inputs(n = 50)
app$expect_values()
# Click button
app$click("go")
app$wait_for_idle()
# Check output
output <- app$get_value(output = "result")
expect_true(is.character(output))
# Snapshot test (visual regression)
app$expect_screenshot()
})
testServer(counterServer, {
# Initially zero
expect_equal(session$returned(), 0)
# Click button
session$setInputs(button = 1)
expect_equal(session$returned(), 1)
# Click again
session$setInputs(button = 2)
expect_equal(session$returned(), 2)
})
rsconnect::deployApp(appDir = ".")
rsconnect::deployApp(
appDir = ".",
server = "connect.example.com",
account = "username"
)
FROM rocker/shiny:4.5.0
RUN install2.r --error bslib dplyr ggplot2
COPY . /srv/shiny-server/myapp/
EXPOSE 3838
CMD ["/usr/bin/shiny-server"]
shiny::runApp(port = 3838, launch.browser = TRUE)
library(bslib)
my_theme <- bs_theme(
version = 5,
preset = "shiny",
bg = "#FFFFFF",
fg = "#333333",
primary = "#0062cc",
base_font = font_google("Open Sans"),
heading_font = font_google("Poppins")
)
ui <- page_sidebar(
theme = my_theme,
# ...
)
For brand-consistent theming, use _brand.yml (see brand-yml skill).
npx claudepluginhub matheus-rech/r-dev-agent --plugin r-devDesigns Shiny app UIs with bslib theming, responsive grids, value boxes, cards, and custom CSS/SCSS. Covers page layouts, accessibility, and brand consistency.
Builds modern Shiny dashboards using bslib (Bootstrap 5) with page layouts, cards, value boxes, navigation, sidebars, and theming. Replaces legacy Shiny UI patterns.
Installs and configures shadcn/ui components with Radix UI, Tailwind CSS, and React Hook Form + Zod validation. Use when initializing shadcn/ui, adding components, or building accessible forms and UI patterns.