flowchart LR A[User Interaction] --> B[Input Widget] B --> C[Reactive Value] C --> D[Server Processing] D --> E[Output Update] E --> F[UI Display] style A fill:#ffebee style B fill:#f3e5f5 style C fill:#e8f5e8 style D fill:#fff3e0 style E fill:#e1f5fe style F fill:#fce4ec
Key Takeaways
- Comprehensive Widget Library: Shiny provides 20+ input controls covering text, numeric, selection, date, file, and action inputs for any user interface need
- Built-in Validation: Most input controls include automatic validation, type checking, and user feedback to ensure data quality and user experience
- Extensive Customization: Every input can be customized with CSS classes, inline styles, JavaScript callbacks, and reactive updates for professional interfaces
- Accessibility by Default: Shiny input controls follow web accessibility standards with proper labeling, keyboard navigation, and screen reader support
- Advanced Integration: Combine multiple inputs with conditional logic, dynamic updates, and reactive programming for sophisticated user experiences
Introduction
Input controls are the bridge between your users and your Shiny applications - they’re how users communicate their intentions, preferences, and data to your reactive systems. Mastering input controls means understanding not just how each widget works, but when to use specific controls, how to customize them for professional interfaces, and how to create intuitive user experiences that guide users naturally through your applications.
This comprehensive guide covers every input control available in Shiny, from basic text inputs to advanced file uploads and custom widgets. You’ll learn practical implementation techniques, customization strategies, and professional design patterns that will elevate your applications from functional tools to polished, user-friendly experiences.
Whether you’re building simple data collection forms or complex interactive dashboards, the techniques in this guide will help you create input interfaces that are not only functional but also intuitive, accessible, and visually appealing.
Input Control Fundamentals
Understanding the foundation of Shiny input controls helps you make informed decisions about which widgets to use and how to implement them effectively.
The Input-Output Connection
Every Shiny input control creates a reactive value that can be accessed in your server logic through the input
object. This reactive connection enables real-time updates and dynamic user interfaces.
Universal Input Properties
All Shiny input controls share common properties that provide consistent behavior and customization options:
Essential Properties:
inputId
: Unique identifier for accessing the input value in server logiclabel
: User-friendly text that describes the input’s purposevalue
orselected
: Initial value when the application loadswidth
: Control width using CSS units or Bootstrap grid classes
Styling and Behavior Properties:
class
: CSS classes for custom stylingstyle
: Inline CSS for specific modificationsdisabled
: Boolean to disable user interactiontitle
: Tooltip text that appears on hover
Input Validation and Feedback
Shiny provides built-in validation mechanisms that work across all input types:
Server-Side Validation:
<- function(input, output, session) {
server # Validate input before processing
observe({
validate(
need(input$user_name != "", "Please enter your name"),
need(nchar(input$user_name) >= 2, "Name must be at least 2 characters"),
need(!grepl("[0-9]", input$user_name), "Name should not contain numbers")
)
# Process validated input
$greeting <- renderText({
outputpaste("Hello,", input$user_name)
})
}) }
Real-Time Feedback:
# Provide immediate feedback using reactive expressions
$input_feedback <- renderUI({
outputif (is.null(input$email) || input$email == "") {
return(NULL)
}
if (grepl("^[\\w\\._%+-]+@[\\w\\.-]+\\.[A-Za-z]{2,}$", input$email)) {
div(class = "text-success", icon("check"), "Valid email address")
else {
} div(class = "text-danger", icon("times"), "Please enter a valid email")
} })
Text and Numeric Input Controls
These fundamental input controls handle the most common data collection needs in web applications.
Text Input Controls
Basic Text Input:
The textInput()
function creates single-line text fields for collecting short text responses.
# Basic text input
textInput(
inputId = "user_name",
label = "Full Name:",
value = "",
placeholder = "Enter your full name"
)
# Customized text input with validation styling
textInput(
inputId = "email",
label = "Email Address:",
value = "",
placeholder = "user@example.com",
width = "100%"
)
Advanced Text Input with Real-Time Validation:
# UI
textInput(
inputId = "username",
label = "Username:",
placeholder = "Choose a unique username"
),uiOutput("username_feedback")
# Server
$username_feedback <- renderUI({
outputreq(input$username)
if (nchar(input$username) < 3) {
div(class = "text-warning",
icon("exclamation-triangle"),
"Username must be at least 3 characters")
else if (nchar(input$username) > 20) {
} div(class = "text-danger",
icon("times"),
"Username must be less than 20 characters")
else if (grepl("^[a-zA-Z0-9_]+$", input$username)) {
} div(class = "text-success",
icon("check"),
"Username is available")
else {
} div(class = "text-danger",
icon("times"),
"Username can only contain letters, numbers, and underscores")
} })
Text Area for Long-Form Input:
The textAreaInput()
function provides multi-line text input for longer responses.
# Basic text area
textAreaInput(
inputId = "comments",
label = "Comments or Feedback:",
value = "",
placeholder = "Please share your thoughts...",
rows = 4,
resize = "vertical"
)
# Advanced text area with character counting
textAreaInput(
inputId = "description",
label = "Project Description:",
placeholder = "Describe your project (max 500 characters)",
rows = 6,
width = "100%"
),div(
style = "text-align: right; margin-top: 5px;",
textOutput("char_count", inline = TRUE),
span("/500 characters", style = "color: #666;")
)
# Server logic for character counting
$char_count <- renderText({
output<- nchar(input$description %||% "")
char_count <- if (char_count > 500) "#d63384" else if (char_count > 400) "#fd7e14" else "#198754"
color paste0('<span style="color: ', color, ';">', char_count, '</span>')
})
Password Input:
Password inputs hide the entered text for security purposes.
# Basic password input
passwordInput(
inputId = "password",
label = "Password:",
placeholder = "Enter secure password"
)
# Password with strength indicator
passwordInput(
inputId = "new_password",
label = "Create Password:",
placeholder = "Minimum 8 characters"
),div(
style = "margin-top: 10px;",
uiOutput("password_strength")
)
# Server logic for password strength
$password_strength <- renderUI({
outputreq(input$new_password)
<- input$new_password
password <- 0
score <- character()
feedback
# Length check
if (nchar(password) >= 8) {
<- score + 1
score else {
} <- c(feedback, "At least 8 characters")
feedback
}
# Character variety checks
if (grepl("[a-z]", password)) score <- score + 1 else feedback <- c(feedback, "Lowercase letter")
if (grepl("[A-Z]", password)) score <- score + 1 else feedback <- c(feedback, "Uppercase letter")
if (grepl("[0-9]", password)) score <- score + 1 else feedback <- c(feedback, "Number")
if (grepl("[^a-zA-Z0-9]", password)) score <- score + 1 else feedback <- c(feedback, "Special character")
# Strength assessment
<- c("Very Weak", "Weak", "Fair", "Good", "Strong")
strength_levels <- c("#dc3545", "#fd7e14", "#ffc107", "#20c997", "#198754")
strength_colors
<- strength_levels[min(score + 1, 5)]
strength <- strength_colors[min(score + 1, 5)]
color
div(
div(
style = paste0("color: ", color, "; font-weight: bold;"),
paste("Strength:", strength)
),if (length(feedback) > 0) {
div(
style = "font-size: 0.9em; color: #6c757d; margin-top: 5px;",
paste("Missing:", paste(feedback, collapse = ", "))
)
}
) })
Numeric Input Controls
Numeric Input:
The numericInput()
function provides validated numeric input with optional constraints.
# Basic numeric input
numericInput(
inputId = "age",
label = "Age:",
value = 25,
min = 0,
max = 120,
step = 1
)
# Decimal input with custom formatting
numericInput(
inputId = "price",
label = "Price ($):",
value = 99.99,
min = 0,
step = 0.01
),div(
style = "margin-top: 5px; font-size: 0.9em; color: #6c757d;",
textOutput("formatted_price", inline = TRUE)
)
# Server logic for price formatting
$formatted_price <- renderText({
outputif (!is.null(input$price) && !is.na(input$price)) {
paste("Formatted:", scales::dollar(input$price))
} })
Slider Inputs:
Sliders provide intuitive numeric input through drag-and-drop interaction.
# Single value slider
sliderInput(
inputId = "opacity",
label = "Chart Opacity:",
min = 0,
max = 1,
value = 0.7,
step = 0.1,
ticks = TRUE,
animate = FALSE
)
# Range slider for selecting intervals
sliderInput(
inputId = "date_range",
label = "Date Range:",
min = as.Date("2020-01-01"),
max = as.Date("2024-12-31"),
value = c(as.Date("2023-01-01"), as.Date("2023-12-31")),
timeFormat = "%Y-%m-%d",
width = "100%"
)
# Animated slider with custom styling
sliderInput(
inputId = "time_progress",
label = "Time Progression:",
min = 1,
max = 100,
value = 1,
step = 1,
animate = animationOptions(interval = 500, loop = TRUE),
width = "100%"
),div(
style = "text-align: center; margin-top: 10px;",
actionButton("play_pause", "Play/Pause", class = "btn-outline-primary btn-sm")
)
Selection Input Controls
Selection controls enable users to choose from predefined options, ensuring data consistency and reducing input errors.
Single Selection Controls
Select Input (Dropdown):
The selectInput()
function creates dropdown menus for single-option selection.
# Basic select input
selectInput(
inputId = "country",
label = "Select Country:",
choices = list(
"North America" = list("USA" = "us", "Canada" = "ca", "Mexico" = "mx"),
"Europe" = list("UK" = "uk", "Germany" = "de", "France" = "fr"),
"Asia" = list("Japan" = "jp", "China" = "cn", "India" = "in")
),selected = NULL,
multiple = FALSE,
selectize = TRUE
)
# Searchable select with custom options
selectInput(
inputId = "dataset",
label = "Choose Dataset:",
choices = NULL, # Will be populated dynamically
selected = NULL,
selectize = TRUE,
options = list(
placeholder = "Type to search datasets...",
maxOptions = 50,
create = FALSE,
plugins = list('remove_button')
)
)
# Server logic for dynamic choices
observe({
# Simulate loading dataset names
<- c("mtcars", "iris", "airquality", "ChickWeight", "PlantGrowth")
dataset_choices names(dataset_choices) <- paste(dataset_choices,
"- R Built-in Dataset")
updateSelectInput(session, "dataset",
choices = dataset_choices,
selected = "mtcars")
})
Radio Buttons:
Radio buttons work best when you have a small number of mutually exclusive options that should be immediately visible.
# Basic radio buttons
radioButtons(
inputId = "analysis_type",
label = "Analysis Type:",
choices = list(
"Descriptive Statistics" = "descriptive",
"Correlation Analysis" = "correlation",
"Regression Analysis" = "regression"
),selected = "descriptive",
width = "100%"
)
# Inline radio buttons for compact layout
radioButtons(
inputId = "chart_type",
label = "Chart Type:",
choices = list("Bar" = "bar", "Line" = "line", "Scatter" = "scatter"),
selected = "bar",
inline = TRUE
)
# Radio buttons with custom styling
div(
class = "radio-group-custom",
radioButtons(
inputId = "theme_selection",
label = "Application Theme:",
choices = list(
"Light Theme" = "light",
"Dark Theme" = "dark",
"Auto (System)" = "auto"
),selected = "light"
)
),
# Custom CSS for styled radio buttons
$style(HTML("
tags .radio-group-custom .radio label {
padding: 10px 15px;
margin: 5px;
border: 2px solid #dee2e6;
border-radius: 8px;
background: #f8f9fa;
cursor: pointer;
transition: all 0.3s ease;
}
.radio-group-custom .radio label:hover {
border-color: #0d6efd;
background: #e7f3ff;
}
.radio-group-custom .radio input:checked ~ label {
border-color: #0d6efd;
background: #0d6efd;
color: white;
}
"))
Multiple Selection Controls
Multiple Select Input:
Enable users to select multiple options from a list.
# Basic multiple select
selectInput(
inputId = "variables",
label = "Select Variables:",
choices = names(mtcars),
selected = c("mpg", "hp", "wt"),
multiple = TRUE,
selectize = TRUE,
options = list(
placeholder = "Choose one or more variables...",
plugins = list('remove_button', 'drag_drop')
)
)
# Multiple select with categories and limits
selectInput(
inputId = "features",
label = "Select Features (max 5):",
choices = list(
"Basic Info" = list("Name" = "name", "Age" = "age", "Email" = "email"),
"Preferences" = list("Color" = "color", "Size" = "size", "Style" = "style"),
"Advanced" = list("Custom Field 1" = "custom1", "Custom Field 2" = "custom2")
),multiple = TRUE,
options = list(
maxItems = 5,
placeholder = "Select up to 5 features..."
) )
Checkbox Groups:
Checkbox groups provide visual clarity for multiple selections and work well when options should be immediately visible.
# Basic checkbox group
checkboxGroupInput(
inputId = "notifications",
label = "Email Notifications:",
choices = list(
"Weekly Summary" = "weekly",
"Important Updates" = "important",
"New Features" = "features",
"Marketing Messages" = "marketing"
),selected = c("weekly", "important"),
width = "100%"
)
# Inline checkbox group
checkboxGroupInput(
inputId = "file_types",
label = "Export Formats:",
choices = list("CSV" = "csv", "Excel" = "xlsx", "JSON" = "json", "PDF" = "pdf"),
selected = "csv",
inline = TRUE
)
# Checkbox group with select all/none functionality
div(
checkboxGroupInput(
inputId = "columns_to_show",
label = "Display Columns:",
choices = names(mtcars),
selected = names(mtcars)[1:3]
),div(
style = "margin-top: 10px;",
actionButton("select_all", "Select All", class = "btn-outline-secondary btn-sm"),
actionButton("select_none", "Select None", class = "btn-outline-secondary btn-sm"),
style = "margin-bottom: 15px;"
)
)
# Server logic for select all/none
observeEvent(input$select_all, {
updateCheckboxGroupInput(session, "columns_to_show",
selected = names(mtcars))
})
observeEvent(input$select_none, {
updateCheckboxGroupInput(session, "columns_to_show",
selected = character(0))
})
Date and Time Input Controls
Date and time inputs provide specialized interfaces for temporal data collection with built-in validation and formatting.
Date Input Controls
Basic Date Input:
# Single date input
dateInput(
inputId = "start_date",
label = "Start Date:",
value = Sys.Date(),
min = Sys.Date() - 365,
max = Sys.Date() + 365,
format = "yyyy-mm-dd",
startview = "month",
weekstart = 1, # Monday
language = "en",
width = "100%"
)
# Date input with custom styling and validation
dateInput(
inputId = "birth_date",
label = "Date of Birth:",
value = NULL,
min = as.Date("1900-01-01"),
max = Sys.Date() - 365*13, # Must be at least 13 years old
format = "mm/dd/yyyy",
placeholder = "Select your birth date"
),uiOutput("age_display")
# Server logic for age calculation
$age_display <- renderUI({
outputif (!is.null(input$birth_date)) {
<- as.numeric(difftime(Sys.Date(), input$birth_date, units = "days")) / 365.25
age <- floor(age)
age_years
if (age_years < 13) {
div(class = "text-danger",
icon("exclamation-triangle"),
"Must be at least 13 years old")
else {
} div(class = "text-info",
icon("info-circle"),
paste("Age:", age_years, "years"))
}
} })
Date Range Input:
# Date range input
dateRangeInput(
inputId = "analysis_period",
label = "Analysis Period:",
start = Sys.Date() - 30,
end = Sys.Date(),
min = as.Date("2020-01-01"),
max = Sys.Date(),
format = "yyyy-mm-dd",
separator = " to ",
width = "100%"
)
# Date range with quick selection buttons
dateRangeInput(
inputId = "report_period",
label = "Reporting Period:",
start = Sys.Date() - 7,
end = Sys.Date()
),div(
style = "margin-top: 10px;",
actionButton("last_7_days", "Last 7 Days", class = "btn-outline-primary btn-sm"),
actionButton("last_30_days", "Last 30 Days", class = "btn-outline-primary btn-sm"),
actionButton("last_quarter", "Last Quarter", class = "btn-outline-primary btn-sm"),
actionButton("last_year", "Last Year", class = "btn-outline-primary btn-sm")
)
# Server logic for quick date selection
observeEvent(input$last_7_days, {
updateDateRangeInput(session, "report_period",
start = Sys.Date() - 7,
end = Sys.Date())
})
observeEvent(input$last_30_days, {
updateDateRangeInput(session, "report_period",
start = Sys.Date() - 30,
end = Sys.Date())
})
observeEvent(input$last_quarter, {
<- Sys.Date()
end_date <- end_date - 90
start_date updateDateRangeInput(session, "report_period",
start = start_date,
end = end_date)
})
observeEvent(input$last_year, {
updateDateRangeInput(session, "report_period",
start = Sys.Date() - 365,
end = Sys.Date())
})
File Input and Action Controls
These specialized controls handle file operations and user actions that trigger specific behaviors in your application.
File Input Controls
Basic File Upload:
# Single file upload
fileInput(
inputId = "data_file",
label = "Upload Data File:",
accept = c(".csv", ".xlsx", ".txt"),
width = "100%",
buttonLabel = "Browse...",
placeholder = "No file selected"
)
# Multiple file upload with progress
fileInput(
inputId = "image_files",
label = "Upload Images:",
multiple = TRUE,
accept = c("image/*"),
width = "100%"
),uiOutput("upload_progress")
# Server logic for file processing
observe({
req(input$data_file)
# Get file information
<- input$data_file
file_info
# Validate file type and size
validate(
need(tools::file_ext(file_info$name) %in% c("csv", "xlsx", "txt"),
"Please upload a CSV, Excel, or text file"),
need(file_info$size < 10 * 1024^2, # 10MB limit
"File size must be less than 10MB")
)
# Process the file
tryCatch({
if (tools::file_ext(file_info$name) == "csv") {
<- read.csv(file_info$datapath)
data else if (tools::file_ext(file_info$name) == "xlsx") {
} <- readxl::read_excel(file_info$datapath)
data
}
# Store processed data
$uploaded_data <- data
values
# Show success message
showNotification("File uploaded successfully!", type = "success")
error = function(e) {
}, showNotification(paste("Error reading file:", e$message), type = "error")
}) })
Advanced File Upload with Preview:
# Advanced file upload interface
div(
class = "file-upload-area",
fileInput(
inputId = "advanced_upload",
label = NULL,
accept = c(".csv", ".xlsx", ".json"),
width = "100%"
),div(
class = "upload-instructions",
icon("cloud-upload", class = "fa-3x"),
h4("Drag & Drop or Click to Upload"),
p("Supported formats: CSV, Excel, JSON (Max 25MB)")
)
),uiOutput("file_preview")
# Server logic for file preview
$file_preview <- renderUI({
outputreq(input$advanced_upload)
<- input$advanced_upload
file_info
div(
class = "file-preview",
h5("File Information:"),
$ul(
tags$li(paste("Name:", file_info$name)),
tags$li(paste("Size:", round(file_info$size / 1024^2, 2), "MB")),
tags$li(paste("Type:", tools::file_ext(file_info$name)))
tags
),
# Show data preview if it's a data file
if (tools::file_ext(file_info$name) %in% c("csv", "xlsx")) {
div(
h5("Data Preview:"),
::dataTableOutput("data_preview")
DT
)
}
)
})
# Custom CSS for file upload styling
$style(HTML("
tags .file-upload-area {
border: 2px dashed #ccc;
border-radius: 10px;
padding: 40px;
text-align: center;
background: #fafafa;
transition: all 0.3s ease;
}
.file-upload-area:hover {
border-color: #007bff;
background: #f0f8ff;
}
.upload-instructions {
color: #666;
margin-top: 20px;
}
.file-preview {
margin-top: 20px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #dee2e6;
}
"))
Action Controls
Action Buttons:
Action buttons trigger specific behaviors and are essential for interactive applications.
# Basic action button
actionButton(
inputId = "run_analysis",
label = "Run Analysis",
class = "btn-primary",
icon = icon("play")
)
# Button with custom styling and loading state
actionButton(
inputId = "process_data",
label = "Process Data",
class = "btn-success btn-lg",
icon = icon("cogs"),
style = "width: 100%; margin-bottom: 10px;"
),div(id = "processing_status", style = "text-align: center;")
# Server logic for button with loading state
observeEvent(input$process_data, {
# Show loading state
updateActionButton(session, "process_data",
label = "Processing...",
icon = icon("spinner fa-spin"))
# Disable button to prevent multiple clicks
::disable("process_data")
shinyjs
# Simulate processing
Sys.sleep(3) # Replace with actual processing
# Reset button state
updateActionButton(session, "process_data",
label = "Process Data",
icon = icon("cogs"))
::enable("process_data")
shinyjs
showNotification("Processing completed!", type = "success")
})
Download Buttons:
Download buttons enable users to export data, reports, or generated content.
# Basic download button
downloadButton(
outputId = "download_data",
label = "Download CSV",
class = "btn-outline-primary",
icon = icon("download")
)
# Multiple download options
div(
class = "download-section",
h4("Export Options:"),
div(
class = "btn-group",
downloadButton("download_csv", "CSV", class = "btn-outline-secondary"),
downloadButton("download_excel", "Excel", class = "btn-outline-secondary"),
downloadButton("download_pdf", "PDF Report", class = "btn-outline-secondary")
)
)
# Server logic for downloads
$download_csv <- downloadHandler(
outputfilename = function() {
paste("data_export_", Sys.Date(), ".csv", sep = "")
},content = function(file) {
write.csv(filtered_data(), file, row.names = FALSE)
}
)
$download_excel <- downloadHandler(
outputfilename = function() {
paste("data_export_", Sys.Date(), ".xlsx", sep = "")
},content = function(file) {
::write.xlsx(filtered_data(), file)
openxlsx
}
)
$download_pdf <- downloadHandler(
outputfilename = function() {
paste("report_", Sys.Date(), ".pdf", sep = "")
},content = function(file) {
# Generate PDF report
::render(
rmarkdowninput = "report_template.Rmd",
output_file = file,
params = list(data = filtered_data())
)
} )
Action Links:
Action links provide a subtle alternative to buttons for less prominent actions.
# Basic action link
actionLink(
inputId = "show_help",
label = "Show Help",
icon = icon("question-circle")
)
# Styled action links
div(
style = "text-align: right; margin-top: 10px;",
actionLink("reset_form", "Reset Form", icon = icon("undo")),
" | ",
actionLink("save_draft", "Save Draft", icon = icon("save")),
" | ",
actionLink("preview", "Preview", icon = icon("eye"))
)
# Server logic for action links
observeEvent(input$show_help, {
showModal(modalDialog(
title = "Help Information",
"This section provides detailed help information about using the application...",
easyClose = TRUE,
footer = modalButton("Close")
))
})
observeEvent(input$reset_form, {
# Reset all form inputs
updateTextInput(session, "user_name", value = "")
updateSelectInput(session, "country", selected = NULL)
updateCheckboxGroupInput(session, "notifications", selected = character(0))
showNotification("Form reset successfully", type = "message")
})
Advanced Input Customization
Custom Input Styling
CSS Classes and Inline Styles:
# Custom styled text input
textInput(
inputId = "styled_input",
label = "Custom Styled Input:",
value = "",
placeholder = "Enter text here...",
class = "form-control-lg",
style = "border-radius: 25px; padding: 15px 20px; border: 2px solid #007bff;"
)
# Input group with icons and buttons
div(
class = "input-group",
div(
class = "input-group-prepend",
span(class = "input-group-text", icon("user"))
),textInput(
inputId = "username_styled",
label = NULL,
placeholder = "Username"
),div(
class = "input-group-append",
actionButton("check_availability", "Check", class = "btn-outline-secondary")
)
)
# Custom slider with value display
div(
class = "custom-slider-container",
sliderInput(
inputId = "custom_slider",
label = "Adjust Value:",
min = 0,
max = 100,
value = 50,
width = "100%"
),div(
class = "slider-value-display",
textOutput("slider_value", inline = TRUE)
)
)
# Server logic for slider value display
$slider_value <- renderText({
outputpaste("Current Value:", input$custom_slider)
})
# Custom CSS for enhanced styling
$style(HTML("
tags .custom-slider-container {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
border-radius: 10px;
color: white;
}
.slider-value-display {
text-align: center;
font-size: 1.2em;
font-weight: bold;
margin-top: 10px;
}
.input-group {
margin-bottom: 15px;
}
.input-group-text {
background-color: #f8f9fa;
border-color: #ced4da;
}
"))
Conditional and Dynamic Inputs
Conditional Panel Inputs:
# Main selection that controls other inputs
selectInput(
inputId = "data_source",
label = "Data Source:",
choices = list(
"Upload File" = "upload",
"Database Connection" = "database",
"API Endpoint" = "api",
"Sample Data" = "sample"
),selected = "upload"
),
# Conditional inputs based on selection
conditionalPanel(
condition = "input.data_source == 'upload'",
wellPanel(
h4("File Upload Options"),
fileInput("upload_file", "Choose File:", accept = c(".csv", ".xlsx")),
checkboxInput("header", "File has header row", TRUE),
selectInput("separator", "Column Separator:",
choices = list("Comma" = ",", "Semicolon" = ";", "Tab" = "\t"))
)
),
conditionalPanel(
condition = "input.data_source == 'database'",
wellPanel(
h4("Database Connection"),
selectInput("db_type", "Database Type:",
choices = c("PostgreSQL", "MySQL", "SQLite", "SQL Server")),
textInput("db_host", "Host:", placeholder = "localhost"),
numericInput("db_port", "Port:", value = 5432),
textInput("db_name", "Database Name:"),
textInput("db_user", "Username:"),
passwordInput("db_password", "Password:")
)
),
conditionalPanel(
condition = "input.data_source == 'api'",
wellPanel(
h4("API Configuration"),
textInput("api_url", "API Endpoint:", placeholder = "https://api.example.com/data"),
selectInput("api_method", "HTTP Method:", choices = c("GET", "POST")),
textAreaInput("api_headers", "Headers (JSON):",
placeholder = '{"Authorization": "Bearer token"}'),
conditionalPanel(
condition = "input.api_method == 'POST'",
textAreaInput("api_body", "Request Body:", placeholder = "Request payload")
)
) )
Dynamic Input Generation:
# UI
numericInput("num_variables", "Number of Variables:", value = 3, min = 1, max = 10),
uiOutput("dynamic_variables")
# Server logic for dynamic input generation
$dynamic_variables <- renderUI({
outputreq(input$num_variables)
# Generate inputs dynamically
<- lapply(1:input$num_variables, function(i) {
input_list div(
class = "row",
div(
class = "col-sm-6",
textInput(paste0("var_name_", i),
label = paste("Variable", i, "Name:"),
value = paste0("Variable_", i))
),div(
class = "col-sm-3",
selectInput(paste0("var_type_", i),
label = "Type:",
choices = c("Numeric", "Character", "Factor"),
selected = "Numeric")
),div(
class = "col-sm-3",
numericInput(paste0("var_default_", i),
label = "Default:",
value = 0)
)
)
})
div(
h4("Variable Configuration:"),
do.call(tagList, input_list)
)
})
# Collect dynamic input values
observe({
req(input$num_variables)
# Collect all dynamic input values
<- data.frame(
variable_config name = sapply(1:input$num_variables, function(i) {
paste0("var_name_", i)]] %||% paste0("Variable_", i)
input[[
}),type = sapply(1:input$num_variables, function(i) {
paste0("var_type_", i)]] %||% "Numeric"
input[[
}),default = sapply(1:input$num_variables, function(i) {
paste0("var_default_", i)]] %||% 0
input[[
}),stringsAsFactors = FALSE
)
# Store configuration for use in other parts of the app
$variable_config <- variable_config
values })
Input Validation and Error Handling
Comprehensive Validation System:
# UI with validation feedback areas
textInput("email_validation", "Email Address:"),
uiOutput("email_feedback"),
numericInput("age_validation", "Age:", value = NULL, min = 13, max = 120),
uiOutput("age_feedback"),
passwordInput("password_validation", "Password:"),
passwordInput("password_confirm", "Confirm Password:"),
uiOutput("password_feedback"),
actionButton("validate_form", "Validate Form", class = "btn-primary")
# Server logic for comprehensive validation
# Email validation
$email_feedback <- renderUI({
outputif (is.null(input$email_validation) || input$email_validation == "") {
return(NULL)
}
<- "^[\\w\\._%+-]+@[\\w\\.-]+\\.[A-Za-z]{2,}$"
email_pattern
if (grepl(email_pattern, input$email_validation)) {
div(class = "alert alert-success",
icon("check-circle"), "Valid email address")
else {
} div(class = "alert alert-danger",
icon("times-circle"), "Please enter a valid email address")
}
})
# Age validation
$age_feedback <- renderUI({
outputif (is.null(input$age_validation) || is.na(input$age_validation)) {
return(div(class = "alert alert-info",
icon("info-circle"), "Please enter your age"))
}
<- input$age_validation
age
if (age < 13) {
div(class = "alert alert-danger",
icon("exclamation-triangle"), "Must be at least 13 years old")
else if (age > 120) {
} div(class = "alert alert-warning",
icon("exclamation-triangle"), "Please verify age (over 120)")
else {
} div(class = "alert alert-success",
icon("check-circle"), paste("Age accepted:", age, "years"))
}
})
# Password validation
$password_feedback <- renderUI({
outputif (is.null(input$password_validation) || input$password_validation == "") {
return(NULL)
}
<- input$password_validation
password <- input$password_confirm %||% ""
confirm
# Password strength checks
<- list(
checks length = nchar(password) >= 8,
lowercase = grepl("[a-z]", password),
uppercase = grepl("[A-Z]", password),
number = grepl("[0-9]", password),
special = grepl("[^a-zA-Z0-9]", password),
match = password == confirm && confirm != ""
)
<- tagList(
feedback_items div(class = if(checks$length) "text-success" else "text-danger",
icon(if(checks$length) "check" else "times"),
"At least 8 characters"),
div(class = if(checks$lowercase) "text-success" else "text-danger",
icon(if(checks$lowercase) "check" else "times"),
"Contains lowercase letter"),
div(class = if(checks$uppercase) "text-success" else "text-danger",
icon(if(checks$uppercase) "check" else "times"),
"Contains uppercase letter"),
div(class = if(checks$number) "text-success" else "text-danger",
icon(if(checks$number) "check" else "times"),
"Contains number"),
div(class = if(checks$special) "text-success" else "text-danger",
icon(if(checks$special) "check" else "times"),
"Contains special character")
)
if (confirm != "") {
<- tagList(
feedback_items
feedback_items,div(class = if(checks$match) "text-success" else "text-danger",
icon(if(checks$match) "check" else "times"),
"Passwords match")
)
}
div(
class = "password-requirements",
h6("Password Requirements:"),
feedback_items
)
})
# Form validation on button click
observeEvent(input$validate_form, {
<- character()
errors
# Collect validation errors
if (is.null(input$email_validation) || !grepl("^[\\w\\._%+-]+@[\\w\\.-]+\\.[A-Za-z]{2,}$", input$email_validation)) {
<- c(errors, "Valid email address required")
errors
}
if (is.null(input$age_validation) || input$age_validation < 13 || input$age_validation > 120) {
<- c(errors, "Age must be between 13 and 120")
errors
}
if (is.null(input$password_validation) || nchar(input$password_validation) < 8) {
<- c(errors, "Password must be at least 8 characters")
errors
}
if (input$password_validation != input$password_confirm) {
<- c(errors, "Passwords must match")
errors
}
# Show results
if (length(errors) == 0) {
showNotification("Form validation successful!", type = "success", duration = 5)
else {
} showNotification(
paste("Validation errors:", paste(errors, collapse = "; ")),
type = "error",
duration = 10
)
} })
Professional Form Design Patterns
Multi-Step Forms (Wizards)
Step-by-Step Form Implementation:
# UI
div(
class = "wizard-container",
# Progress indicator
div(
class = "wizard-progress",
div(class = "progress",
div(class = "progress-bar", role = "progressbar",
style = "width: 33%", id = "wizard-progress-bar")
),div(class = "step-labels",
span(class = "step-label active", "1. Personal Info"),
span(class = "step-label", "2. Preferences"),
span(class = "step-label", "3. Review")
)
),
# Step content
uiOutput("wizard_step_content"),
# Navigation buttons
div(
class = "wizard-navigation",
actionButton("wizard_prev", "Previous", class = "btn-outline-secondary"),
actionButton("wizard_next", "Next", class = "btn-primary"),
actionButton("wizard_submit", "Submit", class = "btn-success", style = "display: none;")
)
)
# Server logic for wizard
<- reactiveValues(current_step = 1, max_steps = 3)
values
$wizard_step_content <- renderUI({
outputswitch(values$current_step,
# Step 1: Personal Information
"1" = div(
class = "wizard-step",
h3("Personal Information"),
fluidRow(
column(6,
textInput("first_name", "First Name:", placeholder = "Enter first name")
),column(6,
textInput("last_name", "Last Name:", placeholder = "Enter last name")
)
),textInput("email_wizard", "Email Address:", placeholder = "your@email.com"),
dateInput("birth_date_wizard", "Date of Birth:", value = NULL),
selectInput("country_wizard", "Country:",
choices = c("", "USA", "Canada", "UK", "Germany", "France"))
),
# Step 2: Preferences
"2" = div(
class = "wizard-step",
h3("Preferences & Settings"),
checkboxGroupInput("interests", "Areas of Interest:",
choices = list("Technology" = "tech", "Science" = "science",
"Business" = "business", "Arts" = "arts")),
radioButtons("contact_preference", "Preferred Contact Method:",
choices = list("Email" = "email", "Phone" = "phone", "SMS" = "sms")),
sliderInput("frequency", "Contact Frequency:", min = 0, max = 5, value = 2,
post = " times per month")
),
# Step 3: Review
"3" = div(
class = "wizard-step",
h3("Review Your Information"),
div(class = "review-section",
h4("Personal Information:"),
uiOutput("review_personal")
),div(class = "review-section",
h4("Preferences:"),
uiOutput("review_preferences")
),checkboxInput("terms_agreement", "I agree to the terms and conditions", FALSE)
)
)
})
# Wizard navigation logic
observeEvent(input$wizard_next, {
# Validate current step before proceeding
<- switch(values$current_step,
current_valid "1" = validate_step_1(),
"2" = validate_step_2(),
"3" = TRUE
)
if (current_valid && values$current_step < values$max_steps) {
$current_step <- values$current_step + 1
valuesupdate_wizard_ui()
}
})
observeEvent(input$wizard_prev, {
if (values$current_step > 1) {
$current_step <- values$current_step - 1
valuesupdate_wizard_ui()
}
})
# Helper function to update wizard UI
<- function() {
update_wizard_ui <- (values$current_step / values$max_steps) * 100
progress_percent
# Update progress bar
runjs(paste0("$('#wizard-progress-bar').css('width', '", progress_percent, "%')"))
# Update step labels
runjs("
$('.step-label').removeClass('active completed');
$('.step-label:nth-child(" + values$current_step + ")').addClass('active');
for(var i = 1; i < " + values$current_step + "; i++) {
$('.step-label:nth-child(' + i + ')').addClass('completed');
}
")
# Update button visibility
if (values$current_step == 1) {
::hide("wizard_prev")
shinyjselse {
} ::show("wizard_prev")
shinyjs
}
if (values$current_step == values$max_steps) {
::hide("wizard_next")
shinyjs::show("wizard_submit")
shinyjselse {
} ::show("wizard_next")
shinyjs::hide("wizard_submit")
shinyjs
}
}
# Validation functions
<- function() {
validate_step_1 <- character()
errors
if (is.null(input$first_name) || input$first_name == "") {
<- c(errors, "First name is required")
errors
}
if (is.null(input$last_name) || input$last_name == "") {
<- c(errors, "Last name is required")
errors
}
if (is.null(input$email_wizard) || !grepl("@", input$email_wizard)) {
<- c(errors, "Valid email is required")
errors
}
if (length(errors) > 0) {
showNotification(paste("Please fix:", paste(errors, collapse = ", ")),
type = "error", duration = 5)
return(FALSE)
}
return(TRUE)
}
<- function() {
validate_step_2 if (is.null(input$interests) || length(input$interests) == 0) {
showNotification("Please select at least one area of interest",
type = "warning", duration = 3)
return(FALSE)
}return(TRUE)
}
# Custom CSS for wizard styling
$style(HTML("
tags .wizard-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.wizard-progress {
margin-bottom: 30px;
}
.step-labels {
display: flex;
justify-content: space-between;
margin-top: 10px;
}
.step-label {
font-size: 0.9em;
color: #6c757d;
transition: color 0.3s ease;
}
.step-label.active {
color: #007bff;
font-weight: bold;
}
.step-label.completed {
color: #28a745;
}
.wizard-step {
min-height: 400px;
padding: 20px;
border: 1px solid #dee2e6;
border-radius: 8px;
background: #fff;
}
.wizard-navigation {
margin-top: 20px;
text-align: right;
}
.review-section {
margin-bottom: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 5px;
}
"))
Responsive Form Layouts
Mobile-Optimized Form Design:
# Responsive form container
div(
class = "responsive-form",
# Form header
div(
class = "form-header",
h2("Contact Information"),
p("Please fill out all required fields marked with *")
),
# Form sections
div(
class = "form-section",
h4("Personal Details"),
# Responsive row layout
div(
class = "row",
div(
class = "col-md-6 col-sm-12",
textInput("resp_first_name", "First Name *", width = "100%")
),div(
class = "col-md-6 col-sm-12",
textInput("resp_last_name", "Last Name *", width = "100%")
)
),
# Full-width email
textInput("resp_email", "Email Address *", width = "100%"),
# Phone and country on same row for desktop, stacked on mobile
div(
class = "row",
div(
class = "col-md-8 col-sm-12",
textInput("resp_phone", "Phone Number", width = "100%")
),div(
class = "col-md-4 col-sm-12",
selectInput("resp_country_code", "Country",
choices = c("+1", "+44", "+49", "+33"), width = "100%")
)
)
),
# Address section
div(
class = "form-section",
h4("Address Information"),
textInput("resp_address", "Street Address", width = "100%"),
div(
class = "row",
div(
class = "col-md-6 col-sm-12",
textInput("resp_city", "City", width = "100%")
),div(
class = "col-md-3 col-sm-6",
textInput("resp_state", "State/Province", width = "100%")
),div(
class = "col-md-3 col-sm-6",
textInput("resp_zip", "ZIP/Postal Code", width = "100%")
)
)
),
# Submit section
div(
class = "form-submit",
div(
class = "row",
div(
class = "col-md-8 col-sm-12",
checkboxInput("resp_newsletter", "Subscribe to newsletter", width = "100%"),
checkboxInput("resp_terms", "I agree to the terms and conditions *", width = "100%")
),div(
class = "col-md-4 col-sm-12",
div(
style = "text-align: right;",
actionButton("resp_submit", "Submit Form",
class = "btn-primary btn-lg btn-block")
)
)
)
)
)
# Responsive CSS
$style(HTML("
tags .responsive-form {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
.form-header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #e9ecef;
}
.form-section {
margin-bottom: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.form-section h4 {
color: #495057;
margin-bottom: 20px;
font-weight: 600;
}
.form-submit {
padding: 20px;
background: #fff;
border: 2px solid #007bff;
border-radius: 8px;
}
/* Mobile optimizations */
@media (max-width: 768px) {
.form-section {
padding: 15px;
}
.form-header h2 {
font-size: 1.5rem;
}
.btn-block {
margin-top: 15px;
}
}
/* Focus states for accessibility */
.form-control:focus {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
/* Required field indicators */
label:after {
content: '';
}
label:contains('*'):after {
content: ' *';
color: #dc3545;
}
"))
Common Issues and Solutions
Issue 1: Input Values Not Updating Reactively
Problem: Input changes don’t trigger reactive updates in server logic.
Solution:
# BAD: Accessing input outside reactive context
<- function(input, output, session) {
server # This won't work - input accessed outside reactive context
<- input$name
user_name
$greeting <- renderText({
outputpaste("Hello", user_name) # Won't update when input changes
})
}
# GOOD: Proper reactive access
<- function(input, output, session) {
server $greeting <- renderText({
outputpaste("Hello", input$name) # Updates when input changes
})
# Or use reactive expression for reuse
<- reactive({
processed_name if (is.null(input$name) || input$name == "") {
"Anonymous"
else {
} ::str_to_title(input$name)
stringr
}
})
$greeting <- renderText({
outputpaste("Hello", processed_name())
}) }
Issue 2: File Upload Errors and Validation
Problem: File uploads fail or cause application errors.
Solution:
# Robust file upload handling
observe({
req(input$file_upload)
<- input$file_upload
file_info
# Comprehensive validation
tryCatch({
# Check file size (10MB limit)
validate(
need(file_info$size < 10 * 1024^2,
"File size must be less than 10MB")
)
# Check file extension
<- tools::file_ext(file_info$name)
file_ext validate(
need(file_ext %in% c("csv", "xlsx", "json"),
"Only CSV, Excel, and JSON files are supported")
)
# Check file readability
if (file_ext == "csv") {
# Test read first few lines
<- read.csv(file_info$datapath, nrows = 5)
test_data validate(
need(ncol(test_data) > 0, "File appears to be empty or corrupted")
)
# Read full file
<- read.csv(file_info$datapath, stringsAsFactors = FALSE)
full_data
else if (file_ext == "xlsx") {
} # Check if readxl is available
validate(
need(requireNamespace("readxl", quietly = TRUE),
"Excel file support requires the 'readxl' package")
)
<- readxl::read_excel(file_info$datapath)
full_data
}
# Store successfully processed data
$uploaded_data <- full_data
values
showNotification(
paste("Successfully loaded", nrow(full_data), "rows and",
ncol(full_data), "columns"),
type = "success"
)
error = function(e) {
}, showNotification(
paste("Error processing file:", e$message),
type = "error",
duration = 10
)
}) })
Issue 3: Input Validation and User Feedback
Problem: Users don’t receive clear feedback about input requirements or errors.
Solution:
# Comprehensive validation with user-friendly feedback
# UI
textInput("validated_input", "Username (3-20 characters):"),
uiOutput("input_validation_feedback"),
# Server
$input_validation_feedback <- renderUI({
outputreq(input$validated_input)
<- input$validated_input
username <- list()
feedback
# Length validation
if (nchar(username) < 3) {
$length <- list(
feedbackstatus = "danger",
message = "Username must be at least 3 characters"
)else if (nchar(username) > 20) {
} $length <- list(
feedbackstatus = "danger",
message = "Username must be less than 20 characters"
)else {
} $length <- list(
feedbackstatus = "success",
message = "Length requirement met"
)
}
# Character validation
if (grepl("^[a-zA-Z0-9_]+$", username)) {
$chars <- list(
feedbackstatus = "success",
message = "Valid characters used"
)else {
} $chars <- list(
feedbackstatus = "danger",
message = "Only letters, numbers, and underscores allowed"
)
}
# Create feedback UI
<- lapply(feedback, function(item) {
feedback_elements div(
class = paste0("alert alert-", item$status, " py-1 px-2 mb-1"),
style = "font-size: 0.9em;",
icon(if(item$status == "success") "check" else "times"),
" ", item$message
)
})
div(
style = "margin-top: 10px;",
do.call(tagList, feedback_elements)
) })
Common Questions About Shiny Input Controls
The choice depends on the data type, number of options, and user experience goals:
For Text Data:
- Short text: Use
textInput()
for names, titles, single-line responses - Long text: Use
textAreaInput()
for comments, descriptions, multi-line content - Sensitive data: Use
passwordInput()
for passwords and confidential information
For Numeric Data:
- Precise values: Use
numericInput()
when users need to enter exact numbers - Range selection: Use
sliderInput()
for intuitive selection within a range - Multiple ranges: Use range sliders for date ranges or numeric intervals
For Selection Data:
- Few options (2-5): Use
radioButtons()
to show all options immediately - Many options: Use
selectInput()
with search capability for large lists - Multiple selections: Use
checkboxGroupInput()
for visible options orselectInput(multiple=TRUE)
for large lists - Yes/No decisions: Use
checkboxInput()
for single boolean choices
Decision Framework:
# Example decision tree implementation
<- function(data_type, num_options, multiple_select = FALSE) {
choose_input_type if (data_type == "text") {
return("textInput or textAreaInput")
else if (data_type == "numeric") {
} return("numericInput or sliderInput")
else if (data_type == "selection") {
} if (multiple_select) {
if (num_options <= 7) return("checkboxGroupInput")
else return("selectInput with multiple=TRUE")
else {
} if (num_options <= 5) return("radioButtons")
else return("selectInput")
}
} }
Implement validation at multiple levels for the best user experience:
Real-time Validation (as user types):
# Provide immediate feedback without being intrusive
$email_feedback <- renderUI({
outputreq(input$email, nchar(input$email) > 0)
if (grepl("@", input$email)) {
if (grepl("^[\\w\\._%+-]+@[\\w\\.-]+\\.[A-Za-z]{2,}$", input$email)) {
div(class = "text-success small", icon("check"), "Valid email format")
else {
} div(class = "text-warning small", icon("exclamation-triangle"), "Checking format...")
}
} })
Server-side Validation (before processing):
# Always validate on the server before using data
<- reactive({
validate_inputs validate(
need(input$name != "", "Name is required"),
need(nchar(input$name) >= 2, "Name must be at least 2 characters"),
need(grepl("^[\\w\\._%+-]+@[\\w\\.-]+\\.[A-Za-z]{2,}$", input$email), "Valid email required")
)return(TRUE)
})
User-Friendly Error Messages:
- Be specific about what’s wrong and how to fix it
- Use positive language (“Please enter…” instead of “Don’t enter…”)
- Provide examples of valid input formats
- Use color and icons to clearly indicate status
- Show validation status near the relevant input field
Progressive Validation:
- Start with basic format checking
- Add advanced validation (uniqueness, availability) after basic requirements are met
- Don’t overwhelm users with all validation messages at once
Dynamic inputs require special handling for value collection and management:
Pattern for Dynamic Input Creation:
# UI
numericInput("num_inputs", "Number of fields:", value = 3, min = 1, max = 10),
uiOutput("dynamic_inputs_ui")
# Server - Generate inputs
$dynamic_inputs_ui <- renderUI({
outputreq(input$num_inputs)
lapply(1:input$num_inputs, function(i) {
div(
style = "margin-bottom: 15px;",
textInput(
inputId = paste0("dynamic_field_", i),
label = paste("Field", i, ":"),
placeholder = paste("Enter value for field", i)
)
)
})
})
# Server - Collect dynamic values
<- reactive({
dynamic_values req(input$num_inputs)
# Collect all dynamic input values
<- sapply(1:input$num_inputs, function(i) {
values paste0("dynamic_field_", i)]] %||% ""
input[[
})
names(values) <- paste0("field_", 1:input$num_inputs)
return(values)
})
Best Practices for Dynamic Inputs:
- Use consistent naming patterns for input IDs (e.g., “field_1”, “field_2”)
- Handle NULL values gracefully with
%||%
operator - Validate dynamic inputs the same way as static inputs
- Provide clear user feedback about the number and purpose of dynamic fields
- Consider performance - too many dynamic inputs can slow the app
- Use
req()
to ensure dynamic inputs exist before accessing them
Advanced Dynamic Input Management:
# Store dynamic input metadata
<- reactiveValues(
values dynamic_config = data.frame(
id = character(0),
label = character(0),
type = character(0),
required = logical(0)
)
)
# Update configuration when inputs change
observe({
req(input$num_inputs)
$dynamic_config <- data.frame(
valuesid = paste0("dynamic_field_", 1:input$num_inputs),
label = paste("Field", 1:input$num_inputs),
type = "text",
required = TRUE,
stringsAsFactors = FALSE
) })
Shiny input controls have good default accessibility, but you can enhance them:
Proper Labeling:
# Always provide descriptive labels
textInput("user_name",
label = "Full Name (required)",
placeholder = "Enter your first and last name")
# Use aria-labels for additional context
textInput("search_query",
label = "Search:",
placeholder = "Enter search terms...",
title = "Search through available datasets and documentation")
Keyboard Navigation:
- All Shiny inputs support keyboard navigation by default
- Tab order follows the order inputs appear in the UI
- Use
tabindex
attribute to customize tab order if needed
Screen Reader Support:
# Provide additional context for screen readers
div(
role = "group",
`aria-labelledby` = "contact-info-heading",
h3(id = "contact-info-heading", "Contact Information"),
textInput("email", "Email Address:"),
textInput("phone", "Phone Number:")
)
Visual Accessibility:
- Ensure sufficient color contrast for validation messages
- Use icons alongside color to indicate status (not color alone)
- Provide clear focus indicators
- Ensure text is readable at 200% zoom
Error Handling for Accessibility:
# Associate error messages with inputs
textInput("accessible_email", "Email Address:",
`aria-describedby` = "email-help email-error"),
div(id = "email-help", class = "form-text",
"We'll never share your email with anyone else."),
uiOutput("email_error_message")
# Server logic for accessible error messages
$email_error_message <- renderUI({
outputif (!is.null(input$accessible_email) && input$accessible_email != "") {
if (!grepl("@", input$accessible_email)) {
div(id = "email-error",
class = "text-danger",
role = "alert",
"Please enter a valid email address")
}
} })
Best Practices:
- Test with keyboard navigation only
- Use semantic HTML elements when customizing
- Provide alternative text for any visual-only information
- Ensure error messages are announced by screen readers
- Test with actual assistive technologies when possible
Large numbers of inputs can impact application performance, but several strategies help:
Lazy Loading and Conditional Rendering:
# Only render inputs when needed
conditionalPanel(
condition = "input.show_advanced_options == true",
# Complex inputs that are only loaded when needed
uiOutput("advanced_options_ui")
)
# Use renderUI for expensive input generation
$advanced_options_ui <- renderUI({
outputreq(input$show_advanced_options)
# Generate complex inputs only when needed
lapply(1:50, function(i) {
numericInput(paste0("param_", i), paste("Parameter", i, ":"), value = 0)
}) })
Debouncing and Throttling:
# Debounce rapidly changing inputs
library(shiny)
# Debounce text input to reduce server calls
<- debounce(reactive(input$search_text), 1000) # 1 second delay
debounced_search
# Use debounced value in expensive operations
<- reactive({
filtered_data <- debounced_search()
search_term if (is.null(search_term) || search_term == "") {
return(original_data())
}
# Expensive filtering operation
filter_large_dataset(original_data(), search_term)
})
Efficient Input Updates:
# Batch update multiple inputs
<- function() {
update_all_inputs # More efficient than individual updates
$sendCustomMessage("updateMultipleInputs", list(
sessionupdates = list(
list(id = "input1", value = "new_value1"),
list(id = "input2", value = "new_value2"),
list(id = "input3", value = "new_value3")
)
))
}
# Use updateSelectInput choices efficiently
observe({
# Update choices only when necessary
if (input$category != values$last_category) {
<- get_choices_for_category(input$category)
new_choices updateSelectInput(session, "subcategory", choices = new_choices)
$last_category <- input$category
values
} })
Memory Management:
# Clean up unused reactive values
observe({
# Remove old dynamic inputs from memory
if (exists("old_dynamic_inputs", envir = values)) {
rm("old_dynamic_inputs", envir = values)
}
})
# Use req() to prevent unnecessary calculations
<- reactive({
expensive_calculation req(input$trigger_calculation) # Only run when needed
req(input$required_input != "") # Skip if input is empty
# Expensive operation here
complex_analysis(input$data_source)
})
Performance Monitoring:
- Use
profvis
package to identify bottlenecks - Monitor reactive log with
reactlog
during development - Test with realistic data volumes and user loads
- Consider using
promises
for long-running operations - Implement progress indicators for slow operations
Test Your Understanding
You’re designing a user registration form that needs to collect the following information:
- User’s preferred communication method (Email, Phone, SMS - only one choice)
- Areas of interest (Technology, Science, Business, Arts, Sports - multiple selections allowed)
- Experience level (Beginner, Intermediate, Advanced, Expert - only one choice)
- Notification frequency (0-10 times per month - precise control needed)
What’s the most appropriate combination of input controls for optimal user experience?
- All selectInput() dropdowns for consistency
- radioButtons() for communication method, checkboxGroupInput() for interests, selectInput() for experience, numericInput() for frequency
- radioButtons() for communication method, selectInput(multiple=TRUE) for interests, radioButtons() for experience, sliderInput() for frequency
- checkboxGroupInput() for all multiple options, textInput() for frequency
- Consider the number of options and whether they should be immediately visible
- Think about the most intuitive way to select a precise numeric value in a range
- Remember the principle of matching input type to data type and user expectations
- Consider which controls work best for single vs. multiple selections
C) radioButtons() for communication method, selectInput(multiple=TRUE) for interests, radioButtons() for experience, sliderInput() for frequency
This combination provides the optimal user experience:
Communication Method - radioButtons():
radioButtons("comm_method", "Preferred Communication:",
choices = list("Email" = "email", "Phone" = "phone", "SMS" = "sms"),
selected = "email")
- Why: Only 3 options, mutually exclusive, should be immediately visible for quick selection
Areas of Interest - selectInput(multiple=TRUE):
selectInput("interests", "Areas of Interest:",
choices = c("Technology", "Science", "Business", "Arts", "Sports"),
multiple = TRUE,
options = list(placeholder = "Select your interests..."))
- Why: 5+ options with multiple selection - dropdown saves space while allowing multiple choices
Experience Level - radioButtons():
radioButtons("experience", "Experience Level:",
choices = list("Beginner", "Intermediate", "Advanced", "Expert"),
inline = TRUE)
- Why: 4 clear progression levels, mutually exclusive, users benefit from seeing all options
Notification Frequency - sliderInput():
sliderInput("frequency", "Notification Frequency (per month):",
min = 0, max = 10, value = 2, step = 1,
post = " times")
- Why: Intuitive for numeric range selection, visual feedback, easier than typing numbers
Why other options are less optimal:
- Option A: Using dropdowns for everything hides options unnecessarily
- Option B: numericInput() for frequency is less intuitive than a slider for this range
- Option D: checkboxGroupInput() for single-selection items violates user expectations
You need to implement a password creation form with these requirements:
- Password must be 8-20 characters
- Must contain at least one uppercase, lowercase, number, and special character
- Confirmation password must match
- Real-time feedback without being annoying
- Submit button only enabled when all requirements are met
What’s the best implementation strategy for user experience and code maintainability?
- Validate everything on form submission with alert messages
- Create a single reactive expression that validates all requirements and updates UI accordingly
- Use separate observers for each validation rule with individual UI updates
- Implement client-side JavaScript validation only
- Consider how to provide helpful real-time feedback without overwhelming users
- Think about code organization and maintainability
- Remember the balance between immediate feedback and user experience
- Consider both validation logic organization and UI update efficiency
B) Create a single reactive expression that validates all requirements and updates UI accordingly
This approach provides the best balance of functionality, performance, and maintainability:
# UI
passwordInput("password", "Create Password:"),
passwordInput("password_confirm", "Confirm Password:"),
uiOutput("password_validation"),
div(style = "margin-top: 15px;",
actionButton("submit_password", "Create Account",
class = "btn-primary", disabled = TRUE))
# Server - Single comprehensive validation reactive
<- reactive({
password_validation # Don't validate empty passwords (avoids immediate red errors)
if (is.null(input$password) || input$password == "") {
return(list(valid = FALSE, show_feedback = FALSE))
}
<- input$password
password <- input$password_confirm %||% ""
confirm
# Validation checks
<- list(
checks length = list(
valid = nchar(password) >= 8 && nchar(password) <= 20,
message = "8-20 characters",
type = "length"
),lowercase = list(
valid = grepl("[a-z]", password),
message = "Lowercase letter",
type = "content"
),uppercase = list(
valid = grepl("[A-Z]", password),
message = "Uppercase letter",
type = "content"
),number = list(
valid = grepl("[0-9]", password),
message = "Number",
type = "content"
),special = list(
valid = grepl("[^a-zA-Z0-9]", password),
message = "Special character",
type = "content"
),match = list(
valid = if(confirm != "") password == confirm else TRUE,
message = "Passwords match",
type = "confirmation",
show_only_if_confirm = TRUE
)
)
# Calculate overall validity
<- all(sapply(checks, function(x) x$valid))
all_valid
return(list(
valid = all_valid,
show_feedback = TRUE,
checks = checks,
overall_strength = sum(sapply(checks[1:5], function(x) x$valid))
))
})
# UI update based on validation
$password_validation <- renderUI({
output<- password_validation()
validation
if (!validation$show_feedback) return(NULL)
# Create feedback elements
<- lapply(validation$checks, function(check) {
feedback_items # Skip confirmation check if no confirm password entered
if (!is.null(check$show_only_if_confirm) &&
is.null(input$password_confirm) || input$password_confirm == "")) {
(return(NULL)
}
<- if(check$valid) "check" else "times"
icon_name <- if(check$valid) "text-success" else "text-muted"
class_name
div(class = paste("requirement-item", class_name),
icon(icon_name, class = "me-2"), check$message)
})
# Strength indicator
<- c("Very Weak", "Weak", "Fair", "Good", "Strong")
strength_levels <- c("#dc3545", "#fd7e14", "#ffc107", "#20c997", "#198754")
strength_colors <- min(validation$overall_strength + 1, 5)
strength_level
div(
class = "password-feedback",
div(class = "password-strength mb-2",
"Strength: ",
span(strength_levels[strength_level],
style = paste0("color: ", strength_colors[strength_level], "; font-weight: bold;"))
),div(class = "requirements-list",
do.call(tagList, compact(feedback_items)))
)
})
# Enable/disable submit button
observe({
<- password_validation()
validation
if (validation$valid) {
::enable("submit_password")
shinyjsupdateActionButton(session, "submit_password",
label = "Create Account",
icon = icon("check"))
else {
} ::disable("submit_password")
shinyjsupdateActionButton(session, "submit_password",
label = "Complete Requirements",
icon = icon("lock"))
} })
Why this approach is superior:
- Single source of truth: All validation logic in one place
- Efficient updates: One reactive expression drives all UI changes
- Maintainable: Easy to modify validation rules or add new ones
- Good UX: Provides helpful feedback without being overwhelming
- Performance: Minimal reactive dependencies and efficient updates
- Extensible: Easy to add new validation requirements
Why other options are less optimal:
- Option A: Poor UX with delayed feedback and disruptive alerts
- Option C: Code duplication and potential inconsistencies between validators
- Option D: Client-side only validation is insecure and doesn’t integrate with Shiny’s reactive system
You’re building a data analysis configuration form with these requirements:
- 50+ configuration parameters organized in 6 categories
- Parameters change based on selected analysis type
- Some parameters are interdependent (changing one affects others’ available options)
- Form state must be saveable and restorable
- Must work well on both desktop and mobile devices
What’s the most effective architectural approach for managing this complexity?
- Single large form with all inputs visible using conditional panels
- Multi-step wizard with validation at each step
- Tabbed interface with reactive modules for each category and centralized state management
- Separate pages for each category with navigation between them
- Consider both user experience and code maintainability with 50+ parameters
- Think about how interdependent parameters are best managed
- Remember the mobile responsiveness requirement
- Consider the complexity of saving and restoring form state
C) Tabbed interface with reactive modules for each category and centralized state management
This architecture handles complexity while maintaining usability and maintainability:
# Main UI structure
<- fluidPage(
ui # Form state management UI
div(class = "form-controls mb-3",
actionButton("save_config", "Save Configuration", class = "btn-outline-primary"),
actionButton("load_config", "Load Configuration", class = "btn-outline-secondary"),
downloadButton("export_config", "Export", class = "btn-outline-info")),
# Analysis type selector (affects all categories)
selectInput("analysis_type", "Analysis Type:",
choices = c("Regression", "Classification", "Clustering", "Time Series")),
# Tabbed interface for parameter categories
tabsetPanel(
id = "config_tabs",
tabPanel("Data", dataConfigUI("data_config")),
tabPanel("Preprocessing", preprocessingConfigUI("preprocessing_config")),
tabPanel("Model", modelConfigUI("model_config")),
tabPanel("Validation", validationConfigUI("validation_config")),
tabPanel("Output", outputConfigUI("output_config")),
tabPanel("Advanced", advancedConfigUI("advanced_config"))
),
# Form submission
div(class = "form-submit mt-4",
actionButton("run_analysis", "Run Analysis", class = "btn-primary btn-lg"))
)
# Centralized state management
<- function(input, output, session) {
server # Central configuration state
<- reactiveValues(
config_state analysis_type = "Regression",
data_params = list(),
preprocessing_params = list(),
model_params = list(),
validation_params = list(),
output_params = list(),
advanced_params = list()
)
# Update central state when analysis type changes
observeEvent(input$analysis_type, {
$analysis_type <- input$analysis_type
config_state
})
# Module servers with shared state
<- dataConfigServer("data_config", config_state)
data_config <- preprocessingConfigServer("preprocessing_config", config_state)
preprocessing_config <- modelConfigServer("model_config", config_state)
model_config <- validationConfigServer("validation_config", config_state)
validation_config <- outputConfigServer("output_config", config_state)
output_config <- advancedConfigServer("advanced_config", config_state)
advanced_config
# Save/Load functionality
observeEvent(input$save_config, {
<- jsonlite::toJSON(reactiveValuesToList(config_state),
config_json pretty = TRUE, auto_unbox = TRUE)
showModal(modalDialog(
title = "Save Configuration",
textAreaInput("config_name", "Configuration Name:",
value = paste("Config", Sys.Date())),
textAreaInput("config_json", "Configuration (JSON):",
value = config_json, rows = 10),
footer = tagList(
actionButton("confirm_save", "Save", class = "btn-primary"),
modalButton("Cancel")
)
))
})
# Configuration loading
observeEvent(input$load_config, {
# Implementation for loading saved configurations
# Could integrate with database or file system
})
}
# Example module for data configuration
<- function(id) {
dataConfigUI <- NS(id)
ns
div(class = "config-section",
h4("Data Configuration"),
# Responsive layout for mobile
div(class = "row",
div(class = "col-md-6 col-sm-12",
selectInput(ns("data_source"), "Data Source:",
choices = c("Upload", "Database", "API"))),
div(class = "col-md-6 col-sm-12",
selectInput(ns("data_format"), "Format:",
choices = c("CSV", "JSON", "Parquet")))
),
# Conditional inputs based on selections
conditionalPanel(
condition = "input.data_source == 'Upload'", ns = ns,
fileInput(ns("upload_file"), "Choose File:")
),
conditionalPanel(
condition = "input.data_source == 'Database'", ns = ns,
textInput(ns("connection_string"), "Connection String:")
),
# Dynamic parameters based on analysis type
uiOutput(ns("analysis_specific_options"))
)
}
<- function(id, shared_state) {
dataConfigServer moduleServer(id, function(input, output, session) {
# React to analysis type changes
$analysis_specific_options <- renderUI({
output<- shared_state$analysis_type
analysis_type
if (analysis_type == "Time Series") {
div(
selectInput(session$ns("time_column"), "Time Column:",
choices = NULL), # Would be populated from data
selectInput(session$ns("frequency"), "Frequency:",
choices = c("Daily", "Weekly", "Monthly"))
)else if (analysis_type == "Clustering") {
} div(
checkboxGroupInput(session$ns("feature_columns"), "Feature Columns:",
choices = NULL), # Would be populated from data
checkboxInput(session$ns("normalize"), "Normalize Features", TRUE)
)
}# Add more analysis-specific options as needed
})
# Update shared state when local inputs change
observe({
$data_params <- list(
shared_statedata_source = input$data_source,
data_format = input$data_format,
time_column = input$time_column,
frequency = input$frequency,
feature_columns = input$feature_columns,
normalize = input$normalize
)
})
# Return module state for parent access if needed
return(reactive({
list(
data_source = input$data_source,
data_format = input$data_format
)
}))
}) }
Why this architecture excels:
Modularity and Maintainability:
- Each category is a separate module with clear boundaries
- Easy to modify or extend individual sections
- Code reusability across similar forms
State Management:
- Centralized state prevents conflicts between interdependent parameters
- Easy to save/restore entire form state
- Clear data flow between modules
User Experience:
- Tabbed interface organizes complexity without overwhelming users
- Responsive design works on mobile and desktop
- Analysis type selection intelligently shows/hides relevant parameters
Scalability:
- Easy to add new parameter categories
- Supports complex parameter interdependencies
- Can handle growth to 100+ parameters without architectural changes
Performance:
- Modules prevent unnecessary reactive updates
- Conditional rendering reduces DOM complexity
- Efficient state synchronization
This approach scales well for complex forms while maintaining excellent user experience and code maintainability.
Conclusion
Mastering Shiny input controls is fundamental to creating exceptional user experiences in your applications. The comprehensive coverage in this guide - from basic text inputs to advanced dynamic forms - provides you with the knowledge to handle any data collection requirement professionally and efficiently.
The key to excellent input design lies in matching the right control to the user’s mental model and task requirements. Whether you’re building simple data collection forms or complex configuration interfaces, the principles of intuitive design, proper validation, and responsive feedback will serve you well throughout your Shiny development career.
Remember that input controls are the primary way users communicate with your applications. Investing time in thoughtful input design, comprehensive validation, and accessible interfaces pays dividends in user satisfaction, data quality, and application success.
Next Steps
Based on your mastery of input controls, here are the recommended paths for advancing your Shiny UI development expertise:
Immediate Next Steps (Complete These First)
- Shiny Output Types and Visualization - Learn to create compelling outputs that respond to your well-designed input controls
- Styling and Custom Themes in Shiny - Apply professional styling to your input controls for branded, polished interfaces
- Practice Exercise: Build a comprehensive data collection form using the advanced patterns learned in this guide, focusing on validation and user experience
Building on Your Foundation (Choose Your Path)
For Advanced UI Development:
For Interactive Features:
For Production Applications:
Long-term Goals (2-4 Weeks)
- Design and implement a complete multi-step form system with advanced validation and state management
- Create a library of reusable input components that can be shared across multiple applications
- Build expertise in custom input widget development for specialized use cases
- Develop proficiency in accessibility testing and optimization for inclusive design
Explore More Articles
Here are more articles from the same category to help you dive deeper into the topic.
Reuse
Citation
@online{kassambara2025,
author = {Kassambara, Alboukadel},
title = {Complete {Guide} to {Shiny} {Input} {Controls:} {Master}
{Every} {Widget}},
date = {2025-05-23},
url = {https://www.datanovia.com/learn/tools/shiny-apps/ui-design/input-controls.html},
langid = {en}
}