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
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 contentblock_page()owns the full app shell.block_sidebar()andblock_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$regionbehaves 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(), andobserveEvent()are unchanged Shiny APIs.update_block_select()follows the same server-update pattern as nativeupdateSelectInput().- Plot
alttext is supplied throughrenderPlot(), 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:
- The page loads with a sidebar and header.
- Changing Region updates both cards and the plot.
- Reset filters returns the select to Americas.
- 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:
- Start with a page or host-app container.
- Compose blocks with ordinary Shiny and
htmltoolstags. - 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
pakand 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.