Get Started with shinyblocks

Build a reactive sales dashboard with a sidebar, filters, metric cards, a plot, theming, and standard Shiny server logic.

What you will build

You will build a single-file Regional sales dashboard: a complete app shell with a collapsible sidebar that switches between Overview and Reports views, a header with a dark mode control, a region filter, two reactive metric cards, a reactive revenue plot, and a reset action. The whole thing is one app.R file with no frontend build step.

  • Responsive page shell with sidebar and header
  • Sidebar nav that switches between Overview and Reports pages
  • Region filter that drives every value on the page
  • Reactive metric cards and a reactive plot
  • Reset action with an automatic busy state
  • Light and dark mode toggle
Prerequisites. You need R, a current Shiny installation, and the ability to install a package from GitHub. You do not need React, Tailwind, Vite, or any Node tooling. Plan on about 10–15 minutes.
The finished app, running live in your browser. It boots in WebAssembly (no server), so the first load takes a few seconds.Open full size

Install shinyblocks

shinyblocks is experimental and not yet on CRAN. Install the current GitHub development release with pak:

install.packages("pak")
pak::pak("nvelden/shinyblocks")

Then load Shiny and shinyblocks at the top of your app. App authors do not need Node, React, Tailwind, or Vite — the package ships its compiled runtime, and this example targets the current GitHub development release.

library(shiny)
library(shinyblocks)

Create app.R

Create an empty directory with a single app.R file. Start it with the imports and a small, deterministic sample dataset. Keeping the data local means the first run has no external failure modes.

library(shiny)
library(shinyblocks)

sales <- data.frame(
  region = rep(c("Americas", "EMEA", "APAC"), each = 6),
  month = rep(month.abb[1:6], times = 3),
  revenue = c(
    82, 91, 96, 104, 111, 119,
    74, 79, 85, 89, 96, 101,
    61, 66, 72, 78, 84, 92
  ) * 1000,
  orders = c(
    410, 452, 471, 509, 548, 581,
    361, 384, 415, 438, 469, 497,
    302, 327, 349, 376, 404, 441
  )
)

report_view <- function(df) {
  data.frame(
    Month = df$month,
    Revenue = sprintf("$%s", format(df$revenue, big.mark = ",", scientific = FALSE)),
    Orders = format(df$orders, big.mark = ",", scientific = FALSE),
    check.names = FALSE
  )
}

Build the page shell

The outer container is block_page(). It owns the full app shell and accepts optional named sidebar and header regions, with body blocks passed through ....

block_page()
├── block_sidebar()
├── block_header()
└── body content
  • block_page() owns the full app shell.
  • block_sidebar() and block_header() are optional named regions.
  • Page body blocks are supplied through ....
  • block_theme() controls semantic colours.
  • block_style() controls visual geometry and feel.

The full ui <- block_page(...) definition is shown in the complete example below; the next two sections cover the body blocks it contains.

Add a filter and reset action

A filter card holds a labelled region select. The compact reset action shares the label row, so it does not create another control column:

block_field(
  block_cluster(
    align = "center",
    justify = "between",
    wrap = FALSE,
    block_field_label("Region", `for` = "region"),
    block_task_button(
      "reset_filters",
      "Reset",
      label_busy = "Resetting...",
      variant = "ghost",
      size = "sm",
      icon = "refresh-cw"
    )
  ),
  block_select(
    "region",
    choices = c("Americas", "EMEA", "APAC"),
    selected = "Americas"
  ),
  block_field_description("All dashboard values use this region.")
)
  • input$region behaves as a normal Shiny input.
  • The label uses a matching for/input id.
  • The task button exposes a busy state for free.
  • Layout around blocks can use ordinary htmltools::div() and inline CSS, as in the complete example's responsive grid.

Add reactive cards and a plot

shinyblocks blocks accept normal Shiny output tags as children. Two metric cards hold standard textOutput():

block_card(
  title = "Revenue",
  description = "Six-month total",
  value = textOutput("revenue", inline = TRUE)
)

block_card(
  title = "Orders",
  description = "Six-month total",
  value = textOutput("orders", inline = TRUE)
)

The plot card frames a block_plot_output():

block_card(
  title = "Monthly revenue",
  description = "Revenue by month for the selected region",
  block_plot_output(
    "revenue_plot",
    aspect = "16/9",
    border = FALSE
  )
)
  • shinyblocks accepts normal Shiny output tags as children.
  • block_plot_output() styles the frame; renderPlot() stays standard Shiny.
  • Output ids must match the server assignments.

Connect the server

The server is ordinary Shiny. A reactive filters the data by region, two render functions fill the cards, one renders the plot, and an observer resets the select.

server <- function(input, output, session) {
  # block_nav(id = "page") reports "overview" or "reports".
  output$current_page <- reactive({
    if (is.null(input$page)) "overview" else input$page
  })
  outputOptions(output, "current_page", suspendWhenHidden = FALSE)

  filtered_sales <- reactive({
    sales[sales$region == input$region, , drop = FALSE]
  })

  output$revenue <- renderText({
    sprintf("$%s", format(
      sum(filtered_sales()$revenue),
      big.mark = ",",
      scientific = FALSE
    ))
  })

  output$orders <- renderText({
    format(
      sum(filtered_sales()$orders),
      big.mark = ",",
      scientific = FALSE
    )
  })

  output$revenue_plot <- renderPlot(
    {
      current <- filtered_sales()

      barplot(
        current$revenue / 1000,
        names.arg = current$month,
        col = "#71717a",
        border = NA,
        xlab = NULL,
        ylab = "Revenue ($000s)"
      )
    },
    alt = "Monthly revenue bar chart for the selected region"
  )

  observe({
    update_block_table(
      session,
      "report_table",
      data = report_view(filtered_sales()),
      striped = TRUE
    )
  })

  observeEvent(input$reset_filters, {
    update_block_select(
      session,
      "region",
      selected = "Americas"
    )
  })
}
  • reactive(), renderText(), renderPlot(), and observeEvent() are unchanged Shiny APIs.
  • update_block_select() follows the same server-update pattern as native updateSelectInput().
  • Plot alt text is supplied through renderPlot(), because Shiny owns the rendered image.

Run the app

Finish the file with shinyApp(ui, server):

shinyApp(ui, server)

Run it from the app directory, or use your IDE's Run App action:

shiny::runApp()

Verify the app behaves as documented:

  1. The page loads with a sidebar and header.
  2. Changing Region updates both cards and the plot.
  3. Reset filters returns the select to Americas.
  4. The theme control switches light/dark mode.

Complete app.R

Here is the entire app in one copyable block. This is the canonical source — the snippets above are slices of it.

library(shiny)
library(shinyblocks)

sales <- data.frame(
  region = rep(c("Americas", "EMEA", "APAC"), each = 6),
  month = rep(month.abb[1:6], times = 3),
  revenue = c(
    82, 91, 96, 104, 111, 119,
    74, 79, 85, 89, 96, 101,
    61, 66, 72, 78, 84, 92
  ) * 1000,
  orders = c(
    410, 452, 471, 509, 548, 581,
    361, 384, 415, 438, 469, 497,
    302, 327, 349, 376, 404, 441
  )
)

report_view <- function(df) {
  data.frame(
    Month = df$month,
    Revenue = sprintf("$%s", format(df$revenue, big.mark = ",", scientific = FALSE)),
    Orders = format(df$orders, big.mark = ",", scientific = FALSE),
    check.names = FALSE
  )
}

ui <- block_page(
  title = "Regional sales",
  theme = block_theme(preset = "zinc"),
  style = block_style("default"),
  sidebar = block_sidebar(
    title = "Acme Analytics",
    collapsible = TRUE,
    block_nav(
      id = "page",
      block_nav_item(
        "Overview",
        value = "overview",
        icon = "layout-dashboard",
        selected = TRUE
      ),
      block_nav_item(
        "Reports",
        value = "reports",
        icon = "file-text"
      )
    )
  ),
  header = block_header(
    block_cluster(
      align = "center",
      justify = "between",
      wrap = FALSE,
      style = "width: 100%;",
      htmltools::div(
        htmltools::tags$strong("Regional sales"),
        htmltools::tags$div(
          style = "color: var(--muted-foreground); font-size: 0.875rem;",
          "Performance for the first six months"
        )
      ),
      block_dark_mode_toggle()
    )
  ),
  block_stack(
    gap = "md",
    block_card(
      title = "Dashboard filters",
      description = "Choose a region or restore the default.",
      block_field(
        block_cluster(
          align = "center",
          justify = "between",
          wrap = FALSE,
          block_field_label("Region", `for` = "region"),
          block_task_button(
            "reset_filters",
            "Reset",
            label_busy = "Resetting...",
            variant = "ghost",
            size = "sm",
            icon = "refresh-cw"
          )
        ),
        block_select(
          "region",
          choices = c("Americas", "EMEA", "APAC"),
          selected = "Americas"
        ),
        block_field_description(
          "All dashboard values use this region."
        )
      )
    ),
    # Overview page. conditionalPanel() shows it only when the sidebar selects
    # it; both pages stay in the document so their outputs keep updating.
    conditionalPanel(
      "output.current_page == 'overview'",
      block_stack(
        gap = "md",
        block_grid(
          min_width = "14rem",
          gap = "md",
          block_value_box(
            title = "Revenue",
            description = "Six-month total",
            value = textOutput("revenue", inline = TRUE),
            icon = "dollar-sign"
          ),
          block_value_box(
            title = "Orders",
            description = "Six-month total",
            value = textOutput("orders", inline = TRUE),
            icon = "package"
          )
        ),
        block_card(
          title = "Monthly revenue",
          description = "Revenue by month for the selected region",
          block_plot_output(
            "revenue_plot",
            aspect = "16/9",
            border = FALSE
          )
        )
      )
    ),
    # Reports page.
    conditionalPanel(
      "output.current_page == 'reports'",
      block_card(
        title = "Monthly breakdown",
        description = "Revenue and orders by month for the selected region",
        block_table(
          report_view(sales[sales$region == "Americas", , drop = FALSE]),
          id = "report_table",
          striped = TRUE
        )
      )
    )
  )
)

server <- function(input, output, session) {
  # block_nav(id = "page") reports "overview" or "reports".
  output$current_page <- reactive({
    if (is.null(input$page)) "overview" else input$page
  })
  outputOptions(output, "current_page", suspendWhenHidden = FALSE)

  filtered_sales <- reactive({
    sales[sales$region == input$region, , drop = FALSE]
  })

  output$revenue <- renderText({
    sprintf("$%s", format(
      sum(filtered_sales()$revenue),
      big.mark = ",",
      scientific = FALSE
    ))
  })

  output$orders <- renderText({
    format(
      sum(filtered_sales()$orders),
      big.mark = ",",
      scientific = FALSE
    )
  })

  output$revenue_plot <- renderPlot(
    {
      current <- filtered_sales()

      barplot(
        current$revenue / 1000,
        names.arg = current$month,
        col = "#71717a",
        border = NA,
        xlab = NULL,
        ylab = "Revenue ($000s)"
      )
    },
    alt = "Monthly revenue bar chart for the selected region"
  )

  observe({
    update_block_table(
      session,
      "report_table",
      data = report_view(filtered_sales()),
      striped = TRUE
    )
  })

  observeEvent(input$reset_filters, {
    update_block_select(
      session,
      "region",
      selected = "Americas"
    )
  })
}

shinyApp(ui, server)

Understand composition

Three rules cover almost every shinyblocks app:

  1. Start with a page or host-app container.
  2. Compose blocks with ordinary Shiny and htmltools tags.
  3. Read inputs and render outputs through normal Shiny server code.
block_page()
├── sidebar: block_nav() → input$page
├── header: title + theme control
└── body
    ├── filter card
    │   ├── block_select()  → input$region
    │   └── task button     → input$reset_filters
    ├── Overview page  (conditionalPanel)
    │   ├── metric cards  ← renderText()
    │   └── plot card     ← renderPlot()
    └── Reports page   (conditionalPanel)
        └── table         ← update_block_table()

Standalone blocks can also be embedded in an existing Shiny UI. block_page() is the full-page entry point, not a requirement for every component.

Customize the look

Swap two arguments on block_page() to restyle the whole app:

theme = block_theme(preset = "mauve")
style = block_style("luma")
  • Themes change semantic colour tokens.
  • Styles change geometry, spacing, surfaces, and motion.
  • theme_mode = "dark" sets a fixed initial mode.
  • block_dark_mode_toggle() lets users switch modes.

See the Theme and Style component pages for the full reference.

Next steps

You can also explore every component interactively in the bundled showcase:

shinyblocks::run_showcase()

Troubleshooting

  • Installation fails: update pak and verify your GitHub access.
  • Output stays blank: confirm the UI and server output ids match.
  • A server update does nothing: pass the constructor's input id to the matching update_block_*() helper.
  • Layout looks unstyled: confirm the UI element is returned/rendered rather than printed or converted to text.
  • Shinylive: deploying to Shinylive needs the release filesystem assets and is an advanced deployment path. See the troubleshooting notes on GitHub.