flowchart TD
A[Advanced UI Components] --> B[Custom HTML Elements]
A --> C[Reusable Modules]
A --> D[JavaScript Integration]
A --> E[External Libraries]
B --> B1[HTML Tags]
B --> B2[CSS Styling]
B --> B3[Event Handling]
C --> C1[UI Modules]
C --> C2[Server Modules]
C --> C3[Module Communication]
D --> D1[Custom Bindings]
D --> D2[Message Passing]
D --> D3[DOM Manipulation]
E --> E1[Widget Libraries]
E --> E2[Visualization Frameworks]
E --> E3[UI Frameworks]
style A fill:#e1f5fe
style B fill:#f3e5f5
style C fill:#e8f5e8
style D fill:#fff3e0
style E fill:#fce4ec
Key Takeaways
- Custom Component Mastery: Create sophisticated UI components beyond Shiny’s built-in widgets using HTML, CSS, and JavaScript integration
- Reusable Module Architecture: Build scalable applications with modular design patterns that promote code reuse and maintainability
- Advanced Interaction Patterns: Implement complex user interactions including drag-and-drop, dynamic content generation, and custom data visualization
- JavaScript Integration Excellence: Seamlessly combine R server logic with JavaScript client-side functionality for enhanced user experiences
- Professional UI Libraries: Leverage external libraries and frameworks to create enterprise-grade interfaces that rival commercial applications
Introduction
Advanced Shiny UI development unlocks the full potential of web-based data applications by combining R’s analytical power with modern web technologies. While Shiny’s built-in components serve many needs effectively, sophisticated applications often require custom components, specialized interactions, and integration with external libraries that extend far beyond the standard toolkit.
This comprehensive guide teaches you to build professional-grade Shiny applications using custom HTML components, JavaScript integration, modular design patterns, and advanced interaction techniques. You’ll learn to create reusable UI components, implement complex user interactions, integrate external libraries, and build sophisticated interfaces that rival commercial web applications.
Whether you’re developing enterprise dashboards, specialized analytical tools, or public-facing applications, mastering advanced UI techniques enables you to create truly exceptional user experiences that differentiate your applications in competitive environments.
Input Controls Cheatsheet - Copy-paste code snippets, validation patterns, and essential input widget syntax.
Instant Reference • All Widget Types • Mobile-Friendly
Before implementing advanced UI components and custom HTML, master Shiny’s fundamental layout system:
Advanced UI techniques build upon solid layout foundations. Understanding how Bootstrap grid works, practicing professional layout patterns, and gaining confidence with responsive design makes advanced UI development much more manageable and successful.
Build your foundation with our Interactive Layout Builder →
Practice with proven layout patterns, understand grid organization, and see responsive behavior in action - then return to implement advanced UI components that integrate seamlessly with your well-structured layouts.
Before creating custom components, ensure you understand all basic input options:
Advanced UI development builds upon solid knowledge of fundamental input widgets. Understanding how each basic widget works, when to use it, and how to customize it is essential for creating sophisticated custom components.
Explore every input type with live customization options and code generation, then apply these patterns to build advanced, professional interfaces with custom HTML components.
Custom HTML Components in Shiny
Understanding how to create custom HTML components provides the foundation for advanced Shiny UI development, allowing you to build exactly the interface elements your application needs.
HTML Tag Functions and Custom Elements
Shiny provides comprehensive HTML tag functions that enable creation of any HTML element:
# Custom card component
custom_card <- function(title, content, icon = NULL, color = "primary") {
div(class = paste("custom-card border-", color, sep = ""),
div(class = "card-header d-flex align-items-center",
if (!is.null(icon)) {
div(class = "card-icon me-2",
tags$i(class = paste("fas fa-", icon, sep = ""))
)
},
h5(title, class = "card-title mb-0")
),
div(class = "card-body",
content
)
)
}
# Usage in UI
ui <- fluidPage(
tags$head(
tags$style(HTML("
.custom-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 1rem;
border-left-width: 4px;
border-left-style: solid;
}
.custom-card.border-primary { border-left-color: #007bff; }
.custom-card.border-success { border-left-color: #28a745; }
.custom-card.border-warning { border-left-color: #ffc107; }
.custom-card.border-danger { border-left-color: #dc3545; }
.card-header {
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
padding: 1rem;
}
.card-body { padding: 1rem; }
.card-icon { color: #6c757d; }
"))
),
div(class = "container-fluid",
div(class = "row",
div(class = "col-md-6",
custom_card("Revenue Analytics",
plotOutput("revenue_plot"),
icon = "chart-line",
color = "primary")
),
div(class = "col-md-6",
custom_card("System Status",
verbatimTextOutput("system_status"),
icon = "server",
color = "success")
)
)
)
)# Custom toggle switch component
toggle_switch <- function(inputId, label, value = FALSE, color = "primary") {
div(class = "toggle-container",
tags$label(class = "toggle-label",
span(label, class = "toggle-text"),
tags$input(
type = "checkbox",
id = inputId,
class = "toggle-input",
checked = if(value) NA else NULL
),
span(class = paste("toggle-slider", color, sep = " "))
)
)
}
# Advanced progress component
progress_ring <- function(value, max_value = 100, size = "md", color = "primary") {
percentage <- (value / max_value) * 100
size_class <- switch(size,
"sm" = "progress-ring-sm",
"md" = "progress-ring-md",
"lg" = "progress-ring-lg",
"progress-ring-md"
)
div(class = paste("progress-ring", size_class, color, sep = " "),
tags$svg(class = "progress-ring-svg",
tags$circle(class = "progress-ring-circle-bg",
cx = "50", cy = "50", r = "45"
),
tags$circle(class = "progress-ring-circle",
cx = "50", cy = "50", r = "45",
style = paste("stroke-dasharray:", 283 * percentage / 100, "283")
)
),
div(class = "progress-ring-text",
span(paste0(round(percentage), "%"), class = "progress-value"),
br(),
span("Complete", class = "progress-label")
)
)
}
# CSS for custom components
custom_css <- HTML("
/* Toggle Switch Styling */
.toggle-container { margin-bottom: 1rem; }
.toggle-label {
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
}
.toggle-text { margin-right: 1rem; font-weight: 500; }
.toggle-input { display: none; }
.toggle-slider {
position: relative;
width: 50px;
height: 24px;
background: #ccc;
border-radius: 12px;
transition: all 0.3s ease;
}
.toggle-slider:before {
content: '';
position: absolute;
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
top: 2px;
left: 2px;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.toggle-input:checked + .toggle-slider.primary {
background: #007bff;
}
.toggle-input:checked + .toggle-slider:before {
transform: translateX(26px);
}
/* Progress Ring Styling */
.progress-ring {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
}
.progress-ring-sm { width: 80px; height: 80px; }
.progress-ring-md { width: 120px; height: 120px; }
.progress-ring-lg { width: 160px; height: 160px; }
.progress-ring-svg {
width: 100%;
height: 100%;
transform: rotate(-90deg);
}
.progress-ring-circle-bg {
fill: none;
stroke: #e9ecef;
stroke-width: 8;
}
.progress-ring-circle {
fill: none;
stroke: #007bff;
stroke-width: 8;
stroke-linecap: round;
transition: stroke-dasharray 0.5s ease;
}
.progress-ring-text {
position: absolute;
text-align: center;
}
.progress-value {
font-size: 1.5rem;
font-weight: bold;
color: #495057;
}
.progress-label {
font-size: 0.8rem;
color: #6c757d;
}
")Advanced Form Components
Create sophisticated form elements that enhance user experience:
# Multi-step form wizard component
form_wizard <- function(steps, current_step = 1) {
div(class = "form-wizard",
# Step indicator
div(class = "wizard-steps",
lapply(seq_along(steps), function(i) {
step_class <- if (i < current_step) "completed" else
if (i == current_step) "active" else "pending"
div(class = paste("wizard-step", step_class),
div(class = "step-number", i),
div(class = "step-title", names(steps)[i])
)
})
),
# Step content
div(class = "wizard-content",
steps[[current_step]]
),
# Navigation buttons
div(class = "wizard-navigation",
if (current_step > 1) {
actionButton("wizard_prev", "Previous",
class = "btn-outline-secondary me-2")
},
if (current_step < length(steps)) {
actionButton("wizard_next", "Next",
class = "btn-primary")
} else {
actionButton("wizard_submit", "Complete",
class = "btn-success")
}
)
)
}
# Rich text editor component
rich_editor <- function(inputId, label = NULL, value = "", height = "200px") {
div(class = "rich-editor-container",
if (!is.null(label)) {
tags$label(label, class = "form-label", `for` = inputId)
},
div(class = "rich-editor-toolbar",
button(class = "editor-btn", type = "button",
onclick = paste0("formatText('", inputId, "', 'bold')"),
tags$i(class = "fas fa-bold")),
button(class = "editor-btn", type = "button",
onclick = paste0("formatText('", inputId, "', 'italic')"),
tags$i(class = "fas fa-italic")),
button(class = "editor-btn", type = "button",
onclick = paste0("formatText('", inputId, "', 'underline')"),
tags$i(class = "fas fa-underline")),
div(class = "btn-separator"),
button(class = "editor-btn", type = "button",
onclick = paste0("formatText('", inputId, "', 'justifyLeft')"),
tags$i(class = "fas fa-align-left")),
button(class = "editor-btn", type = "button",
onclick = paste0("formatText('", inputId, "', 'justifyCenter')"),
tags$i(class = "fas fa-align-center"))
),
div(
id = paste0(inputId, "_editor"),
class = "rich-editor-content",
contenteditable = "true",
style = paste("height:", height),
HTML(value)
),
tags$textarea(
id = inputId,
class = "rich-editor-hidden",
style = "display: none;",
value
)
)
}Shiny Modules for Advanced UI
Shiny modules provide the architecture for building complex, maintainable applications with reusable components.
Creating Reusable UI Modules
Build modular components that can be used across different applications:
# Data filter module UI
data_filter_UI <- function(id) {
ns <- NS(id)
div(class = "data-filter-module",
div(class = "filter-header",
h5("Data Filters", class = "filter-title"),
actionButton(ns("reset"), "Reset",
class = "btn-sm btn-outline-secondary float-end")
),
div(class = "filter-controls",
# Dynamic filter inputs will be generated here
uiOutput(ns("filter_inputs")),
# Apply button
div(class = "filter-actions mt-3",
actionButton(ns("apply"), "Apply Filters",
class = "btn-primary w-100")
)
),
# Filter summary
div(class = "filter-summary mt-3",
uiOutput(ns("filter_summary"))
)
)
}
# Data filter module server
data_filter_Server <- function(id, data, columns = NULL) {
moduleServer(id, function(input, output, session) {
ns <- session$ns
# Reactive values for filters
filters <- reactiveValues()
# Generate filter inputs based on data columns
output$filter_inputs <- renderUI({
req(data())
df <- data()
# Determine which columns to create filters for
filter_cols <- if (is.null(columns)) {
names(df)[sapply(df, function(x) is.numeric(x) || is.factor(x) || is.character(x))]
} else {
intersect(columns, names(df))
}
# Generate appropriate input for each column
filter_ui <- lapply(filter_cols, function(col) {
col_data <- df[[col]]
if (is.numeric(col_data)) {
# Numeric: slider input
div(class = "mb-3",
sliderInput(ns(paste0("filter_", col)),
label = col,
min = min(col_data, na.rm = TRUE),
max = max(col_data, na.rm = TRUE),
value = c(min(col_data, na.rm = TRUE),
max(col_data, na.rm = TRUE)))
)
} else {
# Categorical: checkbox group
unique_vals <- sort(unique(col_data[!is.na(col_data)]))
div(class = "mb-3",
checkboxGroupInput(ns(paste0("filter_", col)),
label = col,
choices = unique_vals,
selected = unique_vals)
)
}
})
do.call(tagList, filter_ui)
})
# Filtered data reactive
filtered_data <- reactive({
req(data())
df <- data()
# Apply each filter
for (col in names(df)) {
filter_input <- input[[paste0("filter_", col)]]
if (!is.null(filter_input)) {
if (is.numeric(df[[col]])) {
df <- df[df[[col]] >= filter_input[1] & df[[col]] <= filter_input[2], ]
} else {
df <- df[df[[col]] %in% filter_input, ]
}
}
}
df
})
# Filter summary
output$filter_summary <- renderUI({
req(data(), filtered_data())
original_rows <- nrow(data())
filtered_rows <- nrow(filtered_data())
div(class = "alert alert-info",
paste("Showing", filtered_rows, "of", original_rows, "records",
if (filtered_rows != original_rows)
paste0(" (", round(filtered_rows/original_rows*100), "%)")
else ""
)
)
})
# Reset filters
observeEvent(input$reset, {
# Reset all filter inputs
# This requires more complex logic to reset dynamically generated inputs
})
# Return filtered data
return(list(
data = filtered_data,
applied = reactive(input$apply)
))
})
}# Advanced chart module UI
chart_module_UI <- function(id) {
ns <- NS(id)
div(class = "chart-module",
div(class = "chart-controls",
div(class = "row",
div(class = "col-md-3",
selectInput(ns("chart_type"), "Chart Type:",
choices = list(
"Scatter Plot" = "scatter",
"Line Chart" = "line",
"Bar Chart" = "bar",
"Histogram" = "histogram",
"Box Plot" = "boxplot"
))
),
div(class = "col-md-3",
selectInput(ns("x_var"), "X Variable:", choices = NULL)
),
div(class = "col-md-3",
selectInput(ns("y_var"), "Y Variable:", choices = NULL)
),
div(class = "col-md-3",
selectInput(ns("color_var"), "Color By:",
choices = list("None" = "none"),
selected = "none")
)
),
# Advanced options (collapsible)
div(class = "advanced-options mt-3",
button(class = "btn btn-outline-secondary btn-sm",
type = "button", `data-bs-toggle` = "collapse",
`data-bs-target` = paste0("#", ns("advanced_collapse")),
"Advanced Options"),
div(id = ns("advanced_collapse"), class = "collapse mt-2",
div(class = "row",
div(class = "col-md-4",
checkboxInput(ns("smooth"), "Add Trend Line", value = FALSE)
),
div(class = "col-md-4",
checkboxInput(ns("facet"), "Create Facets", value = FALSE)
),
div(class = "col-md-4",
sliderInput(ns("alpha"), "Transparency:",
0.1, 1, 0.7, step = 0.1)
)
)
)
)
),
# Chart output
div(class = "chart-display mt-3",
plotlyOutput(ns("chart"), height = "500px")
),
# Chart actions
div(class = "chart-actions mt-2",
downloadButton(ns("download"), "Download Plot",
class = "btn-outline-primary btn-sm")
)
)
}
# Chart module server
chart_module_Server <- function(id, data) {
moduleServer(id, function(input, output, session) {
ns <- session$ns
# Update variable choices when data changes
observe({
req(data())
df <- data()
numeric_vars <- names(df)[sapply(df, is.numeric)]
all_vars <- names(df)
categorical_vars <- names(df)[sapply(df, function(x) is.factor(x) || is.character(x))]
updateSelectInput(session, "x_var", choices = all_vars)
updateSelectInput(session, "y_var", choices = numeric_vars)
updateSelectInput(session, "color_var",
choices = c("None" = "none", categorical_vars))
})
# Generate chart
output$chart <- renderPlotly({
req(data(), input$x_var, input$y_var)
df <- data()
# Create base plot
p <- ggplot(df, aes_string(x = input$x_var, y = input$y_var))
# Add color mapping if selected
if (input$color_var != "none") {
p <- p + aes_string(color = input$color_var)
}
# Add geometry based on chart type
p <- switch(input$chart_type,
"scatter" = p + geom_point(alpha = input$alpha),
"line" = p + geom_line(alpha = input$alpha),
"bar" = p + geom_col(alpha = input$alpha),
"histogram" = ggplot(df, aes_string(x = input$x_var)) +
geom_histogram(alpha = input$alpha, bins = 30),
"boxplot" = p + geom_boxplot(alpha = input$alpha)
)
# Add trend line if requested
if (input$smooth && input$chart_type %in% c("scatter", "line")) {
p <- p + geom_smooth(method = "lm", se = TRUE)
}
# Add faceting if requested
if (input$facet && input$color_var != "none") {
p <- p + facet_wrap(as.formula(paste("~", input$color_var)))
}
# Apply theme
p <- p + theme_minimal() +
theme(legend.position = "bottom")
# Convert to plotly
ggplotly(p) %>%
config(displayModeBar = TRUE,
modeBarButtonsToRemove = c("pan2d", "select2d", "lasso2d"))
})
# Download handler
output$download <- downloadHandler(
filename = function() {
paste0("chart_", Sys.Date(), ".png")
},
content = function(file) {
# Create the plot for download
req(data(), input$x_var, input$y_var)
# Implementation would recreate the ggplot and save it
}
)
# Return reactive values that other modules might need
return(list(
current_plot = reactive(input$chart_type),
variables = reactive(c(input$x_var, input$y_var))
))
})
}Module Communication and Coordination
Implement sophisticated communication between modules for complex applications:
# Main application using multiple coordinated modules
ui <- fluidPage(
titlePanel("Advanced Modular Dashboard"),
div(class = "container-fluid",
div(class = "row",
# Filter sidebar
div(class = "col-md-3",
div(class = "card",
div(class = "card-body",
data_filter_UI("filter_module")
)
)
),
# Main content
div(class = "col-md-9",
# Chart module
div(class = "card mb-3",
div(class = "card-body",
chart_module_UI("chart_module")
)
),
# Data table module
div(class = "card",
div(class = "card-body",
h5("Filtered Data"),
DT::dataTableOutput("filtered_table")
)
)
)
)
)
)
server <- function(input, output, session) {
# Sample data
base_data <- reactive({
mtcars %>%
rownames_to_column("model") %>%
mutate(
cyl = as.factor(cyl),
gear = as.factor(gear)
)
})
# Filter module
filter_results <- data_filter_Server("filter_module", base_data)
# Chart module (uses filtered data)
chart_results <- chart_module_Server("chart_module", filter_results$data)
# Data table (uses filtered data)
output$filtered_table <- DT::renderDataTable({
filter_results$data()
}, options = list(
pageLength = 10,
scrollX = TRUE
))
}JavaScript Integration and Custom Bindings
Integrate JavaScript functionality to create sophisticated interactive components that extend Shiny’s capabilities.
Custom Input Bindings
Create completely custom input widgets with JavaScript:
# Color picker custom input binding
color_picker_input <- function(inputId, label = NULL, value = "#000000",
palette = NULL, width = NULL) {
# Generate the HTML structure
div(class = "form-group shiny-input-container",
if (!is.null(label)) {
tags$label(label, class = "control-label", `for` = inputId)
},
div(
id = inputId,
class = "color-picker-input",
`data-initial-value` = value,
`data-palette` = if (!is.null(palette)) jsonlite::toJSON(palette) else NULL,
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width)) else NULL,
# Color display
div(class = "color-display",
div(class = "color-swatch", style = paste0("background-color: ", value)),
span(class = "color-value", value)
),
# Color palette (if provided)
if (!is.null(palette)) {
div(class = "color-palette",
lapply(palette, function(color) {
div(class = "palette-color",
style = paste0("background-color: ", color),
`data-color` = color)
})
)
},
# Hidden input for form submission
tags$input(type = "hidden", class = "color-value-input")
)
)
}
# JavaScript binding code (would be in www/js/color-picker.js)
color_picker_js <- HTML("
// Color picker input binding
var colorPickerBinding = new Shiny.InputBinding();
$.extend(colorPickerBinding, {
find: function(scope) {
return $(scope).find('.color-picker-input');
},
initialize: function(el) {
var $el = $(el);
var initialValue = $el.data('initial-value');
// Set up color picker functionality
$el.find('.color-swatch').click(function() {
// Create color picker modal or dropdown
showColorPicker($el, initialValue);
});
// Handle palette color clicks
$el.find('.palette-color').click(function() {
var color = $(this).data('color');
updateColor($el, color);
});
},
getValue: function(el) {
return $(el).find('.color-value').text();
},
setValue: function(el, value) {
updateColor($(el), value);
},
subscribe: function(el, callback) {
$(el).on('colorchange.colorPickerBinding', function(e) {
callback();
});
},
unsubscribe: function(el) {
$(el).off('.colorPickerBinding');
}
});
function updateColor($el, color) {
$el.find('.color-swatch').css('background-color', color);
$el.find('.color-value').text(color);
$el.find('.color-value-input').val(color);
$el.trigger('colorchange');
}
function showColorPicker($el, currentColor) {
// Implementation for color picker interface
// Could use a library like Spectrum.js or create custom picker
}
Shiny.inputBindings.register(colorPickerBinding);
")Advanced JavaScript Interactions
Implement complex interactive behaviors with JavaScript integration:
# Drag and drop sortable list
sortable_list_UI <- function(id, items, options = list()) {
ns <- NS(id)
div(
id = ns("sortable_container"),
class = "sortable-container",
# Include necessary JavaScript libraries
tags$script(src = "https://cdn.jsdelivr.net/npm/sortablejs@1.14.0/Sortable.min.js"),
div(
id = ns("sortable_list"),
class = "sortable-list",
lapply(seq_along(items), function(i) {
div(
class = "sortable-item",
`data-id` = i,
div(class = "item-handle", "⋮⋮"),
div(class = "item-content", items[[i]])
)
})
),
# Hidden input to store order
tags$input(
id = ns("item_order"),
type = "hidden",
class = "item-order-input"
),
# Initialize sortable JavaScript
tags$script(HTML(paste0("
$(document).ready(function() {
var sortable = Sortable.create(document.getElementById('", ns("sortable_list"), "'), {
handle: '.item-handle',
animation: 150,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
onEnd: function(evt) {
var order = Array.from(evt.to.children).map(function(item) {
return item.getAttribute('data-id');
});
$('#", ns("item_order"), "').val(JSON.stringify(order)).trigger('change');
}
});
});
")))
)
}
# Kanban board component
kanban_board_UI <- function(id, columns) {
ns <- NS(id)
div(class = "kanban-board",
# Include dragula for drag and drop
tags$script(src = "https://cdn.jsdelivr.net/npm/dragula@3.7.3/dist/dragula.min.js"),
tags$link(rel = "stylesheet",
href = "https://cdn.jsdelivr.net/npm/dragula@3.7.3/dist/dragula.min.css"),
div(class = "kanban-columns",
lapply(names(columns), function(col_name) {
div(class = "kanban-column",
div(class = "column-header",
h5(col_name, class = "column-title"),
span(class = "item-count", length(columns[[col_name]]))
),
div(
id = ns(paste0("column_", gsub(" ", "_", tolower(col_name)))),
class = "column-items",
lapply(columns[[col_name]], function(item) {
div(class = "kanban-item",
`data-item-id` = item$id,
div(class = "item-title", item$title),
if (!is.null(item$description)) {
div(class = "item-description", item$description)
},
if (!is.null(item$tags)) {
div(class = "item-tags",
lapply(item$tags, function(tag) {
span(class = "tag", tag)
})
)
}
)
})
)
)
})
),
# Initialize dragula
tags$script(HTML(paste0("
$(document).ready(function() {
var containers = [",
paste(sapply(names(columns), function(col) {
paste0("'", ns(paste0("column_", gsub(" ", "_", tolower(col)))), "'")
}), collapse = ","), "];
var containerElements = containers.map(function(id) {
return document.getElementById(id);
});
var drake = dragula(containerElements, {
moves: function(el, container, handle) {
return el.classList.contains('kanban-item');
}
});
drake.on('drop', function(el, target, source, sibling) {
// Update item counts
updateItemCounts();
// Send update to server
var itemId = el.getAttribute('data-item-id');
var newColumn = target.id.replace('", ns("column_"), "', '').replace(/_/g, ' ');
var oldColumn = source.id.replace('", ns("column_"), "', '').replace(/_/g, ' ');
Shiny.setInputValue('", ns("item_moved"), "', {
item_id: itemId,
from_column: oldColumn,
to_column: newColumn,
timestamp: new Date().getTime()
}, {priority: 'event'});
});
function updateItemCounts() {
containers.forEach(function(containerId) {
var container = document.getElementById(containerId);
var count = container.children.length;
var column = container.closest('.kanban-column');
column.querySelector('.item-count').textContent = count;
});
}
});
")))
)
}# Real-time collaborative editor
collaborative_editor_UI <- function(id) {
ns <- NS(id)
div(class = "collaborative-editor",
# Editor toolbar
div(class = "editor-toolbar",
div(class = "toolbar-group",
button(class = "toolbar-btn", id = ns("bold"),
type = "button", title = "Bold",
tags$i(class = "fas fa-bold")),
button(class = "toolbar-btn", id = ns("italic"),
type = "button", title = "Italic",
tags$i(class = "fas fa-italic")),
button(class = "toolbar-btn", id = ns("underline"),
type = "button", title = "Underline",
tags$i(class = "fas fa-underline"))
),
div(class = "toolbar-group",
span(class = "user-count", "1 user online"),
div(class = "user-cursors", id = ns("user_cursors"))
)
),
# Main editor area
div(
id = ns("editor"),
class = "collaborative-editor-content",
contenteditable = "true",
placeholder = "Start typing..."
),
# Status bar
div(class = "editor-status",
span(class = "save-status", "Saved"),
span(class = "word-count", "0 words")
),
# WebSocket connection for real-time updates
tags$script(HTML(paste0("
$(document).ready(function() {
var editor = document.getElementById('", ns("editor"), "');
var saveStatus = $('.save-status');
var wordCount = $('.word-count');
// Track changes and send to server
var changeTimeout;
editor.addEventListener('input', function() {
saveStatus.text('Saving...');
clearTimeout(changeTimeout);
changeTimeout = setTimeout(function() {
var content = editor.innerHTML;
Shiny.setInputValue('", ns("content_changed"), "', {
content: content,
timestamp: new Date().getTime()
});
// Update word count
var text = editor.innerText || editor.textContent;
var words = text.trim().split(/\\s+/).filter(function(word) {
return word.length > 0;
});
wordCount.text(words.length + ' words');
saveStatus.text('Saved');
}, 1000);
});
// Handle formatting buttons
$('#", ns("bold"), "').click(function() {
document.execCommand('bold');
editor.focus();
});
$('#", ns("italic"), "').click(function() {
document.execCommand('italic');
editor.focus();
});
$('#", ns("underline"), "').click(function() {
document.execCommand('underline');
editor.focus();
});
});
")))
)
}
# Advanced data visualization with D3.js integration
d3_network_UI <- function(id, width = "100%", height = "400px") {
ns <- NS(id)
div(class = "d3-network-container",
# Include D3.js
tags$script(src = "https://d3js.org/d3.v7.min.js"),
# Control panel
div(class = "network-controls mb-3",
div(class = "row",
div(class = "col-md-3",
sliderInput(ns("force_strength"), "Force Strength:",
-100, 100, -30, step = 10)
),
div(class = "col-md-3",
sliderInput(ns("link_distance"), "Link Distance:",
10, 200, 50, step = 10)
),
div(class = "col-md-3",
selectInput(ns("color_by"), "Color By:",
choices = c("Degree", "Betweenness", "Closeness"))
),
div(class = "col-md-3",
actionButton(ns("reset_simulation"), "Reset Layout",
class = "btn-secondary")
)
)
),
# SVG container for D3 visualization
div(
id = ns("network_svg_container"),
style = paste0("width: ", width, "; height: ", height, ";"),
tags$svg(
id = ns("network_svg"),
width = "100%",
height = "100%"
)
),
# Node information panel
div(id = ns("node_info"), class = "node-info-panel",
style = "display: none;",
div(class = "panel-header",
h6("Node Information", class = "mb-0"),
button(class = "btn-close btn-sm", type = "button",
onclick = paste0("$('#", ns("node_info"), "').hide()"))
),
div(class = "panel-body",
div(id = ns("node_details"))
)
),
# Initialize D3 network
tags$script(HTML(paste0("
$(document).ready(function() {
initializeNetworkVisualization('", ns(""), "');
});
")))
)
}Custom Message Passing
Implement sophisticated communication between R and JavaScript:
# Advanced message passing system
message_system_UI <- function(id) {
ns <- NS(id)
div(class = "message-system",
# Message display area
div(id = ns("message_area"), class = "message-area"),
# Control panel
div(class = "message-controls",
textInput(ns("message_text"), "Message:", placeholder = "Type a message..."),
div(class = "btn-group",
actionButton(ns("send_info"), "Info", class = "btn-info"),
actionButton(ns("send_warning"), "Warning", class = "btn-warning"),
actionButton(ns("send_error"), "Error", class = "btn-danger")
)
),
# JavaScript for message handling
tags$script(HTML(paste0("
// Message system JavaScript
Shiny.addCustomMessageHandler('showMessage', function(message) {
showMessage(message.text, message.type, message.duration);
});
Shiny.addCustomMessageHandler('updateProgress', function(progress) {
updateProgressBar(progress.value, progress.max, progress.text);
});
function showMessage(text, type, duration) {
var messageId = 'msg_' + Date.now();
var alertClass = 'alert-' + type;
var icon = getIconForType(type);
var messageHtml = '<div id=\"' + messageId + '\" class=\"alert ' + alertClass + ' alert-dismissible fade show message-item\">' +
'<i class=\"' + icon + ' me-2\"></i>' + text +
'<button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"alert\"></button>' +
'</div>';
$('#", ns("message_area"), "').prepend(messageHtml);
// Auto-dismiss after duration
if (duration > 0) {
setTimeout(function() {
$('#' + messageId).alert('close');
}, duration);
}
}
function getIconForType(type) {
switch(type) {
case 'info': return 'fas fa-info-circle';
case 'warning': return 'fas fa-exclamation-triangle';
case 'error': return 'fas fa-times-circle';
case 'success': return 'fas fa-check-circle';
default: return 'fas fa-info-circle';
}
}
")))
)
}
# Server logic for message system
message_system_Server <- function(id) {
moduleServer(id, function(input, output, session) {
# Send different types of messages
observeEvent(input$send_info, {
session$sendCustomMessage(
type = 'showMessage',
message = list(
text = input$message_text %||% "Information message",
type = 'info',
duration = 5000
)
)
})
observeEvent(input$send_warning, {
session$sendCustomMessage(
type = 'showMessage',
message = list(
text = input$message_text %||% "Warning message",
type = 'warning',
duration = 7000
)
)
})
observeEvent(input$send_error, {
session$sendCustomMessage(
type = 'showMessage',
message = list(
text = input$message_text %||% "Error message",
type = 'error',
duration = 0 # Don't auto-dismiss errors
)
)
})
})
}External Library Integration
Leverage powerful external libraries to create professional-grade interfaces.
Chart.js Integration
Create advanced charts with Chart.js library:
# Chart.js wrapper for Shiny
chartjs_output <- function(outputId, width = "100%", height = "400px") {
div(
id = outputId,
class = "chartjs-output",
style = paste0("width: ", width, "; height: ", height, ";"),
# Include Chart.js
tags$script(src = "https://cdn.jsdelivr.net/npm/chart.js"),
# Canvas for chart
tags$canvas(
id = paste0(outputId, "_canvas"),
width = "400",
height = "200"
)
)
}
# Render function for Chart.js
renderChartjs <- function(expr, env = parent.frame(), quoted = FALSE) {
if (!quoted) { expr <- substitute(expr) }
renderUI({
chart_data <- eval(expr, env)
# Generate unique ID for this chart
chart_id <- paste0("chart_", sample.int(10000, 1))
tagList(
tags$canvas(id = chart_id),
tags$script(HTML(paste0("
var ctx = document.getElementById('", chart_id, "');
var chart = new Chart(ctx, ", jsonlite::toJSON(chart_data, auto_unbox = TRUE), ");
")))
)
})
}
# Example usage
ui <- fluidPage(
titlePanel("Advanced Chart.js Integration"),
div(class = "container-fluid",
div(class = "row",
div(class = "col-md-6",
h4("Sales Performance"),
uiOutput("sales_chart")
),
div(class = "col-md-6",
h4("Market Share"),
uiOutput("market_chart")
)
)
)
)
server <- function(input, output, session) {
output$sales_chart <- renderChartjs({
list(
type = 'line',
data = list(
labels = c('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'),
datasets = list(
list(
label = 'Sales',
data = c(12, 19, 3, 5, 2, 3),
borderColor = 'rgb(75, 192, 192)',
backgroundColor = 'rgba(75, 192, 192, 0.2)',
tension = 0.1
)
)
),
options = list(
responsive = TRUE,
maintainAspectRatio = FALSE,
scales = list(
y = list(
beginAtZero = TRUE
)
)
)
)
})
output$market_chart <- renderChartjs({
list(
type = 'doughnut',
data = list(
labels = c('Product A', 'Product B', 'Product C'),
datasets = list(
list(
data = c(300, 50, 100),
backgroundColor = c('#FF6384', '#36A2EB', '#FFCE56')
)
)
),
options = list(
responsive = TRUE,
maintainAspectRatio = FALSE
)
)
})
}Advanced Data Grid Components
Implement sophisticated data grids with external libraries:
# Advanced data grid using ag-Grid
ag_grid_output <- function(outputId, height = "400px") {
div(
id = outputId,
class = "ag-grid-container",
style = paste0("height: ", height, "; width: 100%;"),
# Include ag-Grid
tags$script(src = "https://cdn.jsdelivr.net/npm/ag-grid-community/dist/ag-grid-community.min.js"),
tags$link(rel = "stylesheet",
href = "https://cdn.jsdelivr.net/npm/ag-grid-community/styles/ag-grid.css"),
tags$link(rel = "stylesheet",
href = "https://cdn.jsdelivr.net/npm/ag-grid-community/styles/ag-theme-alpine.css"),
# Grid container
div(
id = paste0(outputId, "_grid"),
class = "ag-theme-alpine",
style = paste0("height: ", height, "; width: 100%;")
)
)
}
# Render function for ag-Grid
renderAgGrid <- function(expr, env = parent.frame(), quoted = FALSE) {
if (!quoted) { expr <- substitute(expr) }
renderUI({
grid_config <- eval(expr, env)
grid_id <- paste0("grid_", sample.int(10000, 1))
tagList(
div(
id = grid_id,
class = "ag-theme-alpine",
style = "height: 400px; width: 100%;"
),
tags$script(HTML(paste0("
$(document).ready(function() {
var gridOptions = ", jsonlite::toJSON(grid_config, auto_unbox = TRUE), ";
var gridDiv = document.querySelector('#", grid_id, "');
new agGrid.Grid(gridDiv, gridOptions);
});
")))
)
})
}Performance Optimization for Advanced UI
Ensure complex UI components maintain excellent performance.
Efficient Rendering Strategies
Optimize rendering for complex components:
# Virtualized list for large datasets
virtualized_list_UI <- function(id, item_height = 50, container_height = "400px") {
ns <- NS(id)
div(class = "virtualized-list-container",
# Include virtual scrolling library
tags$script(src = "https://cdn.jsdelivr.net/npm/react-window@1.8.6/dist/index.umd.js"),
div(
id = ns("list_container"),
class = "list-container",
style = paste0("height: ", container_height, "; overflow: auto;"),
# Scrollable area
div(
id = ns("scroll_area"),
class = "scroll-area"
)
),
# Virtual scrolling implementation
tags$script(HTML(paste0("
// Virtual scrolling implementation
function createVirtualizedList(containerId, items, itemHeight) {
var container = document.getElementById(containerId);
var scrollArea = container.querySelector('.scroll-area');
var containerHeight = container.clientHeight;
var visibleItems = Math.ceil(containerHeight / itemHeight) + 2;
var startIndex = 0;
function renderItems() {
var scrollTop = container.scrollTop;
startIndex = Math.floor(scrollTop / itemHeight);
var endIndex = Math.min(startIndex + visibleItems, items.length);
// Clear existing items
scrollArea.innerHTML = '';
// Set total height for scrollbar
scrollArea.style.height = (items.length * itemHeight) + 'px';
scrollArea.style.position = 'relative';
// Render visible items
for (var i = startIndex; i < endIndex; i++) {
var item = document.createElement('div');
item.className = 'list-item';
item.style.position = 'absolute';
item.style.top = (i * itemHeight) + 'px';
item.style.height = itemHeight + 'px';
item.style.width = '100%';
item.innerHTML = items[i];
scrollArea.appendChild(item);
}
}
container.addEventListener('scroll', renderItems);
renderItems(); // Initial render
}
")))
)
}
# Lazy loading component
lazy_load_component <- function(id, loader_function, trigger_distance = 100) {
ns <- NS(id)
div(
id = ns("lazy_container"),
class = "lazy-load-container",
# Content area
div(id = ns("content_area"), class = "content-area"),
# Loading indicator
div(id = ns("loading_indicator"), class = "loading-indicator text-center py-3",
style = "display: none;",
div(class = "spinner-border spinner-border-sm me-2"),
span("Loading more content...")
),
# Intersection observer for lazy loading
tags$script(HTML(paste0("
$(document).ready(function() {
var container = document.getElementById('", ns("lazy_container"), "');
var loadingIndicator = document.getElementById('", ns("loading_indicator"), "');
var isLoading = false;
// Create intersection observer
var observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting && !isLoading) {
isLoading = true;
loadingIndicator.style.display = 'block';
// Trigger server-side loading
Shiny.setInputValue('", ns("load_more"), "', {
timestamp: new Date().getTime()
}, {priority: 'event'});
}
});
}, {
root: null,
rootMargin: '", trigger_distance, "px',
threshold: 0
});
// Observe loading indicator
observer.observe(loadingIndicator);
// Handle loading completion
Shiny.addCustomMessageHandler('", ns("loading_complete"), "', function(data) {
isLoading = false;
loadingIndicator.style.display = 'none';
if (data.hasMore) {
loadingIndicator.style.display = 'block';
}
});
});
")))
)
}Memory Management
Implement efficient memory management for complex components:
# Component lifecycle management
component_manager <- function() {
list(
# Component registry
components = list(),
# Register component
register = function(id, cleanup_fn) {
components[[id]] <<- list(
cleanup = cleanup_fn,
created = Sys.time()
)
},
# Cleanup component
cleanup = function(id) {
if (id %in% names(components)) {
if (!is.null(components[[id]]$cleanup)) {
try(components[[id]]$cleanup(), silent = TRUE)
}
components[[id]] <<- NULL
}
},
# Cleanup all components
cleanup_all = function() {
for (id in names(components)) {
cleanup(id)
}
},
# Get component info
get_info = function() {
data.frame(
id = names(components),
created = sapply(components, function(x) as.character(x$created)),
stringsAsFactors = FALSE
)
}
)
}
# Usage in server
server <- function(input, output, session) {
# Initialize component manager
comp_mgr <- component_manager()
# Register cleanup on session end
onStop(function() {
comp_mgr$cleanup_all()
})
# Register components as they're created
output$complex_chart <- renderPlotly({
# Create complex chart
p <- create_complex_chart()
# Register cleanup
comp_mgr$register("complex_chart", function() {
# Cleanup resources
gc()
})
p
})
}Common Issues and Solutions
Issue 1: Custom JavaScript Not Loading or Executing
Problem: Custom JavaScript code doesn’t execute or causes errors in the browser console.
Solution:
Debug JavaScript integration systematically:
# Proper JavaScript loading and error handling
ui <- fluidPage(
tags$head(
# Load dependencies in correct order
tags$script(src = "https://code.jquery.com/jquery-3.6.0.min.js"),
tags$script(src = "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"),
# Custom JavaScript with error handling
tags$script(HTML("
$(document).ready(function() {
console.log('JavaScript loaded successfully');
// Wrap custom code in try-catch
try {
// Your custom JavaScript here
initializeCustomComponents();
} catch (error) {
console.error('Error initializing components:', error);
// Send error to Shiny for debugging
Shiny.setInputValue('js_error', {
message: error.message,
stack: error.stack,
timestamp: new Date().getTime()
});
}
});
function initializeCustomComponents() {
// Component initialization code
}
"))
),
# Your UI content
)
server <- function(input, output, session) {
# Handle JavaScript errors
observeEvent(input$js_error, {
cat("JavaScript Error:", input$js_error$message, "\n")
# Log for debugging
})
}Issue 2: Module Communication Problems
Problem: Shiny modules don’t communicate properly or data doesn’t update between modules.
Solution:
Implement proper module communication patterns:
# Correct module communication
# Parent module that coordinates child modules
parent_module_Server <- function(id) {
moduleServer(id, function(input, output, session) {
# Shared reactive values
shared_data <- reactiveValues(
dataset = mtcars,
filters = list(),
selections = list()
)
# Filter module
filter_result <- filter_module_Server("filter",
data = reactive(shared_data$dataset)
)
# Update shared data when filter changes
observe({
shared_data$dataset <- filter_result$filtered_data()
})
# Chart module using filtered data
chart_result <- chart_module_Server("chart",
data = reactive(shared_data$dataset)
)
# Table module using the same filtered data
table_result <- table_module_Server("table",
data = reactive(shared_data$dataset)
)
# Return shared state for external access
return(shared_data)
})
}
# Proper reactive dependencies in child modules
filter_module_Server <- function(id, data) {
moduleServer(id, function(input, output, session) {
# Filtered data with proper reactive dependencies
filtered_data <- reactive({
req(data()) # Ensure data is available
df <- data()
# Apply filters
if (!is.null(input$category_filter)) {
df <- df[df$category %in% input$category_filter, ]
}
df
})
# Return reactive expression, not reactive value
return(list(
filtered_data = filtered_data,
filter_count = reactive(nrow(filtered_data()))
))
})
}Issue 3: Performance Issues with Complex UI
Problem: Application becomes slow or unresponsive with complex custom components.
Solution:
Implement performance optimization strategies:
# Performance optimization techniques
server <- function(input, output, session) {
# Use debouncing for expensive operations
debounced_input <- reactive({
input$complex_filter
}) %>% debounce(500) # Wait 500ms after user stops typing
# Cache expensive computations
expensive_computation <- reactive({
req(debounced_input())
# Expensive operation here
result <- perform_complex_analysis(debounced_input())
result
})
# Use bindCache for stable caching
cached_result <- reactive({
expensive_computation()
}) %>% bindCache(debounced_input())
# Conditional rendering for complex outputs
output$complex_visualization <- renderPlotly({
req(input$show_advanced_viz) # Only render when needed
if (nrow(cached_result()) > 10000) {
# Simplified visualization for large datasets
create_summary_plot(cached_result())
} else {
# Full visualization for smaller datasets
create_detailed_plot(cached_result())
}
})
# Use observe instead of reactive for side effects
observe({
# Update UI elements without creating reactive dependencies
session$sendCustomMessage("updateStatus", list(
records = nrow(cached_result()),
timestamp = Sys.time()
))
})
}
# Client-side performance optimization
performance_js <- HTML("
// Throttle scroll events
function throttle(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Optimize DOM updates
function batchDOMUpdates(updates) {
requestAnimationFrame(() => {
updates.forEach(update => update());
});
}
// Memory leak prevention
function cleanupEventListeners(element) {
const clone = element.cloneNode(true);
element.parentNode.replaceChild(clone, element);
return clone;
}
")- Test JavaScript thoroughly across different browsers and devices
- Implement proper error handling for both R and JavaScript components
- Use module patterns to maintain clean, reusable code architecture
- Optimize performance from the start - don’t wait until problems arise
- Document custom components for team collaboration and maintenance
Common Questions About Advanced Shiny UI
Create custom components when built-in widgets don’t meet specific user experience requirements or when you need specialized functionality. Good candidates include: industry-specific interfaces (like trading dashboards), complex data entry forms, specialized visualization components, or when you need to match exact brand design requirements. However, start with built-in components and customize them first - only build completely custom components when modification isn’t sufficient.
Use Shiny modules for reusable application logic and UI patterns that need server-side reactivity. Use custom HTML components for presentation-layer elements that primarily need styling and client-side behavior. Modules are better for complex features like filtering systems or chart builders, while custom HTML components work well for specialized input controls, layout elements, or visual enhancements. Often, you’ll combine both approaches in sophisticated applications.
Start with CDN links for development, then bundle for production. Include libraries in the correct dependency order, wrap initialization code in document ready handlers, and create Shiny input/output bindings when you need two-way communication. Use session$sendCustomMessage() for R-to-JavaScript communication and custom input bindings for JavaScript-to-R communication. Always include error handling and fallbacks for when libraries fail to load.
Implement lazy loading, virtual scrolling, and conditional rendering. Use req() to prevent unnecessary computations, implement debouncing for user inputs, and cache expensive operations with bindCache(). On the client side, use requestAnimationFrame for DOM updates, implement proper event listener cleanup, and consider using Web Workers for heavy JavaScript computations. Monitor performance regularly and optimize bottlenecks as they appear.
Always validate and sanitize user inputs both client-side and server-side. Avoid using HTML() with user-generated content without sanitization. Be cautious with external libraries - use reputable sources and specific versions rather than “latest”. Implement Content Security Policy (CSP) headers when possible, and never trust client-side validation alone. Consider the implications of any data that flows between client and server, especially in multi-user applications.
Test Your Understanding
You’re building a complex Shiny application with multiple modules that need to share data. Which approach provides the best architecture for module communication?
- Use global variables to share data between modules
- Pass reactive expressions between modules and use a coordinator module
- Have each module maintain its own copy of the data independently
- Use session storage to share data between modules
- Think about reactivity and data consistency
- Consider maintainability and debugging complexity
- Remember Shiny’s reactive programming principles
B) Pass reactive expressions between modules and use a coordinator module
This approach provides the best architecture because:
# Coordinator module pattern
parent_module_Server <- function(id) {
moduleServer(id, function(input, output, session) {
# Shared reactive values managed centrally
shared_state <- reactiveValues(
data = mtcars,
filters = list(),
selections = list()
)
# Child modules receive reactive expressions
filter_result <- filter_module_Server("filter",
data = reactive(shared_state$data))
# Updates flow through reactive system
observe({
shared_state$data <- filter_result$filtered_data()
})
chart_module_Server("chart",
data = reactive(shared_state$data))
})
}Why this is optimal:
- Maintains reactivity: Changes propagate automatically through the reactive system
- Single source of truth: Coordinator module manages shared state centrally
- Easy debugging: Clear data flow and dependency tracking
- Scalable: Easy to add new modules or modify data flow
Why other options are problematic:
- A: Global variables break Shiny’s reactive system
- C: Data inconsistency and synchronization problems
- D: Session storage bypasses Shiny’s reactive system
Complete this custom input binding code to create a color picker that communicates with Shiny:
var colorPickerBinding = new Shiny.InputBinding();
$.extend(colorPickerBinding, {
find: function(scope) {
return $(scope).find('.color-picker-input');
},
getValue: function(el) {
// Fill in: How to get the current color value
return ________;
},
setValue: function(el, value) {
// Fill in: How to set the color value
________;
},
subscribe: function(el, callback) {
// Fill in: How to listen for changes
________;
}
});- Think about what DOM element stores the color value
- Consider what events should trigger the callback
- Remember that Shiny input bindings need consistent value access
var colorPickerBinding = new Shiny.InputBinding();
$.extend(colorPickerBinding, {
find: function(scope) {
return $(scope).find('.color-picker-input');
},
getValue: function(el) {
return $(el).find('.color-value').text();
},
setValue: function(el, value) {
$(el).find('.color-swatch').css('background-color', value);
$(el).find('.color-value').text(value);
},
subscribe: function(el, callback) {
$(el).on('colorchange.colorPickerBinding', function(e) {
callback();
});
}
});Key concepts:
getValue(): Retrieves current value from DOM element (usually from a specific child element)setValue(): Updates both visual display and stored valuesubscribe(): Listens for custom events and triggers Shiny’s callback system- Event namespacing: Using
.colorPickerBindingprevents conflicts with other event handlers
Your Shiny application has a complex custom component that becomes slow with large datasets. Which combination of techniques provides the most effective performance optimization?
- Increase server memory and use faster hardware only
- Implement virtual scrolling, debouncing, and conditional rendering
- Simplify the UI by removing custom components entirely
- Load all data at startup and cache everything in memory
- Think about what causes performance bottlenecks in web applications
- Consider both client-side and server-side optimization techniques
- Remember that performance optimization should maintain functionality
B) Implement virtual scrolling, debouncing, and conditional rendering
This combination addresses the main performance bottlenecks:
# Server-side optimization
server <- function(input, output, session) {
# Debouncing for user inputs
debounced_filter <- reactive({
input$filter_text
}) %>% debounce(300)
# Conditional rendering
output$complex_component <- renderUI({
req(input$show_advanced_view) # Only render when needed
data <- filtered_data()
if (nrow(data) > 1000) {
# Simplified view for large datasets
summary_component(data)
} else {
# Full component for smaller datasets
detailed_component(data)
}
})
}// Client-side virtual scrolling
function createVirtualizedList(items, containerHeight, itemHeight) {
const visibleItems = Math.ceil(containerHeight / itemHeight);
function renderVisibleItems(scrollTop) {
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(startIndex + visibleItems, items.length);
// Only render items currently visible
return items.slice(startIndex, endIndex);
}
}Why this approach works:
- Virtual scrolling: Only renders visible items, reducing DOM size
- Debouncing: Prevents excessive server requests during rapid user input
- Conditional rendering: Adapts complexity based on data size and user needs
- Maintains functionality: Users get full features when appropriate
Why other options are insufficient:
- A: Hardware improvements don’t address algorithmic inefficiencies
- C: Reduces functionality unnecessarily
- D: Can cause memory issues and doesn’t address rendering bottlenecks
Conclusion
Mastering advanced UI components and custom HTML in Shiny transforms you from a developer who uses existing tools into one who creates entirely new interactive experiences. The techniques you’ve learned - from custom HTML components and Shiny modules to JavaScript integration and external library incorporation - provide the foundation for building truly sophisticated applications that can compete with any web-based platform.
Advanced Shiny UI development requires balancing power with performance, creativity with maintainability, and innovation with usability. The modular design patterns, custom component architectures, and JavaScript integration techniques you’ve mastered enable you to build applications that scale from small departmental tools to enterprise-wide platforms while maintaining code quality and user experience excellence.
Your journey through advanced UI development positions you to tackle the most challenging interface requirements and create applications that truly differentiate themselves through superior user experience and innovative functionality.
Next Steps
Based on your advanced UI development expertise, here are the recommended paths for continued growth:
Immediate Next Steps (Complete These First)
- Dynamic UI Generation and Conditional Interfaces - Apply advanced UI techniques to create completely dynamic applications
- JavaScript Integration for Advanced Interactions - Deepen your JavaScript integration skills with complex scenarios
- Practice Exercise: Build a comprehensive component library that can be reused across multiple Shiny applications
Building on Your Foundation (Choose Your Path)
For Enterprise Development:
- Shiny Modules for Scalable Applications
- Testing and Debugging Advanced Applications
- Security Best Practices for Complex Applications
For Specialized Applications:
For Professional Development:
Long-term Goals (2-4 Weeks)
- Create a comprehensive Shiny component library with documentation and examples
- Build a flagship application that showcases advanced UI techniques and serves as a portfolio piece
- Contribute to the Shiny community by open-sourcing reusable components or writing about advanced techniques
- Develop expertise in specific areas like data visualization frameworks or specialized industry interfaces
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 = {Advanced {UI} {Components} and {Custom} {HTML:} {Shiny}
{Development} {Guide}},
date = {2025-05-23},
url = {https://www.datanovia.com/learn/tools/shiny-apps/ui-design/advanced-ui.html},
langid = {en}
}
