Layout options

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

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.

skip_to_main()

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.

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:

size Width
"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.

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)