Dynamic UI with renderUI and insertUI
Generate UI elements on-the-fly based on user input and server state.
Dynamic UI with renderUI and insertUI is a free R Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the R Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Dynamic UI — Why and When
Sometimes the UI you need cannot be fully determined at app startup — it depends on user choices, loaded data, or runtime conditions. Shiny provides two mechanisms: renderUI() for replacing a placeholder's entire contents, and insertUI()/removeUI() for adding or removing elements without replacing everything.
library(shiny)
# Static UI: columns always shown
# ui <- fluidPage(selectInput('x', ...), selectInput('y', ...))
# Dynamic UI: columns depend on which dataset was loaded
# ui <- fluidPage(fileInput('upload', ...), uiOutput('var_selectors'))renderUI() and uiOutput()
renderUI({...}) in the server builds and returns a UI element reactively. uiOutput('id') in the UI creates a placeholder that displays whatever renderUI produces. The entire content of the placeholder is replaced each time the reactive reruns.
ui <- fluidPage(
selectInput('dataset', 'Dataset:', choices = c('iris', 'mtcars')),
uiOutput('column_selector') # placeholder
)
server <- function(input, output, session) {
output$column_selector <- renderUI({
df <- get(input$dataset) # reactive dependency on dataset
selectInput('col', 'Choose Column:',
choices = names(df))
})
}
shinyApp(ui, server)renderUI() with Multiple Controls
Return a tagList() from renderUI() to render multiple UI elements at once. This is the standard pattern when the number of controls depends on the data — for example, generating one filter per column in a dataset.
server <- function(input, output, session) {
output$filters <- renderUI({
df <- get(input$dataset)
nums <- names(df)[sapply(df, is.numeric)]
# One slider per numeric column
sliders <- lapply(nums, function(col) {
rng <- range(df[[col]], na.rm = TRUE)
sliderInput(
inputId = paste0('filter_', col),
label = col,
min = rng[1], max = rng[2],
value = rng
)
})
tagList(sliders) # render all sliders
})
}insertUI() — Add Elements Dynamically
insertUI(selector, where, ui) inserts a UI element into the DOM without rebuilding the entire page. Use CSS selectors to target the insertion point. where can be 'afterEnd', 'beforeEnd', 'afterBegin', or 'beforeBegin'.
server <- function(input, output, session) {
item_count <- reactiveVal(0)
observeEvent(input$add_item, {
item_count(item_count() + 1)
id <- paste0('item_', item_count())
insertUI(
selector = '#item_container', # CSS selector
where = 'beforeEnd',
ui = div(
id = id,
style = 'padding: 5px; border: 1px solid #ccc; margin: 3px;',
textInput(id, paste('Item', item_count()), value = '')
)
)
})
}removeUI() — Delete Elements
removeUI(selector) removes the first DOM element matching the CSS selector. Use multiple = TRUE to remove all matching elements. This is the complement to insertUI() for list-based UIs where users can add and remove items.
server <- function(input, output, session) {
observeEvent(input$remove_last, {
# Remove the last item div
n <- isolate(item_count())
if (n > 0) {
removeUI(selector = paste0('#item_', n))
item_count(n - 1)
}
})
observeEvent(input$clear_all, {
# Remove all items matching a class
removeUI(selector = '.dynamic-item', multiple = TRUE)
item_count(0)
})
}conditionalPanel() — Client-Side Visibility
conditionalPanel(condition, ...) shows or hides UI elements based on a JavaScript condition evaluated in the browser. Unlike renderUI(), elements are always in the DOM — just visible or hidden. This is faster for simple show/hide logic because no server round-trip is needed.
ui <- fluidPage(
selectInput('plot_type', 'Plot Type:',
choices = c('scatter', 'histogram', 'boxplot')),
# Only shown when scatter is selected
conditionalPanel(
condition = "input.plot_type == 'scatter'",
selectInput('x_var', 'X Variable:', choices = names(mtcars)),
selectInput('y_var', 'Y Variable:', choices = names(mtcars))
),
# Only shown when histogram is selected
conditionalPanel(
condition = "input.plot_type == 'histogram'",
sliderInput('bins', 'Number of bins:', 5, 50, 20)
),
plotOutput('main_plot')
)updateSelectInput() — Modify Existing Inputs
Instead of replacing an entire input control, use updateSelectInput(session, id, choices, selected) to update only the choices or selected value of an existing selectInput. Similar update functions exist for all input types: updateSliderInput(), updateTextInput(), etc.
server <- function(input, output, session) {
# When dataset changes, update the column selector
observeEvent(input$dataset, {
df <- get(input$dataset)
updateSelectInput(
session = session,
inputId = 'column',
choices = names(df),
selected = names(df)[1]
)
})
# Reset slider when reset button pressed
observeEvent(input$reset_btn, {
updateSliderInput(session, 'n_items',
value = 20, min = 1, max = 100)
})
}updateSliderInput() and updateNumericInput()
Update functions send a message to the browser to modify the current value, min, max, or label of an existing input. This is more efficient than destroying and recreating the control with renderUI(), and it preserves the control's position in the page layout.
server <- function(input, output, session) {
# When data loads, set slider max to actual data length
observeEvent(input$load_data, {
data <- readRDS(input$upload$datapath)
updateSliderInput(session, 'row_slider',
min = 1,
max = nrow(data),
value = c(1, min(50, nrow(data)))
)
updateNumericInput(session, 'page_size',
max = nrow(data),
value = min(10, nrow(data))
)
})
}Accessing renderUI Inputs with req()
Inputs created dynamically with renderUI() are NULL until rendered. Use req(input$dynamic_input) in any server code that depends on dynamically created inputs to prevent errors during initialisation.
server <- function(input, output, session) {
# dynamic input: 'col' is created by renderUI
output$column_selector <- renderUI({
selectInput('col', 'Column:', choices = names(get(input$dataset)))
})
output$summary_stats <- renderPrint({
req(input$col) # wait until 'col' exists
req(input$dataset) # and dataset is chosen
df <- get(input$dataset)
summary(df[[input$col]])
})
}Dynamic Tabs with appendTab()
appendTab(inputId, tab, select) adds a new tab to a tabsetPanel() at runtime. removeTab(inputId, target) removes one. This pattern enables workflows where users open, close, and switch between dynamically generated analysis views.
ui <- fluidPage(
actionButton('add_tab', 'Open New Analysis Tab'),
tabsetPanel(id = 'analysis_tabs', type = 'tabs')
)
server <- function(input, output, session) {
tab_count <- reactiveVal(0)
observeEvent(input$add_tab, {
tab_count(tab_count() + 1)
n <- tab_count()
appendTab(
inputId = 'analysis_tabs',
select = TRUE,
tab = tabPanel(
title = paste('Analysis', n),
value = paste0('tab_', n),
plotOutput(paste0('plot_', n)),
actionButton(paste0('close_', n), 'Close')
)
)
})
}renderUI vs insertUI — When to Use Which
Choose the right tool based on your use case:
- renderUI(): Replace a placeholder's entire content reactively. Best when the whole block changes together.
- insertUI(): Add new elements to existing content. Best for growing lists where you don't want to re-render everything.
- updateXxxInput(): Modify an existing input in-place. Best for changing choices or values without re-rendering.
- conditionalPanel(): Show/hide without server round-trip. Best for simple visibility logic.
# Decision guide:
# - 'Show filter only when advanced mode' -> conditionalPanel
# - 'Populate column choices from loaded CSV' -> updateSelectInput
# - 'Show different controls per selected tab' -> renderUI
# - 'Add a row editor for each row user adds' -> insertUI
# - 'Remove a row editor' -> removeUI
# Combining strategies:
server <- function(input, output, session) {
# Use renderUI for column selectors (replaces whole block)
output$col_selectors <- renderUI({ ... })
# Use insertUI for dynamically added rows
observeEvent(input$add_row, { insertUI(...) })
# Use conditionalPanel for show/hide (in UI, no server code)
}Quick Check
What is the key difference between renderUI() and insertUI() for building dynamic UI in Shiny?
Dynamic UI Recap
Key takeaways from Dynamic UI with renderUI and insertUI:
renderUI({}) + uiOutput('id'): replace a placeholder's entire content reactively.- Return
tagList()fromrenderUI()to render multiple elements at once. insertUI(selector, where, ui): inject elements into any DOM location.removeUI(selector): remove elements by CSS selector.conditionalPanel(condition, ...): client-side show/hide without server.updateSelectInput(),updateSliderInput(): modify existing inputs in-place.- Always use
req(input$dynamic_id)to guard against NULL before dynamic inputs render.
# Typical pattern: dynamic column selector
ui <- fluidPage(
fileInput('upload', 'Upload CSV'),
uiOutput('col_ui'), # placeholder
plotOutput('col_plot')
)
server <- function(input, output, session) {
df <- reactive({
req(input$upload)
read.csv(input$upload$datapath)
})
output$col_ui <- renderUI({
req(df())
selectInput('col', 'Column:', choices = names(df()))
})
output$col_plot <- renderPlot({
req(input$col)
hist(df()[[input$col]])
})
}Frequently asked questions
Is the “Dynamic UI with renderUI and insertUI” lesson free?
Yes — the full text of “Dynamic UI with renderUI and insertUI” is free to read here on the web, and the R Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the R Academy course, upgrade to CoddyKit PRO.
What will I learn in “Dynamic UI with renderUI and insertUI”?
Generate UI elements on-the-fly based on user input and server state. You practise R Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start R Academy?
No prior experience is required. R Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Dynamic UI with renderUI and insertUI” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this R Academy lesson?
Yes. Every R Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Reactive Programming Deep Dive
- Shiny Modules for Code Reuse
- Dynamic UI with renderUI and insertUI
- Deploying Shiny Apps