--- title: "Layout options" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Layout options} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) library(shinyGovstyle) ``` ## 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. ```{r, eval = FALSE} 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](https://design-system.service.gov.uk/components/skip-link/). ### `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. ```{r, eval = FALSE} 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](https://design-system.service.gov.uk/components/cookie-banner/). ### `header()` Creates a GOV.UK styled header bar, optionally containing your department logo, name and service name. This is not the official [GOV.UK header](https://design-system.service.gov.uk/components/header/), as that should only be used on GOV.UK domains. If you believe you have an R Shiny app on a GOV.UK domain, please [raise an issue](https://github.com/dfe-analytical-services/shinyGovstyle/issues/new/choose) to request this as an addition to the package. ```{r, eval = FALSE} header( org_name = "Department for Education", service_name = "My dashboard" ) ``` ### `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. ```{r, eval = FALSE} banner( inputId = "phase-banner", type = "Beta", label = paste0( "This is a new service \u2014 your ", 'feedback 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](https://design-system.service.gov.uk/components/phase-banner/). ### `footer()` Creates a GOV.UK styled footer, though like the header, this is not an offical version as that should only be used on a GOV.UK domain. Use `full = TRUE` to include the OGL licence logo and Crown copyright statement. You can add support links that point either to internal hidden tab panels or to external URLs. ```{r, eval = FALSE} # Minimal footer footer() # Footer with support links footer( links = c( `Accessibility statement` = "accessibility_footer_link", `Cookies` = "cookies_footer_link" ) ) ``` Internal links use auto-generated inputIDs — the link text lowercased with non-alphanumeric characters replaced by underscores — that you handle with `observeEvent()` in your server to switch the active tab panel. --- ## The main content area `gov_main_layout()` produces a `
` wrapping a `
`. The outer `
` constrains content width; the `
` element carries the responsive vertical padding. Everything between the page-level components and the footer lives inside it. ```{r, eval = FALSE} gov_main_layout( # your content here ) ``` The `id` (default `"main"`) is applied directly to the `
` element, which is the correct target for `skip_to_main()`. The `
` 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. ```{r, eval = FALSE} 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: ```{r, eval = FALSE} 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"`: ```{r, eval = FALSE} gov_main_layout( gov_row( gov_box( size = "full", heading_text("Page title", size = "l") ) ) ) ``` ### `gov_text()` A wrapper that produces a `

` paragraph element. For full guidance on `gov_text()` and all other text functions, see the [Headings and text](headings-and-text.html) 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: ```{r, eval = FALSE} 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: ```{r, eval = FALSE} 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: ```{r, eval = FALSE} # 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: ```{r, eval = FALSE} # 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. ```{r, eval = FALSE} # 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") }) ``` ### Footer-only pages Some pages — such as an accessibility statement, privacy notice, or cookies information page — should not appear in the service navigation but still need to be reachable. The standard pattern is to add a link in `footer()` and a corresponding hidden tab panel, but to omit the link from `service_navigation()`. Because the user navigates to these pages outside of the service navigation, there is no active nav item to highlight. You do not need to call `update_service_navigation()` for these transitions. However, you should call it when navigating *back* to a main page from a footer-linked page, so the correct nav item becomes active again. ```{r, eval = FALSE} # ui.R — footer link, no entry in service_navigation() footer( full = TRUE, links = c(`Accessibility statement` = "accessibility_footer_link") ) # ui.R — tab panel exists in the hidden tabset but not in service_navigation() shiny::tabsetPanel( type = "hidden", id = "main_panels", shiny::tabPanel("Summary", value = "nav_summary", "Content"), shiny::tabPanel("Accessibility statement", value = "accessibility_panel", "Content") ) # server.R — navigate to the footer page (no update_service_navigation needed) shiny::observeEvent(input$accessibility_footer_link, { shiny::updateTabsetPanel(session, "main_panels", selected = "accessibility_panel") }) ``` ### 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__ui()` and `mod__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: ```{r, eval = FALSE} 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) ```