# 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 — `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.

    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.

### `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, 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 to request this as an addition to the package.

    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.

    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.

### `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.

    # 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 `<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.

### 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")
    })

### 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.

    # 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_<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)