flowchart TD A[Shiny Layout System] --> B[Bootstrap Foundation] B --> C[12-Column Grid] B --> D[Responsive Breakpoints] B --> E[Pre-built Components] C --> F[Precise Element Control] D --> G[Multi-Device Support] E --> H[Professional Styling] F --> I[Custom Layouts] G --> I H --> I style A fill:#e1f5fe style B fill:#f3e5f5 style I fill:#e8f5e8
Key Takeaways
- Layout Foundation: Shiny’s layout system is built on Bootstrap’s responsive grid, providing professional, mobile-friendly interfaces out of the box
- FluidPage vs FixedPage: Choose fluidPage for responsive applications that adapt to screen sizes, fixedPage for controlled, consistent layouts
- Grid System Mastery: The 12-column grid system enables precise control over element positioning and responsive behavior across devices
- Modern Design Patterns: Implement professional patterns like dashboards, sidebars, tabs, and cards for intuitive user experiences
- Responsive by Default: All Shiny layouts automatically adapt to different screen sizes, but understanding breakpoints enables advanced customization
Introduction
Creating professional, user-friendly interfaces is crucial for Shiny application success. Users judge applications within seconds of opening them, and a well-designed layout can mean the difference between engagement and abandonment. Fortunately, Shiny provides powerful layout systems that make creating beautiful, responsive interfaces accessible to R developers without requiring extensive web design knowledge.
This comprehensive guide will teach you to master Shiny’s layout systems, from basic page structures to advanced responsive patterns. You’ll learn when to use different layout approaches, how to implement professional design patterns, and how to create interfaces that work seamlessly across devices and screen sizes.
By the end of this tutorial, you’ll be able to design Shiny applications that not only function well but also provide exceptional user experiences that reflect positively on your technical expertise and attention to detail.
Layout Systems Cheatsheet - Essential patterns, grid examples, and responsive design code for professional Shiny interfaces.
Instant Reference • Layout Patterns • Mobile-Friendly
Understanding Shiny’s Layout Foundation
Shiny’s layout system is built on Bootstrap, the world’s most popular CSS framework, which provides a robust foundation for responsive web design. This integration gives Shiny applications professional appearance and behavior without requiring custom CSS knowledge.
The Bootstrap Integration Advantage
Responsive Grid System:
Bootstrap’s 12-column grid system automatically adapts to different screen sizes, ensuring your applications look great on desktop computers, tablets, and mobile devices.
Pre-built Components:
Shiny leverages Bootstrap’s extensive component library, providing buttons, forms, navigation elements, and other UI components with consistent styling and behavior.
Professional Aesthetics:
Bootstrap’s design principles ensure that Shiny applications have modern, professional appearances that users expect from contemporary web applications.
Core Layout Functions: FluidPage vs FixedPage
Shiny provides two primary page layout functions, each suited for different application requirements and design goals.
FluidPage: Responsive and Flexible
fluidPage()
creates responsive layouts that automatically adjust to different screen sizes, making them ideal for applications that need to work across various devices.
When to Use FluidPage:
- Applications accessed on multiple device types
- Dashboards and analytics tools for business users
- Public-facing applications with diverse audiences
- Modern applications requiring responsive behavior
FluidPage Characteristics:
- Container width adapts to viewport size
- Automatic responsive breakpoints for different screen sizes
- Fluid grid behavior with percentage-based widths
- Mobile-first design approach ensuring accessibility
Basic FluidPage Structure:
library(shiny)
<- fluidPage(
ui # Page title
titlePanel("Responsive Shiny Application"),
# Main content area
fluidRow(
column(4,
h3("Sidebar Content"),
p("This column adapts to screen size")
),column(8,
h3("Main Content"),
p("This column also responds to viewport changes")
)
)
)
<- function(input, output) {
server # Server logic
}
shinyApp(ui = ui, server = server)
FixedPage: Controlled and Consistent
fixedPage()
creates layouts with fixed maximum widths, providing consistent appearance across different screen sizes while maintaining responsive behavior within the fixed container.
When to Use FixedPage:
- Applications requiring consistent visual presentation
- Scientific or research applications with precise formatting needs
- Applications with complex layouts that need predictable behavior
- Situations where content overflow should be controlled
FixedPage Characteristics:
- Fixed maximum container width (typically 1170px on large screens)
- Centered layout with margins on larger screens
- Consistent element sizing across different viewports
- Predictable layout behavior for complex interfaces
Basic FixedPage Structure:
<- fixedPage(
ui titlePanel("Fixed-Width Application"),
fixedRow(
column(3,
wellPanel(
h4("Control Panel"),
sliderInput("obs", "Observations:",
min = 1, max = 1000, value = 500)
)
),column(9,
h3("Results Display"),
plotOutput("distPlot")
)
) )
Layout Comparison Matrix
Feature | FluidPage | FixedPage |
---|---|---|
Container Width | Viewport-responsive | Fixed maximum width |
Mobile Behavior | Fully responsive | Responsive within fixed bounds |
Design Predictability | Variable | Consistent |
Content Overflow | Adapts naturally | Controlled and contained |
Best Use Cases | Dashboards, public apps | Scientific tools, complex layouts |
User Experience | Modern, flexible | Professional, controlled |
FluidPage vs FixedPage Comparison
Experiment with Shiny’s two main layout systems below. Here’s how to see the key differences:
- Change ‘fluidPage’ to ‘fixedPage’ on line 7 and click Run
- Resize your browser window to see how each layout responds differently
- Notice the blue container border behavior:
- fluidPage: Container stretches to full browser width
- fixedPage: Container has maximum width (~1170px) and centers on wide screens
- Try switching back and forth to really see the difference in responsive behavior
Understanding when to use each layout type is crucial for creating professional Shiny applications that work well across different devices and screen sizes!
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 750
#| editorHeight: 250
# Simple FluidPage vs FixedPage Comparison
# Change "fluidPage" to "fixedPage" below to see the difference!
library(shiny)
library(bsicons)
# Try changing this between fluidPage and fixedPage
ui <- fluidPage(
titlePanel("Layout Comparison Demo"),
# Add some custom styling to visualize the container
tags$head(
tags$style(HTML("
.container-fluid, .container {
border: 3px solid #007bff;
background-color: #f8f9fa;
min-height: 400px;
margin-top: 20px;
}
.demo-content {
background: white;
border: 2px dashed #6c757d;
padding: 20px;
margin: 15px;
border-radius: 8px;
text-align: center;
}
.sidebar-demo {
background: #e3f2fd;
border-left: 4px solid #2196f3;
}
.main-demo {
background: #f3e5f5;
border-left: 4px solid #9c27b0;
}
.width-display {
position: fixed;
top: 10px;
right: 10px;
background: #28a745;
color: white;
padding: 10px;
border-radius: 5px;
font-family: monospace;
z-index: 1000;
}
"))
),
# Width indicator
div(class = "width-display",
"Browser width: resize window to see effect"),
# Main content area
fluidRow(
column(4,
div(class = "demo-content sidebar-demo",
h4("Sidebar Area"),
p("This represents a typical sidebar with controls"),
selectInput("dummy1", "Sample Input:",
choices = c("Option 1", "Option 2")),
sliderInput("dummy2", "Sample Slider:",
min = 1, max = 100, value = 50)
)
),
column(8,
div(class = "demo-content main-demo",
h4("Main Content Area"),
p("This is where your plots, tables, and outputs would go"),
div(style = "background: #e9ecef; padding: 30px; margin: 20px 0; border-radius: 5px;",
h5("Sample Plot Area"),
p(bs_icon("bar-chart-fill"), " Imagine a beautiful plot here")
),
div(style = "background: #e9ecef; padding: 20px; margin: 20px 0; border-radius: 5px;",
h5("Sample Table Area"),
p(bs_icon("table"), " Data table would be displayed here")
)
)
)
),
# Comparison info
hr(),
div(style = "background: #f8f9fa; padding: 20px; border-radius: 5px;",
h4("Key Differences:"),
div(class = "row",
div(class = "col-md-6",
h5(bs_icon("arrows-expand"), " fluidPage"),
tags$ul(
tags$li("Uses full browser width"),
tags$li("Stretches on wide screens"),
tags$li("Better for dashboards"),
tags$li("Responsive by default")
)
),
div(class = "col-md-6",
h5(bs_icon("aspect-ratio"), " fixedPage"),
tags$ul(
tags$li("Maximum width ~1170px"),
tags$li("Centers on wide screens"),
tags$li("Consistent presentation"),
tags$li("Better for documents")
)
)
)
)
)
server <- function(input, output, session) {
# No server logic needed for this demo
}
shinyApp(ui = ui, server = server)
The 12-Column Grid System
Shiny’s grid system divides the page width into 12 equal columns, providing flexible and precise control over element positioning and responsive behavior.
Grid System Fundamentals
Column Width Specification:
Each element can span 1-12 columns, with the total width of elements in a row typically summing to 12 for optimal layout.
Responsive Behavior:
Columns automatically stack vertically on smaller screens, ensuring content remains accessible and readable on mobile devices.
Nested Grids:
Columns can contain their own rows and columns, enabling complex layouts with precise control over element positioning.
Basic Grid Implementation
Simple Two-Column Layout:
<- fluidPage(
ui fluidRow(
column(6,
h3("Left Column"),
p("This column takes up half the page width")
),column(6,
h3("Right Column"),
p("This column takes up the other half")
)
) )
Three-Column Layout with Different Widths:
<- fluidPage(
ui fluidRow(
column(3,
h4("Sidebar"),
p("Navigation or controls")
),column(6,
h4("Main Content"),
p("Primary content area")
),column(3,
h4("Additional Info"),
p("Secondary content or widgets")
)
) )
Complex Nested Grid Layout:
<- fluidPage(
ui # Header row
fluidRow(
column(12,
h2("Application Header"),
style = "background-color: #f8f9f"
)
),
# Main content row
fluidRow(
# Sidebar
column(3,
wellPanel(
h4("Controls"),
# Nested row within column
fluidRow(
column(12,
selectInput("dataset", "Dataset:",
choices = c("mtcars", "iris"))
)
),fluidRow(
column(6,
numericInput("n", "N:", value = 10)
),column(6,
checkboxInput("header", "Header", TRUE)
)
)
)
),
# Main content area
column(9,
# Nested tabs within main column
tabsetPanel(
tabPanel("Plot", plotOutput("plot")),
tabPanel("Summary", verbatimTextOutput("summary")),
tabPanel("Table", tableOutput("table"))
)
)
) )
Responsive Grid Behavior
Automatic Stacking:
On mobile devices (screen width < 768px), columns automatically stack vertically, ensuring content remains readable without horizontal scrolling.
Flexible Column Sizing:
# This layout adapts gracefully to different screen sizes
<- fluidPage(
ui fluidRow(
column(12, h2("Header")),
column(4, "Sidebar content"),
column(8, "Main content")
),# On mobile: Header -> Sidebar -> Main content (stacked vertically)
# On desktop: Header spans full width, Sidebar and Main side-by-side
)
Interactive Layout Builder
Build stunning Shiny layouts visually and generate production-ready code:
- Choose a layout pattern - Start with proven professional designs (Sidebar, Dashboard, Cards)
- Add and arrange elements - Click to add headers, sidebars, main areas, and cards
- Configure responsive behavior - See how your layout adapts across Desktop, Tablet, and Mobile
- Watch live statistics - Monitor layout efficiency and get smart troubleshooting suggestions
- Generate clean code - Copy production-ready Shiny code or download complete applications
- Edit elements dynamically - Click any element to adjust width and labels on the fly
Key Learning: Visual layout construction makes Bootstrap’s grid system intuitive, enabling you to design professional interfaces that work seamlessly across all devices and screen sizes.
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 1600
# Professional Shiny Layout Builder
# The most comprehensive interactive layout construction system for Shiny
# Features: Drag-drop interface, live responsive preview, code generation, layout patterns
library(shiny)
library(bslib)
library(bsicons)
library(stringr)
library(shinyjs)
# Workaround for Chromium Issue 468227
downloadButton <- function(...) {
tag <- shiny::downloadButton(...)
tag$attribs$download <- NULL
tag
}
ui <- fluidPage(
theme = bs_theme(version = 5, bootswatch = "cosmo"),
useShinyjs(), # Initialize shinyjs
tags$head(
tags$style(HTML("
/* Professional Layout Builder Styling */
.layout-builder-container {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 25px;
border-radius: 12px;
margin-bottom: 25px;
box-shadow: 0 8px 32px rgba(0,0,0,0.15);
}
.layout-builder-container h2 {
color: white;
margin-bottom: 15px;
font-weight: 600;
}
.layout-builder-container p {
opacity: 0.95;
margin-bottom: 0;
}
.construction-panel {
background: #f8f9fa;
border: 2px solid #dee2e6;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
.layout-canvas {
background: white;
border: 3px dashed #007bff;
border-radius: 10px;
min-height: 400px;
padding: 20px;
margin: 20px 0;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
}
.layout-canvas.has-content {
border-style: solid;
border-color: #28a745;
}
.canvas-placeholder {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: #6c757d;
font-size: 1.1rem;
opacity: 0.7;
pointer-events: none;
}
.layout-element {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 8px;
padding: 15px;
margin: 8px 0;
text-align: center;
font-weight: 500;
position: relative;
min-height: 80px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
transition: all 0.3s ease;
cursor: grab;
user-select: none;
}
.layout-element:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
.layout-element:active {
cursor: grabbing;
}
.layout-element.dragging {
opacity: 0.5;
transform: rotate(5deg);
z-index: 1000;
}
.drop-zone {
border: 2px dashed #007bff;
border-radius: 8px;
background: rgba(0, 123, 255, 0.1);
min-height: 60px;
display: flex;
align-items: center;
justify-content: center;
margin: 8px 0;
transition: all 0.3s ease;
opacity: 0;
transform: scaleY(0);
cursor: pointer;
}
.drop-zone.active {
opacity: 1;
transform: scaleY(1);
}
.drop-zone:hover {
background: rgba(0, 123, 255, 0.2);
border-color: #0056b3;
transform: scaleY(1) scale(1.02);
}
.drop-zone-text {
color: #007bff;
font-style: italic;
font-size: 0.9rem;
font-weight: 500;
}
.element-sidebar { background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%); }
.element-main { background: linear-gradient(135deg, #4ecdc4 0%, #26d0ce 100%); }
.element-card { background: linear-gradient(135deg, #ffa726 0%, #ff9800 100%); }
.element-header { background: linear-gradient(135deg, #ab47bc 0%, #8e24aa 100%); }
.element-footer { background: linear-gradient(135deg, #78909c 0%, #546e7a 100%); }
.element-width-indicator {
font-size: 0.8rem;
opacity: 0.9;
margin-top: 5px;
padding: 2px 8px;
background: rgba(255,255,255,0.2);
border-radius: 12px;
}
.layout-controls {
background: white;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 15px;
margin: 10px 0;
}
.pattern-button {
margin: 5px;
font-size: 0.9rem;
border-radius: 6px;
transition: all 0.2s ease;
}
.pattern-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.device-preview {
border: 2px solid #dee2e6;
border-radius: 8px;
margin: 10px 0;
overflow: hidden;
transition: all 0.3s ease;
position: relative;
}
.device-header {
background: #495057;
color: white;
padding: 8px 15px;
font-size: 0.9rem;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
}
.device-content {
padding: 15px;
background: #f8f9fa;
min-height: 200px;
}
.mobile-preview { max-width: 375px; }
.tablet-preview { max-width: 768px; }
.desktop-preview { max-width: 100%; }
.code-output {
background: #1e1e1e;
color: #d4d4d4;
padding: 20px;
border-radius: 8px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 0.9rem;
line-height: 1.5;
overflow-x: auto;
margin: 15px 0;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.code-keyword { color: #569cd6; }
.code-string { color: #ce9178; }
.code-comment { color: #6a9955; }
.code-function { color: #dcdcaa; }
.stats-panel {
background: linear-gradient(135deg, #e8f5e8 0%, #d4edda 100%);
border: 1px solid #c3e6cb;
border-radius: 8px;
padding: 15px;
margin: 15px 0;
}
.stats-item {
display: flex;
justify-content: space-between;
align-items: center;
margin: 8px 0;
padding: 5px 0;
border-bottom: 1px solid rgba(0,0,0,0.1);
}
.stats-item:last-child {
border-bottom: none;
}
.stats-value {
font-weight: 600;
color: #155724;
}
.troubleshoot-panel {
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 8px;
padding: 15px;
margin: 15px 0;
display: none;
}
.troubleshoot-panel.show {
display: block;
animation: slideDown 0.3s ease;
}
@keyframes slideDown {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.copy-button {
position: absolute;
top: 10px;
right: 10px;
background: #28a745;
color: white;
border: none;
border-radius: 4px;
padding: 5px 10px;
font-size: 0.8rem;
cursor: pointer;
opacity: 0.8;
transition: opacity 0.2s ease;
}
.copy-button:hover {
opacity: 1;
}
.layout-tips {
background: #e3f2fd;
border-left: 4px solid #2196f3;
padding: 15px;
margin: 15px 0;
border-radius: 0 6px 6px 0;
}
.advanced-options {
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 6px;
padding: 15px;
margin: 10px 0;
}
.responsive-indicator {
position: absolute;
top: 5px;
left: 5px;
background: rgba(0,0,0,0.7);
color: white;
padding: 3px 8px;
border-radius: 3px;
font-size: 0.7rem;
font-weight: bold;
}
/* Mobile responsive adjustments */
@media (max-width: 768px) {
.layout-element {
margin: 5px 0;
padding: 12px;
min-height: 60px;
}
.pattern-button {
margin: 3px;
font-size: 0.8rem;
}
.device-preview {
margin: 15px 0;
}
}
")),
# Drag and Drop JavaScript - Traditional approach
tags$script(HTML("
$(document).ready(function() {
let isDragging = false;
let draggedElement = null;
let draggedIndex = null;
let startX = 0;
let startY = 0;
let dragThreshold = 5; // pixels to move before starting drag
// Mouse down - prepare for potential drag
$(document).on('mousedown', '.layout-element', function(e) {
e.preventDefault();
const element = $(this);
startX = e.clientX;
startY = e.clientY;
// Store potential drag data
draggedElement = element;
draggedIndex = $('.layout-element').index(element);
// Add temporary mouse move listener
$(document).on('mousemove.tempdrag', function(moveEvent) {
const deltaX = Math.abs(moveEvent.clientX - startX);
const deltaY = Math.abs(moveEvent.clientY - startY);
// If moved enough, start drag
if (!isDragging && (deltaX > dragThreshold || deltaY > dragThreshold)) {
startDrag();
}
});
});
function startDrag() {
isDragging = true;
// Visual feedback
draggedElement.addClass('dragging');
$('body').addClass('dragging-active');
$('body').css('cursor', 'grabbing');
// Show drop zones
showDropZones();
// Add drag instructions
if ($('.drag-instructions').length === 0) {
$('#layout-canvas').prepend('<div class=\"drag-instructions\" style=\"background: #007bff; color: white; padding: 10px; border-radius: 6px; margin-bottom: 15px; text-align: center; font-weight: bold;\">📋 Release mouse over a blue drop zone to place the element</div>');
}
}
function showDropZones() {
// Remove existing drop zones
$('.drop-zone').remove();
// Add drop zone before each element (except the dragged one)
$('.layout-element').not('.dragging').each(function() {
$(this).closest('.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12').before('<div class=\"col-12\"><div class=\"drop-zone active\"><div class=\"drop-zone-text\">📍 Drop here</div></div></div>');
});
// Add drop zone at the very end
$('#layout-canvas .row:last').append('<div class=\"col-12\"><div class=\"drop-zone active\"><div class=\"drop-zone-text\">📍 Drop at end</div></div></div>');
}
// Mouse up - handle drop or click
$(document).on('mouseup', function(e) {
// Remove temporary mouse move listener
$(document).off('mousemove.tempdrag');
if (isDragging) {
// Handle drop
const dropZone = $(e.target).closest('.drop-zone');
if (dropZone.length > 0) {
const dropIndex = $('.drop-zone.active').index(dropZone);
// Send reorder request to Shiny
Shiny.setInputValue('reorder_elements', {
from: draggedIndex,
to: dropIndex,
timestamp: Date.now()
}, {priority: 'event'});
}
endDrag();
} else if (draggedElement) {
// Handle click (for editing)
const elementId = draggedElement.data('element-id');
if (elementId) {
Shiny.setInputValue('element_edit', {
id: elementId,
timestamp: Date.now()
}, {priority: 'event'});
}
}
// Reset
draggedElement = null;
draggedIndex = null;
});
function endDrag() {
isDragging = false;
if (draggedElement) {
draggedElement.removeClass('dragging');
}
$('.drop-zone').remove();
$('.drag-instructions').remove();
$('body').removeClass('dragging-active');
$('body').css('cursor', '');
}
// Handle mouse leave to cancel drag if mouse goes outside window
$(document).on('mouseleave', function() {
if (isDragging) {
endDrag();
}
$(document).off('mousemove.tempdrag');
draggedElement = null;
draggedIndex = null;
});
// Prevent text selection during drag
$(document).on('selectstart', function() {
if (isDragging) {
return false;
}
});
});
"))
),
# Header
div(class = "layout-builder-container",
h2(bs_icon("grid-3x3-gap"), " Professional Shiny Layout Builder"),
p("Construct professional layouts visually, preview responsive behavior, and generate production-ready Shiny code")
),
# Main interface
fluidRow(
# Left Panel - Construction Tools
column(4,
div(class = "construction-panel",
h4(bs_icon("tools"), " Layout Construction"),
# Layout Pattern Presets
h5("Quick Start Patterns:"),
div(style = "text-align: center; margin: 15px 0;",
actionButton("pattern_blank", "Blank Canvas", class = "btn btn-outline-secondary pattern-button"),
actionButton("pattern_sidebar", "Sidebar App", class = "btn btn-outline-primary pattern-button"),
br(),
actionButton("pattern_dashboard", "Dashboard", class = "btn btn-outline-info pattern-button"),
actionButton("pattern_tabs", "Tabbed Layout", class = "btn btn-outline-success pattern-button"),
br(),
actionButton("pattern_navbar", "Multi-Page", class = "btn btn-outline-warning pattern-button"),
actionButton("pattern_cards", "Card Layout", class = "btn btn-outline-danger pattern-button")
),
hr(),
# Element Library
h5("Add Elements:"),
div(style = "margin: 15px 0;",
actionButton("add_header", tagList(bs_icon("layout-text-window-reverse"), " Header"),
class = "btn btn-sm btn-outline-purple pattern-button"),
actionButton("add_sidebar", tagList(bs_icon("layout-sidebar"), " Sidebar"),
class = "btn btn-sm btn-outline-danger pattern-button"),
br(),
actionButton("add_main", tagList(bs_icon("layout-text-window"), " Main Area"),
class = "btn btn-sm btn-outline-info pattern-button"),
actionButton("add_card", tagList(bs_icon("card-text"), " Card"),
class = "btn btn-sm btn-outline-warning pattern-button"),
br(),
actionButton("add_footer", tagList(bs_icon("layout-text-sidebar-reverse"), " Footer"),
class = "btn btn-sm btn-outline-secondary pattern-button")
),
div(style = "background: #e8f5e8; border: 1px solid #c3e6cb; border-radius: 6px; padding: 10px; margin: 10px 0; font-size: 0.85rem;",
strong(bs_icon("info-circle"), " How to Use:"), br(),
"• ", strong("Click:"), " Edit element properties", br(),
"• ", strong("Click & drag:"), " Move elements around", br(),
"• ", strong("Drop zones:"), " Release over blue areas to place"
),
hr(),
# Layout Controls
div(class = "layout-controls",
h6("Layout Settings:"),
sliderInput("container_width", "Container Max Width:",
min = 100, max = 100, value = 100, step = 5,
post = "%"),
selectInput("layout_type", "Page Type:",
choices = list(
"Fluid (Responsive)" = "fluid",
"Fixed (Consistent)" = "fixed",
"Dashboard" = "dashboard"
),
selected = "fluid",
selectize = FALSE),
checkboxInput("show_grid", "Show Grid Lines", value = TRUE),
checkboxInput("enable_responsive_preview", "Responsive Preview", value = TRUE)
)
)
),
# Center Panel - Canvas
column(5,
h4(bs_icon("easel"), " Layout Canvas"),
# Canvas
div(id = "layout-canvas", class = "layout-canvas",
uiOutput("canvas_content")
),
# Layout Statistics
uiOutput("layout_stats"),
# Troubleshooting
uiOutput("layout_troubleshoot")
),
# Right Panel - Preview & Code
column(3,
h4(bs_icon("eye"), " Live Preview"),
# Device Preview Toggle
div(style = "text-align: center; margin: 10px 0;",
radioButtons("preview_device", "Preview Device:",
choices = list(
"Desktop" = "desktop",
"Tablet" = "tablet",
"Mobile" = "mobile"
),
selected = "desktop",
inline = TRUE)
),
# Responsive Preview
uiOutput("responsive_preview"),
# Code Generation
br(),
h5(bs_icon("code-slash"), " Generated Code"),
div(style = "text-align: center; margin: 10px 0;",
actionButton("copy_code", tagList(bs_icon("clipboard"), " Copy Code"),
class = "btn btn-success btn-sm"),
downloadButton("download_code", tagList(bs_icon("download"), " Download"),
class = "btn btn-outline-success btn-sm")
),
uiOutput("generated_code")
)
),
# Educational Panel
wellPanel(
h4(bs_icon("mortarboard"), " Layout Design Principles"),
div(class = "layout-tips",
h6(bs_icon("lightbulb"), " Pro Tips:"),
tags$ul(
tags$li("Sidebar layouts work best for data analysis applications"),
tags$li("Dashboard layouts excel for business metrics and KPIs"),
tags$li("Tabbed interfaces organize complex multi-step workflows"),
tags$li("Card layouts provide flexible content organization"),
tags$li("Always test responsive behavior across device sizes")
)
),
div(class = "row",
div(class = "col-md-6",
h6("Layout Best Practices:"),
tags$ul(
tags$li("Keep related controls grouped together"),
tags$li("Use consistent spacing and alignment"),
tags$li("Provide clear visual hierarchy"),
tags$li("Ensure mobile-friendly touch targets")
)
),
div(class = "col-md-6",
h6("Common Patterns:"),
tags$ul(
tags$li("Sidebar: 3-9 or 4-8 column split"),
tags$li("Equal columns: 6-6 or 4-4-4"),
tags$li("Featured: 8-4 or 9-3 split"),
tags$li("Cards: 3-3-3-3 or 6-6 grid")
)
)
)
)
)
server <- function(input, output, session) {
# Reactive values for layout state
layout_elements <- reactiveVal(list())
# Track layout statistics
layout_stats <- reactive({
elements <- layout_elements()
if (length(elements) == 0) return(NULL)
total_columns <- sum(sapply(elements, function(x) x$width))
num_elements <- length(elements)
has_sidebar <- any(sapply(elements, function(x) x$type == "sidebar"))
has_header <- any(sapply(elements, function(x) x$type == "header"))
list(
total_columns = total_columns,
num_elements = num_elements,
has_sidebar = has_sidebar,
has_header = has_header,
efficiency = round(min(total_columns / 12, 1) * 100, 1)
)
})
# Layout canvas rendering
output$canvas_content <- renderUI({
elements <- layout_elements()
if (length(elements) == 0) {
div(class = "canvas-placeholder",
div(
bs_icon("plus-circle", size = "3rem"),
br(), br(),
"Click a pattern or add elements to start building your layout",
br(),
tags$small("Drag elements to reorder • Click to configure")
)
)
} else {
# Group elements into rows (every 12 columns = new row)
current_row_width <- 0
rows <- list()
current_row <- list()
for (i in seq_along(elements)) {
element <- elements[[i]]
if (current_row_width + element$width > 12 && length(current_row) > 0) {
# Start new row
rows <- append(rows, list(current_row))
current_row <- list(element)
current_row_width <- element$width
} else {
current_row <- append(current_row, list(element))
current_row_width <- current_row_width + element$width
}
}
# Add final row
if (length(current_row) > 0) {
rows <- append(rows, list(current_row))
}
# Render rows
row_elements <- lapply(seq_along(rows), function(row_idx) {
row_cols <- lapply(seq_along(rows[[row_idx]]), function(col_idx) {
element <- rows[[row_idx]][[col_idx]]
column(element$width,
div(class = paste("layout-element", paste0("element-", element$type)),
`data-element-id` = element$id,
`data-element-index` = i,
# Element icon and label
bs_icon(element$icon),
br(),
strong(element$label),
# Width indicator
div(class = "element-width-indicator",
paste0(element$width, "/12 (", round(element$width/12*100, 1), "%)"))
)
)
})
fluidRow(row_cols)
})
do.call(div, row_elements)
}
})
# Update canvas class based on content
observe({
elements <- layout_elements()
if (length(elements) > 0) {
runjs("$('#layout-canvas').addClass('has-content');")
} else {
runjs("$('#layout-canvas').removeClass('has-content');")
}
})
# Layout statistics panel
output$layout_stats <- renderUI({
stats <- layout_stats()
if (is.null(stats)) return(NULL)
div(class = "stats-panel",
h6(bs_icon("graph-up"), " Layout Statistics"),
div(class = "stats-item",
span("Elements:"),
span(class = "stats-value", stats$num_elements)
),
div(class = "stats-item",
span("Total Columns:"),
span(class = "stats-value", paste0(stats$total_columns, "/12"))
),
div(class = "stats-item",
span("Width Efficiency:"),
span(class = "stats-value", paste0(stats$efficiency, "%"))
),
div(class = "stats-item",
span("Has Sidebar:"),
span(class = "stats-value", if(stats$has_sidebar) "Yes" else "No")
),
div(class = "stats-item",
span("Has Header:"),
span(class = "stats-value", if(stats$has_header) "Yes" else "No")
)
)
})
# Troubleshooting panel
output$layout_troubleshoot <- renderUI({
stats <- layout_stats()
if (is.null(stats)) return(NULL)
issues <- c()
if (stats$total_columns > 12) {
issues <- c(issues, "Columns exceed 12 - elements will wrap to next row")
}
if (stats$total_columns < 8) {
issues <- c(issues, "Low width utilization - consider adding more content")
}
if (stats$num_elements == 1) {
issues <- c(issues, "Single element layout - consider adding sidebar or additional sections")
}
if (length(issues) > 0) {
div(class = "troubleshoot-panel show",
h6(bs_icon("exclamation-triangle"), " Layout Suggestions"),
tags$ul(
lapply(issues, function(issue) tags$li(issue))
)
)
}
})
# Responsive preview
output$responsive_preview <- renderUI({
if (!input$enable_responsive_preview) return(NULL)
elements <- layout_elements()
if (length(elements) == 0) return(div("No layout to preview"))
device_class <- switch(input$preview_device,
"mobile" = "mobile-preview",
"tablet" = "tablet-preview",
"desktop" = "desktop-preview")
device_icon <- switch(input$preview_device,
"mobile" = "phone",
"tablet" = "tablet",
"desktop" = "laptop")
# Simulate responsive stacking for mobile
if (input$preview_device == "mobile") {
preview_elements <- lapply(elements, function(element) {
div(style = "background: #e9ecef; border: 1px solid #dee2e6; border-radius: 4px; padding: 10px; margin: 5px 0; text-align: center;",
element$label)
})
} else {
# Show side-by-side for tablet/desktop
preview_elements <- list(
div(style = "display: flex; flex-wrap: wrap; gap: 5px;",
lapply(elements, function(element) {
width_pct <- if (input$preview_device == "tablet") {
min(element$width / 12 * 100, 100)
} else {
element$width / 12 * 100
}
div(style = paste0("flex: 0 0 ", width_pct, "%; background: #e9ecef; border: 1px solid #dee2e6; border-radius: 4px; padding: 8px; text-align: center; font-size: 0.8rem;"),
element$label)
})
)
)
}
div(class = paste("device-preview", device_class),
div(class = "device-header",
bs_icon(device_icon),
paste(str_to_title(input$preview_device), "Preview")
),
div(class = "device-content",
preview_elements
)
)
})
# Generated code
output$generated_code <- renderUI({
elements <- layout_elements()
if (length(elements) == 0) {
return(div(class = "code-output", "# No layout elements to generate code"))
}
# Generate Shiny code
code_lines <- c()
# Page type
if (input$layout_type == "fluid") {
code_lines <- c(code_lines, "ui <- fluidPage(")
} else if (input$layout_type == "fixed") {
code_lines <- c(code_lines, "ui <- fixedPage(")
} else {
code_lines <- c(code_lines, "library(shinydashboard)", "", "ui <- dashboardPage(")
}
code_lines <- c(code_lines, " titlePanel(\"My Shiny Application\"),", "")
# Group elements into rows
current_row_width <- 0
rows <- list()
current_row <- list()
for (element in elements) {
if (current_row_width + element$width > 12 && length(current_row) > 0) {
rows <- append(rows, list(current_row))
current_row <- list(element)
current_row_width <- element$width
} else {
current_row <- append(current_row, list(element))
current_row_width <- current_row_width + element$width
}
}
if (length(current_row) > 0) {
rows <- append(rows, list(current_row))
}
# Generate fluidRow code
for (i in seq_along(rows)) {
if (i > 1) code_lines <- c(code_lines, "")
code_lines <- c(code_lines, " fluidRow(")
for (j in seq_along(rows[[i]])) {
element <- rows[[i]][[j]]
comma <- if (j < length(rows[[i]])) "," else ""
content <- switch(element$type,
"header" = "h1(\"Page Header\")",
"sidebar" = "wellPanel(h4(\"Sidebar\"), p(\"Controls go here\"))",
"main" = "div(h3(\"Main Content\"), p(\"Charts and outputs here\"))",
"card" = "wellPanel(h4(\"Card Title\"), p(\"Card content\"))",
"footer" = "div(\"Footer content\")")
code_lines <- c(code_lines, paste0(" column(", element$width, ", ", content, ")", comma))
}
row_comma <- if (i < length(rows)) "," else ""
code_lines <- c(code_lines, paste0(" )", row_comma))
}
code_lines <- c(code_lines, ")")
# Format with syntax highlighting
formatted_code <- paste(code_lines, collapse = "\n")
div(
div(class = "copy-button", onclick = "navigator.clipboard.writeText(this.parentElement.querySelector('pre').textContent)", "Copy"),
tags$pre(formatted_code)
)
})
# Pattern button handlers
observeEvent(input$pattern_blank, {
layout_elements(list())
})
observeEvent(input$pattern_sidebar, {
layout_elements(list(
list(id = "sb1", type = "sidebar", label = "Sidebar", width = 3, icon = "layout-sidebar"),
list(id = "main1", type = "main", label = "Main Content", width = 9, icon = "layout-text-window")
))
})
observeEvent(input$pattern_dashboard, {
layout_elements(list(
list(id = "header1", type = "header", label = "Dashboard Header", width = 12, icon = "layout-text-window-reverse"),
list(id = "card1", type = "card", label = "Metric 1", width = 4, icon = "card-text"),
list(id = "card2", type = "card", label = "Metric 2", width = 4, icon = "card-text"),
list(id = "card3", type = "card", label = "Metric 3", width = 4, icon = "card-text"),
list(id = "main1", type = "main", label = "Chart Area", width = 8, icon = "bar-chart"),
list(id = "side1", type = "sidebar", label = "Controls", width = 4, icon = "sliders")
))
})
observeEvent(input$pattern_tabs, {
layout_elements(list(
list(id = "header1", type = "header", label = "Application Header", width = 12, icon = "layout-text-window-reverse"),
list(id = "main1", type = "main", label = "Tabbed Content Area", width = 12, icon = "layout-text-window")
))
})
observeEvent(input$pattern_navbar, {
layout_elements(list(
list(id = "header1", type = "header", label = "Navigation Bar", width = 12, icon = "layout-text-window-reverse"),
list(id = "main1", type = "main", label = "Page Content", width = 12, icon = "layout-text-window"),
list(id = "footer1", type = "footer", label = "Page Footer", width = 12, icon = "layout-text-sidebar-reverse")
))
})
observeEvent(input$pattern_cards, {
layout_elements(list(
list(id = "card1", type = "card", label = "Card 1", width = 6, icon = "card-text"),
list(id = "card2", type = "card", label = "Card 2", width = 6, icon = "card-text"),
list(id = "card3", type = "card", label = "Card 3", width = 4, icon = "card-text"),
list(id = "card4", type = "card", label = "Card 4", width = 4, icon = "card-text"),
list(id = "card5", type = "card", label = "Card 5", width = 4, icon = "card-text")
))
})
# Add element handlers
observeEvent(input$add_header, {
current <- layout_elements()
new_id <- paste0("header", length(current) + 1)
current[[length(current) + 1]] <- list(
id = new_id,
type = "header",
label = "Header",
width = 12,
icon = "layout-text-window-reverse"
)
layout_elements(current)
})
observeEvent(input$add_sidebar, {
current <- layout_elements()
new_id <- paste0("sidebar", length(current) + 1)
current[[length(current) + 1]] <- list(
id = new_id,
type = "sidebar",
label = "Sidebar",
width = 3,
icon = "layout-sidebar"
)
layout_elements(current)
})
observeEvent(input$add_main, {
current <- layout_elements()
new_id <- paste0("main", length(current) + 1)
current[[length(current) + 1]] <- list(
id = new_id,
type = "main",
label = "Main Area",
width = 9,
icon = "layout-text-window"
)
layout_elements(current)
})
observeEvent(input$add_card, {
current <- layout_elements()
new_id <- paste0("card", length(current) + 1)
current[[length(current) + 1]] <- list(
id = new_id,
type = "card",
label = "Card",
width = 4,
icon = "card-text"
)
layout_elements(current)
})
observeEvent(input$add_footer, {
current <- layout_elements()
new_id <- paste0("footer", length(current) + 1)
current[[length(current) + 1]] <- list(
id = new_id,
type = "footer",
label = "Footer",
width = 12,
icon = "layout-text-sidebar-reverse"
)
layout_elements(current)
})
# Handle element reordering
observeEvent(input$reorder_elements, {
req(input$reorder_elements)
elements <- layout_elements()
if (length(elements) == 0) return()
from_index <- input$reorder_elements$from + 1 # JavaScript is 0-based, R is 1-based
to_index <- input$reorder_elements$to + 1
# Validate indices
if (from_index < 1 || from_index > length(elements) || to_index < 1 || to_index > length(elements) + 1) {
return()
}
# Remove element from original position
element_to_move <- elements[[from_index]]
elements <- elements[-from_index]
# Adjust to_index if we removed an element before it
if (to_index > from_index) {
to_index <- to_index - 1
}
# Insert element at new position
if (to_index > length(elements)) {
elements <- append(elements, list(element_to_move))
} else {
elements <- append(elements, list(element_to_move), after = to_index - 1)
}
layout_elements(elements)
})
# Element click handler - now handles both drag and edit
observeEvent(input$element_click, {
# This will be triggered by double-click for editing
})
# Remove the separate double-click handler since we're back to click for edit
# Element editing is now handled in the main mouse event system
# Handle element editing (now via double-click)
observeEvent(input$element_edit, {
element_data <- input$element_edit
# Find the element
elements <- layout_elements()
element_idx <- which(sapply(elements, function(x) x$id == element_data$id))
if (length(element_idx) > 0) {
element <- elements[[element_idx]]
# Show modal for editing
showModal(modalDialog(
title = paste("Edit", element$label),
textInput("edit_label", "Label:", value = element$label),
sliderInput("edit_width", "Width (columns):",
min = 1, max = 12, value = element$width, step = 1),
footer = tagList(
actionButton("delete_element", "Delete", class = "btn btn-danger"),
modalButton("Cancel"),
actionButton("save_element", "Save Changes", class = "btn btn-primary")
),
# Store element ID for saving
tags$script(HTML(paste0("window.editingElementId = '", element_data$id, "';")))
))
}
})
# Save element changes
observeEvent(input$save_element, {
elements <- layout_elements()
element_id <- input$element_edit$id
element_idx <- which(sapply(elements, function(x) x$id == element_id))
if (length(element_idx) > 0) {
elements[[element_idx]]$label <- input$edit_label
elements[[element_idx]]$width <- input$edit_width
layout_elements(elements)
}
removeModal()
})
# Delete element
observeEvent(input$delete_element, {
elements <- layout_elements()
element_id <- input$element_edit$id
element_idx <- which(sapply(elements, function(x) x$id == element_id))
if (length(element_idx) > 0) {
elements <- elements[-element_idx]
layout_elements(elements)
}
removeModal()
})
# Copy code to clipboard
observeEvent(input$copy_code, {
runjs("
const codeElement = document.querySelector('#generated_code > div > pre');
if (codeElement) {
navigator.clipboard.writeText(codeElement.textContent).then(function() {
// Show success feedback
const btn = document.getElementById('copy_code');
const originalText = btn.innerHTML;
btn.innerHTML = '<i class=\"bi bi-check\"></i> Copied!';
btn.classList.add('btn-success');
setTimeout(function() {
btn.innerHTML = originalText;
btn.classList.remove('btn-success');
}, 2000);
});
}
")
})
# Download code handler
output$download_code <- downloadHandler(
filename = function() {
paste0("shiny_layout_", Sys.Date(), ".R")
},
content = function(file) {
elements <- layout_elements()
if (length(elements) == 0) {
code_content <- c(
"# Generated Shiny Layout",
"# Created with Datanovia Layout Builder",
"",
"library(shiny)",
"",
"ui <- fluidPage(",
" titlePanel(\"My Shiny Application\"),",
" ",
" # Add your layout elements here",
" p(\"No layout elements added yet\")",
")",
"",
"server <- function(input, output, session) {",
" # Add your server logic here",
"}",
"",
"shinyApp(ui = ui, server = server)"
)
} else {
# Generate complete Shiny app code
code_lines <- c(
"# Generated Shiny Layout",
"# Created with Datanovia Layout Builder",
"",
"library(shiny)"
)
# Add additional libraries if needed
if (input$layout_type == "dashboard") {
code_lines <- c(code_lines, "library(shinydashboard)")
}
code_lines <- c(code_lines, "")
# Page type
if (input$layout_type == "fluid") {
code_lines <- c(code_lines, "ui <- fluidPage(")
} else if (input$layout_type == "fixed") {
code_lines <- c(code_lines, "ui <- fixedPage(")
} else {
code_lines <- c(code_lines, "ui <- dashboardPage(")
}
code_lines <- c(code_lines, " titlePanel(\"My Shiny Application\"),", "")
# Group elements into rows
current_row_width <- 0
rows <- list()
current_row <- list()
for (element in elements) {
if (current_row_width + element$width > 12 && length(current_row) > 0) {
rows <- append(rows, list(current_row))
current_row <- list(element)
current_row_width <- element$width
} else {
current_row <- append(current_row, list(element))
current_row_width <- current_row_width + element$width
}
}
if (length(current_row) > 0) {
rows <- append(rows, list(current_row))
}
# Generate fluidRow code
for (i in seq_along(rows)) {
if (i > 1) code_lines <- c(code_lines, "")
code_lines <- c(code_lines, " fluidRow(")
for (j in seq_along(rows[[i]])) {
element <- rows[[i]][[j]]
comma <- if (j < length(rows[[i]])) "," else ""
content <- switch(element$type,
"header" = paste0("h1(\"", element$label, "\")"),
"sidebar" = paste0("wellPanel(h4(\"", element$label, "\"), p(\"Controls go here\"))"),
"main" = paste0("div(h3(\"", element$label, "\"), p(\"Charts and outputs here\"))"),
"card" = paste0("wellPanel(h4(\"", element$label, "\"), p(\"Card content\"))"),
"footer" = paste0("div(\"", element$label, " content\")"))
code_lines <- c(code_lines, paste0(" column(", element$width, ", ", content, ")", comma))
}
row_comma <- if (i < length(rows)) "," else ""
code_lines <- c(code_lines, paste0(" )", row_comma))
}
code_lines <- c(code_lines, ")")
# Add server code
code_lines <- c(code_lines,
"",
"server <- function(input, output, session) {",
" # Add your server logic here",
" ",
" # Example:",
" # output$plot <- renderPlot({",
" # plot(mtcars$mpg, mtcars$wt)",
" # })",
"}",
"",
"shinyApp(ui = ui, server = server)"
)
code_content <- code_lines
}
writeLines(code_content, file)
}
)
}
shinyApp(ui = ui, server = server)
Professional Layout Patterns
Understanding and implementing professional layout patterns elevates your applications from functional tools to polished, user-friendly experiences.
Dashboard Layout Pattern
Dashboard layouts organize multiple related visualizations and metrics in a structured, scannable format.
Professional Dashboard Implementation:
library(shinydashboard)
<- dashboardPage(
ui # Dashboard header
dashboardHeader(title = "Analytics Dashboard"),
# Dashboard sidebar
dashboardSidebar(
sidebarMenu(
menuItem("Overview", tabName = "overview", icon = icon("dashboard")),
menuItem("Detailed Analysis", tabName = "analysis", icon = icon("chart-line")),
menuItem("Settings", tabName = "settings", icon = icon("cog"))
)
),
# Dashboard body
dashboardBody(
tabItems(
# Overview tab
tabItem(tabName = "overview",
# Key metrics row
fluidRow(
valueBoxOutput("total_sales"),
valueBoxOutput("growth_rate"),
valueBoxOutput("customer_count")
),
# Charts row
fluidRow(
box(
title = "Sales Trend", status = "primary", solidHeader = TRUE,
width = 8, height = "400px",
plotOutput("sales_trend", height = "350px")
),box(
title = "Top Products", status = "success", solidHeader = TRUE,
width = 4, height = "400px",
plotOutput("top_products", height = "350px")
)
)
),
# Analysis tab
tabItem(tabName = "analysis",
fluidRow(
box(
title = "Analysis Controls", status = "warning", solidHeader = TRUE,
width = 3,
selectInput("time_period", "Time Period:",
choices = c("Last 7 days", "Last 30 days", "Last quarter")),
selectInput("segment", "Customer Segment:",
choices = c("All", "Premium", "Standard", "Basic"))
),box(
title = "Detailed Analysis", status = "info", solidHeader = TRUE,
width = 9,
plotOutput("detailed_analysis", height = "500px")
)
)
)
)
) )
Tabbed Interface Pattern
Tabbed interfaces organize related content into logical sections, reducing cognitive load and improving navigation efficiency.
Advanced Tabbed Layout:
<- fluidPage(
ui titlePanel("Multi-Level Tabbed Interface"),
# Primary navigation tabs
tabsetPanel(
id = "main_tabs",
# Data Input Tab
tabPanel("Data Input",
value = "data_input",
h3("Data Import and Configuration"),
fluidRow(
column(6,
wellPanel(
h4("Data Source"),
radioButtons("data_source", "Choose data source:",
choices = list("Upload CSV" = "upload",
"Use Sample Data" = "sample",
"Connect to Database" = "database")),
conditionalPanel(
condition = "input.data_source == 'upload'",
fileInput("file", "Choose CSV File:")
),
conditionalPanel(
condition = "input.data_source == 'sample'",
selectInput("sample_data", "Sample Dataset:",
choices = c("mtcars", "iris", "diamonds"))
)
)
),column(6,
wellPanel(
h4("Data Configuration"),
uiOutput("column_selector"),
br(),
actionButton("process_data", "Process Data",
class = "btn-primary")
)
)
)
),
# Analysis Tab with nested tabs
tabPanel("Analysis",
value = "analysis",
h3("Data Analysis and Visualization"),
tabsetPanel(
tabPanel("Exploratory Analysis",
fluidRow(
column(4,
h4("Variable Selection"),
uiOutput("variable_controls")
),column(8,
plotOutput("exploratory_plot", height = "400px"),
br(),
verbatimTextOutput("exploratory_summary")
)
)
),
tabPanel("Statistical Testing",
fluidRow(
column(3,
wellPanel(
h4("Test Configuration"),
selectInput("test_type", "Statistical Test:",
choices = c("t-test", "ANOVA", "Chi-square")),
uiOutput("test_parameters")
)
),column(9,
h4("Test Results"),
verbatimTextOutput("test_results"),
br(),
plotOutput("test_visualization")
)
)
),
tabPanel("Advanced Modeling",
p("Advanced statistical modeling interface would go here...")
)
)
),
# Results Tab
tabPanel("Results & Export",
value = "results",
h3("Analysis Results and Export Options"),
fluidRow(
column(8,
h4("Final Results"),
plotOutput("final_plot", height = "500px")
),column(4,
wellPanel(
h4("Export Options"),
h5("Plot Export"),
selectInput("plot_format", "Format:",
choices = c("PNG", "PDF", "SVG")),
downloadButton("download_plot", "Download Plot"),
br(), br(),
h5("Data Export"),
selectInput("data_format", "Format:",
choices = c("CSV", "Excel", "JSON")),
downloadButton("download_data", "Download Data"),
br(), br(),
h5("Report Generation"),
textInput("report_title", "Report Title:",
value = "Analysis Report"),
downloadButton("download_report", "Generate Report")
)
)
)
)
) )
Professional Layout Patterns Showcase
Explore real-world Shiny layout patterns below and discover which one fits your application needs:
- Select different layout patterns from the dropdown to see how professional applications organize their interfaces
- Study the visual structure - notice how each pattern arranges controls, content, and navigation differently
- Read the use case guidance to understand when each pattern works best for your specific needs
- Toggle “Show Code Example” to see the actual Shiny implementation for each pattern
- Consider your users - think about whether they need controls (sidebar), metrics (dashboard), workflows (tabs), or multi-page navigation (navbar)
Understanding these proven patterns will help you choose the right foundation for your application and create interfaces that users find intuitive and professional!
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 1200
# Professional Layout Patterns Showcase
# Fully shinylive-compatible version
library(shiny)
library(bsicons)
ui <- fluidPage(
tags$head(
tags$style(HTML("
.pattern-selector {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.demo-container {
border: 2px solid #dee2e6;
border-radius: 8px;
min-height: 500px;
background: white;
position: relative;
overflow: hidden;
}
.pattern-label {
position: absolute;
top: 10px;
right: 15px;
background: #0d6efd;
color: white;
padding: 6px 12px;
border-radius: 15px;
font-size: 0.8rem;
font-weight: bold;
z-index: 1000;
}
.sidebar-demo {
background: #e3f2fd;
border-right: 2px solid #2196f3;
padding: 15px;
min-height: 450px;
}
.main-demo {
background: #f8f9fa;
padding: 15px;
min-height: 450px;
}
.dashboard-header {
background: #478bca;
color: white;
padding: 15px;
text-align: center;
}
.value-box-demo {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 8px;
text-align: center;
margin: 10px 0;
}
.chart-area {
background: white;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 20px;
margin: 10px 0;
text-align: center;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.tab-content-demo {
background: white;
border: 1px solid #dee2e6;
border-radius: 0 6px 6px 6px;
padding: 20px;
min-height: 300px;
}
.navbar-demo {
background: #478bca;
color: white;
padding: 10px 20px;
margin-bottom: 20px;
}
.navbar-brand {
font-weight: bold;
font-size: 1.2rem;
}
.navbar-nav {
list-style: none;
display: flex;
gap: 20px;
margin: 0;
padding: 0;
}
.use-case-info {
background: #e8f5e8;
border-left: 4px solid #00a988;
padding: 15px;
margin: 15px 0;
border-radius: 0 6px 6px 0;
}
.code-preview {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 15px;
margin: 15px 0;
font-family: monospace;
font-size: 0.9rem;
}
"))
),
titlePanel("Professional Layout Patterns Showcase"),
# Pattern Selector - using basic selectInput without complex styling
div(class = "pattern-selector",
fluidRow(
column(8,
selectInput("layout_pattern",
label = "Choose Layout Pattern:",
choices = list(
"Sidebar Layout (Most Popular)" = "sidebar",
"Dashboard Layout (Business Apps)" = "dashboard",
"Tabbed Interface (Multi-Section)" = "tabbed",
"Navbar Layout (Multi-Page Apps)" = "navbar"
),
selected = "sidebar",
width = "100%")
),
column(4,
div(style = "padding-top: 25px;",
checkboxInput("show_code", "Show Code Example", value = FALSE)
)
)
)
),
# Dynamic Layout Demo
uiOutput("layout_demo"),
# Use Case Information
uiOutput("use_case_info"),
# Code Example
uiOutput("code_section")
)
server <- function(input, output, session) {
# Main layout demo
output$layout_demo <- renderUI({
req(input$layout_pattern)
if (input$layout_pattern == "sidebar") {
div(class = "demo-container",
div(class = "pattern-label", "Sidebar Layout"),
div(style = "display: flex; min-height: 450px;",
# Sidebar
div(class = "sidebar-demo", style = "flex: 0 0 25%;",
div(bs_icon("sliders"), " Controls Panel"),
br(), br(),
div(style = "background: white; padding: 10px; border-radius: 4px; margin: 10px 0;",
"Dataset Selection", br(),
tags$small("Choose data source")
),
div(style = "background: white; padding: 10px; border-radius: 4px; margin: 10px 0;",
"Analysis Options", br(),
tags$small("Configure parameters")
),
div(style = "background: white; padding: 10px; border-radius: 4px; margin: 10px 0;",
"Export Settings", br(),
tags$small("Download options")
),
div(style = "background: #0d6efd; color: white; padding: 10px; border-radius: 4px; text-align: center; margin-top: 20px;",
"Run Analysis")
),
# Main content
div(class = "main-demo", style = "flex: 1;",
h4(bs_icon("bar-chart"), " Analysis Results"),
div(class = "chart-area",
div(
bs_icon("graph-up", size = "3rem", class = "text-muted"),
br(), br(),
"Interactive Plot Area", br(),
tags$small("Charts and visualizations appear here")
)
),
div(class = "chart-area",
div(
bs_icon("table", size = "2rem", class = "text-muted"),
br(), br(),
"Data Table Output", br(),
tags$small("Results and data tables display here")
)
)
)
)
)
} else if (input$layout_pattern == "dashboard") {
div(class = "demo-container",
div(class = "pattern-label", "Dashboard Layout"),
# Header
div(class = "dashboard-header",
h3(bs_icon("speedometer2"), " Business Analytics Dashboard")
),
# Value boxes row
div(style = "padding: 20px;",
fluidRow(
column(4,
div(class = "value-box-demo",
h2("$2.4M"),
p("Total Revenue")
)
),
column(4,
div(class = "value-box-demo",
h2("89%"),
p("Success Rate")
)
),
column(4,
div(class = "value-box-demo",
h2("1,247"),
p("Active Users")
)
)
),
# Charts row
fluidRow(
column(8,
div(class = "chart-area",
div(
bs_icon("graph-up-arrow", size = "3rem", class = "text-muted"),
br(), br(),
"Revenue Trend Chart", br(),
tags$small("Time series visualization")
)
)
),
column(4,
div(class = "chart-area",
div(
bs_icon("pie-chart", size = "2rem", class = "text-muted"),
br(), br(),
"Market Share", br(),
tags$small("Distribution breakdown")
)
)
)
)
)
)
} else if (input$layout_pattern == "tabbed") {
div(class = "demo-container",
div(class = "pattern-label", "Tabbed Interface"),
div(style = "padding: 20px;",
h4(bs_icon("folder"), " Multi-Section Application"),
# Tab navigation (simulated)
div(style = "border-bottom: 2px solid #dee2e6; margin-bottom: 20px;",
div(style = "display: flex; gap: 0;",
div(style = "background: #0d6efd; color: white; padding: 10px 20px; border-radius: 6px 6px 0 0;",
"Data Input"),
div(style = "background: #f8f9fa; padding: 10px 20px; border: 1px solid #dee2e6; border-bottom: none;",
"Analysis"),
div(style = "background: #f8f9fa; padding: 10px 20px; border: 1px solid #dee2e6; border-bottom: none;",
"Results"),
div(style = "background: #f8f9fa; padding: 10px 20px; border: 1px solid #dee2e6; border-bottom: none;",
"Export")
)
),
# Tab content
div(class = "tab-content-demo",
h5(bs_icon("upload"), " Data Input Section"),
p("This tab contains data upload and configuration options."),
div(style = "background: #f8f9fa; padding: 15px; border-radius: 4px; margin: 15px 0;",
strong("File Upload Area"), br(),
"CSV, Excel, or database connections"
),
div(style = "background: #f8f9fa; padding: 15px; border-radius: 4px; margin: 15px 0;",
strong("Data Preview"), br(),
"Sample data display and validation"
),
div(style = "background: #f8f9fa; padding: 15px; border-radius: 4px; margin: 15px 0;",
strong("Column Mapping"), br(),
"Configure variable types and roles"
)
)
)
)
} else if (input$layout_pattern == "navbar") {
div(class = "demo-container",
div(class = "pattern-label", "Navbar Layout"),
# Navigation bar
div(class = "navbar-demo",
div(style = "display: flex; justify-content: space-between; align-items: center;",
div(class = "navbar-brand", bs_icon("app"), " MyApp"),
div(class = "navbar-nav",
div("Home"),
div("Analysis"),
div("Reports"),
div("Settings")
)
)
),
# Page content
div(style = "padding: 20px;",
h4(bs_icon("house"), " Home Page"),
p("Welcome to the multi-page Shiny application. Each navbar item represents a different page or section."),
fluidRow(
column(6,
div(class = "chart-area",
div(
bs_icon("activity", size = "2rem", class = "text-muted"),
br(), br(),
"Quick Stats", br(),
tags$small("Overview metrics")
)
)
),
column(6,
div(class = "chart-area",
div(
bs_icon("calendar-check", size = "2rem", class = "text-muted"),
br(), br(),
"Recent Activity", br(),
tags$small("Latest updates")
)
)
)
),
div(style = "background: #fff3cd; border: 1px solid #ffc107; padding: 15px; border-radius: 6px; margin-top: 20px;",
bs_icon("info-circle"), " ",
strong("Navigation:"), " Click navbar items to switch between different sections of your application."
)
)
)
}
})
# Use case information
output$use_case_info <- renderUI({
req(input$layout_pattern)
if (input$layout_pattern == "sidebar") {
div(class = "use-case-info",
h5(bs_icon("lightbulb"), " When to Use Sidebar Layout"),
strong("Perfect for:"),
tags$ul(
tags$li("Data analysis applications with many input controls"),
tags$li("Dashboards where users need easy access to filters"),
tags$li("Interactive reporting tools with parameter selection"),
tags$li("Scientific applications with configuration options")
),
div(class = "row",
column(6,
strong(bs_icon("check-circle", class = "text-success"), " Advantages:"),
tags$ul(
tags$li("Clear separation of controls and content"),
tags$li("Maximizes main content area"),
tags$li("Familiar pattern for users")
)
),
column(6,
strong(bs_icon("exclamation-triangle", class = "text-warning"), " Considerations:"),
tags$ul(
tags$li("Limited sidebar space on mobile"),
tags$li("Can feel cluttered with many controls")
)
)
)
)
} else if (input$layout_pattern == "dashboard") {
div(class = "use-case-info",
h5(bs_icon("lightbulb"), " When to Use Dashboard Layout"),
strong("Perfect for:"),
tags$ul(
tags$li("Executive dashboards with key performance indicators"),
tags$li("Business intelligence applications"),
tags$li("Monitoring systems with multiple metrics"),
tags$li("Financial reporting and analytics platforms")
),
div(class = "row",
column(6,
strong(bs_icon("check-circle", class = "text-success"), " Advantages:"),
tags$ul(
tags$li("Professional business appearance"),
tags$li("Great for displaying metrics"),
tags$li("Scalable for complex applications")
)
),
column(6,
strong(bs_icon("exclamation-triangle", class = "text-warning"), " Considerations:"),
tags$ul(
tags$li("Requires shinydashboard package"),
tags$li("More complex to customize")
)
)
)
)
} else if (input$layout_pattern == "tabbed") {
div(class = "use-case-info",
h5(bs_icon("lightbulb"), " When to Use Tabbed Interface"),
strong("Perfect for:"),
tags$ul(
tags$li("Multi-step workflows (data → analysis → results)"),
tags$li("Applications with distinct functional areas"),
tags$li("Complex tools requiring organized navigation"),
tags$li("Educational applications with different modules")
),
div(class = "row",
column(6,
strong(bs_icon("check-circle", class = "text-success"), " Advantages:"),
tags$ul(
tags$li("Organizes complex interfaces"),
tags$li("Reduces cognitive load"),
tags$li("Clear workflow progression")
)
),
column(6,
strong(bs_icon("exclamation-triangle", class = "text-warning"), " Considerations:"),
tags$ul(
tags$li("Users might miss content in other tabs"),
tags$li("Doesn't work well for related content")
)
)
)
)
} else if (input$layout_pattern == "navbar") {
div(class = "use-case-info",
h5(bs_icon("lightbulb"), " When to Use Navbar Layout"),
strong("Perfect for:"),
tags$ul(
tags$li("Multi-page applications with distinct sections"),
tags$li("Portfolio or showcase applications"),
tags$li("Applications requiring user authentication"),
tags$li("Complex systems with different user roles")
),
div(class = "row",
column(6,
strong(bs_icon("check-circle", class = "text-success"), " Advantages:"),
tags$ul(
tags$li("Natural multi-page navigation"),
tags$li("Familiar web pattern"),
tags$li("Good for large applications")
)
),
column(6,
strong(bs_icon("exclamation-triangle", class = "text-warning"), " Considerations:"),
tags$ul(
tags$li("Page switching can be slow"),
tags$li("State management complexity")
)
)
)
)
}
})
# Code examples
output$code_section <- renderUI({
if (input$show_code) {
req(input$layout_pattern)
if (input$layout_pattern == "sidebar") {
code_text <- 'ui <- fluidPage(
titlePanel("My Analysis App"),
sidebarLayout(
sidebarPanel(
selectInput("dataset", "Dataset:", choices = datasets),
sliderInput("bins", "Bins:", min = 5, max = 50, value = 30),
actionButton("analyze", "Run Analysis")
),
mainPanel(
tabsetPanel(
tabPanel("Plot", plotOutput("histogram")),
tabPanel("Summary", verbatimTextOutput("summary")),
tabPanel("Data", tableOutput("data"))
)
)
)
)'
} else if (input$layout_pattern == "dashboard") {
code_text <- 'library(shinydashboard)
ui <- dashboardPage(
dashboardHeader(title = "Business Dashboard"),
dashboardSidebar(
sidebarMenu(
menuItem("Overview", tabName = "overview"),
menuItem("Reports", tabName = "reports")
)
),
dashboardBody(
tabItems(
tabItem("overview",
fluidRow(
valueBoxOutput("revenue"),
valueBoxOutput("users"),
valueBoxOutput("growth")
),
fluidRow(
box(plotOutput("trend_chart"), width = 8),
box(plotOutput("pie_chart"), width = 4)
)
)
)
)
)'
} else if (input$layout_pattern == "tabbed") {
code_text <- 'ui <- fluidPage(
titlePanel("Multi-Step Analysis"),
tabsetPanel(
tabPanel("Data Input",
h3("Upload and Configure Data"),
fileInput("file", "Choose CSV File"),
uiOutput("column_selector")
),
tabPanel("Analysis",
h3("Statistical Analysis"),
fluidRow(
column(4, wellPanel(
selectInput("test_type", "Test:", choices = tests),
numericInput("alpha", "Significance:", value = 0.05)
)),
column(8,
plotOutput("analysis_plot"),
verbatimTextOutput("test_results")
)
)
),
tabPanel("Results",
h3("Final Results and Export"),
downloadButton("report", "Download Report")
)
)
)'
} else if (input$layout_pattern == "navbar") {
code_text <- 'ui <- navbarPage(
title = "My Application",
tabPanel("Home",
h2("Welcome"),
p("Application overview and quick stats"),
fluidRow(
column(6, plotOutput("overview_chart")),
column(6, tableOutput("recent_activity"))
)
),
tabPanel("Analysis",
h2("Data Analysis"),
sidebarLayout(
sidebarPanel(
selectInput("variable", "Variable:", choices = vars)
),
mainPanel(
plotOutput("analysis_plot")
)
)
),
tabPanel("Settings",
h2("Application Settings"),
checkboxInput("advanced", "Advanced Mode"),
selectInput("theme", "Theme:", choices = themes)
)
)'
}
div(class = "code-preview",
h5(bs_icon("code-slash"), " Code Implementation"),
tags$pre(tags$code(code_text))
)
}
})
}
shinyApp(ui = ui, server = server)
Responsive Design Principles
Creating applications that work seamlessly across different devices and screen sizes is essential for modern web applications.
Understanding Responsive Breakpoints
Bootstrap defines several breakpoints that determine how layouts adapt to different screen sizes:
- Extra Small (xs): < 768px (phones)
- Small (sm): ≥ 768px (tablets)
- Medium (md): ≥ 992px (small desktops)
- Large (lg): ≥ 1200px (large desktops)
Responsive Design Implementation
Mobile-First Approach:
<- fluidPage(
ui # Mobile-optimized header
fluidRow(
column(12,
h2("Responsive Application"),
style = "text-align: center; margin-bottom: 20px;"
)
),
# Responsive content layout
fluidRow(
# On mobile: full width, stacked vertically
# On desktop: sidebar takes 1/4, main takes 3/4
column(3,
wellPanel(
h4("Controls"),
selectInput("dataset", "Dataset:",
choices = c("Dataset 1", "Dataset 2")),
# Mobile-friendly slider
sliderInput("parameter", "Parameter:",
min = 1, max = 100, value = 50),
# Full-width button on mobile
actionButton("analyze", "Run Analysis",
class = "btn-primary btn-block")
)
),
column(9,
# Responsive plot that adapts to container width
plotOutput("main_plot", height = "auto"),
br(),
# Responsive table
div(
style = "overflow-x: auto;", # Enable horizontal scroll on small screens
tableOutput("results_table")
)
)
) )
Conditional Content Based on Screen Size:
# Using JavaScript to detect screen size and show different content
<- fluidPage(
ui $head(
tags$script(HTML("
tags $(document).ready(function() {
function checkScreenSize() {
if ($(window).width() < 768) {
$('.desktop-only').hide();
$('.mobile-only').show();
} else {
$('.desktop-only').show();
$('.mobile-only').hide();
}
}
checkScreenSize();
$(window).resize(checkScreenSize);
});
"))
),
# Desktop version - full feature set
div(class = "desktop-only",
h3("Desktop Version"),
fluidRow(
column(4, "Sidebar content"),
column(8, "Main content with advanced features")
)
),
# Mobile version - simplified interface
div(class = "mobile-only", style = "display: none;",
h3("Mobile Version"),
div("Simplified mobile interface"),
actionButton("full_version", "Switch to Full Version")
) )
Mobile Optimization Best Practices
Touch-Friendly Interface Elements:
# Larger buttons and touch targets for mobile
actionButton("mobile_button", "Action",
class = "btn-primary btn-lg btn-block",
style = "margin: 10px 0; min-height: 44px;")
# Mobile-optimized input controls
sliderInput("mobile_slider", "Value:",
min = 1, max = 100, value = 50,
width = "100%")
Simplified Navigation for Small Screens:
# Collapsible navigation for mobile
<- navbarPage(
ui title = "My App",
id = "nav",
collapsible = TRUE, # Enable collapsible navigation
tabPanel("Home", "Home content"),
tabPanel("Analysis", "Analysis content"),
tabPanel("Settings", "Settings content")
)
Responsive Breakpoint Visualizer
Discover how Shiny layouts automatically adapt to different screen sizes using Bootstrap’s responsive breakpoint system:
- Click the device buttons (Mobile, Tablet, Desktop, Wide Screen) to simulate different screen sizes
- Watch the layout transform - notice how columns stack, resize, and reorganize based on available space
- Observe the breakpoint indicators - see which Bootstrap responsive class is active at each screen size
- Study the stacking behavior - understand how multi-column layouts become mobile-friendly automatically
- Note the explanations - learn why each breakpoint behaves differently and what users expect at each size
Understanding responsive breakpoints is crucial for creating Shiny applications that work seamlessly across all devices - from smartphones to ultrawide monitors!
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 1200
# Responsive Breakpoint Visualizer
# Interactive demo showing how Shiny layouts adapt at different screen sizes
library(shiny)
library(bslib)
library(bsicons)
ui <- fluidPage(
theme = bs_theme(version = 5, bootswatch = "cosmo"),
tags$head(
tags$style(HTML("
.device-simulator {
background: #f8f9fa;
border: 2px solid #dee2e6;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.device-controls {
text-align: center;
margin-bottom: 20px;
}
.device-btn {
margin: 5px;
font-size: 0.9rem;
min-width: 120px;
}
.device-btn.active {
background-color: #0d6efd;
border-color: #0d6efd;
color: white;
}
.viewport-container {
background: white;
border: 3px solid #0d6efd;
border-radius: 8px;
margin: 20px auto;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.viewport-label {
position: absolute;
top: -25px;
left: 50%;
transform: translateX(-50%);
background: #0d6efd;
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 0.8rem;
font-weight: bold;
}
.breakpoint-indicator {
text-align: center;
padding: 10px;
background: linear-gradient(135deg, #478bca 0%, #3170ac 100%);
color: white;
margin-bottom: 15px;
border-radius: 6px;
}
.content-demo {
padding: 15px;
min-height: 300px;
}
.demo-sidebar {
background: #e3f2fd;
border: 2px solid #2196f3;
border-radius: 6px;
padding: 15px;
margin: 10px 0;
text-align: center;
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.demo-main {
background: #f3e5f5;
border: 2px solid #9c27b0;
border-radius: 6px;
padding: 15px;
margin: 10px 0;
text-align: center;
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.demo-card {
background: #fff3e0;
border: 2px solid #ff9800;
border-radius: 6px;
padding: 15px;
margin: 10px 0;
text-align: center;
min-height: 80px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.stacking-indicator {
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 6px;
padding: 10px;
margin: 10px 0;
text-align: center;
font-weight: bold;
}
.breakpoint-info {
background: #e8f5e8;
border-left: 4px solid #00a988;
padding: 15px;
margin: 15px 0;
border-radius: 0 6px 6px 0;
}
.current-specs {
background: white;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 15px;
margin: 10px 0;
font-family: monospace;
}
/* Device-specific viewport sizes */
.mobile-viewport { max-width: 375px; }
.tablet-viewport { max-width: 768px; }
.desktop-viewport { max-width: 1200px; }
.wide-viewport { max-width: 1600px; }
/* Responsive column stacking */
.mobile-viewport .demo-column {
width: 100% !important;
max-width: 100% !important;
flex: 0 0 100% !important;
}
.tablet-viewport .demo-column.col-md-4,
.tablet-viewport .demo-column.col-md-3 {
width: 50% !important;
max-width: 50% !important;
flex: 0 0 50% !important;
}
.tablet-viewport .demo-column.col-md-8,
.tablet-viewport .demo-column.col-md-9 {
width: 100% !important;
max-width: 100% !important;
flex: 0 0 100% !important;
}
"))
),
titlePanel("Responsive Breakpoint Visualizer"),
# Device Simulator Controls
div(class = "device-simulator",
h4(bs_icon("phone"), " Device Simulator"),
div(class = "device-controls",
p("Click buttons to simulate different screen sizes and watch how the layout adapts:"),
actionButton("mobile",
label = tagList(bs_icon("phone"), " Mobile"),
class = "btn btn-outline-primary device-btn"),
actionButton("tablet",
label = tagList(bs_icon("tablet"), " Tablet"),
class = "btn btn-outline-info device-btn"),
actionButton("desktop",
label = tagList(bs_icon("laptop"), " Desktop"),
class = "btn btn-outline-success device-btn"),
actionButton("wide",
label = tagList(bs_icon("tv"), " Wide Screen"),
class = "btn btn-outline-warning device-btn")
),
# Current viewport info
uiOutput("viewport_info")
),
# Viewport Container
div(id = "viewport-container",
class = "viewport-container desktop-viewport",
div(class = "viewport-label", "Desktop View (1200px)"),
# Breakpoint indicator
uiOutput("breakpoint_indicator"),
# Demo content
div(class = "content-demo",
# Stacking indicator
uiOutput("stacking_info"),
# Sidebar layout demo
div(id = "responsive-layout",
div(style = "display: flex; flex-wrap: wrap; margin: 0 -10px;",
# Sidebar
div(class = "demo-column",
style = "flex: 0 0 25%; max-width: 25%; padding: 0 10px;",
div(class = "demo-sidebar",
bs_icon("sliders"),
br(),
strong("Sidebar"),
br(),
tags$small("Controls & Navigation")
)
),
# Main content
div(class = "demo-column",
style = "flex: 0 0 75%; max-width: 75%; padding: 0 10px;",
div(class = "demo-main",
bs_icon("bar-chart"),
br(),
strong("Main Content"),
br(),
tags$small("Charts & Analysis")
)
)
),
# Three column demo
h5("Three-Column Layout:"),
div(style = "display: flex; flex-wrap: wrap; margin: 0 -10px;",
div(class = "demo-column",
style = "flex: 0 0 33.333%; max-width: 33.333%; padding: 0 10px;",
div(class = "demo-card",
bs_icon("graph-up"),
br(),
strong("Chart 1")
)
),
div(class = "demo-column",
style = "flex: 0 0 33.333%; max-width: 33.333%; padding: 0 10px;",
div(class = "demo-card",
bs_icon("table"),
br(),
strong("Data Table")
)
),
div(class = "demo-column",
style = "flex: 0 0 33.333%; max-width: 33.333%; padding: 0 10px;",
div(class = "demo-card",
bs_icon("pie-chart"),
br(),
strong("Chart 2")
)
)
)
)
)
),
# Breakpoint explanation
uiOutput("breakpoint_explanation"),
# Code example
wellPanel(
h4(bs_icon("code-slash"), " Responsive Code Example"),
p("Here's how Shiny automatically handles responsive behavior:"),
tags$pre(tags$code(
'ui <- fluidPage(\n',
' fluidRow(\n',
' column(3, # 25% width on desktop\n',
' # Automatically becomes full width on mobile\n',
' wellPanel("Sidebar Content")\n',
' ),\n',
' column(9, # 75% width on desktop \n',
' # Stacks below sidebar on mobile\n',
' plotOutput("main_plot")\n',
' )\n',
' )\n',
')'
)),
div(class = "breakpoint-info",
bs_icon("info-circle"), " ",
strong("Automatic Responsive Behavior:"),
" Shiny uses Bootstrap's responsive grid system. Columns automatically stack vertically on screens smaller than 768px, ensuring your content remains accessible on all devices without additional code!"
)
)
)
server <- function(input, output, session) {
# Reactive values for current device
current_device <- reactiveVal("desktop")
current_width <- reactiveVal(1200)
# Device button handlers
observeEvent(input$mobile, {
current_device("mobile")
current_width(375)
# Update viewport container
removeUI(selector = "#viewport-container")
insertUI(
selector = "body",
where = "beforeEnd",
ui = div(id = "viewport-container",
class = "viewport-container mobile-viewport",
div(class = "viewport-label", "Mobile View (375px)"),
uiOutput("breakpoint_indicator"),
div(class = "content-demo",
uiOutput("stacking_info"),
div(id = "responsive-layout",
div(style = "display: flex; flex-wrap: wrap; margin: 0 -10px;",
div(class = "demo-column mobile-stack",
style = "flex: 0 0 100%; max-width: 100%; padding: 0 10px;",
div(class = "demo-sidebar",
bs_icon("sliders"), br(),
strong("Sidebar"), br(),
tags$small("Full width on mobile")
)
),
div(class = "demo-column mobile-stack",
style = "flex: 0 0 100%; max-width: 100%; padding: 0 10px;",
div(class = "demo-main",
bs_icon("bar-chart"), br(),
strong("Main Content"), br(),
tags$small("Stacked below sidebar")
)
)
),
h5("Three-Column Layout (Stacked):"),
div(style = "display: flex; flex-wrap: wrap; margin: 0 -10px;",
div(class = "demo-column",
style = "flex: 0 0 100%; max-width: 100%; padding: 0 10px;",
div(class = "demo-card",
bs_icon("graph-up"), br(), strong("Chart 1")
)
),
div(class = "demo-column",
style = "flex: 0 0 100%; max-width: 100%; padding: 0 10px;",
div(class = "demo-card",
bs_icon("table"), br(), strong("Data Table")
)
),
div(class = "demo-column",
style = "flex: 0 0 100%; max-width: 100%; padding: 0 10px;",
div(class = "demo-card",
bs_icon("pie-chart"), br(), strong("Chart 2")
)
)
)
)
)
)
)
# Update button states using JavaScript
runjs("
$('.device-btn').removeClass('btn-primary btn-info btn-success btn-warning').addClass('btn-outline-primary btn-outline-info btn-outline-success btn-outline-warning');
$('#mobile').removeClass('btn-outline-primary').addClass('btn-primary');
")
})
observeEvent(input$tablet, {
current_device("tablet")
current_width(768)
# Update viewport container for tablet
removeUI(selector = "#viewport-container")
insertUI(
selector = "body",
where = "beforeEnd",
ui = div(id = "viewport-container",
class = "viewport-container tablet-viewport",
div(class = "viewport-label", "Tablet View (768px)"),
uiOutput("breakpoint_indicator"),
div(class = "content-demo",
uiOutput("stacking_info"),
div(id = "responsive-layout",
div(style = "display: flex; flex-wrap: wrap; margin: 0 -10px;",
div(class = "demo-column",
style = "flex: 0 0 50%; max-width: 50%; padding: 0 10px;",
div(class = "demo-sidebar",
bs_icon("sliders"), br(),
strong("Sidebar"), br(),
tags$small("50% width on tablet")
)
),
div(class = "demo-column",
style = "flex: 0 0 50%; max-width: 50%; padding: 0 10px;",
div(class = "demo-main",
bs_icon("bar-chart"), br(),
strong("Main Content"), br(),
tags$small("50% width on tablet")
)
)
),
h5("Three-Column Layout (Responsive):"),
div(style = "display: flex; flex-wrap: wrap; margin: 0 -10px;",
div(class = "demo-column",
style = "flex: 0 0 50%; max-width: 50%; padding: 0 10px;",
div(class = "demo-card",
bs_icon("graph-up"), br(), strong("Chart 1")
)
),
div(class = "demo-column",
style = "flex: 0 0 50%; max-width: 50%; padding: 0 10px;",
div(class = "demo-card",
bs_icon("table"), br(), strong("Data Table")
)
),
div(class = "demo-column",
style = "flex: 0 0 100%; max-width: 100%; padding: 0 10px;",
div(class = "demo-card",
bs_icon("pie-chart"), br(), strong("Chart 2 (Full Width)")
)
)
)
)
)
)
)
# Update button states using JavaScript
runjs("
$('.device-btn').removeClass('btn-primary btn-info btn-success btn-warning').addClass('btn-outline-primary btn-outline-info btn-outline-success btn-outline-warning');
$('#tablet').removeClass('btn-outline-info').addClass('btn-info');
")
})
observeEvent(input$desktop, {
current_device("desktop")
current_width(1200)
# Reset to desktop layout (default)
session$reload()
})
observeEvent(input$wide, {
current_device("wide")
current_width(1600)
# Update viewport container for wide screen
removeUI(selector = "#viewport-container")
insertUI(
selector = "body",
where = "beforeEnd",
ui = div(id = "viewport-container",
class = "viewport-container wide-viewport",
div(class = "viewport-label", "Wide Screen View (1600px)"),
uiOutput("breakpoint_indicator"),
div(class = "content-demo",
uiOutput("stacking_info"),
div(id = "responsive-layout",
div(style = "display: flex; flex-wrap: wrap; margin: 0 -10px;",
div(class = "demo-column",
style = "flex: 0 0 20%; max-width: 20%; padding: 0 10px;",
div(class = "demo-sidebar",
bs_icon("sliders"), br(),
strong("Sidebar"), br(),
tags$small("Narrower on wide screens")
)
),
div(class = "demo-column",
style = "flex: 0 0 80%; max-width: 80%; padding: 0 10px;",
div(class = "demo-main",
bs_icon("bar-chart"), br(),
strong("Main Content"), br(),
tags$small("More space for content")
)
)
),
h5("Three-Column Layout (Side by Side):"),
div(style = "display: flex; flex-wrap: wrap; margin: 0 -10px;",
div(class = "demo-column",
style = "flex: 0 0 33.333%; max-width: 33.333%; padding: 0 10px;",
div(class = "demo-card",
bs_icon("graph-up"), br(), strong("Chart 1")
)
),
div(class = "demo-column",
style = "flex: 0 0 33.333%; max-width: 33.333%; padding: 0 10px;",
div(class = "demo-card",
bs_icon("table"), br(), strong("Data Table")
)
),
div(class = "demo-column",
style = "flex: 0 0 33.333%; max-width: 33.333%; padding: 0 10px;",
div(class = "demo-card",
bs_icon("pie-chart"), br(), strong("Chart 2")
)
)
)
)
)
)
)
# Update button states using JavaScript
runjs("
$('.device-btn').removeClass('btn-primary btn-info btn-success btn-warning').addClass('btn-outline-primary btn-outline-info btn-outline-success btn-outline-warning');
$('#wide').removeClass('btn-outline-warning').addClass('btn-warning');
")
})
# Viewport info display
output$viewport_info <- renderUI({
div(class = "current-specs",
strong("Current Simulation: "), current_device(), " (", current_width(), "px width)",
br(),
"Bootstrap Class: ",
switch(current_device(),
"mobile" = "xs (extra small)",
"tablet" = "sm (small)",
"desktop" = "md/lg (medium/large)",
"wide" = "xl (extra large)")
)
})
# Breakpoint indicator
output$breakpoint_indicator <- renderUI({
info <- switch(current_device(),
"mobile" = list(text = "Mobile Breakpoint: < 768px", class = ""),
"tablet" = list(text = "Tablet Breakpoint: 768px - 991px", class = ""),
"desktop" = list(text = "Desktop Breakpoint: 992px - 1199px", class = ""),
"wide" = list(text = "Wide Screen Breakpoint: ≥ 1200px", class = ""))
div(class = "breakpoint-indicator",
bs_icon("rulers"), " ", info$text
)
})
# Stacking information
output$stacking_info <- renderUI({
message <- switch(current_device(),
"mobile" = "📱 All columns stack vertically for easy mobile reading",
"tablet" = "📱 Columns adapt to tablet size - some side by side, some stacked",
"desktop" = "💻 Optimal desktop layout with proper proportions",
"wide" = "🖥️ Enhanced layout taking advantage of wide screen space")
div(class = "stacking-indicator",
message
)
})
# Breakpoint explanation
output$breakpoint_explanation <- renderUI({
explanation <- switch(current_device(),
"mobile" = list(
title = "Mobile Breakpoint (< 768px)",
points = c(
"All columns become full width (100%)",
"Content stacks vertically for easy scrolling",
"Touch-friendly spacing and larger buttons",
"Horizontal scrolling is avoided"
)
),
"tablet" = list(
title = "Tablet Breakpoint (768px - 991px)",
points = c(
"Columns start to share horizontal space",
"2-column layouts work well at this size",
"Wider content areas than mobile",
"Good balance between mobile and desktop"
)
),
"desktop" = list(
title = "Desktop Breakpoint (992px+)",
points = c(
"Full multi-column layouts are effective",
"Sidebar layouts work optimally",
"Users expect more information density",
"Mouse interaction allows smaller targets"
)
),
"wide" = list(
title = "Wide Screen Breakpoint (1200px+)",
points = c(
"Extra space can show more content",
"Consider max-width containers to prevent line length issues",
"Opportunity for additional sidebars or panels",
"Enhanced data visualization space"
)
))
div(class = "breakpoint-info",
h5(bs_icon("info-circle"), " ", explanation$title),
tags$ul(
lapply(explanation$points, function(x) tags$li(x))
)
)
})
}
shinyApp(ui = ui, server = server)
Advanced Layout Techniques
Custom CSS Integration
While Shiny’s built-in layouts cover most needs, custom CSS can provide additional control and branding opportunities.
Adding Custom Styles:
<- fluidPage(
ui # Include custom CSS
$head(
tags$style(HTML("
tags .custom-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
margin-bottom: 20px;
border-radius: 8px;
}
.metric-box {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
text-align: center;
margin-bottom: 20px;
}
.metric-value {
font-size: 2.5rem;
font-weight: bold;
color: #0d6efd;
}
.metric-label {
font-size: 0.9rem;
color: #6c757d;
text-transform: uppercase;
letter-spacing: 1px;
}
"))
),
# Custom header
div(class = "custom-header",
h1("Analytics Dashboard", style = "margin: 0;"),
p("Real-time business metrics and insights", style = "margin: 0; opacity: 0.9;")
),
# Custom metric boxes
fluidRow(
column(4,
div(class = "metric-box",
div(class = "metric-value", "1,234"),
div(class = "metric-label", "Total Sales")
)
),column(4,
div(class = "metric-box",
div(class = "metric-value", "89%"),
div(class = "metric-label", "Success Rate")
)
),column(4,
div(class = "metric-box",
div(class = "metric-value", "567"),
div(class = "metric-label", "Active Users")
)
)
) )
Dynamic Layout Generation
Creating layouts that adapt based on data or user preferences:
<- function(input, output, session) {
server
# Generate dynamic UI based on user selection
$dynamic_layout <- renderUI({
outputif (input$layout_style == "grid") {
# Grid layout
fluidRow(
lapply(1:input$num_panels, function(i) {
column(12 / input$num_panels,
wellPanel(
h4(paste("Panel", i)),
plotOutput(paste0("plot_", i), height = "300px")
)
)
})
)else if (input$layout_style == "tabs") {
} # Tabbed layout
do.call(tabsetPanel,
lapply(1:input$num_panels, function(i) {
tabPanel(paste("Tab", i),
plotOutput(paste0("plot_", i), height = "400px"))
})
)else {
} # Stacked layout
div(
lapply(1:input$num_panels, function(i) {
div(
h4(paste("Section", i)),
plotOutput(paste0("plot_", i), height = "300px"),
hr()
)
})
)
}
}) }
Common Issues and Solutions
Issue 1: Layout Breaking on Small Screens
Problem: Elements overflow or become unusable on mobile devices.
Solution:
# BAD: Fixed widths that don't adapt
column(4,
width = "300px", # Fixed width causes problems
# content
)
# GOOD: Responsive column system
column(4, # Uses Bootstrap's responsive grid
div(style = "min-width: 250px;", # Minimum width for usability
# content
)
)
# BETTER: Mobile-specific adjustments
column(4,
class = "col-lg-4 col-md-6 col-sm-12", # Different sizes for different screens
# content
)
Issue 2: Cluttered Interface with Too Many Elements
Problem: Interface becomes overwhelming with too many visible elements.
Solution:
# Use collapsible panels for advanced options
conditionalPanel(
condition = "input.show_advanced == true",
wellPanel(
h4("Advanced Options"),
# Advanced controls here
)
),
checkboxInput("show_advanced", "Show Advanced Options", FALSE)
# Group related controls in tabs
tabsetPanel(
tabPanel("Basic",
# Essential controls
),tabPanel("Advanced",
# Advanced controls
),tabPanel("Export",
# Export options
) )
Issue 3: Inconsistent Spacing and Alignment
Problem: Elements appear misaligned or have inconsistent spacing.
Solution:
# Use wellPanel for consistent grouping
wellPanel(
h4("Control Group"),
selectInput(...),
sliderInput(...),
actionButton(...)
)
# Use hr() for visual separation
fluidRow(
column(6, "Content 1"),
column(6, "Content 2")
),hr(), # Horizontal rule for separation
fluidRow(
column(12, "Full width content")
)
# Consistent margin and padding with custom CSS
$style(HTML("
tags .control-group {
margin-bottom: 20px;
padding: 15px;
}
"))
Common Questions About Shiny Layout Systems
Use fluidPage()
for modern, responsive applications that need to work across different devices. This is the default choice for most applications, especially dashboards, analytics tools, and public-facing apps where users access them on various screen sizes.
Use fixedPage()
for applications requiring consistent visual presentation regardless of screen size. This works well for scientific applications, research tools, or complex interfaces where precise element positioning is crucial.
Use dashboardPage()
from shinydashboard for business dashboards and admin interfaces that need professional appearance with value boxes, menu systems, and structured layouts.
Use navbarPage()
for multi-section applications where users need to navigate between different functional areas, similar to traditional websites.
The choice depends on your users’ needs, the type of content you’re displaying, and whether responsive behavior or consistent presentation is more important for your use case.
Professional appearance comes from several key elements:
Consistent spacing and alignment:
# Use wellPanel for grouped controls
wellPanel(
h4("Analysis Options"),
selectInput(...),
sliderInput(...),
actionButton(..., class = "btn-primary")
)
Clear visual hierarchy:
- Use proper heading levels (h1, h2, h3, h4)
- Group related elements together
- Provide adequate white space between sections
Modern color schemes and typography:
# Add custom CSS for professional styling
$head(tags$style(HTML("
tags .navbar { background-color: #2c3e50; }
h1, h2, h3 { color: #2c3e50; }
.btn-primary { background-color: #3498db; }
")))
Professional layout patterns:
- Use sidebar layouts for data applications
- Implement tabbed interfaces for complex workflows
- Add loading indicators and progress feedback
- Include helpful tooltips and user guidance
Responsive design:
- Test on different screen sizes
- Use the Bootstrap grid system properly
- Ensure touch-friendly interface elements
Organize controls using progressive disclosure:
Group related controls:
tabsetPanel(
tabPanel("Data Selection",
# Core data selHeyection controls
),tabPanel("Analysis Options",
# Statistical analysis parameters
),tabPanel("Visualization",
# Plotting and display options
) )
Use conditional panels:
conditionalPanel(
condition = "input.analysis_type == 'advanced'",
# Show advanced options only when needed
)
Implement collapsible sections:
checkboxInput("show_advanced", "Show Advanced Options"),
conditionalPanel(
condition = "input.show_advanced",
wellPanel(
h4("Advanced Configuration"),
# Advanced controls here
) )
Create intuitive workflows:
- Start with essential controls visible
- Use wizard-style interfaces for complex processes
- Provide clear labels and help text
- Group controls by function, not by type
Use visual cues:
- Different background colors for control groups
- Icons to indicate different types of functionality
- Progress indicators for multi-step processes
- Clear action buttons that stand out
The key is to show users what they need when they need it, rather than overwhelming them with every possible option at once.
Mobile optimization requires several considerations:
Use responsive grid patterns:
# Columns stack vertically on mobile automatically
fluidRow(
column(4, "Sidebar"), # Becomes full-width on mobile
column(8, "Main content") # Stacks below sidebar
)
Design touch-friendly interfaces:
# Larger buttons for touch interaction
actionButton("action", "Execute",
class = "btn-primary btn-lg",
style = "min-height: 44px; width: 100%;")
# Appropriate spacing between elements
div(style = "margin: 15px 0;",
selectInput(...))
Simplify mobile interfaces:
# Hide non-essential elements on small screens
div(class = "hidden-xs hidden-sm",
# Desktop-only advanced controls
),
# Mobile-specific simplified controls
div(class = "visible-xs visible-sm",
# Essential controls only
)
Test on actual devices:
- Use browser developer tools to simulate mobile screens
- Test touch interactions and scrolling behavior
- Ensure text remains readable without zooming
- Verify that all functionality is accessible
Consider mobile-first design: Start with the mobile layout and progressively enhance for larger screens, rather than trying to retrofit desktop designs for mobile.
Layout complexity affects performance in several ways:
Simple layouts are faster:
# Faster: Simple structure
fluidPage(
sidebarLayout(
sidebarPanel(...),
mainPanel(...)
)
)
# Slower: Deeply nested structure
fluidPage(
fluidRow(
column(3,
fluidRow(
column(6, fluidRow(column(12, ...))),
column(6, fluidRow(column(12, ...)))
)
)
) )
Dynamic UI generation has overhead:
renderUI()
recreates DOM elements, which is expensive- Static layouts load faster and use less memory
- Use dynamic UI only when necessary for functionality
Large numbers of input/output elements:
- Each reactive element adds overhead
- Use
req()
to prevent unnecessary calculations - Consider pagination for large datasets
- Implement lazy loading for complex visualizations
CSS and JavaScript optimization:
- Minimize custom CSS and JavaScript
- Use Bootstrap classes instead of custom styles when possible
- Avoid complex animations that impact performance
- Test with realistic data volumes, not just small examples
Best practices:
- Measure performance with real data
- Use browser developer tools to identify bottlenecks
- Optimize the most frequently used parts of your application
- Consider server resources when designing for multiple concurrent users
Test Your Understanding
You’re building a financial dashboard that will be used by executives on tablets during meetings and by analysts on desktop computers for detailed analysis. The dashboard needs to display key metrics prominently and provide access to detailed charts. Which layout approach would be most appropriate?
fixedPage()
with a rigid grid layout for consistent presentation
fluidPage()
with responsive design and conditional content
dashboardPage()
from shinydashboard with value boxes and collapsible sections
navbarPage()
with separate sections for different user types
- Consider the different user types and their device preferences
- Think about the need for both summary metrics and detailed analysis
- Remember the importance of professional appearance for executive use
- Consider responsive behavior across different screen sizes
C) dashboardPage()
from shinydashboard with value boxes and collapsible sections
This scenario is perfect for shinydashboard because:
Professional Executive Interface:
dashboardPage(
dashboardHeader(title = "Executive Dashboard"),
dashboardSidebar(
sidebarMenu(
menuItem("Executive Summary", icon = icon("dashboard")),
menuItem("Detailed Analysis", icon = icon("chart-line"))
)
),
dashboardBody(
# Key metrics for executives
fluidRow(
valueBoxOutput("revenue"),
valueBoxOutput("growth"),
valueBoxOutput("profit_margin")
),
# Collapsible detailed sections for analysts
fluidRow(
box(title = "Revenue Trend", collapsible = TRUE,
plotOutput("revenue_chart")),
box(title = "Detailed Analytics", collapsible = TRUE,
# Complex analysis tools
)
)
) )
Why this works best:
- Value boxes prominently display key metrics for executives
- Responsive design works well on both tablets and desktops
- Collapsible sections allow detailed analysis without cluttering executive view
- Professional appearance appropriate for C-level presentations
- Menu structure supports different user workflows
- Built-in responsive behavior handles different screen sizes automatically
Options A and B don’t provide the professional dashboard aesthetics needed, while option D creates unnecessary navigation complexity for this focused use case.
You need to create a layout where content is displayed in 4 columns on large screens, 2 columns on medium screens, and 1 column on small screens. Each column contains a chart and some summary text. What’s the best implementation approach?
- Use fixed column widths with CSS media queries
- Create separate layouts for each screen size using conditional panels
- Use Bootstrap’s responsive column classes with Shiny’s column system
- Use JavaScript to dynamically adjust column counts
- Consider Bootstrap’s built-in responsive capabilities
- Think about maintenance and code complexity
- Remember that Shiny is built on Bootstrap’s grid system
- Consider how automatic responsive behavior works
C) Use Bootstrap’s responsive column classes with Shiny’s column system
This leverages Bootstrap’s powerful responsive grid system that Shiny is built on:
<- fluidPage(
ui h2("Responsive Chart Gallery"),
fluidRow(
# Each column: 3/12 on large, 6/12 on medium, 12/12 on small
lapply(1:8, function(i) {
column(3, # Default column width
class = "col-lg-3 col-md-6 col-sm-12", # Responsive classes
wellPanel(
h4(paste("Chart", i)),
plotOutput(paste0("chart_", i), height = "250px"),
br(),
p(paste("Summary text for chart", i))
)
)
})
) )
How the responsive behavior works:
- Large screens (≥1200px):
col-lg-3
= 4 columns per row (3/12 × 4 = 12/12) - Medium screens (992-1199px):
col-md-6
= 2 columns per row (6/12 × 2 = 12/12)
- Small screens (<992px):
col-sm-12
= 1 column per row (12/12 × 1 = 12/12)
Why this approach is superior:
- Leverages Bootstrap’s tested responsive system
- Automatic breakpoint handling without custom JavaScript
- Minimal code complexity - works with Shiny’s existing column system
- Maintains accessibility and semantic structure
- Easy to modify breakpoints if requirements change
- No JavaScript dependencies or complex conditional logic needed
The other options require significantly more code, custom CSS, or JavaScript that essentially recreates what Bootstrap already provides optimally.
You’re designing a data analysis application with the following requirements:
- 20+ input controls for analysis configuration
- Multiple visualization outputs that update based on inputs
- Need to accommodate both novice and expert users
- Must work well on desktop and tablet devices
What’s the best layout strategy to prevent interface overwhelm while maintaining full functionality?
- Display all controls in a single sidebar with the main content area for outputs
- Use a tabbed interface to group related controls with conditional panels for expert features
- Create separate “Simple” and “Advanced” modes with completely different interfaces
- Use a wizard-style interface that guides users through configuration steps
- Consider progressive disclosure principles
- Think about how expert users want quick access to all features
- Remember the need to work across different device sizes
- Consider maintenance and development complexity
B) Use a tabbed interface to group related controls with conditional panels for expert features
This approach provides the best balance of functionality and usability:
<- fluidPage(
ui titlePanel("Advanced Data Analysis Platform"),
sidebarLayout(
sidebarPanel(
width = 4, # Wider sidebar for complex controls
# Tabbed control organization
tabsetPanel(
id = "control_tabs",
tabPanel("Data",
h4("Data Configuration"),
selectInput("dataset", "Dataset:", choices = datasets),
selectInput("variables", "Variables:", choices = NULL, multiple = TRUE),
# Expert features in collapsible section
checkboxInput("show_data_advanced", "Advanced Data Options"),
conditionalPanel(
condition = "input.show_data_advanced",
wellPanel(
selectInput("missing_handling", "Missing Values:",
choices = c("Remove", "Impute", "Keep")),
numericInput("sample_size", "Sample Size:", value = 1000),
checkboxInput("stratified", "Stratified Sampling")
)
)
),
tabPanel("Analysis",
h4("Analysis Methods"),
selectInput("analysis_type", "Primary Analysis:",
choices = c("Descriptive", "Regression", "Classification")),
# Dynamic UI based on analysis type
uiOutput("analysis_options"),
# Expert statistical options
checkboxInput("show_stats_advanced", "Advanced Statistics"),
conditionalPanel(
condition = "input.show_stats_advanced",
wellPanel(
numericInput("confidence_level", "Confidence Level:",
value = 0.95, min = 0.8, max = 0.99, step = 0.01),
selectInput("correction_method", "Multiple Comparison Correction:",
choices = c("None", "Bonferroni", "FDR")),
checkboxInput("bootstrap", "Bootstrap Confidence Intervals")
)
)
),
tabPanel("Visualization",
h4("Plot Configuration"),
selectInput("plot_type", "Chart Type:",
choices = c("Scatter", "Line", "Bar", "Box")),
selectInput("color_scheme", "Color Palette:",
choices = c("Default", "Viridis", "Set1")),
# Advanced plotting options
checkboxInput("show_plot_advanced", "Advanced Plot Options"),
conditionalPanel(
condition = "input.show_plot_advanced",
wellPanel(
sliderInput("plot_width", "Plot Width:",
min = 400, max = 1200, value = 800),
sliderInput("plot_height", "Plot Height:",
min = 300, max = 800, value = 500),
textInput("plot_title", "Custom Title:"),
checkboxInput("show_grid", "Show Grid Lines", TRUE)
)
)
)
),
br(),
# Action button outside tabs for visibility
actionButton("run_analysis", "Run Analysis",
class = "btn-primary btn-lg btn-block")
),
mainPanel(
width = 8,
# Results in organized tabs
tabsetPanel(
tabPanel("Summary",
h3("Analysis Summary"),
verbatimTextOutput("summary_output")),
tabPanel("Visualization",
h3("Results Visualization"),
plotOutput("main_plot", height = "600px")),
tabPanel("Detailed Results",
h3("Statistical Details"),
verbatimTextOutput("detailed_results")),
tabPanel("Diagnostics",
h3("Model Diagnostics"),
plotOutput("diagnostic_plots"))
)
)
) )
Why this approach excels:
Progressive Disclosure:
- Essential controls are immediately visible
- Advanced options are available but not overwhelming
- Users can expand complexity as needed
Logical Organization:
- Controls grouped by function (Data, Analysis, Visualization)
- Related options stay together
- Clear workflow from data → analysis → visualization
User Experience Benefits:
- Novice users see only essential controls initially
- Expert users can quickly access advanced features
- Tablet compatibility through responsive sidebar layout
- Reduced cognitive load through organized sections
Maintenance Advantages:
- Easy to add new features to appropriate tabs
- Clear code organization mirrors user interface
- Single interface supports all user types
- Responsive design handles device differences automatically
This approach scales well as requirements grow and provides excellent user experience for both novice and expert users.
Conclusion
Mastering Shiny’s layout systems transforms your applications from functional tools into professional, user-friendly experiences that reflect well on your technical expertise. The layout patterns and responsive design principles covered in this guide provide the foundation for creating applications that not only work well but also engage and delight users.
The key to exceptional Shiny layouts lies in understanding your users’ needs and choosing the appropriate layout patterns to serve those needs effectively. Whether you’re building simple dashboards with sidebar layouts, complex analytics platforms with tabbed interfaces, or responsive applications that work across devices, the principles and techniques in this guide will serve you well.
Remember that great layout design is iterative - start with solid foundations using Shiny’s built-in layout functions, then refine based on user feedback and real-world usage patterns. The investment in thoughtful layout design pays dividends in user satisfaction, application adoption, and professional credibility.
Next Steps
Based on your mastery of layout systems and design patterns, here are the recommended paths for continuing your Shiny UI development expertise:
Immediate Next Steps (Complete These First)
- Complete Guide to Shiny Input Controls - Master the input widgets that populate your well-designed layouts
- Shiny Output Types and Visualization - Learn to create compelling outputs that make the most of your layout structure
- Practice Exercise: Redesign an existing application using the professional layout patterns learned in this guide, focusing on responsive behavior and user experience
Building on Your Foundation (Choose Your Path)
For Advanced UI Design:
For Interactive Features:
For Production Applications:
Long-term Goals (2-4 Weeks)
- Design and implement a complete responsive dashboard that showcases advanced layout techniques
- Create a reusable layout framework that can be applied across multiple applications
- Develop expertise in custom CSS and advanced styling techniques for branded applications
- Build a portfolio of applications that demonstrate mastery of professional UI design principles
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 {Layout} {Systems} and {Design} {Patterns:} {Complete}
{Guide}},
date = {2025-05-23},
url = {https://www.datanovia.com/learn/tools/shiny-apps/ui-design/layout-systems.html},
langid = {en}
}