build-shiny-module
Build reusable Shiny modules with proper namespace isolation using NS(). Covers module UI/server pairs, reactive return values, inter-module communication, and nested module composition. Use when extracting a reusable component from a growing Shiny app, building a UI widget used in multiple places, encapsulating complex reactive logic behind a clean interface, or composing larger applications from smaller, testable units.
Best use case
build-shiny-module is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Build reusable Shiny modules with proper namespace isolation using NS(). Covers module UI/server pairs, reactive return values, inter-module communication, and nested module composition. Use when extracting a reusable component from a growing Shiny app, building a UI widget used in multiple places, encapsulating complex reactive logic behind a clean interface, or composing larger applications from smaller, testable units.
Teams using build-shiny-module should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/build-shiny-module/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How build-shiny-module Compares
| Feature / Agent | build-shiny-module | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Build reusable Shiny modules with proper namespace isolation using NS(). Covers module UI/server pairs, reactive return values, inter-module communication, and nested module composition. Use when extracting a reusable component from a growing Shiny app, building a UI widget used in multiple places, encapsulating complex reactive logic behind a clean interface, or composing larger applications from smaller, testable units.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Build Shiny Module
Create reusable Shiny UI/server module pairs with proper namespace isolation, reactive communication, and composability.
## When to Use
- Extracting a reusable component from a growing Shiny app
- Building a UI widget that will be used in multiple places
- Encapsulating complex reactive logic behind a clean interface
- Composing larger applications from smaller, testable units
## Inputs
- **Required**: Module purpose and functionality description
- **Required**: Input/output contract (what the module receives and returns)
- **Optional**: Whether the module nests other modules (default: no)
- **Optional**: Framework context (golem, rhino, or vanilla)
## Procedure
### Step 1: Define the Module Interface
Before writing code, define what the module accepts and returns:
```
Module: data_filter
Inputs: reactive dataset, column names to filter on
Outputs: reactive filtered dataset
UI: filter controls (selectInput, sliderInput, dateRangeInput)
```
**Got:** Clear contract specifying reactive inputs, reactive outputs, and UI elements.
**If fail:** If the interface is unclear, the module is probably too broad. Split it into smaller modules with single responsibilities.
### Step 2: Create the Module UI Function
```r
#' Data Filter Module UI
#'
#' @param id Module namespace ID
#' @return A tagList of filter controls
#' @export
dataFilterUI <- function(id) {
ns <- NS(id)
tagList(
selectInput(
ns("column"),
"Filter column",
choices = NULL
),
uiOutput(ns("filter_control")),
actionButton(ns("apply"), "Apply Filter", class = "btn-primary")
)
}
```
Key rules:
- Function name follows `<name>UI` convention
- First argument is always `id`
- Create `ns <- NS(id)` at the top
- Wrap every `inputId` and `outputId` with `ns()`
- Return a `tagList()` to allow flexible placement
**Got:** UI function that creates namespaced input/output elements.
**If fail:** If IDs collide when using the module twice, check that every ID is wrapped with `ns()`. Common miss: IDs inside `renderUI()` or `uiOutput()` — these need `ns()` too.
### Step 3: Create the Module Server Function
```r
#' Data Filter Module Server
#'
#' @param id Module namespace ID
#' @param data Reactive expression returning a data frame
#' @param columns Character vector of filterable column names
#' @return Reactive expression returning the filtered data frame
#' @export
dataFilterServer <- function(id, data, columns) {
moduleServer(id, function(input, output, session) {
ns <- session$ns
# Update column choices when data changes
observeEvent(data(), {
available <- intersect(columns, names(data()))
updateSelectInput(session, "column", choices = available)
})
# Dynamic filter control based on selected column
output$filter_control <- renderUI({
req(input$column)
col_data <- data()[[input$column]]
if (is.numeric(col_data)) {
sliderInput(
ns("value_range"),
"Range",
min = min(col_data, na.rm = TRUE),
max = max(col_data, na.rm = TRUE),
value = range(col_data, na.rm = TRUE)
)
} else {
selectInput(
ns("value_select"),
"Values",
choices = unique(col_data),
multiple = TRUE,
selected = unique(col_data)
)
}
})
# Return filtered data as a reactive
filtered <- eventReactive(input$apply, {
req(input$column)
col <- input$column
df <- data()
if (is.numeric(df[[col]])) {
req(input$value_range)
df[df[[col]] >= input$value_range[1] &
df[[col]] <= input$value_range[2], ]
} else {
req(input$value_select)
df[df[[col]] %in% input$value_select, ]
}
}, ignoreNULL = FALSE)
return(filtered)
})
}
```
Key rules:
- Function name follows `<name>Server` convention
- First argument is always `id`
- Additional arguments are reactive expressions or static values
- Use `moduleServer(id, function(input, output, session) { ... })`
- Use `session$ns` for dynamic UI created inside the server
- Return reactive values explicitly
**Got:** Server function that processes inputs and returns reactive output.
**If fail:** If reactive values don't update, check that inputs from dynamic UI use `session$ns` (not the outer `ns`). If the module returns NULL, ensure `return()` is the last expression inside `moduleServer()`.
### Step 4: Wire the Module into the Parent App
```r
# In app_ui.R or ui
ui <- page_sidebar(
title = "Analysis App",
sidebar = sidebar(
dataFilterUI("filter1")
),
card(
DT::dataTableOutput("table")
)
)
# In app_server.R or server
server <- function(input, output, session) {
# Raw data source
raw_data <- reactive({ mtcars })
# Call module — capture its return value
filtered_data <- dataFilterServer(
"filter1",
data = raw_data,
columns = c("cyl", "mpg", "hp", "wt")
)
# Use the module's returned reactive
output$table <- DT::renderDataTable({
filtered_data()
})
}
```
**Got:** Module appears in the UI and its returned reactive flows into downstream outputs.
**If fail:** If the module UI doesn't render, verify the `id` string matches between UI and server calls. If the returned reactive is NULL, check that the server function returns a value.
### Step 5: Compose Nested Modules (Optional)
For modules that contain other modules:
```r
analysisUI <- function(id) {
ns <- NS(id)
tagList(
dataFilterUI(ns("filter")),
plotOutput(ns("plot"))
)
}
analysisServer <- function(id, data) {
moduleServer(id, function(input, output, session) {
# Call inner module with namespaced ID
filtered <- dataFilterServer("filter", data = data, columns = names(data()))
output$plot <- renderPlot({
req(filtered())
plot(filtered())
})
return(filtered)
})
}
```
Key rule: In the UI, nest with `ns("inner_id")`. In the server, call with just `"inner_id"` — `moduleServer` handles the namespace chaining.
**Got:** Inner module renders correctly within the outer module's namespace.
**If fail:** If the inner module's UI doesn't appear, you likely forgot `ns()` around the inner module's ID in the outer UI function. If server communication breaks, check that the inner module ID matches (no `ns()` in the server call).
### Step 6: Test the Module in Isolation
```r
# Quick test app for the module
if (interactive()) {
shiny::shinyApp(
ui = fluidPage(
dataFilterUI("test"),
DT::dataTableOutput("result")
),
server = function(input, output, session) {
data <- reactive(iris)
filtered <- dataFilterServer("test", data, names(iris))
output$result <- DT::renderDataTable(filtered())
}
)
}
```
**Got:** Module works correctly in the minimal test app.
**If fail:** If the module fails in isolation but works in the full app (or vice versa), check for implicit dependencies on global variables or parent session state.
## Validation
- [ ] Module UI function accepts `id` as first argument and uses `NS(id)`
- [ ] Every input/output ID in the UI is wrapped with `ns()`
- [ ] Module server uses `moduleServer(id, function(input, output, session) { ... })`
- [ ] Dynamic UI in server uses `session$ns` for IDs
- [ ] Module can be instantiated multiple times without ID collisions
- [ ] Reactive return values are accessible to the parent app
- [ ] Module works in a minimal standalone test app
## Pitfalls
- **Forgetting `ns()` in `renderUI()`**: Dynamic UI created inside the server must use `session$ns` — the outer `ns` is not available inside `moduleServer()`.
- **Passing non-reactive data**: Module arguments that change over time must be reactive expressions. Pass `reactive(data)` not `data`.
- **ID mismatch**: The `id` string in the UI call must exactly match the `id` in the server call.
- **Not returning reactives**: If the module computes something the parent needs, it must `return()` a reactive. Forgetting this is a silent bug.
- **Namespace in nested modules**: In UI: `ns("inner_id")`. In server: just `"inner_id"`. Mixing these up causes namespace double-wrapping or missing prefixes.
## Related Skills
- `scaffold-shiny-app` — set up the app structure before adding modules
- `test-shiny-app` — test modules with testServer() unit tests
- `design-shiny-ui` — bslib layout and theming for module UIs
- `optimize-shiny-performance` — cache and async patterns within modulesRelated Skills
scaffold-shiny-app
Scaffold a new Shiny application using golem (production R package), rhino (enterprise), or vanilla (quick prototype) structure. Covers framework selection, project initialization, and first module creation. Use to start a new interactive web app in R, create a dashboard or data explorer prototype, set up a production Shiny app as an R package with golem, or bootstrap an enterprise Shiny project with rhino.
optimize-shiny-performance
Profile and optimize Shiny application performance using profvis, bindCache, memoise, async/promises, debounce/throttle, and ExtendedTask for long-running computations. Use when the app feels slow or unresponsive during user interaction, when server resources are exhausted under concurrent load, when specific operations create bottlenecks, or when preparing an app for production deployment with many concurrent users.
optimize-docker-build-cache
Optimize Docker build times using layer caching, multi-stage builds, BuildKit features, and dependency-first copy patterns. Applicable to R, Node.js, and Python projects. Use when Docker builds are slow due to repeated package installations, when rebuilds reinstall all dependencies on every code change, when image sizes are unnecessarily large, or when CI/CD pipeline builds are a bottleneck.
design-shiny-ui
Design Shiny application UIs using bslib for theming, layout_columns for responsive grids, value boxes, cards, and custom CSS/SCSS. Covers page layouts, accessibility, and brand consistency. Use when building a new Shiny app UI from scratch, modernizing an existing app from fluidPage to bslib, applying brand theming, making a Shiny app responsive across screen sizes, or improving accessibility of a Shiny application.
deploy-shinyproxy
Deploy ShinyProxy for hosting multiple containerized Shiny applications. Covers ShinyProxy Docker deployment, application.yml configuration, Shiny app Docker images, authentication, container backends, usage tracking, and scaling. Use when hosting multiple Shiny apps behind a single entry point, needing per-app authentication and access control, deploying Shiny apps as isolated Docker containers, or scaling beyond single-app deployment with usage analytics and audit logging.
deploy-shiny-app
Deploy Shiny applications to shinyapps.io, Posit Connect, or Docker containers. Covers rsconnect configuration, manifest generation, Dockerfile creation, and deployment verification. Use when publishing a Shiny app for external or internal users, moving from local development to a hosted environment, containerizing a Shiny app for Kubernetes or Docker deployment, or setting up automated deployment pipelines.
build-tcg-deck
Build a competitive or casual trading card game deck. Covers archetype selection, mana/energy curve analysis, win condition identification, meta-game positioning, and sideboard construction for Pokemon TCG, Magic: The Gathering, Flesh and Blood, and other TCGs. Use when building a new deck for a tournament format or casual play, adapting an existing deck to a changed meta-game, evaluating whether a new set warrants a deck change, or converting a deck concept into a tournament-ready list.
build-sequential-circuit
Build sequential (stateful) logic circuits including latches, flip-flops, registers, counters, and finite state machines. Covers SR latch, D and JK flip-flops, binary/BCD/ring counters, and Mealy/Moore FSM design with clock signal and timing analysis. Use when a circuit must remember past inputs, count events, or implement a state-dependent control sequence.
build-pkgdown-site
Build and deploy a pkgdown documentation site for an R package to GitHub Pages. Covers _pkgdown.yml configuration, theming, article organization, reference index customization, and deployment methods. Use when creating a documentation site for a new or existing package, customizing layout or navigation, fixing 404 errors on a deployed site, or migrating between branch-based and GitHub Actions deployment methods.
build-parameterized-report
Create parameterized Quarto or R Markdown reports that can be rendered with different inputs to generate multiple variations. Covers parameter definitions, programmatic rendering, and batch generation. Use when generating the same report for different departments, regions, or time periods; creating client-specific reports from a single template; building dashboards that filter to specific subsets; or automating recurring reports with varying inputs.
build-grafana-dashboards
Create production-ready Grafana dashboards with reusable panels, template variables, annotations, and provisioning for version-controlled dashboard deployment. Use when creating visual representations of Prometheus, Loki, or other data source metrics, building operational dashboards for SRE teams, migrating from manual dashboard creation to version-controlled provisioning, or establishing executive-level SLO compliance reporting.
build-feature-store
Build a feature store using Feast for centralized feature management, configure offline and online stores for batch and real-time serving, define feature views with transformations, and implement point-in-time correct joins for ML pipelines. Use when managing features for multiple ML models, ensuring training-serving consistency, serving low-latency features for real-time inference, reusing feature definitions across projects, or building a feature catalog for discovery and governance.