flowchart TD
A[Web Accessibility] --> B[Visual Impairments]
A --> C[Hearing Impairments]
A --> D[Motor Impairments]
A --> E[Cognitive Impairments]
B --> B1[Screen Readers]
B --> B2[High Contrast]
B --> B3[Magnification]
B --> B4[Color Blindness]
C --> C1[Captions]
C --> C2[Transcripts]
C --> C3[Visual Indicators]
D --> D1[Keyboard Navigation]
D --> D2[Voice Control]
D --> D3[Switch Access]
D --> D4[Touch Alternatives]
E --> E1[Clear Language]
E --> E2[Consistent Navigation]
E --> E3[Error Prevention]
E --> E4[Help Systems]
style A fill:#e1f5fe
style B fill:#f3e5f5
style C fill:#e8f5e8
style D fill:#fff3e0
style E fill:#fce4ec
Key Takeaways
- Universal Design Principles: Build applications that work for everyone from the start, reducing the need for costly accessibility retrofits and creating better experiences for all users
- WCAG Compliance Framework: Implement Web Content Accessibility Guidelines systematically to ensure your applications meet legal requirements and accessibility standards
- Performance-First Architecture: Design reactive systems and data processing pipelines that maintain responsiveness even with large datasets and multiple concurrent users
- Assistive Technology Support: Create applications that work seamlessly with screen readers, keyboard navigation, and other assistive technologies
- Monitoring and Optimization: Establish continuous monitoring for both accessibility compliance and performance metrics to maintain quality over time
Introduction
Accessibility and performance are not optional features in professional Shiny development - they are fundamental requirements that determine whether your applications can serve all users effectively. Accessible applications ensure that people with disabilities can use your analytical tools, while high-performance applications provide responsive experiences that encourage data exploration and decision-making.
Modern organizations increasingly recognize that accessibility is both a legal requirement and a business advantage. Applications that work for users with diverse abilities often provide better experiences for everyone. Similarly, performance optimization that reduces loading times and improves responsiveness benefits all users while reducing infrastructure costs and improving user satisfaction.
This comprehensive guide covers the essential techniques for building Shiny applications that are both accessible and performant. You’ll learn to implement WCAG compliance standards, optimize reactive systems for large-scale data processing, and create inclusive user experiences that work across different devices and assistive technologies. These practices distinguish professional applications from basic prototypes and ensure your analytical tools can serve diverse user communities effectively.
Accessibility Fundamentals
Understanding Web Accessibility Standards
Web accessibility ensures that applications are usable by people with diverse abilities and disabilities:
WCAG 2.1 Compliance Framework
The Web Content Accessibility Guidelines (WCAG) provide comprehensive standards organized around four principles:
POUR Principles:
- Perceivable: Information must be presentable in ways users can perceive
- Operable: Interface components must be operable by all users
- Understandable: Information and UI operation must be understandable
- Robust: Content must be robust enough for interpretation by assistive technologies
Conformance Levels:
- Level A: Minimum accessibility features (basic compliance)
- Level AA: Standard accessibility features (recommended target)
- Level AAA: Enhanced accessibility features (specialized use cases)
Implementing Accessible Shiny Components
Semantic HTML and ARIA Labels
Create applications that work seamlessly with assistive technologies:
# Comprehensive accessible UI component library
create_accessible_ui_components <- function() {
# Accessible input components with proper labeling
accessible_text_input <- function(id, label, description = NULL, required = FALSE, ...) {
# Generate unique IDs for proper association
input_id <- id
desc_id <- paste0(id, "_desc")
error_id <- paste0(id, "_error")
# Build ARIA attributes
aria_attrs <- list(
`aria-describedby` = if (!is.null(description)) desc_id else NULL,
`aria-required` = if (required) "true" else NULL,
`aria-invalid` = "false" # Will be updated by validation
)
div(class = "form-group",
# Proper label association
tags$label(
`for` = input_id,
class = if (required) "required" else NULL,
label,
if (required) span(class = "required-indicator", "*", `aria-label` = "required")
),
# Input with accessibility attributes
textInput(
inputId = input_id,
label = NULL, # Already provided above
...
) %>% tagAppendAttributes(!!!aria_attrs),
# Description text
if (!is.null(description)) {
div(id = desc_id, class = "form-help-text", description)
},
# Error message container (hidden by default)
div(id = error_id, class = "form-error-text", style = "display: none;",
role = "alert", `aria-live` = "polite")
)
}
# Accessible select input with proper grouping
accessible_select_input <- function(id, label, choices, description = NULL, required = FALSE, ...) {
input_id <- id
desc_id <- paste0(id, "_desc")
div(class = "form-group",
tags$label(
`for` = input_id,
class = if (required) "required" else NULL,
label,
if (required) span(class = "required-indicator", "*", `aria-label` = "required")
),
# Enhanced select with proper ARIA
selectInput(
inputId = input_id,
label = NULL,
choices = choices,
...
) %>% tagAppendAttributes(
`aria-describedby` = if (!is.null(description)) desc_id else NULL,
`aria-required` = if (required) "true" else NULL
),
if (!is.null(description)) {
div(id = desc_id, class = "form-help-text", description)
}
)
}
# Accessible data table with proper headers and navigation
accessible_data_table <- function(data, table_id, caption = NULL, summary = NULL) {
# Generate table with proper semantic structure
table_html <- tags$table(
id = table_id,
class = "table table-striped table-hover",
role = "table",
`aria-label` = caption %||% "Data table",
# Caption for screen readers
if (!is.null(caption)) {
tags$caption(caption)
},
# Table header with proper scope
tags$thead(
tags$tr(
lapply(names(data), function(col_name) {
tags$th(
scope = "col",
role = "columnheader",
`aria-sort` = "none", # Will be updated by sorting logic
col_name
)
})
)
),
# Table body with row headers where appropriate
tags$tbody(
lapply(seq_len(min(nrow(data), 100)), function(row_idx) { # Limit for performance
row_data <- data[row_idx, ]
tags$tr(
# First column as row header if it's an identifier
tags$th(scope = "row", role = "rowheader", row_data[[1]]),
# Remaining columns as data cells
lapply(2:ncol(row_data), function(col_idx) {
tags$td(
role = "gridcell",
`aria-describedby` = paste0(table_id, "_col_", col_idx),
as.character(row_data[[col_idx]])
)
})
)
})
)
)
# Wrap table with navigation and summary
div(class = "table-container",
# Summary for screen readers
if (!is.null(summary)) {
div(class = "sr-only", summary)
},
# Table navigation instructions
div(class = "table-instructions",
"Use arrow keys to navigate table cells, Tab to move between interactive elements."
),
# Scrollable table wrapper
div(class = "table-responsive",
`aria-label` = "Scrollable data table",
table_html
),
# Table statistics
div(class = "table-stats", `aria-live` = "polite",
paste("Showing", min(nrow(data), 100), "of", nrow(data), "rows")
)
)
}
# Accessible plot with alternative text and data table
accessible_plot_output <- function(id, alt_text, data_table = NULL) {
plot_id <- id
table_id <- paste0(id, "_table")
desc_id <- paste0(id, "_desc")
div(class = "accessible-plot-container",
# Plot with proper labeling
div(class = "plot-wrapper",
plotOutput(
outputId = plot_id,
width = "100%",
height = "400px"
) %>% tagAppendAttributes(
role = "img",
`aria-labelledby` = desc_id,
`aria-describedby` = if (!is.null(data_table)) table_id else NULL
),
# Alternative text description
div(id = desc_id, class = "plot-description",
h4("Chart Description"),
p(alt_text)
)
),
# Alternative data table
if (!is.null(data_table)) {
div(class = "plot-alternative",
h4("Data Table Alternative"),
p("The following table contains the data represented in the chart above."),
div(id = table_id,
DT::dataTableOutput(paste0(plot_id, "_datatable"))
)
)
},
# Download options
div(class = "plot-actions",
h4("Download Options"),
downloadButton(paste0(plot_id, "_download"), "Download Chart as PNG",
class = "btn btn-secondary"),
if (!is.null(data_table)) {
downloadButton(paste0(plot_id, "_data_download"), "Download Data as CSV",
class = "btn btn-secondary")
}
)
)
}
# Accessible navigation menu
accessible_navigation <- function(menu_items, current_page = NULL) {
nav_id <- "main_navigation"
tags$nav(
role = "navigation",
`aria-label` = "Main navigation",
id = nav_id,
tags$ul(
class = "nav nav-tabs",
role = "tablist",
lapply(names(menu_items), function(item_name) {
item_config <- menu_items[[item_name]]
is_current <- !is.null(current_page) && current_page == item_name
tags$li(
class = "nav-item",
role = "presentation",
tags$a(
class = paste("nav-link", if (is_current) "active" else ""),
href = item_config$href %||% paste0("#", gsub("\\s+", "_", tolower(item_name))),
role = "tab",
`aria-selected` = if (is_current) "true" else "false",
`aria-current` = if (is_current) "page" else NULL,
tabindex = if (is_current) "0" else "-1",
item_name,
# Add screen reader text for current page
if (is_current) {
span(class = "sr-only", " (current page)")
}
)
)
})
)
)
}
list(
accessible_text_input = accessible_text_input,
accessible_select_input = accessible_select_input,
accessible_data_table = accessible_data_table,
accessible_plot_output = accessible_plot_output,
accessible_navigation = accessible_navigation
)
}# Comprehensive keyboard navigation system
implement_keyboard_navigation <- function() {
# Custom JavaScript for enhanced keyboard support
keyboard_navigation_js <- "
// Enhanced keyboard navigation for Shiny applications
$(document).ready(function() {
// Skip link functionality
$('.skip-link').on('click keydown', function(e) {
if (e.type === 'click' || e.keyCode === 13 || e.keyCode === 32) {
e.preventDefault();
var target = $(this.getAttribute('href'));
if (target.length) {
target.attr('tabindex', '-1').focus();
}
}
});
// Enhanced table navigation
$('table[role=\"table\"]').each(function() {
var table = $(this);
var cells = table.find('td, th');
var currentCell = 0;
// Add tabindex to cells
cells.attr('tabindex', '-1');
if (cells.length > 0) {
$(cells[0]).attr('tabindex', '0');
}
// Arrow key navigation
cells.on('keydown', function(e) {
var cell = $(this);
var row = cell.closest('tr');
var rowIndex = row.index();
var cellIndex = cell.index();
var newCell;
switch(e.keyCode) {
case 37: // Left arrow
newCell = cell.prev('td, th');
break;
case 38: // Up arrow
newCell = row.prev('tr').find('td, th').eq(cellIndex);
break;
case 39: // Right arrow
newCell = cell.next('td, th');
break;
case 40: // Down arrow
newCell = row.next('tr').find('td, th').eq(cellIndex);
break;
default:
return;
}
if (newCell && newCell.length > 0) {
e.preventDefault();
cells.attr('tabindex', '-1');
newCell.attr('tabindex', '0').focus();
}
});
});
// Modal dialog focus management
$('.modal').on('shown.bs.modal', function() {
var modal = $(this);
var focusableElements = modal.find('button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])');
var firstElement = focusableElements.first();
var lastElement = focusableElements.last();
// Focus first element
firstElement.focus();
// Trap focus within modal
modal.on('keydown', function(e) {
if (e.keyCode === 9) { // Tab key
if (e.shiftKey) {
if (document.activeElement === firstElement[0]) {
e.preventDefault();
lastElement.focus();
}
} else {
if (document.activeElement === lastElement[0]) {
e.preventDefault();
firstElement.focus();
}
}
}
// Close on Escape
if (e.keyCode === 27) {
modal.modal('hide');
}
});
});
// Enhanced dropdown navigation
$('.dropdown-toggle').on('keydown', function(e) {
var dropdown = $(this).next('.dropdown-menu');
var items = dropdown.find('a, button');
switch(e.keyCode) {
case 40: // Down arrow - open and focus first item
e.preventDefault();
dropdown.addClass('show');
items.first().focus();
break;
case 27: // Escape - close dropdown
dropdown.removeClass('show');
$(this).focus();
break;
}
});
// Dropdown menu item navigation
$('.dropdown-menu a, .dropdown-menu button').on('keydown', function(e) {
var item = $(this);
var menu = item.closest('.dropdown-menu');
var items = menu.find('a, button');
var currentIndex = items.index(item);
switch(e.keyCode) {
case 38: // Up arrow
e.preventDefault();
if (currentIndex > 0) {
items.eq(currentIndex - 1).focus();
} else {
items.last().focus();
}
break;
case 40: // Down arrow
e.preventDefault();
if (currentIndex < items.length - 1) {
items.eq(currentIndex + 1).focus();
} else {
items.first().focus();
}
break;
case 27: // Escape
menu.removeClass('show');
menu.prev('.dropdown-toggle').focus();
break;
}
});
// ARIA live region updates
function announceToScreenReader(message, priority) {
priority = priority || 'polite';
var announcement = $('<div>')
.attr('aria-live', priority)
.attr('aria-atomic', 'true')
.addClass('sr-only')
.text(message);
$('body').append(announcement);
setTimeout(function() {
announcement.remove();
}, 1000);
}
// Expose announcement function globally
window.announceToScreenReader = announceToScreenReader;
// Custom focus management for Shiny updates
$(document).on('shiny:value', function(event) {
// Announce dynamic content updates
if (event.target.id && $(event.target).attr('aria-live')) {
var element = $(event.target);
var message = element.text() || element.val() || 'Content updated';
announceToScreenReader('Updated: ' + message);
}
});
});
"
# Skip links for main content navigation
create_skip_links <- function() {
div(class = "skip-links",
tags$a(
href = "#main-content",
class = "skip-link sr-only-focusable",
"Skip to main content"
),
tags$a(
href = "#navigation",
class = "skip-link sr-only-focusable",
"Skip to navigation"
)
)
}
# Focus management for dynamic content updates
manage_focus_updates <- function(session) {
# Function to announce updates to screen readers
announce_update <- function(message, priority = "polite") {
session$sendCustomMessage(
type = "announce",
message = list(
text = message,
priority = priority
)
)
}
# Function to manage focus after content updates
set_focus_after_update <- function(target_id) {
session$sendCustomMessage(
type = "setFocus",
message = list(target = target_id)
)
}
list(
announce_update = announce_update,
set_focus_after_update = set_focus_after_update
)
}
# Keyboard shortcut system
implement_keyboard_shortcuts <- function() {
shortcuts_js <- "
// Keyboard shortcuts
$(document).on('keydown', function(e) {
// Only handle shortcuts when not in input fields
if ($(e.target).is('input, textarea, select')) {
return;
}
// Handle modifier key combinations
if (e.altKey) {
switch(e.keyCode) {
case 49: // Alt + 1 - Main content
e.preventDefault();
$('#main-content').attr('tabindex', '-1').focus();
announceToScreenReader('Navigated to main content');
break;
case 50: // Alt + 2 - Navigation
e.preventDefault();
$('#navigation').attr('tabindex', '-1').focus();
announceToScreenReader('Navigated to navigation');
break;
case 51: // Alt + 3 - Search
e.preventDefault();
$('input[type=\"search\"], input[placeholder*=\"search\" i]').first().focus();
announceToScreenReader('Navigated to search');
break;
}
}
// Global shortcuts without modifiers
switch(e.keyCode) {
case 191: // ? - Show help
if (e.shiftKey) {
e.preventDefault();
showHelp();
}
break;
}
});
"
# Help modal for keyboard shortcuts
help_modal <- modalDialog(
title = "Keyboard Shortcuts",
tags$dl(
tags$dt("Alt + 1"), tags$dd("Jump to main content"),
tags$dt("Alt + 2"), tags$dd("Jump to navigation"),
tags$dt("Alt + 3"), tags$dd("Jump to search"),
tags$dt("Tab"), tags$dd("Move to next interactive element"),
tags$dt("Shift + Tab"), tags$dd("Move to previous interactive element"),
tags$dt("Enter or Space"), tags$dd("Activate buttons and links"),
tags$dt("Arrow keys"), tags$dd("Navigate within tables and menus"),
tags$dt("Escape"), tags$dd("Close modals and dropdowns"),
tags$dt("? (Shift + /)"), tags$dd("Show this help dialog")
),
footer = modalButton("Close"),
easyClose = TRUE
)
list(
shortcuts_js = shortcuts_js,
help_modal = help_modal
)
}
list(
keyboard_navigation_js = keyboard_navigation_js,
create_skip_links = create_skip_links,
manage_focus_updates = manage_focus_updates,
implement_keyboard_shortcuts = implement_keyboard_shortcuts
)
}# Screen reader optimization techniques
optimize_for_screen_readers <- function() {
# ARIA live regions for dynamic content
create_aria_live_regions <- function() {
list(
# Status messages (polite updates)
status_region = div(
id = "status-region",
class = "sr-only",
`aria-live` = "polite",
`aria-atomic` = "true",
role = "status"
),
# Alert messages (assertive updates)
alert_region = div(
id = "alert-region",
class = "sr-only",
`aria-live` = "assertive",
`aria-atomic` = "true",
role = "alert"
),
# Progress updates
progress_region = div(
id = "progress-region",
class = "sr-only",
`aria-live` = "polite",
role = "progressbar",
`aria-valuenow` = "0",
`aria-valuemin` = "0",
`aria-valuemax` = "100"
)
)
}
# Enhanced table markup for screen readers
enhance_table_accessibility <- function(tableOutput_id, caption, summary = NULL) {
# JavaScript to enhance table after rendering
enhance_js <- sprintf("
$(document).on('shiny:value', function(event) {
if (event.target.id === '%s') {
var table = $('#%s table');
if (table.length > 0) {
// Add table caption
if (!table.find('caption').length) {
table.prepend('<caption>%s</caption>');
}
// Add scope attributes to headers
table.find('thead th').attr('scope', 'col');
table.find('tbody th').attr('scope', 'row');
// Add table summary if provided
%s
// Add sort indicators
table.find('th[aria-sort]').each(function() {
var th = $(this);
var sortState = th.attr('aria-sort');
var sortText = '';
switch(sortState) {
case 'ascending':
sortText = ', sorted ascending';
break;
case 'descending':
sortText = ', sorted descending';
break;
default:
sortText = ', sortable';
}
th.attr('aria-label', th.text() + sortText);
});
}
}
});",
tableOutput_id, tableOutput_id, caption,
if (!is.null(summary)) {
sprintf("table.attr('summary', '%s');", summary)
} else {
""
}
)
tags$script(HTML(enhance_js))
}
# Form field enhancements for screen readers
enhance_form_accessibility <- function() {
# JavaScript to improve form field announcements
form_enhancement_js <- "
$(document).ready(function() {
// Enhanced form field descriptions
$('.form-group').each(function() {
var group = $(this);
var input = group.find('input, select, textarea');
var label = group.find('label');
var helpText = group.find('.form-help-text');
var errorText = group.find('.form-error-text');
if (input.length && label.length) {
// Ensure proper label association
var inputId = input.attr('id');
if (inputId) {
label.attr('for', inputId);
}
// Build describedby attribute
var describedBy = [];
if (helpText.length) {
describedBy.push(helpText.attr('id'));
}
if (errorText.length) {
describedBy.push(errorText.attr('id'));
}
if (describedBy.length > 0) {
input.attr('aria-describedby', describedBy.join(' '));
}
}
});
// Enhanced error announcements
$('.form-error-text').on('DOMSubtreeModified', function() {
var errorDiv = $(this);
if (errorDiv.is(':visible') && errorDiv.text().trim()) {
announceToScreenReader('Error: ' + errorDiv.text(), 'assertive');
}
});
// Progress indicator announcements
$('.progress').each(function() {
var progress = $(this);
var progressBar = progress.find('.progress-bar');
if (progressBar.length) {
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'attributes' && mutation.attributeName === 'aria-valuenow') {
var value = progressBar.attr('aria-valuenow');
var max = progressBar.attr('aria-valuemax') || 100;
var percentage = Math.round((value / max) * 100);
announceToScreenReader('Progress: ' + percentage + ' percent complete');
}
});
});
observer.observe(progressBar[0], {
attributes: true,
attributeFilter: ['aria-valuenow']
});
}
});
});
"
tags$script(HTML(form_enhancement_js))
}
# Chart accessibility enhancements
enhance_chart_accessibility <- function(chart_id, chart_title, chart_description, data_summary) {
chart_enhancement_js <- sprintf("
$(document).on('shiny:value', function(event) {
if (event.target.id === '%s') {
var chartContainer = $('#%s');
var chart = chartContainer.find('img, canvas, svg').first();
if (chart.length > 0) {
// Add proper role and labels
chart.attr('role', 'img');
chart.attr('aria-label', '%s');
// Create detailed description
var description = '%s. %s';
chart.attr('aria-describedby', '%s_desc');
// Add or update description element
var descId = '%s_desc';
var descElement = $('#' + descId);
if (descElement.length === 0) {
chartContainer.after('<div id=\"' + descId + '\" class=\"sr-only\">' + description + '</div>');
} else {
descElement.text(description);
}
// Announce chart update
announceToScreenReader('Chart updated: %s');
}
}
});",
chart_id, chart_id, chart_title, chart_description, data_summary, chart_id, chart_id, chart_title
)
tags$script(HTML(chart_enhancement_js))
}
list(
create_aria_live_regions = create_aria_live_regions,
enhance_table_accessibility = enhance_table_accessibility,
enhance_form_accessibility = enhance_form_accessibility,
enhance_chart_accessibility = enhance_chart_accessibility
)
}Color and Contrast Accessibility
Ensure your applications work for users with visual impairments:
# Color accessibility implementation
implement_color_accessibility <- function() {
# WCAG-compliant color palette
accessible_color_palette <- list(
# High contrast colors (AA compliant)
primary = list(
blue = "#0066CC", # 4.5:1 contrast ratio on white
dark_blue = "#003D7A", # 7:1 contrast ratio on white
light_blue = "#E6F3FF" # Background color
),
status = list(
success = "#28A745", # Green with 4.5:1 contrast
warning = "#FFC107", # Yellow with dark text
danger = "#DC3545", # Red with 4.5:1 contrast
info = "#17A2B8" # Teal with 4.5:1 contrast
),
neutral = list(
dark = "#212529", # Very dark gray
medium = "#6C757D", # Medium gray
light = "#F8F9FA", # Very light gray
white = "#FFFFFF"
),
# Colorblind-friendly data visualization palette
data_viz = list(
categorical = c("#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b"),
sequential = colorRampPalette(c("#f7fbff", "#08519c"))(9),
diverging = colorRampPalette(c("#d73027", "#f7f7f7", "#1a9850"))(9)
)
)
# Contrast checking utilities
check_contrast_ratio <- function(color1, color2) {
# Convert hex colors to RGB
rgb1 <- col2rgb(color1)
rgb2 <- col2rgb(color2)
# Calculate relative luminance
calc_luminance <- function(rgb) {
srgb <- rgb / 255
# Apply gamma correction
linear <- ifelse(srgb <= 0.03928, srgb / 12.92, ((srgb + 0.055) / 1.055)^2.4)
# Calculate luminance
0.2126 * linear[1] + 0.7152 * linear[2] + 0.0722 * linear[3]
}
lum1 <- calc_luminance(rgb1)
lum2 <- calc_luminance(rgb2)
# Calculate contrast ratio
bright <- max(lum1, lum2)
dark <- min(lum1, lum2)
(bright + 0.05) / (dark + 0.05)
}
# Validate color combinations
validate_color_combinations <- function(palette) {
results <- list()
for (bg_name in names(palette)) {
for (fg_name in names(palette)) {
if (bg_name != fg_name) {
bg_color <- palette[[bg_name]]
fg_color <- palette[[fg_name]]
contrast <- check_contrast_ratio(bg_color, fg_color)
results[[paste0(fg_name, "_on_", bg_name)]] <- list(
foreground = fg_color,
background = bg_color,
contrast_ratio = contrast,
aa_compliant = contrast >= 4.5,
aaa_compliant = contrast >= 7.0
)
}
}
}
results
}
# High contrast theme toggle
create_high_contrast_toggle <- function() {
# CSS for high contrast mode
high_contrast_css <- "
.high-contrast {
filter: contrast(150%) brightness(150%);
}
.high-contrast .btn {
border: 2px solid !important;
font-weight: bold !important;
}
.high-contrast .form-control {
border: 2px solid #000 !important;
background-color: #fff !important;
color: #000 !important;
}
.high-contrast .table {
border: 2px solid #000 !important;
}
.high-contrast .table th,
.high-contrast .table td {
border: 1px solid #000 !important;
background-color: #fff !important;
color: #000 !important;
}
.high-contrast .alert {
border: 3px solid !important;
font-weight: bold !important;
}
"
# JavaScript for theme toggle
theme_toggle_js <- "
$(document).ready(function() {
// Check for saved preference
if (localStorage.getItem('highContrast') === 'true') {
$('body').addClass('high-contrast');
$('#contrast-toggle').attr('aria-pressed', 'true').text('Disable High Contrast');
}
// Toggle functionality
$('#contrast-toggle').on('click', function() {
var body = $('body');
var isHighContrast = body.hasClass('high-contrast');
if (isHighContrast) {
body.removeClass('high-contrast');
$(this).attr('aria-pressed', 'false').text('Enable High Contrast');
localStorage.setItem('highContrast', 'false');
announceToScreenReader('High contrast mode disabled');
} else {
body.addClass('high-contrast');
$(this).attr('aria-pressed', 'true').text('Disable High Contrast');
localStorage.setItem('highContrast', 'true');
announceToScreenReader('High contrast mode enabled');
}
});
});
"
list(
css = tags$style(HTML(high_contrast_css)),
js = tags$script(HTML(theme_toggle_js)),
toggle_button = actionButton(
"contrast-toggle",
"Enable High Contrast",
class = "btn btn-outline-secondary",
`aria-pressed` = "false",
title = "Toggle high contrast mode for better visibility"
)
)
}
# Colorblind-friendly visualization
create_colorblind_friendly_plots <- function() {
# Safe color combinations for different types of colorblindness
colorblind_safe_palette <- list(
# Protanopia/Deuteranopia safe (red-green colorblind)
red_green_safe = c("#1f77b4", "#ff7f0e", "#9467bd", "#17becf", "#e377c2", "#8c564b"),
# Tritanopia safe (blue-yellow colorblind)
blue_yellow_safe = c("#d62728", "#2ca02c", "#ff7f0e", "#9467bd", "#8c564b", "#e377c2"),
# Monochromacy safe (complete colorblindness)
monochrome_safe = c("#000000", "#404040", "#808080", "#bfbfbf", "#ffffff")
)
# Function to create accessible ggplot theme
theme_accessible <- function(base_size = 12, colorblind_type = "red_green_safe") {
safe_colors <- colorblind_safe_palette[[colorblind_type]]
ggplot2::theme_minimal(base_size = base_size) +
ggplot2::theme(
# High contrast elements
panel.grid.major = ggplot2::element_line(color = "#000000", size = 0.5),
panel.grid.minor = ggplot2::element_blank(),
axis.line = ggplot2::element_line(color = "#000000", size = 0.8),
axis.text = ggplot2::element_text(color = "#000000", size = base_size),
axis.title = ggplot2::element_text(color = "#000000", size = base_size + 2, face = "bold"),
plot.title = ggplot2::element_text(color = "#000000", size = base_size + 4, face = "bold"),
legend.text = ggplot2::element_text(color = "#000000", size = base_size),
legend.title = ggplot2::element_text(color = "#000000", size = base_size + 1, face = "bold"),
# Patterns and shapes for distinction
strip.text = ggplot2::element_text(color = "#000000", face = "bold"),
strip.background = ggplot2::element_rect(fill = "#f0f0f0", color = "#000000")
)
}
# Function to add patterns to plots for colorblind users
add_pattern_support <- function(plot_obj, pattern_type = "shape") {
if (pattern_type == "shape") {
# Use different shapes instead of just colors
plot_obj + ggplot2::scale_shape_manual(values = c(16, 17, 15, 3, 7, 8))
} else if (pattern_type == "linetype") {
# Use different line types
plot_obj + ggplot2::scale_linetype_manual(values = c("solid", "dashed", "dotted", "dotdash", "longdash", "twodash"))
}
}
list(
colorblind_safe_palette = colorblind_safe_palette,
theme_accessible = theme_accessible,
add_pattern_support = add_pattern_support
)
}
list(
accessible_color_palette = accessible_color_palette,
check_contrast_ratio = check_contrast_ratio,
validate_color_combinations = validate_color_combinations,
create_high_contrast_toggle = create_high_contrast_toggle,
create_colorblind_friendly_plots = create_colorblind_friendly_plots
)
}Performance Optimization Strategies
Reactive System Optimization
Build efficient reactive systems that scale with application complexity:
# Comprehensive performance optimization framework
create_performance_optimization_framework <- function() {
# Reactive caching strategies
implement_smart_caching <- function() {
# Multi-level caching system
cache_manager <- list(
# Level 1: Session-level cache (fastest)
session_cache = reactiveValues(),
# Level 2: Application-level cache (shared across sessions)
app_cache = list(),
# Level 3: Persistent cache (survives app restarts)
persistent_cache_dir = "cache/"
)
# Smart caching wrapper for expensive operations
cached_reactive <- function(expr, cache_key, cache_level = "session",
expiry_minutes = 60, invalidate_on = NULL) {
reactive({
# Generate cache key
full_key <- paste0(cache_key, "_", digest::digest(list(invalidate_on), algo = "md5"))
# Check cache based on level
cached_value <- switch(cache_level,
"session" = cache_manager$session_cache[[full_key]],
"app" = cache_manager$app_cache[[full_key]],
"persistent" = load_from_persistent_cache(full_key)
)
# Check if cache is valid
if (!is.null(cached_value) && is_cache_valid(cached_value, expiry_minutes)) {
logger::log_debug("Cache hit for key: {full_key}")
return(cached_value$data)
}
# Cache miss - compute value
logger::log_debug("Cache miss for key: {full_key} - computing...")
start_time <- Sys.time()
result <- expr
computation_time <- as.numeric(difftime(Sys.time(), start_time, units = "secs"))
logger::log_info("Computed {full_key} in {computation_time} seconds")
# Store in cache
cache_entry <- list(
data = result,
timestamp = Sys.time(),
computation_time = computation_time
)
switch(cache_level,
"session" = {
cache_manager$session_cache[[full_key]] <- cache_entry
},
"app" = {
cache_manager$app_cache[[full_key]] <- cache_entry
},
"persistent" = {
save_to_persistent_cache(full_key, cache_entry)
}
)
result
}) %>% bindCache(invalidate_on)
}
# Cache invalidation utilities
invalidate_cache <- function(pattern = NULL, cache_level = "all") {
if (cache_level %in% c("session", "all")) {
if (is.null(pattern)) {
cache_manager$session_cache <- reactiveValues()
} else {
keys_to_remove <- grep(pattern, names(cache_manager$session_cache), value = TRUE)
for (key in keys_to_remove) {
cache_manager$session_cache[[key]] <- NULL
}
}
}
if (cache_level %in% c("app", "all")) {
if (is.null(pattern)) {
cache_manager$app_cache <- list()
} else {
keys_to_remove <- grep(pattern, names(cache_manager$app_cache), value = TRUE)
for (key in keys_to_remove) {
cache_manager$app_cache[[key]] <- NULL
}
}
}
if (cache_level %in% c("persistent", "all")) {
clear_persistent_cache(pattern)
}
}
# Cache statistics
get_cache_stats <- function() {
list(
session_cache_size = length(cache_manager$session_cache),
app_cache_size = length(cache_manager$app_cache),
persistent_cache_size = count_persistent_cache_files(),
cache_hit_rate = calculate_cache_hit_rate(),
memory_usage = object.size(cache_manager)
)
}
list(
cached_reactive = cached_reactive,
invalidate_cache = invalidate_cache,
get_cache_stats = get_cache_stats
)
}
# Asynchronous processing for long-running operations
implement_async_processing <- function() {
library(promises)
library(future)
# Configure future plan for parallel processing
setup_async_backend <- function(workers = 4) {
plan(multisession, workers = workers)
}
# Async wrapper for expensive computations
async_reactive <- function(expr, session = getDefaultReactiveDomain()) {
# Create reactive values for state management
state <- reactiveValues(
status = "idle",
result = NULL,
error = NULL,
progress = 0
)
# Function to start async computation
start_computation <- function() {
state$status <- "running"
state$error <- NULL
state$progress <- 0
# Create promise for async execution
promise_obj <- future({
expr
}) %...>% (function(result) {
state$result <- result
state$status <- "completed"
state$progress <- 100
# Announce completion to screen readers
if (!is.null(session)) {
session$sendCustomMessage("announceUpdate",
"Computation completed successfully")
}
result
}) %...!% (function(error) {
state$error <- error$message
state$status <- "error"
logger::log_error("Async computation failed: {error$message}")
# Announce error to screen readers
if (!is.null(session)) {
session$sendCustomMessage("announceUpdate",
paste("Computation failed:", error$message))
}
NULL
})
promise_obj
}
list(
state = state,
start = start_computation
)
}
# Progress reporting for long operations
create_progress_reporter <- function(session, total_steps) {
progress <- shiny::Progress$new(session, min = 0, max = total_steps)
progress$set(message = "Processing...", value = 0)
# Update function
update_progress <- function(step, message = NULL) {
progress$set(value = step, message = message)
# Update ARIA progress indicator
percentage <- round((step / total_steps) * 100)
session$sendCustomMessage("updateProgress", list(
value = step,
max = total_steps,
percentage = percentage,
message = message
))
}
# Cleanup function
close_progress <- function() {
progress$close()
}
list(
update = update_progress,
close = close_progress
)
}
list(
setup_async_backend = setup_async_backend,
async_reactive = async_reactive,
create_progress_reporter = create_progress_reporter
)
}
# Data optimization techniques
implement_data_optimization <- function() {
# Efficient data loading with pagination
paginated_data_loader <- function(data_source, page_size = 1000, filters = NULL) {
reactive({
# Apply filters first to reduce data size
if (!is.null(filters) && length(filters) > 0) {
# Implement server-side filtering for databases
data_source <- apply_server_side_filters(data_source, filters)
}
# Get total count for pagination
total_count <- get_data_count(data_source)
# Load data in chunks
load_data_chunk <- function(offset = 0, limit = page_size) {
get_data_chunk(data_source, offset, limit)
}
list(
loader = load_data_chunk,
total_count = total_count,
page_size = page_size
)
})
}
# Memory-efficient data processing
process_large_dataset <- function(data, processing_func, chunk_size = 10000) {
total_rows <- nrow(data)
num_chunks <- ceiling(total_rows / chunk_size)
results <- list()
for (i in seq_len(num_chunks)) {
start_row <- (i - 1) * chunk_size + 1
end_row <- min(i * chunk_size, total_rows)
chunk <- data[start_row:end_row, ]
results[[i]] <- processing_func(chunk)
# Force garbage collection after each chunk
gc()
}
# Combine results
do.call(rbind, results)
}
# Intelligent data sampling for previews
create_smart_sample <- function(data, target_size = 1000, preserve_distribution = TRUE) {
if (nrow(data) <= target_size) {
return(data)
}
if (preserve_distribution) {
# Stratified sampling to preserve data distribution
categorical_cols <- sapply(data, function(x) is.factor(x) || is.character(x))
if (any(categorical_cols)) {
# Use first categorical column for stratification
strata_col <- names(categorical_cols)[which(categorical_cols)[1]]
# Sample proportionally from each stratum
strata_counts <- table(data[[strata_col]])
strata_props <- strata_counts / sum(strata_counts)
strata_samples <- round(strata_props * target_size)
sampled_indices <- c()
for (stratum in names(strata_samples)) {
stratum_indices <- which(data[[strata_col]] == stratum)
sample_size <- min(strata_samples[[stratum]], length(stratum_indices))
if (sample_size > 0) {
sampled_indices <- c(sampled_indices,
sample(stratum_indices, sample_size))
}
}
return(data[sampled_indices, ])
}
}
# Simple random sampling
sampled_indices <- sample(nrow(data), target_size)
data[sampled_indices, ]
}
list(
paginated_data_loader = paginated_data_loader,
process_large_dataset = process_large_dataset,
create_smart_sample = create_smart_sample
)
}
# UI performance optimization
implement_ui_optimization <- function() {
# Lazy loading for UI components
lazy_ui_component <- function(ui_function, trigger_condition) {
conditionalPanel(
condition = trigger_condition,
ui_function()
)
}
# Virtual scrolling for large lists
create_virtual_scroll_list <- function(items, item_height = 50, visible_count = 20) {
# JavaScript for virtual scrolling implementation
virtual_scroll_js <- sprintf("
$(document).ready(function() {
var items = %s;
var itemHeight = %d;
var visibleCount = %d;
var containerHeight = itemHeight * visibleCount;
var container = $('#virtual-scroll-container');
var viewport = $('<div>').css({
height: containerHeight + 'px',
overflow: 'auto',
border: '1px solid #ccc'
});
var content = $('<div>').css({
height: (items.length * itemHeight) + 'px',
position: 'relative'
});
viewport.append(content);
container.append(viewport);
function renderVisibleItems(scrollTop) {
var startIndex = Math.floor(scrollTop / itemHeight);
var endIndex = Math.min(startIndex + visibleCount + 1, items.length);
content.empty();
for (var i = startIndex; i < endIndex; i++) {
var item = $('<div>').css({
position: 'absolute',
top: (i * itemHeight) + 'px',
height: itemHeight + 'px',
width: '100%%',
padding: '10px',
borderBottom: '1px solid #eee'
}).text(items[i]);
content.append(item);
}
}
viewport.on('scroll', function() {
renderVisibleItems(this.scrollTop);
});
renderVisibleItems(0);
});
", jsonlite::toJSON(items), item_height, visible_count)
tagList(
div(id = "virtual-scroll-container"),
tags$script(HTML(virtual_scroll_js))
)
}
# Debounced input handling
create_debounced_input <- function(input_id, delay_ms = 300) {
# JavaScript for input debouncing
debounce_js <- sprintf("
$(document).ready(function() {
var timer;
$('#%s').on('input', function() {
var input = this;
clearTimeout(timer);
timer = setTimeout(function() {
$(input).trigger('change');
}, %d);
});
});
", input_id, delay_ms)
tags$script(HTML(debounce_js))
}
list(
lazy_ui_component = lazy_ui_component,
create_virtual_scroll_list = create_virtual_scroll_list,
create_debounced_input = create_debounced_input
)
}
list(
caching = implement_smart_caching(),
async = implement_async_processing(),
data = implement_data_optimization(),
ui = implement_ui_optimization()
)
}Resource Management and Monitoring
Implement comprehensive monitoring for both accessibility and performance:
# Comprehensive monitoring and resource management
create_monitoring_framework <- function() {
# Performance monitoring system
implement_performance_monitoring <- function() {
# Performance metrics collection
performance_metrics <- reactiveValues(
response_times = numeric(),
memory_usage = numeric(),
cpu_usage = numeric(),
active_sessions = 0,
cache_hit_rate = 0,
error_count = 0
)
# Real-time performance tracking
track_performance <- function(operation_name, expr) {
start_time <- Sys.time()
start_memory <- pryr::mem_used()
tryCatch({
result <- expr
# Calculate metrics
end_time <- Sys.time()
end_memory <- pryr::mem_used()
response_time <- as.numeric(difftime(end_time, start_time, units = "secs"))
memory_delta <- as.numeric(end_memory - start_memory)
# Store metrics
performance_metrics$response_times <- c(
tail(performance_metrics$response_times, 99), # Keep last 100 measurements
response_time
)
performance_metrics$memory_usage <- c(
tail(performance_metrics$memory_usage, 99),
as.numeric(end_memory)
)
# Log performance data
logger::log_info("Performance: {operation_name} - {response_time}s, {memory_delta} bytes")
result
}, error = function(e) {
performance_metrics$error_count <- performance_metrics$error_count + 1
logger::log_error("Performance tracking error in {operation_name}: {e$message}")
stop(e)
})
}
# Performance dashboard
create_performance_dashboard <- function() {
fluidRow(
column(3,
valueBoxOutput("avg_response_time")
),
column(3,
valueBoxOutput("memory_usage")
),
column(3,
valueBoxOutput("active_sessions")
),
column(3,
valueBoxOutput("error_rate")
)
),
fluidRow(
column(6,
plotOutput("response_time_trend")
),
column(6,
plotOutput("memory_usage_trend")
)
),
fluidRow(
column(12,
h4("Performance Alerts"),
DT::dataTableOutput("performance_alerts")
)
)
}
# Performance alerts
check_performance_thresholds <- function() {
alerts <- list()
# Check average response time
if (length(performance_metrics$response_times) > 0) {
avg_response_time <- mean(tail(performance_metrics$response_times, 10))
if (avg_response_time > 5) { # 5 second threshold
alerts$slow_response <- list(
severity = "warning",
message = paste("Average response time is", round(avg_response_time, 2), "seconds"),
recommendation = "Consider optimizing queries or adding caching"
)
}
}
# Check memory usage trend
if (length(performance_metrics$memory_usage) > 10) {
recent_memory <- tail(performance_metrics$memory_usage, 10)
memory_trend <- lm(recent_memory ~ seq_along(recent_memory))
if (summary(memory_trend)$coefficients[2, 1] > 0) { # Positive slope
alerts$memory_leak <- list(
severity = "critical",
message = "Memory usage is increasing over time",
recommendation = "Check for memory leaks in reactive expressions"
)
}
}
# Check error rate
if (performance_metrics$error_count > 10) { # More than 10 errors
alerts$high_error_rate <- list(
severity = "critical",
message = paste("High error count:", performance_metrics$error_count),
recommendation = "Review error logs and implement additional error handling"
)
}
alerts
}
list(
track_performance = track_performance,
create_performance_dashboard = create_performance_dashboard,
check_performance_thresholds = check_performance_thresholds,
metrics = performance_metrics
)
}
# Accessibility monitoring
implement_accessibility_monitoring <- function() {
# Accessibility compliance checking
check_accessibility_compliance <- function(session) {
compliance_checks <- list()
# Check for missing alt text on images
check_alt_text_js <- "
var images = $('img');
var missing_alt = images.filter(function() {
return !$(this).attr('alt') || $(this).attr('alt').trim() === '';
});
Shiny.setInputValue('accessibility_missing_alt', missing_alt.length);
"
# Check for proper heading hierarchy
check_heading_hierarchy_js <- "
var headings = $('h1, h2, h3, h4, h5, h6');
var hierarchy_issues = [];
var previous_level = 0;
headings.each(function() {
var current_level = parseInt(this.tagName.charAt(1));
if (current_level > previous_level + 1) {
hierarchy_issues.push('Heading level jumps from h' + previous_level + ' to h' + current_level);
}
previous_level = current_level;
});
Shiny.setInputValue('accessibility_heading_issues', hierarchy_issues);
"
# Check for form labels
check_form_labels_js <- "
var inputs = $('input, select, textarea').not('[type=\"hidden\"]');
var unlabeled = inputs.filter(function() {
var input = $(this);
var id = input.attr('id');
var hasLabel = $('label[for=\"' + id + '\"]').length > 0;
var hasAriaLabel = input.attr('aria-label') || input.attr('aria-labelledby');
return !hasLabel && !hasAriaLabel;
});
Shiny.setInputValue('accessibility_unlabeled_inputs', unlabeled.length);
"
# Execute accessibility checks
session$sendCustomMessage("runAccessibilityChecks", list(
alt_text = check_alt_text_js,
heading_hierarchy = check_heading_hierarchy_js,
form_labels = check_form_labels_js
))
}
# Accessibility testing utilities
create_accessibility_testing_suite <- function() {
# Color contrast testing
test_color_contrast <- function(color_combinations) {
results <- list()
for (combo_name in names(color_combinations)) {
combo <- color_combinations[[combo_name]]
contrast_ratio <- check_contrast_ratio(combo$foreground, combo$background)
results[[combo_name]] <- list(
contrast_ratio = contrast_ratio,
wcag_aa_pass = contrast_ratio >= 4.5,
wcag_aaa_pass = contrast_ratio >= 7.0,
recommendation = if (contrast_ratio < 4.5) {
"Increase contrast for WCAG AA compliance"
} else if (contrast_ratio < 7.0) {
"Good for AA, consider increasing for AAA compliance"
} else {
"Excellent contrast ratio"
}
)
}
results
}
# Keyboard navigation testing
test_keyboard_navigation <- function(session) {
keyboard_test_js <- "
// Test keyboard navigation
var focusableElements = $('button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])');
var navigationIssues = [];
// Check tab order
var tabIndexIssues = focusableElements.filter(function() {
var tabIndex = $(this).attr('tabindex');
return tabIndex && parseInt(tabIndex) > 0;
});
if (tabIndexIssues.length > 0) {
navigationIssues.push('Positive tabindex values found - may disrupt natural tab order');
}
// Check for focus indicators
var elementsWithoutFocus = focusableElements.filter(function() {
var element = $(this);
element.focus();
var hasFocusStyle = element.css('outline') !== 'none' ||
element.css('box-shadow') !== 'none' ||
element.hasClass('focus');
element.blur();
return !hasFocusStyle;
});
if (elementsWithoutFocus.length > 0) {
navigationIssues.push(elementsWithoutFocus.length + ' elements lack visible focus indicators');
}
Shiny.setInputValue('accessibility_keyboard_issues', navigationIssues);
"
session$sendCustomMessage("testKeyboardNavigation", keyboard_test_js)
}
# Screen reader testing simulation
simulate_screen_reader_experience <- function(content) {
# Extract text content as screen reader would
text_content <- extract_text_content(content)
# Check for proper semantic structure
semantic_issues <- list()
# Check for landmark regions
if (!grepl("<main|role=[\"']main[\"']", content)) {
semantic_issues$missing_main <- "No main landmark found"
}
if (!grepl("<nav|role=[\"']navigation[\"']", content)) {
semantic_issues$missing_nav <- "No navigation landmark found"
}
# Check for heading structure
headings <- extract_headings(content)
if (length(headings) == 0) {
semantic_issues$no_headings <- "No headings found for content structure"
}
# Check for list structure
lists <- extract_lists(content)
if (any(grepl("•|\\*|-", text_content)) && length(lists) == 0) {
semantic_issues$unmarked_lists <- "Content appears to have lists but not marked up as such"
}
list(
text_content = text_content,
semantic_issues = semantic_issues,
reading_level = calculate_reading_level(text_content)
)
}
list(
test_color_contrast = test_color_contrast,
test_keyboard_navigation = test_keyboard_navigation,
simulate_screen_reader_experience = simulate_screen_reader_experience
)
}
# Accessibility reporting
generate_accessibility_report <- function(session) {
# Run all accessibility checks
check_accessibility_compliance(session)
# Wait for results and compile report
observe({
req(input$accessibility_missing_alt)
req(input$accessibility_heading_issues)
req(input$accessibility_unlabeled_inputs)
report <- list(
timestamp = Sys.time(),
missing_alt_text = input$accessibility_missing_alt,
heading_hierarchy_issues = input$accessibility_heading_issues,
unlabeled_inputs = input$accessibility_unlabeled_inputs,
recommendations = generate_accessibility_recommendations(
input$accessibility_missing_alt,
input$accessibility_heading_issues,
input$accessibility_unlabeled_inputs
)
)
# Store report for review
save_accessibility_report(report)
# Update accessibility dashboard
update_accessibility_dashboard(report)
})
}
list(
check_accessibility_compliance = check_accessibility_compliance,
create_accessibility_testing_suite = create_accessibility_testing_suite,
generate_accessibility_report = generate_accessibility_report
)
}
# Resource cleanup and optimization
implement_resource_cleanup <- function() {
# Memory cleanup for sessions
cleanup_session_resources <- function(session) {
session$onSessionEnded(function() {
# Clear reactive values
if (exists("performance_metrics")) {
rm(performance_metrics, envir = .GlobalEnv)
}
# Clear large objects from session
session_objects <- ls(envir = session$userData)
large_objects <- session_objects[sapply(session_objects, function(obj) {
tryCatch({
object.size(get(obj, envir = session$userData)) > 1024^2 # 1MB threshold
}, error = function(e) FALSE)
})]
for (obj in large_objects) {
rm(list = obj, envir = session$userData)
}
# Force garbage collection
gc()
logger::log_info("Session resources cleaned up")
})
}
# Database connection management
manage_database_connections <- function() {
# Connection pool for efficient resource usage
connection_pool <- pool::dbPool(
drv = RPostgres::Postgres(),
dbname = Sys.getenv("DB_NAME"),
host = Sys.getenv("DB_HOST"),
username = Sys.getenv("DB_USER"),
password = Sys.getenv("DB_PASSWORD"),
minSize = 2,
maxSize = 10,
idleTimeout = 3600000 # 1 hour
)
# Cleanup function
cleanup_connections <- function() {
pool::poolClose(connection_pool)
}
# Register cleanup on app exit
onStop(cleanup_connections)
connection_pool
}
# File cleanup for temporary uploads
cleanup_temporary_files <- function(max_age_hours = 24) {
temp_dir <- tempdir()
# Find old files
files <- list.files(temp_dir, full.names = TRUE, recursive = TRUE)
file_ages <- Sys.time() - file.mtime(files)
old_files <- files[file_ages > as.difftime(max_age_hours, units = "hours")]
# Remove old files
if (length(old_files) > 0) {
unlink(old_files, recursive = TRUE)
logger::log_info("Cleaned up {length(old_files)} temporary files")
}
}
# Schedule regular cleanup
schedule_cleanup_tasks <- function() {
# Clean temporary files every hour
later::later(cleanup_temporary_files, delay = 3600, loop = TRUE)
# Run garbage collection every 30 minutes
later::later(gc, delay = 1800, loop = TRUE)
# Check performance thresholds every 5 minutes
later::later(function() {
alerts <- check_performance_thresholds()
if (length(alerts) > 0) {
send_performance_alerts(alerts)
}
}, delay = 300, loop = TRUE)
}
list(
cleanup_session_resources = cleanup_session_resources,
manage_database_connections = manage_database_connections,
cleanup_temporary_files = cleanup_temporary_files,
schedule_cleanup_tasks = schedule_cleanup_tasks
)
}
list(
performance = implement_performance_monitoring(),
accessibility = implement_accessibility_monitoring(),
cleanup = implement_resource_cleanup()
)
}Testing Accessibility and Performance
Automated Testing Frameworks
Implement comprehensive testing for both accessibility compliance and performance benchmarks:
# Comprehensive testing framework for accessibility and performance
create_testing_framework <- function() {
# Accessibility testing suite
implement_accessibility_testing <- function() {
library(testthat)
library(shinytest2)
# WCAG compliance tests
test_wcag_compliance <- function(app_driver) {
# Test color contrast
test_that("Color contrast meets WCAG AA standards", {
# Get computed styles
contrast_results <- app_driver$get_js("
var elements = document.querySelectorAll('*');
var results = [];
elements.forEach(function(el) {
var style = window.getComputedStyle(el);
var color = style.color;
var backgroundColor = style.backgroundColor;
if (color && backgroundColor &&
color !== 'rgba(0, 0, 0, 0)' &&
backgroundColor !== 'rgba(0, 0, 0, 0)') {
results.push({
element: el.tagName,
color: color,
backgroundColor: backgroundColor
});
}
});
return results;
")
# Check each color combination
for (result in contrast_results) {
contrast_ratio <- calculate_contrast_from_rgb(result$color, result$backgroundColor)
expect_gte(contrast_ratio, 4.5,
info = paste("Element", result$element, "has insufficient contrast"))
}
})
# Test keyboard navigation
test_that("All interactive elements are keyboard accessible", {
# Get all focusable elements
focusable_elements <- app_driver$get_js("
var focusable = document.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'
);
return Array.from(focusable).map(el => ({
tagName: el.tagName,
id: el.id,
tabIndex: el.tabIndex,
hasVisibleFocus: false // Will be tested
}));
")
# Test each element for keyboard accessibility
for (element in focusable_elements) {
# Focus the element
if (element$id != "") {
app_driver$set_inputs(!!element$id := NULL)
# Check if element has visible focus indicator
has_focus <- app_driver$get_js(sprintf("
var el = document.getElementById('%s');
if (el) {
el.focus();
var style = window.getComputedStyle(el);
return style.outline !== 'none' ||
style.boxShadow !== 'none' ||
el.classList.contains('focus');
}
return false;
", element$id))
expect_true(has_focus,
info = paste("Element", element$tagName, "lacks visible focus indicator"))
}
}
})
# Test semantic HTML structure
test_that("Page has proper semantic structure", {
# Check for landmarks
landmarks <- app_driver$get_js("
return {
hasMain: document.querySelector('main, [role=\"main\"]') !== null,
hasNav: document.querySelector('nav, [role=\"navigation\"]') !== null,
headingCount: document.querySelectorAll('h1, h2, h3, h4, h5, h6').length,
hasH1: document.querySelector('h1') !== null
};
")
expect_true(landmarks$hasMain, "Page should have a main landmark")
expect_true(landmarks$hasH1, "Page should have an h1 heading")
expect_gt(landmarks$headingCount, 0, "Page should have heading structure")
})
# Test alt text on images
test_that("All images have appropriate alt text", {
images_without_alt <- app_driver$get_js("
var images = document.querySelectorAll('img');
var withoutAlt = [];
images.forEach(function(img) {
if (!img.alt || img.alt.trim() === '') {
withoutAlt.push({
src: img.src,
id: img.id || 'no-id'
});
}
});
return withoutAlt;
")
expect_equal(length(images_without_alt), 0,
info = paste("Images without alt text:",
paste(sapply(images_without_alt, function(x) x$src), collapse = ", ")))
})
# Test form accessibility
test_that("All form inputs have proper labels", {
unlabeled_inputs <- app_driver$get_js("
var inputs = document.querySelectorAll('input, select, textarea');
var unlabeled = [];
inputs.forEach(function(input) {
if (input.type === 'hidden') return;
var id = input.id;
var hasLabel = document.querySelector('label[for=\"' + id + '\"]') !== null;
var hasAriaLabel = input.getAttribute('aria-label') ||
input.getAttribute('aria-labelledby');
if (!hasLabel && !hasAriaLabel) {
unlabeled.push({
type: input.type || input.tagName,
id: id || 'no-id'
});
}
});
return unlabeled;
")
expect_equal(length(unlabeled_inputs), 0,
info = paste("Unlabeled inputs found:",
paste(sapply(unlabeled_inputs, function(x) x$type), collapse = ", ")))
})
}
# Screen reader simulation tests
test_screen_reader_experience <- function(app_driver) {
test_that("Content is properly announced to screen readers", {
# Test ARIA live regions
live_regions <- app_driver$get_js("
return {
statusRegions: document.querySelectorAll('[aria-live=\"polite\"]').length,
alertRegions: document.querySelectorAll('[aria-live=\"assertive\"]').length,
roles: Array.from(document.querySelectorAll('[role]')).map(el => el.getAttribute('role'))
};
")
expect_gt(live_regions$statusRegions + live_regions$alertRegions, 0,
"App should have ARIA live regions for dynamic content")
})
test_that("Tables have proper headers and structure", {
table_structure <- app_driver$get_js("
var tables = document.querySelectorAll('table');
var results = [];
tables.forEach(function(table) {
var hasCaption = table.querySelector('caption') !== null;
var hasHeaders = table.querySelectorAll('th').length > 0;
var headerScopes = Array.from(table.querySelectorAll('th')).every(function(th) {
return th.getAttribute('scope') !== null;
});
results.push({
hasCaption: hasCaption,
hasHeaders: hasHeaders,
properScopes: headerScopes
});
});
return results;
")
for (table in table_structure) {
expect_true(table$hasHeaders, "Tables should have header cells")
expect_true(table$properScopes, "Table headers should have scope attributes")
}
})
}
list(
test_wcag_compliance = test_wcag_compliance,
test_screen_reader_experience = test_screen_reader_experience
)
}
# Performance testing suite
implement_performance_testing <- function() {
library(shinyloadtest)
library(microbenchmark)
# Load testing with multiple users
test_concurrent_users <- function(app_url, max_users = 10, duration_minutes = 5) {
# Record user session
recording_file <- "loadtest_recording.log"
# Create recording if it doesn't exist
if (!file.exists(recording_file)) {
record_session(app_url, recording_file)
}
# Run load test with increasing user counts
results <- list()
for (user_count in c(1, 5, max_users)) {
test_name <- paste0("users_", user_count)
output_dir <- paste0("loadtest_", test_name)
shinycannon(
recording_file,
app_url,
workers = user_count,
loaded_duration_minutes = duration_minutes,
output_dir = output_dir
)
# Parse results
results[[test_name]] <- parse_shinycannon_output(output_dir)
}
results
}
# Response time testing
test_response_times <- function(app_driver) {
test_that("Response times are within acceptable limits", {
# Test initial page load
start_time <- Sys.time()
app_driver$wait_for_idle(timeout = 30000)
initial_load_time <- as.numeric(difftime(Sys.time(), start_time, units = "secs"))
expect_lt(initial_load_time, 5, "Initial page load should be under 5 seconds")
# Test input response times
input_response_times <- list()
# Test text input response
start_time <- Sys.time()
app_driver$set_inputs(test_input = "test value")
app_driver$wait_for_idle()
input_response_times$text_input <- as.numeric(difftime(Sys.time(), start_time, units = "secs"))
# Test button click response
start_time <- Sys.time()
app_driver$click("test_button")
app_driver$wait_for_idle()
input_response_times$button_click <- as.numeric(difftime(Sys.time(), start_time, units = "secs"))
# Verify all response times are reasonable
for (interaction in names(input_response_times)) {
expect_lt(input_response_times[[interaction]], 3,
info = paste(interaction, "response time should be under 3 seconds"))
}
})
}
# Memory usage testing
test_memory_usage <- function(app_driver) {
test_that("Memory usage remains stable", {
initial_memory <- as.numeric(pryr::mem_used())
# Perform multiple operations
for (i in 1:10) {
app_driver$set_inputs(test_data = runif(1000))
app_driver$wait_for_idle()
# Force garbage collection
gc()
}
final_memory <- as.numeric(pryr::mem_used())
memory_increase <- final_memory - initial_memory
# Memory increase should be reasonable (less than 100MB)
expect_lt(memory_increase, 100 * 1024^2,
"Memory usage increase should be less than 100MB")
})
}
# Database performance testing
test_database_performance <- function(connection) {
test_that("Database queries perform within acceptable limits", {
# Test simple query
simple_query_time <- microbenchmark(
DBI::dbGetQuery(connection, "SELECT 1"),
times = 10
)
expect_lt(median(simple_query_time$time) / 1e6, 100, # Convert to milliseconds
"Simple queries should complete under 100ms")
# Test complex query (if applicable)
complex_query_time <- microbenchmark(
DBI::dbGetQuery(connection, "SELECT COUNT(*) FROM large_table WHERE condition = 'value'"),
times = 5
)
expect_lt(median(complex_query_time$time) / 1e6, 5000, # 5 seconds
"Complex queries should complete under 5 seconds")
})
}
# Benchmarking reactive expressions
benchmark_reactive_performance <- function(reactive_expr, test_data) {
# Benchmark reactive expression execution
benchmark_results <- microbenchmark(
reactive_expr(test_data$small),
reactive_expr(test_data$medium),
reactive_expr(test_data$large),
times = 10
)
# Performance should scale reasonably with data size
small_time <- median(benchmark_results$time[1:10])
medium_time <- median(benchmark_results$time[11:20])
large_time <- median(benchmark_results$time[21:30])
# Check that performance scaling is reasonable (not exponential)
scaling_factor <- large_time / small_time
expect_lt(scaling_factor, 100, "Performance should scale reasonably with data size")
list(
small_data_time = small_time / 1e6, # Convert to milliseconds
medium_data_time = medium_time / 1e6,
large_data_time = large_time / 1e6,
scaling_factor = scaling_factor
)
}
list(
test_concurrent_users = test_concurrent_users,
test_response_times = test_response_times,
test_memory_usage = test_memory_usage,
test_database_performance = test_database_performance,
benchmark_reactive_performance = benchmark_reactive_performance
)
}
# Automated testing pipeline
create_testing_pipeline <- function() {
# Comprehensive test suite runner
run_full_test_suite <- function(app_url, database_connection = NULL) {
results <- list()
# Initialize app driver
app_driver <- AppDriver$new(app_url)
tryCatch({
# Run accessibility tests
results$accessibility <- run_accessibility_tests(app_driver)
# Run performance tests
results$performance <- run_performance_tests(app_driver)
# Run database tests if connection provided
if (!is.null(database_connection)) {
results$database <- run_database_tests(database_connection)
}
# Generate comprehensive report
results$summary <- generate_test_summary(results)
}, finally = {
app_driver$stop()
})
results
}
# CI/CD integration
setup_ci_testing <- function() {
# GitHub Actions workflow for testing
github_workflow <- '
name: Accessibility and Performance Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up R
uses: r-lib/actions/setup-r@v2
with:
r-version: "4.3"
- name: Install dependencies
run: |
Rscript -e "install.packages(c(\\"shiny\\", \\"testthat\\", \\"shinytest2\\"))"
- name: Run accessibility tests
run: |
Rscript -e "source(\\"tests/test_accessibility.R\\")"
- name: Run performance tests
run: |
Rscript -e "source(\\"tests/test_performance.R\\")"
- name: Upload test results
uses: actions/upload-artifact@v3
with:
name: test-results
path: test-results/
'
# Write workflow file
dir.create(".github/workflows", recursive = TRUE, showWarnings = FALSE)
writeLines(github_workflow, ".github/workflows/accessibility-performance-tests.yml")
}
list(
run_full_test_suite = run_full_test_suite,
setup_ci_testing = setup_ci_testing
)
}
list(
accessibility = implement_accessibility_testing(),
performance = implement_performance_testing(),
pipeline = create_testing_pipeline()
)
}Common Questions About Accessibility and Performance
Focus on high-impact, low-effort improvements first: (1) Add alt text to images and proper labels to form inputs, (2) Ensure sufficient color contrast (4.5:1 ratio minimum), (3) Implement keyboard navigation for all interactive elements, (4) Add skip links and heading structure. These changes provide immediate accessibility benefits with minimal code changes. Use automated testing tools to identify issues quickly, then address them systematically based on user impact and legal requirements.
Implement a multi-layered testing approach: (1) Use microbenchmark to test individual functions with datasets of different sizes, (2) Implement reactive expression caching and test cache hit rates, (3) Use shinyloadtest to simulate concurrent users with realistic data loads, (4) Monitor memory usage patterns during extended sessions to detect leaks. Start with representative sample data, then gradually scale up to production data sizes. Focus on optimizing the bottlenecks that have the biggest impact on user experience.
Provide multiple ways to access the information: (1) Add comprehensive alt text describing the chart’s main message and trends, (2) Include a data table alternative that screen readers can navigate, (3) Provide text summaries of key insights and statistical findings, (4) Use ARIA labels to describe interactive chart elements, (5) Implement keyboard navigation for interactive charts. Consider creating simplified versions of complex visualizations that convey the same insights in a more accessible format.
Monitor key metrics continuously: (1) Response times for critical user actions (should be under 3 seconds), (2) Memory usage trends to detect leaks, (3) Database query performance and connection pool utilization, (4) Error rates and types, (5) User session duration and abandonment patterns. Set up automated alerts for threshold breaches and create dashboards that show performance trends over time. Use tools like profvis for periodic deep-dive performance analysis.
Design with accessibility from the start rather than retrofitting: (1) Use semantic HTML and ARIA labels for custom interactive elements, (2) Provide keyboard alternatives for mouse-dependent interactions, (3) Implement focus management for dynamic content updates, (4) Use ARIA live regions to announce changes to screen readers, (5) Offer multiple ways to access the same functionality (e.g., both drag-and-drop and form-based interfaces). Test with actual assistive technologies and users with disabilities to ensure your solutions work in practice.
Common bottlenecks include: (1) Reactive inefficiency - use req(), isolate(), and bindCache() strategically, (2) Large data processing - implement pagination, sampling, and server-side filtering, (3) Database queries - use connection pooling, indexing, and query optimization, (4) Memory leaks - properly clean up reactive values and large objects in session$onSessionEnded(), (5) Excessive UI updates - debounce user inputs and batch UI updates. Profile your application regularly and optimize the slowest operations first.
Implement systematic compliance checking: (1) Perceivable - ensure 4.5:1 color contrast, provide alt text, offer text alternatives for media, (2) Operable - make all functionality keyboard accessible, provide skip links, avoid seizure-inducing content, (3) Understandable - use clear language, consistent navigation, provide input assistance and error identification, (4) Robust - use valid HTML, ensure compatibility with assistive technologies. Use automated testing tools like axe-core for initial screening, but always complement with manual testing and user feedback from people with disabilities.
Implement efficient real-time data handling: (1) Use reactivePoll() or reactiveFileReader() with appropriate intervals, (2) Implement data sampling for large real-time streams, (3) Use asynchronous processing with the promises package for non-blocking operations, (4) Cache processed results and only update when necessary, (5) Consider using websockets for true real-time updates, (6) Implement progressive data loading - show recent data immediately, load historical data in background. Monitor resource usage carefully and implement circuit breakers to prevent system overload during data spikes.
Test Your Understanding
You’re building a Shiny dashboard with complex data visualizations for a government agency that must meet WCAG 2.1 AA compliance. Which accessibility implementation approach provides the most comprehensive coverage?
- Add alt text to charts and ensure color contrast meets standards
- Implement keyboard navigation and screen reader support only
- Provide alternative data tables, keyboard navigation, proper semantic structure, and assistive technology support
- Focus on high contrast mode and large font options
- Consider all four WCAG principles: Perceivable, Operable, Understandable, Robust
- Think about different types of disabilities and assistive technologies
- Remember that government agencies often have strict compliance requirements
- Consider both the technical implementation and user experience aspects
C) Provide alternative data tables, keyboard navigation, proper semantic structure, and assistive technology support
Comprehensive WCAG 2.1 AA compliance requires addressing all accessibility principles:
# Comprehensive accessibility implementation
implement_full_accessibility <- function() {
list(
# Perceivable
perceivable = list(
alt_text = "Descriptive alt text for all images and charts",
color_contrast = "4.5:1 minimum contrast ratio",
alternative_formats = "Data tables for chart alternatives",
scalable_text = "Text that can be resized to 200% without loss of functionality"
),
# Operable
operable = list(
keyboard_navigation = "All functionality available via keyboard",
focus_management = "Logical tab order and visible focus indicators",
timing_controls = "User control over time limits",
seizure_prevention = "No content that flashes more than 3 times per second"
),
# Understandable
understandable = list(
semantic_structure = "Proper headings, landmarks, and HTML semantics",
consistent_navigation = "Predictable navigation and interaction patterns",
input_assistance = "Clear labels, instructions, and error messages",
language_identification = "Page language specified and clear content"
),
# Robust
robust = list(
valid_markup = "Standards-compliant HTML and ARIA",
assistive_technology = "Compatible with screen readers and other AT",
progressive_enhancement = "Functional without JavaScript",
cross_platform = "Works across different browsers and devices"
)
)
}Why this approach is comprehensive:
- Addresses all WCAG principles systematically
- Serves multiple disability types (visual, motor, cognitive, hearing)
- Provides redundant access methods (visual + auditory + tactile)
- Meets legal compliance requirements for government agencies
- Improves usability for everyone, not just users with disabilities
Your Shiny application processes large datasets (100k+ rows) and serves 50+ concurrent users. Response times are becoming unacceptable. Which optimization strategy should you implement first?
- Upgrade server hardware to handle the load
- Implement reactive caching, data pagination, and asynchronous processing
- Reduce the dataset size by removing less important columns
- Limit the number of concurrent users
- Consider which approach addresses the root cause rather than symptoms
- Think about scalability and cost-effectiveness
- Remember that performance issues often stem from inefficient code rather than insufficient resources
- Consider the impact on user experience and functionality
B) Implement reactive caching, data pagination, and asynchronous processing
This approach addresses the fundamental performance bottlenecks systematically:
# Comprehensive performance optimization strategy
implement_performance_optimization <- function() {
list(
# Reactive caching - prevents redundant calculations
caching_strategy = list(
implementation = "Use bindCache() for expensive reactive expressions",
benefit = "Eliminates repeated calculations for same inputs",
impact = "Can reduce response times by 80-90% for cached operations"
),
# Data pagination - reduces memory usage and transfer time
pagination_strategy = list(
implementation = "Load data in chunks of 1000-5000 rows",
benefit = "Reduces initial load time and memory consumption",
impact = "Enables handling of unlimited dataset sizes"
),
# Asynchronous processing - prevents UI blocking
async_strategy = list(
implementation = "Use promises package for long-running operations",
benefit = "Keeps UI responsive during data processing",
impact = "Improves perceived performance and user experience"
),
# Additional optimizations
supporting_optimizations = list(
database_optimization = "Use indexed queries and connection pooling",
ui_optimization = "Implement debounced inputs and lazy loading",
memory_management = "Regular cleanup and garbage collection"
)
)
}Why this is the best first approach: - Addresses root causes: Inefficient reactive dependencies, large data transfers, blocking operations - Cost-effective: Software optimization is cheaper than hardware upgrades - Scalable solution: These techniques scale with user load and data size - Preserves functionality: No need to reduce features or limit users - Compound benefits: Each optimization amplifies the others’ effectiveness
Why other options are less optimal: - Hardware upgrade (A): Expensive, doesn’t address inefficient code, limited scalability - Data reduction (C): May compromise analytical value and functionality - User limits (D): Reduces business value and doesn’t solve underlying issues
You need to implement a comprehensive accessibility testing strategy for your Shiny application development workflow. Which combination of testing approaches provides the most effective coverage?
- Manual testing with screen readers only
- Automated testing tools like axe-core only
- Automated testing + manual testing + user testing with people with disabilities
- Color contrast checking and keyboard navigation testing only
- Consider the limitations of each testing approach
- Think about what automated tools can and cannot detect
- Remember that real users provide insights that testing tools miss
- Consider the development workflow and continuous improvement
C) Automated testing + manual testing + user testing with people with disabilities
A comprehensive accessibility testing strategy requires multiple complementary approaches:
# Multi-layered accessibility testing framework
comprehensive_accessibility_testing <- function() {
list(
# Layer 1: Automated Testing (catches obvious issues quickly)
automated_testing = list(
tools = c("axe-core", "Pa11y", "WAVE", "Lighthouse"),
coverage = "~30-40% of WCAG issues",
benefits = "Fast, consistent, integrates with CI/CD",
limitations = "Cannot test user experience or context-dependent issues"
),
# Layer 2: Manual Testing (validates complex interactions)
manual_testing = list(
methods = c("Keyboard navigation", "Screen reader testing", "Voice control"),
coverage = "~60-70% of WCAG issues",
benefits = "Tests real user workflows and complex interactions",
limitations = "Time-intensive, requires expertise with assistive technologies"
),
# Layer 3: User Testing (validates real-world usability)
user_testing = list(
participants = "People with various disabilities using their own assistive technology",
coverage = "Real-world usability and effectiveness",
benefits = "Identifies issues no testing tool can detect",
limitations = "Most expensive, requires recruitment and coordination"
),
# Integration strategy
workflow_integration = list(
development = "Automated tests in IDE and CI pipeline",
pre_release = "Manual testing of key user workflows",
post_release = "User feedback and periodic accessibility audits"
)
)
}Why this combination is most effective:
- Automated testing catches 30-40% of issues quickly and consistently
- Manual testing validates complex interactions and user workflows
- User testing reveals real-world usability issues that tools miss
- Complementary coverage - each method catches different types of issues
- Scalable approach - can be integrated into development workflow
- Continuous improvement - feedback loop drives ongoing accessibility enhancement
Why single approaches are insufficient:
- Automated only: Misses context, usability, and complex interaction issues
- Manual only: Time-intensive, may miss systematic issues, limited by tester’s knowledge
- Limited scope testing: Addresses only specific aspects, misses comprehensive coverage
Conclusion
Accessibility and performance are not competing priorities in Shiny development - they are complementary aspects of creating applications that truly serve all users effectively. Accessible applications ensure that analytical insights reach diverse audiences, while high-performance applications provide the responsive experiences that encourage data exploration and informed decision-making.
The frameworks and techniques covered in this guide provide the foundation for building Shiny applications that meet both accessibility standards and performance expectations. From WCAG compliance strategies that make applications usable by people with disabilities to performance optimization techniques that maintain responsiveness under load, these practices ensure your applications can serve their intended purpose for all users.
Professional Shiny development requires this dual focus on accessibility and performance from the earliest stages of application design. The investment in inclusive design and performance optimization pays dividends in user satisfaction, legal compliance, and operational efficiency. Applications built with these principles become more maintainable, scalable, and valuable to organizations and their stakeholders.
Next Steps
Based on the comprehensive accessibility and performance framework you’ve learned, here are the recommended paths for implementing these practices:
Immediate Next Steps (Complete These First)
- Security Best Practices - Implement security measures that complement accessibility features and don’t compromise performance
- Testing and Debugging Best Practices - Integrate accessibility and performance testing into your comprehensive testing strategy
- Practice Exercise: Conduct accessibility and performance audits of an existing Shiny application using the frameworks and testing approaches provided
Building on Your Foundation (Choose Your Path)
For Production Readiness:
For Advanced Performance Optimization:
- Advanced Concepts Overview
- Database Connectivity and Performance
- JavaScript Integration for Custom Performance
For Inclusive Design Leadership:
Long-term Goals (2-4 Weeks)
- Implement a complete accessibility compliance program including automated testing, manual validation, and user feedback systems
- Establish performance monitoring and optimization workflows for all production applications
- Create accessibility and performance guidelines for your development team
- Build relationships with accessibility consultants and users with disabilities for ongoing feedback and validation
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 = {Shiny {Accessibility} and {Performance:} {Inclusive} and
{Efficient} {Applications}},
date = {2025-05-23},
url = {https://www.datanovia.com/learn/tools/shiny-apps/best-practices/accessibility-performance.html},
langid = {en}
}
