Shiny Data Dashboards
Last updated on 2025-06-03 | Edit this page
Overview
Questions
- What is the relationship between the data applications we created in the previous episode, and data dashboards (which we’ll create in this one)?
- What is shinydashboard, and what is it’s role in the Shiny ecosystem?
- How can shinydashboard simplify the dashboard creation process?
Objectives
- Explore shinydashboard and gain practice in using this package to make simple data dashboards.
- Connect the process of making a dashboard with shinydashboard to more general principles of application development in Shiny (introduced in previous episodes)
Preliminaries
Before proceeding with this Episode, please install and load the following libraries (if you haven’t already done so):
R
# load libraries
library(shiny)
library(shinydashboard)
OUTPUT
Attaching package: 'shinydashboard'
OUTPUT
The following object is masked from 'package:graphics':
box
R
library(tidyverse)
OUTPUT
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr 1.1.4 ✔ readr 2.1.5
✔ forcats 1.0.0 ✔ stringr 1.5.1
✔ ggplot2 3.5.2 ✔ tibble 3.2.1
✔ lubridate 1.9.4 ✔ tidyr 1.3.1
✔ purrr 1.0.4
OUTPUT
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag() masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
R
library(nycflights13)
library(DT)
OUTPUT
Attaching package: 'DT'
The following objects are masked from 'package:shiny':
dataTableOutput, renderDataTable
Introduction
A Shiny data application is an interactive web application lets users explore or manipulate data, including those without data science expertise. In the previous episode, we gained experience in developing such applications by applying some fundamental principles of Shiny application development that we learned earlier. We can think of a data dashboard as a specific kind of Shiny data application, one that emphasizes the display of multiple interrelated pieces of information in a structured, systematic, and visually appealing way. Dashboards can often convey more information than simpler applications, and are well-suited to providing important context and supporting decisionmaking.
By itself, the base shiny package we’ve been working with has all the tools and functions necessary to create more structured dashboards, but doing so is non-trivial, and requires fairly advanced knowledge of the Shiny environment. That’s where the shinydashboard package comes in. Part of the broader Shiny ecosystem (i.e. packages adjacent to the core shiny package that extend and broaden its functionality, including those we’ve worked with such as shinythemes and shinyjs), the shinydashboard package provides a suite of functions that make it relatively straighforward to build dashboards using the principles of Shiny application development. The package does much of the work involved in getting from basic Shiny data applications to more multidimensional and context-rich dashboards, which makes it easier than it otherwise would be to make data dashboards using the Shiny tools and principles we’ve been learning.
At this point, your knowledge of Shiny is sufficiently advanced that picking up shinydashboard will be quite manageable; it involves learning some new functions, but as you’ll see, the underlying principles at work are the same as the principles that underlie Shiny. Now that you’ve done the heavy lifting in learning the essentials of Shiny, and have developed some Shiny data applications, that learning curve for shinydashboard will be less steep, but still yield a significant payoff.
Getting oriented to shinydashboard
As a way of getting oriented to shinydashboard, consider the following script, which creates a basic dashboard structure using functions from the shinydashboard package.
R
ui <- dashboardPage(
dashboardHeader(
title = "My Dashboard"
),
dashboardSidebar(
sidebarMenu(
menuItem(
"Dashboard",
tabName = "dashboard",
icon = icon("dashboard")
),
menuItem(
"About",
tabName = "about",
icon = icon("info-circle")
)
)
),
dashboardBody(
tabItems(
tabItem(
tabName = "dashboard",
h2("Main Dashboard"),
plotOutput("myplot")
),
tabItem(
tabName = "about",
h2("About This App"),
p("This app was built using shinydashboard.")
)
)
)
)
# Define the server-side logic
server <- function(input, output) {
output$myplot <- renderPlot({
hist(rnorm(100))
})
}
# Run the Shiny application
shinyApp(ui, server)
When you launch the dashboard application, you will see something that looks like this:

Challenge 1: Understand the code to create a dashboard
After launching the (mostly) empty dashboard application structure above, closely examine the dashboard and connect its various features to the parts of the code that created it. Then, as a way of solidifying your understanding, comment the code. Finally, identify some similarities and differences between the code to create a more general Shiny application, and to create a dashboard.
A thoroughly commented version of the script above would look something like this:
R
# Define the User Interface (UI)
ui <- dashboardPage( # Start the overall dashboard page layout
dashboardHeader( # Define the top header of the dashboard
title = "My Dashboard" # Title displayed in the dashboard header
), # End of dashboardHeader
dashboardSidebar( # Start the sidebar layout
sidebarMenu( # Create a menu container for sidebar items
menuItem( # Create first menu item
"Dashboard", # Text shown in sidebar
tabName = "dashboard", # Identifier used to link to tab content
icon = icon("dashboard") # Display an icon (text input; still allowed here)
), # End of first menuItem
menuItem( # Create second menu item
"About", # Text shown in sidebar
tabName = "about", # Identifier for linking to about tab
icon = icon("info-circle") # Icon to accompany label
) # End of second menuItem
) # End of sidebarMenu
), # End of dashboardSidebar
dashboardBody( # Start the main content body of the dashboard
tabItems( # Container that holds tab panels
tabItem( # First tab panel
tabName = "dashboard", # Must match tabName in sidebar menu item
h2("Main Dashboard"), # Large heading for this tab
plotOutput("myplot") # Placeholder for plot to be rendered in server
), # End of first tabItem
tabItem( # Second tab panel
tabName = "about", # Must match second menu item's tabName
h2("About This App"), # Heading for the About section
p("This app was built using shinydashboard.") # Informative paragraph
) # End of second tabItem
) # End of tabItems container
) # End of dashboardBody
) # End of dashboardPage definition
# Define the server-side logic
server <- function(input, output) { # Start server function
output$myplot <- renderPlot({ # Define a plot output
hist(rnorm(100)) # Generate a histogram of 100 random values
}) # End of renderPlot
} # End of server function
# Run the Shiny application
shinyApp(ui, server) # Launch the app using the UI and server
You’ll notice that in broad terms, the code to create a standard
application is quite similar to this code to create a dashboard using
shinydashboard functions. Notice, for example, the structure of
the code, which is divided into a section for the UI and one for the
server; as before, the application is launched by bringing these
elements together within shinyApp. The basic logic of a Shiny
app is also on display here; space for outputs is reserved in the UI,
and these outputs are then populated in the server code. For example,
space for the plot created within the renderPlot()
function
is reserved with plotOutput("myplot")
, and the plot created
in the server is linked back to its UI placeholder using dollar sign
notation to reference the output ID. Right now, the dashboard isn’t an
interactive one that takes in user inputs and automatically updates the
outputs in accordance with the principle of reactivity, but you may be
able to anticipate this possibility, based on the overall structure of
the dashboard application.
The main difference you’ll notice is that the user interface is more
structured (with sections such as a sidebar menu and tabbed content)
than the interface of a regular application, and this structured
interface is implemented using shinydashboard functions that
help generate a dashboard layout “off the shelf”. Note, for example, the
use of dashboardPage()
rather than fluidPage()
as the base-level UI function.
The UI is organized by dashboardPage()
into three main
sections:
-
Header, which shows the dashboard title at the top
via
dashboardHeader()
-
Sidebar which shows a vertical panel with a
navigation menu on the side, via
dashboardSidebar()
- A Body, where associated with each tab is
displayed, via
dashboardBody()
In this example, the sidebar menu of the dashboard includes two menu items:
- One is called “Dashboard”, and labeled internally with
tabName="dashboard"
- The other is called “About”, and labelled internally with
tabName="about"
The body of the dashboard has two items, with each one linked to a tab in the sidebar menu.
- The “dashboard” item the body contains a title and plot output.
- the “about” item in the body contains a title and a short paragraph of text.
The items in the dashboard’s boy are linked back to their tabs in the
sidebar (so that clicking one of the tabs brings the user to the
appropriate item in the body) via their internal identifiers. For
example, the “dashboard” item in the body is linked to its corresponding
tab in the sidebar via tabName = "dashboard"
.
The syntax on the server side is identical to what you’ve seen in previous episodes.
Building a simple flight delay dashboard
Now that we’re oriented to to the basic layout of shinydashboard and the code used to generate it, let’s get some practice developing a more substantive (yet still relatively simple) dashboard that features an interactive dimension. As in the previous Episode, we’ll use flights data from the nycflights13 package. Building a data dashboard of flight delays will allow us to compare the dashboard to the application we built in the previous lesson, and appreciate the richer options for the display of information offered by the dashboard format.
As before, we’ll start by verbally outlining the dashboard, which we’ll name “NYC Flights Dashboard”:
- For simplicity, we’ll only include one menu item in the sidebar, named “Dashboard”, corresponding to the main dashboard page. On the sidebar, we’ll also include our Month and Day input widgets.
- The body of the dashboard has a single page containing a variety of
plots, tables, and value boxes The first row of this page will contain
two value boxes containing information on the number of total departing
flights, and the average flight delay (we’ll reserve space for these
boxes using the
valueBoxOutput()
placeholder. The second row will contain a plot of the top 10 departure delays for the selected time frame, and a table of the top 5 departure delays; space for the plot is reserved in the UI usingplotOutput()
, while space for the table is reserved usingtableOutput()
(this time, unlike the previous episode, we’ll use Shiny’s in-built table output functions rather than those from DT, so you can see the difference in appearance). Finally, in the third row of the dashboard page, we’ll reserve space for a plot of the average delay by destination. * In the server, we’ll first create a reactive expression that filters the dataset based on user specifications, and use this expression within arenderValueBox()
function to create the code to display the total number of flights on the selected day. We’ll also create a second reactive expression that takes the first reactive expression, and removes the non-delayed flights. This expression will be used to calculate the value for the second value box, i.e. the average delay. - This second reactive expression will also be used in the subsequent render functions that calculate the output values for the plots and tables specified in the UI.
When we translate this outline into code, we get something that looks like the following:
R
# Define UI
ui <- dashboardPage(
dashboardHeader(title = "NYC Flights Dashboard"),
dashboardSidebar(
sidebarMenu(
menuItem("Dashboard", tabName = "dashboard", icon = icon("plane")),
selectInput("month", "Month", choices = 1:12, selected = 1), # month selector
selectInput("day", "Day", choices = 1:31, selected = 1) # day selector
) # closes sidebar menu
), # closes dashboard sidebar
dashboardBody(
tabItems(
tabItem(tabName = "dashboard",
fluidRow(
valueBoxOutput("flight_count"),
valueBoxOutput("avg_delay")
), # closes first fluidRow
fluidRow( # opens second row
box(title = "Top 10 Departure Delays", width = 6,
plotOutput("delay_plot")),
box(title = "Top 5 Delayed Flights", width = 6,
tableOutput("top_delay_table"))
), # closes second row
fluidRow( # opens third row
box(title = "Average Delay by Destination", width = 12,
plotOutput("dest_delay_plot"))
) # closes third fluidRow
) # closes tabItem
) # closes tabItems
) # closes dashboardBody
) # closes dashboardPage
# Define server logic
server <- function(input, output) {
# Reactive dataset for all flights (no dep_delay filter)
all_flights <- reactive({
flights %>%
filter(month == as.numeric(input$month),
day == as.numeric(input$day))
})
# Reactive dataset for flights with non-missing dep_delay
filtered_flights <- reactive({
all_flights() %>%
filter(!is.na(dep_delay))
})
# Value box for total flights (all flights, including NA dep_delay)
output$flight_count <- renderValueBox({
count <- nrow(all_flights())
valueBox(count, "Flights", icon = icon("plane-departure"), color = "blue")
})
# Value box for average departure delay
output$avg_delay <- renderValueBox({
avg <- round(mean(filtered_flights()$dep_delay, na.rm = TRUE), 1)
valueBox(paste0(avg, " min"), "Avg. Departure Delay", icon = icon("clock"), color = "orange")
})
# Bar plot for top 10 departure delays
output$delay_plot <- renderPlot({
top10 <- filtered_flights() %>%
arrange(desc(dep_delay)) %>%
slice_head(n = 10)
ggplot(top10, aes(x = reorder(paste(carrier, flight), dep_delay), y = dep_delay)) +
geom_col(fill = "tomato") +
coord_flip() +
labs(title = "Top 10 Departure Delays", x = "Flight", y = "Delay (min)") +
theme_minimal()
})
# Table of top 5 delayed flights
output$top_delay_table <- renderTable({
filtered_flights() %>%
arrange(desc(dep_delay)) %>%
slice_head(n = 5) %>%
select(carrier, flight, dep_delay, dest) %>%
rename(Airline = carrier, Flight = flight, Delay = dep_delay, Destination = dest)
})
# Bar plot for average delay by destination
output$dest_delay_plot <- renderPlot({
filtered_flights() %>%
group_by(dest) %>%
summarise(avg_delay = mean(dep_delay, na.rm = TRUE), n = n()) %>%
filter(n >= 3) %>% # Require at least 3 flights for reliability
ggplot(aes(x = reorder(dest, avg_delay), y = avg_delay)) +
geom_col(fill = "steelblue") +
coord_flip() +
labs(title = "Average Departure Delay by Destination", x = "Destination", y = "Avg. Delay (min)") +
theme_minimal()
})
}
# Run the app
shinyApp(ui, server)
Some additional things to note about this script:
- Note that within the dashboard’s body, tab item is organized into
rows using
fluidRow()
. Each row spans a total width of 12 units. You can arrange content within these rows using boxes of varying widths (for example, three boxes of width 4 or two boxes of width 6, as long as the total does not exceed 12). These boxes function as “containers” for UI elements such as plots, tables, value boxes etc. - To correctly display value boxes, it’s not enough to compute a value
inside
renderValueBox()
and expect it to appear automatically via its identifier. You must use thevalueBox()
function insiderenderValueBox()
to explicitly define what should be displayed and how—this ensures the value appears in the intended format in the UI.
When the app is launched, it will look something like this:

Challenge 2: Enhance the flight delay dashboard
Please add a checkbox input to the dashboard that allows users to select their airport(s) of interest. Make sure that the app’s outputs update in response to the user’s selection of the airport(s).
To add a checkbox-based “airport selector”, use
checkboxGroupInput()
in the UI; to ensure that this filter
is appropriately used in the calculation of the outputs, make sure to
update the argument to the filter()
function in the first
reactive expression in the server. The revised code will look something
like this:
R
# Define UI
ui <- dashboardPage(
dashboardHeader(title = "NYC Flights Dashboard"),
dashboardSidebar(
sidebarMenu(
menuItem("Dashboard", tabName = "dashboard", icon = icon("plane")),
selectInput("month", "Month", choices = 1:12, selected = 1), # month selector
selectInput("day", "Day", choices = 1:31, selected = 1), # day selector
checkboxGroupInput("origin", "Airport of Origin", # airport selector
choices=unique(flights$origin),
selecte=unique(flights$origin))
) # closes sidebar menu
), # closes dashboard sidebar
dashboardBody(
tabItems(
tabItem(tabName = "dashboard",
fluidRow(
valueBoxOutput("flight_count"),
valueBoxOutput("avg_delay")
), # closes first fluidRow
fluidRow( # opens second row
box(title = "Top 10 Departure Delays", width = 6,
plotOutput("delay_plot")),
box(title = "Top 5 Delayed Flights", width = 6,
tableOutput("top_delay_table"))
), # closes second row
fluidRow( # opens third row
box(title = "Average Delay by Destination", width = 12,
plotOutput("dest_delay_plot"))
) # closes third fluidRow
) # closes tabItem
) # closes tabItems
) # closes dashboardBody
) # closes dashboardPage
# Define server logic
server <- function(input, output) {
# Reactive dataset for all flights (no dep_delay filter)
all_flights <- reactive({
flights %>%
filter(month == as.numeric(input$month),
day == as.numeric(input$day),
origin %in% input$origin)
})
# Reactive dataset for flights with non-missing dep_delay
filtered_flights <- reactive({
all_flights() %>%
filter(!is.na(dep_delay))
})
# Value box for total flights (all flights, including NA dep_delay)
output$flight_count <- renderValueBox({
count <- nrow(all_flights())
valueBox(count, "Flights", icon = icon("plane-departure"), color = "blue")
})
# Value box for average departure delay
output$avg_delay <- renderValueBox({
avg <- round(mean(filtered_flights()$dep_delay, na.rm = TRUE), 1)
valueBox(paste0(avg, " min"), "Avg. Departure Delay", icon = icon("clock"), color = "orange")
})
# Bar plot for top 10 departure delays
output$delay_plot <- renderPlot({
top10 <- filtered_flights() %>%
arrange(desc(dep_delay)) %>%
slice_head(n = 10)
ggplot(top10, aes(x = reorder(paste(carrier, flight), dep_delay), y = dep_delay)) +
geom_col(fill = "tomato") +
coord_flip() +
labs(title = "Top 10 Departure Delays", x = "Flight", y = "Delay (min)") +
theme_minimal()
})
# Table of top 5 delayed flights
output$top_delay_table <- renderTable({
filtered_flights() %>%
arrange(desc(dep_delay)) %>%
slice_head(n = 5) %>%
select(carrier, flight, dep_delay, dest) %>%
rename(Airline = carrier, Flight = flight, Delay = dep_delay, Destination = dest)
})
# Bar plot for average delay by destination
output$dest_delay_plot <- renderPlot({
filtered_flights() %>%
group_by(dest) %>%
summarise(avg_delay = mean(dep_delay, na.rm = TRUE), n = n()) %>%
filter(n >= 3) %>% # Require at least 3 flights for reliability
ggplot(aes(x = reorder(dest, avg_delay), y = avg_delay)) +
geom_col(fill = "steelblue") +
coord_flip() +
labs(title = "Average Departure Delay by Destination", x = "Destination", y = "Avg. Delay (min)") +
theme_minimal()
})
}
# Run the app
shinyApp(ui, server)
The result will look something like this:

Key Points
- shinydashboard is a package that makes it easier to develop data dashboards using Shiny principles
- We can think of a data dashboard as a specific type of data application with a more structured layout, and the ability to display larger amounts of information in context
- The basic principles involved in building a dashboard with shinydashboard are the same as those for any Shiny application, but shinydashboards has unique functions associated with the dashboard layout
- In shinydashboard, the UI is divided into three main parts:
dashboardHeader()
,dashboardSidebar(
), anddashboardBody()
. These allow for a clear separation of the header, navigation sidebar, and main content body, which contributes to a structured, user-friendly layout. - shinydashboard makes it easy to organize content using boxes, value boxes, and fluid rows. Each of these can be customized with specific widths to ensure an optimal layout for displaying information like charts, tables, or textual content.
- Just like any other Shiny application, dashboards can include
interactive elements like inputs (e.g., sliders, dropdowns, checkboxes)
to filter or modify the displayed data in real time. You can create
dynamic outputs using render functions, such as
renderPlot()
that are familiar from before, as well as render functions unique to shinydashboard such asrenderValueBox()
.