Overview
shinyGovstyle provides a set of layout functions that produce the
HTML structure GOV.UK Frontend CSS expects. This vignette covers all of
the layout functions available and explains how they fit together to
build a complete app.
The layout functions fall into two groups:
- Page-level components —
header(),
footer(), banner(),
cookieBanner(), skip_to_main(), and
service_navigation() — form the frame of the page that sits
outside the main content area.
- Content layout functions —
gov_main_layout(), gov_row(),
gov_box(), and gov_layout() — structure
content within the main content area.
Page-level components
These components form the outer frame of every page. They sit outside
the main content area and are consistent across all pages of your
app.
+-------------------------------------------------------+
| skip_to_main() [visually hidden, keyboard only] |
+-------------------------------------------------------+
| cookieBanner() [optional] |
+-------------------------------------------------------+
| header() |
+-------------------------------------------------------+
| service_navigation() [optional, multi-page apps] |
+-------------------------------------------------------+
| banner() [optional, e.g. Beta or Alpha] |
+-------------------------------------------------------+
| |
| gov_main_layout() ← id = "main" |
| +--------------------------------------------------+ |
| | your content goes here | |
| +--------------------------------------------------+ |
| |
+-------------------------------------------------------+
| footer() |
+-------------------------------------------------------+
skip_to_main()
Provides a visually hidden “Skip to main content” link that becomes
visible when focused by a keyboard user. This is an accessibility
requirement and should always be the first element in your UI, before
the header.
By default it links to #main, which matches the
id applied by gov_main_layout(). If you change
the inputID argument of gov_main_layout(),
pass the same value to skip_to_main().
For more information, read the documentation for the GOV.UK
Skip link component.
cookieBanner()
Displays a GOV.UK-styled cookie consent banner. It requires
shinyjs::useShinyjs() to be present in the UI. All element
IDs within the banner are preset — see ?cookieBanner for
the server-side observeEvent pattern needed to handle
accept and reject interactions.
shinyjs::useShinyjs()
cookieBanner("My service name")
For more information, including when this should be used, read the
documentation for the GOV.UK
Cookie banner component.
banner()
Displays a phase banner immediately below the header, used to
indicate the maturity of your service and give a clear route for users
to provide feedback.
banner(
inputId = "phase-banner",
type = "Beta",
label = paste0(
"This is a new service \u2014 your ",
'<a class="govuk-link" href="#">feedback</a> will help us to improve it.'
)
)
For more information on when and how to use this, read the
documentation for the GOV.UK
Phase banner component.
The main content area
gov_main_layout() produces a
<div class="govuk-width-container"> wrapping a
<main class="govuk-main-wrapper">. The outer
<div> constrains content width; the
<main> element carries the responsive vertical
padding. Everything between the page-level components and the footer
lives inside it.
gov_main_layout(
# your content here
)
The id (default "main") is applied directly
to the <main> element, which is the correct target
for skip_to_main(). The <main> element
also carries role="main" and tabindex="-1", so
keyboard focus moves to it when the skip link is activated.
The primary layout system
Inside gov_main_layout(), content is structured using a
three-function grid system: gov_row(),
gov_box(), and optionally gov_text().
gov_main_layout()
└── gov_row()
├── gov_box(size = "two-thirds")
│ └── [your content]
└── gov_box(size = "one-third")
└── [your content]
gov_row()
Creates a GOV.UK grid row. You can have multiple rows inside
gov_main_layout(), each stacked vertically.
gov_main_layout(
gov_row(
# columns go here
),
gov_row(
# another row
)
)
gov_box()
Creates a column within a row. The size argument
controls the column width using GOV.UK Frontend’s grid classes:
"full" |
100% |
"one-half" |
50% |
"two-thirds" |
66% |
"one-third" |
33% |
"three-quarters" |
75% |
"one-quarter" |
25% |
Sizes within a row should add up to a full width. For example,
"two-thirds" and "one-third" sit side by
side:
gov_main_layout(
gov_row(
gov_box(
size = "two-thirds",
heading_text("Main content", size = "l"),
# inputs, text, etc.
),
gov_box(
size = "one-third",
heading_text("Sidebar", size = "m"),
# supporting content
)
)
)
For a simple single-column layout, use
size = "full":
gov_main_layout(
gov_row(
gov_box(
size = "full",
heading_text("Page title", size = "l")
)
)
)
gov_text()
A wrapper that produces a <p class="govuk-body">
paragraph element. For full guidance on gov_text() and all
other text functions, see the Headings
and text vignette.
gov_layout() — legacy alternative
Warning: gov_layout() is not
recommended for new development and may be removed in a future release.
Use gov_main_layout() with gov_row() and
gov_box() instead.
gov_layout() is a single-function alternative that
combines a width container and a column in one call:
gov_layout(
size = "two-thirds",
heading_text("Page title", size = "l"),
# content
)
It is well suited to simple, single-column apps where you want a
width constraint without setting up the full
gov_main_layout() / gov_row() /
gov_box() hierarchy.
As soon as your app needs more than one column, multiple
rows, or a combination of widths, switch to the full system.
Nesting gov_layout() inside gov_main_layout()
will produce doubled-up width container HTML and cause the content to
appear visually inset from the page-level components.
Multi-page dashboards
For apps with multiple sections, use
service_navigation() in combination with a hidden tab
panel. The navigation bar renders as a row of links below the header;
clicking a link fires a Shiny input that you use in your server to
switch the visible panel.
Setting up navigation links
Pass a named character vector to service_navigation().
The names are displayed as link text; the values become the
inputIDs:
service_navigation(
c(
"Summary" = "nav_summary",
"Detailed data" = "nav_detail",
"User guide" = "nav_guide"
)
)
If you pass an unnamed vector, inputIDs are auto-generated by
lowercasing the text and replacing non-alphanumeric characters with
underscores (e.g. "Detailed data" becomes
detailed_data).
Wiring navigation to panels
Use a hidden tab panel for the content area and
observeEvent() in your server to switch panels when a
navigation link is clicked. When the user clicks a service navigation
link, the JavaScript binding updates the active state automatically —
you only need to switch the panel:
# ui.R — shiny tabsetPanel
shiny::tabsetPanel(
type = "hidden",
id = "main_panels",
shiny::tabPanel("Summary", value = "nav_summary", "Content"),
shiny::tabPanel("Detailed data", value = "nav_detail", "Content"),
shiny::tabPanel("User guide", value = "nav_guide", "Content")
)
# server.R — nav link click: JS handles the active state, just switch the panel
shiny::observeEvent(input$nav_summary, {
shiny::updateTabsetPanel(session, "main_panels", selected = "nav_summary")
})
If you prefer bslib tab panels, use
bslib::navset_hidden() and bslib::nav_select()
instead:
# ui.R — bslib navset_hidden
bslib::navset_hidden(
id = "main_panels",
bslib::nav_panel("Summary", value = "nav_summary", "Content"),
bslib::nav_panel("Detailed data", value = "nav_detail", "Content"),
bslib::nav_panel("User guide", value = "nav_guide", "Content")
)
# server.R
shiny::observeEvent(input$nav_summary, {
bslib::nav_select("main_panels", "nav_summary")
})
Repeat the observeEvent block for each navigation
link.
update_service_navigation() is only needed when
navigation is triggered programmatically — for example,
via a next / back button — because in that case the nav link itself is
not clicked and the active state does not update automatically. See
?update_service_navigation for full details and
examples.
# server.R — programmatic navigation: must update both the panel and the nav
shiny::observeEvent(input$next_btn, {
shiny::updateTabsetPanel(session, "main_panels", selected = "nav_detail")
shinyGovstyle::update_service_navigation(session, "nav_detail")
})
Modularising the code
Once an app has multiple pages, it is strongly recommended to use
Shiny modules to keep each page’s UI and server logic self-contained.
The inst/example_app bundled with this package demonstrates
this pattern: each page is a module in
inst/example_app/modules/, with
mod_<name>_ui() and
mod_<name>_server() functions called from the
top-level ui.R and server.R. This keeps
individual files focused and makes it straightforward to add or remove
pages without touching the overall app structure.
Complete example
The following is a minimal but complete multi-page app that uses all
of the layout components covered in this vignette:
library(shiny)
library(shinyGovstyle)
ui <- bslib::page_fluid(
skip_to_main(),
header(
org_name = "My department",
service_name = "My dashboard"
),
service_navigation(
c(
"Summary" = "nav_summary",
"About" = "nav_about"
)
),
banner(
inputId = "phase",
type = "Beta",
label = "This is a new service."
),
gov_main_layout(
shiny::tabsetPanel(
type = "hidden",
id = "main_panels",
shiny::tabPanel(
"Summary", value = "nav_summary",
gov_row(
gov_box(
size = "two-thirds",
heading_text("Summary", size = "l"),
gov_text("Welcome to the summary page.")
),
gov_box(
size = "one-third",
heading_text("Quick facts", size = "m"),
gov_text("Supporting information goes here.")
)
)
),
shiny::tabPanel(
"About", value = "nav_about",
gov_row(
gov_box(
size = "full",
heading_text("About this dashboard", size = "l"),
gov_text("This page describes the dashboard.")
)
)
),
shiny::tabPanel(
"Accessibility statement", value = "accessibility_panel",
gov_row(
gov_box(
size = "full",
heading_text("Accessibility statement", size = "l"),
gov_text("This page describes the accessibility of the dashboard.")
)
)
)
)
),
footer(
links = c(`Accessibility statement` = "accessibility_footer_link")
)
)
server <- function(input, output, session) {
shiny::observeEvent(input$nav_summary, {
shiny::updateTabsetPanel(session, "main_panels", selected = "nav_summary")
})
shiny::observeEvent(input$nav_about, {
shiny::updateTabsetPanel(session, "main_panels", selected = "nav_about")
})
shiny::observeEvent(input$accessibility_footer_link, {
shiny::updateTabsetPanel(session, "main_panels",
selected = "accessibility_panel")
})
}
shiny::shinyApp(ui, server)