rguides

Building Dashboards with Quarto: Interactive Data Displays in R

Building dashboards with Quarto turns an R analysis into an interactive, visually appealing web page that stakeholders can explore without touching code. A Quarto dashboard arranges key metrics, charts, and data tables in a responsive grid — rows and columns that reflow on different screen sizes. Value boxes surface headline numbers, tabsets organise content into navigable sections, and the optional Shiny runtime adds real-time interactivity. This tutorial covers the dashboard YAML format, layout components, and deployment options from local preview to Posit Connect.

What you’ll learn

This tutorial covers the key concepts and practical techniques for working with Building Dashboards with Quarto. By the end, you will know how to apply the core functions in real data analysis workflows.

What is a Quarto dashboard?

A Quarto dashboard is an HTML document with a special format that arranges content in a grid. Each cell in the grid can contain text, plots, tables, or interactive components. The browser renders the dashboard without requiring a Shiny server, though you can add Shiny for full interactivity.

Dashboards differ from regular Quarto documents in three ways:

  1. The document format is dashboard instead of html
  2. Content lives in a grid layout defined by YAML
  3. Special components like value boxes and tabsets organize information

Setting up your dashboard

Create a new Quarto file with the dashboard format. In RStudio, choose File > New File > Quarto Dashboard. Alternatively, create a .qmd file with this YAML header:

---
title: "My Dashboard"
format: dashboard
---

The dashboard format enables several layout options. You define the page structure in the YAML, then add content in markdown and code cells below.

Layout options

Quarto dashboards support several layout mechanisms. Choose based on how you want to present your data.

Pages and rows

The simplest layout uses pages. Each page is a separate tab in the dashboard:

---
format: dashboard
pages:
  - title: "Overview"
    blocks:
      - |
        ## Welcome
        This is the overview page.
  - title: "Details"
    blocks:
      - |
        ## Detailed View
        More information here.
---

Place navigation in a sidebar that stays visible while users explore content:

---
title: "Sales Dashboard"
format: dashboard
theme: cosmo
sidebar:
  - title: "Navigation"
    items:
      - text: "Overview"
        href: "#overview"
      - text: "Trends"
        href: "#trends"
content:
  - id: overview
    blocks:
      - |
        ## Sales Overview
        Welcome to the sales dashboard.
  - id: trends
    blocks:
      - |
        ## Sales Trends
        Charts and analysis go here.
---

Value boxes

Value boxes highlight key metrics at the top of your dashboard. They display a number with a label and optional icon:

```
#| value-boxes
library(bslib)
library(gapminder)

total_gdp <- gapminder::gapminder |>
  dplyr::filter(year == 2007) |>
  dplyr::summarise(sum(gdpPercap * pop)) |>
  dplyr::pull()

bslib::value_box(
  "Total GDP",
  scales::dollar(total_gdp),
  showcase = bsicons::bs_icon("graph-up-arrow"),
  theme = "primary"
)
```r

The value-boxes cell option tells Quarto to render the output as value boxes. Each value box appears as a card in a responsive grid.

Tabsets

Group related content into tabs within any section:

## Analysis

::: panel-tabset
### Chart View

```
#| fig-width: 6
plot(mtcars$wt, mtcars$mpg)
```r

### Table View

```
#| echo: false
head(mtcars)
```r
:::

The `::: panel-tabset` div wraps content into clickable tabs. Users see one tab at a time, keeping the interface clean.

Adding interactive components

Plotly charts

Make plots interactive with the Plotly library:

```
#| fig-width: 8
library(plotly)

plot_ly(mtcars, x = ~wt, y = ~mpg, color = ~factor(cyl),
        mode = "markers", type = "scatter")
```r

Plotly renders interactive charts that users can zoom, pan, and hover for details. This works without a Shiny server.

Shiny integration

For full interactivity, embed a Shiny app in your dashboard:

---
format: dashboard
server: shiny
---

```
library(shiny)

ui <- fluidPage(
  sliderInput("n", "Number of points:", 10, 100, 50),
  plotOutput("plot")
)

server <- function(input, output) {
  output$plot <- renderPlot({
    plot(rnorm(input$n))
  })
}

shinyApp(ui, server)
```r

The server: shiny option runs a Shiny server in the background. Users interact with controls, and the output updates in real-time.

Complete example

Here is a practical dashboard showing sales metrics:

---
title: "Quarterly Sales Dashboard"
format: dashboard
theme: cosmo
---

```
#| value-boxes
library(bslib)
library(tidyverse)

quarterly_sales <- tibble(
  quarter = c("Q1", "Q2", "Q3", "Q4"),
  sales = c(125000, 142000, 138000, 165000),
  growth = c(8.2, 13.6, -2.8, 19.6)
)

total_sales <- sum(quarterly_sales$sales)
best_quarter <- quarterly_sales |>
  filter(sales == max(sales)) |>
  pull(quarter)
avg_growth <- mean(quarterly_sales$growth)

bslib::value_box(
  "Total Annual Sales",
  scales::dollar(total_sales),
  showcase = bsicons::bs_icon("currency-dollar"),
  theme = "primary"
)
```r

```
#| value-boxes
bslib::value_box(
  "Best Quarter",
  best_quarter,
  showcase = bsicons::bs_icon("trophy-fill"),
  theme = "success"
)
```r

```
#| value-boxes
bslib::value_box(
  "Average Growth",
  paste0(round(avg_growth, 1), "%"),
  showcase = bsicons::bs_icon("graph-up-arrow"),
  theme = "info"
)
```r

## Sales by Quarter

```
#| fig-height: 5
ggplot(quarterly_sales, aes(x = quarter, y = sales, fill = quarter)) +
  geom_col() +
  scale_fill_brewer(palette = "Blues") +
  labs(title = "Quarterly Sales", y = "Sales ($)") +
  theme_minimal() +
  theme(legend.position = "none")
```r

## Growth Trends

::: panel-tabset
### Bar Chart

```
#| fig-height: 4
ggplot(quarterly_sales, aes(x = quarter, y = growth, fill = growth > 0)) +
  geom_col() +
  scale_fill_manual(values = c("firebrick", "forestgreen")) +
  labs(title = "Quarter-over-Quarter Growth", y = "Growth (%)") +
  theme_minimal() +
  theme(legend.position = "none")
```r

### Table

```
#| echo: false
quarterly_sales |>
  mutate(
    sales = scales::dollar(sales),
    growth = paste0(growth, "%")
  ) |>
  knitr::kable()
```r
:::

This example combines value boxes, a bar chart, and a tabset with both a chart and table view.

Publishing your dashboard

GitHub pages

Publish to GitHub Pages by enabling it in your repository settings and committing the rendered HTML:

quarto render dashboard.qmd
git add dashboard.html
git commit -m "Update dashboard"
git push

GitHub Pages serves your dashboard at https://username.github.io/repo/dashboard.html.

Quarto pub

Use Quarto Pub for free hosting:

quarto publish quarto-pub dashboard.qmd

Your dashboard appears at your-account.quarto.pub/dashboard.

Connect servers

For enterprise deployment, publish to RStudio Connect or Shiny Server. These platforms handle authentication and provide monitoring:

quarto publish connect dashboard.qmd

Dashboard layout

Quarto dashboards use YAML front matter with format: dashboard. Content is organized into rows and columns using Markdown headers. ## Row {height=300} creates a row with a specified pixel height. ### Column {width=30%} creates a column using a percentage of the row width. {.tabset} creates a tabbed panel within a layout region.

Value boxes

bslib::value_box() creates KPI cards for dashboards. Quarto automatically styles them within a dashboard context. Use value_box(title = "Revenue", value = "$1.2M", showcase = bsicons::bs_icon("currency-dollar")) for a card with an icon. Multiple value boxes in a row create a KPI strip. bg-* CSS classes control background color: bg-success (green), bg-warning (yellow), bg-danger (red).

Interactivity options

Quarto dashboards can be static (pure HTML, no server required) or interactive (powered by Shiny). Static dashboards use htmlwidgets like plotly, leaflet, and DT for client-side interactivity. Shiny-powered dashboards add server: shiny to the YAML header and require a Shiny server for deployment. Static dashboards can be hosted on GitHub Pages or any web server; Shiny dashboards need Posit Connect or Shiny Server.

Deployment

quarto publish quarto-pub deploys to Quarto Pub (free hosting). quarto publish gh-pages deploys to GitHub Pages. For Posit Connect: rsconnect::deployDoc("dashboard.qmd"). Static Quarto dashboards are self-contained HTML files that can be emailed or hosted anywhere. For scheduled reports, GitHub Actions can regenerate and publish the dashboard on a schedule.

Dashboard layout syntax

Quarto dashboards use a YAML-based layout system where headings control the structure. First-level headings (#) create pages in a multi-page dashboard. Second-level headings (##) create rows or columns depending on the orientation setting. Third-level headings (###) define individual cards (panels).

The default orientation is columns, so ## headings stack left to right and fill the remaining height. orientation: rows makes ## headings stack top to bottom instead. Within a column, cards stack vertically by default.

---
format:
  dashboard:
    orientation: columns
---

Control the relative width of columns with the .column class and width attribute:

## {width=30%}
Card here

## {width=70%}
Wide card here

Value boxes and metrics

Value boxes display a single key metric prominently. In a Quarto dashboard, use bslib::value_box() inside a code chunk:

library(bslib)
value_box(
  title = "Total Revenue",
  value = scales::dollar(1234567),
  showcase = bsicons::bs_icon("currency-dollar"),
  theme = "primary"
)

The showcase can be a Bootstrap icon, an inline plot (sparkline), or any htmlwidget. The theme controls the background color: "primary", "success", "danger", "warning" follow Bootstrap color conventions.

Tabsets and navigation

Cards can contain tabsets to show multiple views of data without taking extra vertical space. Wrap card content in a tabset:

### Revenue

::: {.tabset}

#### By Month
[monthly chart here]

#### By Region
[regional chart here]

:::

For multi-page dashboards, each first-level heading creates a navigation tab at the top. Add an icon to a page tab with the attribute {icon="bar-chart"}. A sidebar for global filters applies across all pages when placed in the first column with the .sidebar class.

Shiny integration

Quarto dashboards can embed Shiny reactivity by adding server: shiny to the YAML. This requires the document to be served by a Shiny process rather than rendered to static HTML. Input widgets go in a sidebar; outputs are defined in code chunks with #| context: server for server-side code.

The advantage over a plain Shiny app is that the dashboard layout and text are easier to write in Markdown, and Quarto handles the HTML/CSS framework. The tradeoff is that the full Quarto rendering pipeline runs at startup, which is slower than a minimal Shiny app.

For dashboards that do not need server-side computation, Quarto’s static dashboards with crosstalk provide client-side filtering and linking between htmlwidgets without a server. crosstalk::SharedData$new(df) creates a shared data source, and compatible widgets (DT, plotly, leaflet) respond to selections in any of the others.

Theming and branding

Quarto dashboards use Bootstrap 5 for layout and can apply any Bootswatch theme: theme: flatly, theme: darkly, etc. Custom CSS goes in a custom.scss file referenced with theme: [flatly, custom.scss]. Override Bootstrap variables in the SCSS to change fonts, primary color, and spacing without rewriting component styles.

The logo: key in the YAML adds a logo to the navigation bar. navbar-bg: sets the navigation bar background color with a hex value or CSS color name. These options apply globally to all pages of the dashboard.

Data refresh and scheduled reports

Static Quarto dashboards render once and display fixed data. For dashboards that should show current data, scheduled re-rendering provides a simple alternative to a full Shiny server.

A GitHub Actions workflow renders the dashboard on a schedule (schedule: cron: "0 6 * * *") and publishes the output. The workflow installs R and packages, runs quarto render dashboard.qmd, and deploys to GitHub Pages or another static hosting service. The dashboard is “fresh” as of the last scheduled render but does not require a live R server.

For more frequent updates, a Posit Connect server can schedule rendering at any interval, caching the output between renders. This handles the common case where data updates hourly but the dashboard only needs to show the latest complete picture.

When data freshness matters but a Shiny server is too heavy, Quarto + scheduled rendering is the practical middle ground. It works with existing static hosting infrastructure and eliminates server management overhead.

Accessibility and mobile layout

Quarto dashboards use Bootstrap 5, which is responsive by default. Cards reflow to single-column layout on small screens. Test mobile layout by resizing the browser window during development.

For accessibility: alt text on plots (fig-alt: "Description of chart" in chunk options), sufficient color contrast (test with browser accessibility tools), and keyboard navigation support. Bootstrap 5’s built-in accessibility features handle most navigation for free.

Color-blind accessible palettes are important for dashboards used by general audiences. scale_fill_viridis_d() in ggplot2 and the viridis color scale in plotly produce good results. The colorspace package provides additional palettes and a simulation function for checking how plots appear to color-blind users.

Card customization

Cards are the primary layout unit in Quarto dashboards. card() from the bslib package wraps arbitrary content with a border and optional header. In Quarto dashboard markdown, ### headings create cards automatically.

The height attribute on a card or column controls vertical size: ### My Card {height=300} sets the card to 300 pixels tall. Without explicit heights, cards fill available space proportionally.

card_header() and card_footer() from bslib add styled header and footer sections within a rendered card, useful for adding chart titles or data source attributions that are separate from the plotly or ggplot2 figure title.

card_image("path/to/image.png", href = "url") creates a card with an image link. This is useful for dashboard landing pages with navigation tiles.

Input sidebar for filters

A sidebar section starts with ## {.sidebar} as the column header. Inside the sidebar, R code chunks contain Shiny input widgets:

sliderInput("year", "Year", min = 2020, max = 2024, value = 2024)
selectInput("region", "Region", choices = c("All", "North", "South"))

A column marked with .sidebar becomes the input panel. Sidebar inputs are only functional when the dashboard uses server: shiny in the YAML. For static dashboards without Shiny, use crosstalk shared data for client-side filtering.

Deploying Quarto dashboards

Static Quarto dashboards are HTML files deployable anywhere that serves HTML: GitHub Pages, Netlify, Amazon S3, any web server. quarto publish gh-pages deploys to GitHub Pages with one command. The dashboard file is self-contained when embed-resources: true is set in the YAML.

Shiny-powered dashboards require a Shiny server. quarto publish connect deploys to Posit Connect. rsconnect::deployApp() works for standard Shiny-based dashboards. The dashboard format is a wrapper around a Shiny app, so all standard Shiny deployment options apply.

For automated refresh, a scheduled job (cron, GitHub Actions, or Posit Connect scheduler) renders the dashboard on a schedule and publishes the output. The data queries run at render time, not at view time, the dashboard is a snapshot of the data at the last render.

Design principles for dashboards

A good dashboard answers specific questions efficiently. The best dashboards are designed around the decisions the user needs to make, not around all the data available. Each chart and table should answer a question the user actually asks. Decorative charts and charts that require expertise to interpret belong in detailed reports, not dashboards.

Information hierarchy matters. Key metrics (KPIs) go at the top, these are the numbers a user checks first to determine whether anything needs attention. Supporting detail goes in the body. Context and explanation go in tooltips, footnotes, and auxiliary sections. This hierarchy matches how users scan: top to bottom, most important to least.

Interactivity should reduce cognitive load, not add to it. A filter control is valuable when users regularly need to see a specific subset. Animation is valuable when change over time is the point. Drill-down is valuable when users need to move from summary to detail. If an interactive feature would not change the user’s decision, it probably should not be there.

Color use in dashboards follows specific rules. Use color to encode information, not to decorate. Use one consistent color for positive/normal state and a second for attention-required state (often red or orange). Avoid using more than four or five colors, beyond that, colors become difficult to distinguish and match to a legend. Test your color choices for accessibility; about eight percent of men have some form of color vision deficiency.

Typography in dashboards should be minimal. One sans-serif font, two or three size levels, bold for emphasis. Decorative fonts and extensive text styling slow reading. Numbers should be formatted consistently: same decimal places for comparable metrics, same currency symbols, same scale (thousands vs millions) across a dashboard.

Reactive dashboards without Shiny

For dashboards that need filtering without a Shiny server, crosstalk enables client-side linking. Multiple htmlwidgets (plotly, leaflet, DT) can share a crosstalk SharedData object. Selecting points in one widget highlights corresponding rows in linked widgets, and DT filters update all connected charts.

This client-side approach has limits: it processes all data in the browser, so it works for hundreds to low thousands of rows but not millions. Computations happen at render time, so the dashboard is a snapshot. For large datasets or real-time data, Shiny is necessary.

The distinction matters for deployment: a crosstalk-based dashboard is a static HTML file deployable anywhere without a server. A Shiny dashboard requires a running R process. Choose based on data size and freshness requirements.

Dashboard design principles

A dashboard is not a report. Reports have linear narrative structure; dashboards are scanned, not read. Design for the user who spends thirty seconds on the page before deciding whether to dig deeper. Put the most important metric at the top left — where eyes go first. Use consistent visual encoding: the same color means the same thing throughout. Limit the number of charts to what can be absorbed in one view.

Quarto dashboards use a column and row layout with explicit sizing. Cards hold individual visualizations or text. Value boxes display key metrics prominently. The structure maps directly to the dashboard grid: columns at the top level, rows inside columns, cards inside rows. The layout is responsive — narrower screens stack columns vertically — so test the dashboard at multiple viewport widths.

Data refresh and scheduled rendering

A static Quarto dashboard embeds data at render time. When data changes, re-render the document to update it. For dashboards that should reflect current data without manual intervention, schedule the rendering with a cron job or a GitHub Actions workflow that runs the render command and deploys the output. The schedule frequency should match how often the underlying data changes and how stale the dashboard is acceptable to be.

For dashboards that need live data without re-rendering, add a Shiny runtime. Setting server: shiny in the YAML header turns the dashboard into a Shiny application where reactive expressions connect to data sources that update in real time. This adds deployment complexity — the dashboard requires a Shiny server rather than simple static file hosting — but enables genuinely dynamic content.

Value boxes and KPI display

Value boxes display a single metric prominently with an icon and optional trend indicator. They are the right format for the metrics that summarize overall status: total revenue, error rate, number of active users. Use value boxes for numbers that stakeholders check first, and charts for the breakdown or trend behind those numbers.

The display value in a value box should be the final, formatted number the user should see: “$1.2M” not “1200000”. Pre-format numbers in R before passing them to the value box, using formatC, scales::dollar, or scales::comma for appropriate formatting. Including a unit label in the value box title rather than in the number itself keeps the number readable and the title informative.

Summary

Quarto dashboards let you create polished, interactive data presentations. The key concepts are:

  • Use format: dashboard in your YAML header
  • Arrange content with pages, sidebars, and value boxes
  • Add interactivity through Plotly, HTML widgets, or embedded Shiny apps
  • Publish via GitHub Pages, Quarto Pub, or Connect

Start with a simple layout. Add value boxes for key metrics. Then layer in charts and interactive components as needed.

Next steps

Now that you understand building dashboards with quarto, explore these related topics to deepen your knowledge and apply these techniques in more complex scenarios.

See also