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

Every app starts with **`gov_page()`**, the outer wrapper for the whole page. Inside it go:

- **Page-level components** (`header()`, `footer()`, `phase_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()` holds the main content area, and `gov_row()` and `gov_box()` add grid columns inside it when you need them.

---

## `gov_page()`

`gov_page()` is a small wrapper around [bslib](https://rstudio.github.io/bslib/)'s `page_fluid()`. `bslib` is now generally the recommended way to build R Shiny UIs, in place of `shiny::fluidPage()`, and other `bslib` components can be mixed freely into a shinyGovstyle app. Use `gov_page()` as the very first thing your UI creates: everything else in this vignette goes inside it.

```{r, eval = FALSE}
ui <- gov_page(
  title = "My dashboard",
  # header(), footer(), and the rest of your page go here
)
```

Three arguments are worth knowing about:

- **`lang`**: the page's language, `"en"` by default. Screen readers rely on this to pick the right pronunciation and voice, so it matters for accessibility even on a page you never plan to translate. Set it to `"cy"` for a Welsh-language service, for example.
- **`description`**: a short summary of the page, added as a `<meta name="description">` tag for search engines and some assistive technology. Optional; there's no tag at all if you leave it out. Keep it to one concise sentence: search engines typically truncate meta descriptions at around 150-160 characters.
- **`width`** (see [Page width](#page-width) below): sets a default width for every shinyGovstyle component used inside it, so you don't have to repeat `width = ` on each one individually.

### Page width {#page-width}

By default, `gov_page()` lets shinyGovstyle components fill the browser window (`width = "full"`), which suits most dashboard-style apps. For an ordinary content page, where GOV.UK's usual narrower reading width is more appropriate, pass `width` to `gov_page()`:

```{r, eval = FALSE}
gov_page(
  width = "standard", # or "three-quarters", or a CSS length like "1400px"
  # ...
)
```

- **`"full"`** (the default): no maximum width, so the page fills the browser window, and also removes the gaps between grid columns, so content can run edge to edge.
- **`"standard"`**: GOV.UK's usual 960px content width, the better choice for an ordinary content-style page.
- **`"three-quarters"`**: three-quarters of the browser window's width (never narrower than standard), keeping GOV.UK's own left/right margins.
- **A CSS length**, e.g. `"1400px"` or `"90vw"`, for a specific maximum width instead of one of the presets above.

Every shinyGovstyle component that renders part of the page frame (`header()`, `footer()`, `phase_banner()`, `cookieBanner()`, `service_navigation()`, `gov_main_layout()`) also accepts its own `width` argument. Setting it on `gov_page()` sets the default for all of them at once. Setting it on an individual component overrides that default just for that one component, which is useful if, say, everything on your page should be wider except the footer:

```{r, eval = FALSE}
gov_page(
  width = "three-quarters",
  header(...),
  gov_main_layout(...),
  footer(..., width = "standard") # this one stays at the standard width
)
```

### Content width vs page width {#content-width}

`gov_page(width = ...)` only sets the *outer* ceiling: how wide the page frame is allowed to get. It's a separate thing from how wide any one piece of *content* looks, which you control on the content itself.

**Recommended setup:** for a dashboard-style app, set `gov_page(width = "full")` so wide tables, charts and value boxes get the room they need. Then, for any text-heavy page or section within that same app (a cookies page, a user guide, an accessibility statement), wrap its content in a `div` with GOV.UK's `govuk-!-width-two-thirds` class, rather than letting it stretch the full width of the page. The class works on its own, without the grid, and goes back to full width on small screens.

> **Avoid full-width body text.** GOV.UK's own [Design System guidance](https://design-system.service.gov.uk/styles/layout/) recommends keeping body text to a two-thirds width even on a wide page, so lines don't get so long they're hard to read (aim for no more than about 75 characters per line). Paragraphs on a `"full"`-width page with no width set produce lines that stretch the entire screen: technically valid, but noticeably harder to read. Reserve full width for content that actually benefits from the extra room, like a wide table.

In practice this means: pick a page `width` for the *space you need* (e.g. `"full"` for a dashboard with wide tables), then narrow the sections that need it, independently of the page:

```{r, eval = FALSE}
gov_page(
  width = "full",
  gov_main_layout(
    shiny::tags$div(
      class = "govuk-!-width-two-thirds",
      gov_text("Readable body text stays narrow, even on a wide page.")
    ),
    govTable(my_wide_data)
  )
)
```

The example showcase app applies the same idea: its Cookies tab sits in a two-thirds column, so it reads as a normal, narrow text page even though the app itself uses `gov_page(width = "full")`.

---

## Page-level components

These components form the outer frame of every page, inside `gov_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]   |
+-------------------------------------------------------+
|  phase_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"
)
```

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

The `feedback_url` argument is the recommended shortcut for the standard GOV.UK feedback wording: pass a URL (or a `mailto:` link for contact-style text) and the text is generated for you:

```{r, eval = FALSE}
phase_banner(
  inputId = "phase-banner",
  type = "Beta",
  feedback_url = "https://example.com/feedback"
)
```

For custom wording, use `label` instead (exactly one of `label` or `feedback_url` should be supplied):

```{r, eval = FALSE}
phase_banner(
  inputId = "phase-banner",
  type = "Beta",
  label = shiny::tagList(
    "This is a new service \u2014 your ",
    shiny::tags$a(class = "govuk-link", href = "#", "feedback"),
    " will help us to improve it."
  )
)
```

`label` also accepts a plain character string or raw HTML (e.g. `"...<a class=\"govuk-link\" href=\"#\">feedback</a>..."`); the `tagList()` form above is preferred for readability and to avoid hand-written HTML.

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

```{r, eval = FALSE}
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.

---

## Laying out content

Content goes straight into `gov_main_layout()`. Items stack one below the other, so a page that is just a heading, some text and a table needs no extra layout functions:

```{r, eval = FALSE}
gov_main_layout(
  heading_text("Page title", size = "l"),
  gov_text("Some introductory text."),
  govTable(my_data)
)
```

### A simple text page

For a page that is mostly text, use the standard page width and keep the text to two-thirds of it, as the [GOV.UK layout guidance](https://design-system.service.gov.uk/styles/layout/) recommends for readable line lengths. GOV.UK's `govuk-!-width-two-thirds` class does this without the grid, and goes back to full width on small screens:

```{r, eval = FALSE}
gov_page(
  width = "standard",
  gov_main_layout(
    shiny::tags$div(
      class = "govuk-!-width-two-thirds",
      heading_text("Cookies", size = "l"),
      gov_text("This page explains how we use cookies.")
    )
  )
)
```

### Columns with `gov_row()` and `gov_box()`

Reach for the grid only when you need **side-by-side columns**, or a section that should sit in a grid column of a set width. It's a two-function system: `gov_row()` holds the columns, and `gov_box()` is each column.

```
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
    )
  )
)
```

A single full-width box adds nothing over putting the content directly in `gov_main_layout()`, so leave the grid out in that case.

### `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](headings-and-text.html) vignette.

---

## `gov_layout()` (deprecated)

`gov_layout()` is deprecated as of shinyGovstyle 0.3.0 and will be removed in shinyGovstyle 1.0.0. Use `gov_main_layout()` with `gov_row()` and `gov_box()` instead; the deprecation warning shows how each argument maps across.

---

## 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 and 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, the link text is used to make the inputIDs instead.

Either way, every inputID is cleaned up by lowercasing it and replacing non-alphanumeric characters with underscores, so `"Detailed data"` becomes `detailed_data` and `"Nav-Summary"` becomes `nav_summary`. Each link must end up with a unique inputID. `service_navigation_server()`, `navigate_to()`, and `update_service_navigation()` clean up the ids you pass them in the same way, so you can use either form there.

Put `service_navigation()` in your app's top-level UI and wire it up from the top-level server function, not inside a Shiny module. Because of the clean-up, an id wrapped in `ns()` (e.g. `NS("mod", "summary")`) becomes `mod_summary`, which no longer matches the module's namespaced input.

### Wiring navigation to panels

Use a hidden tab panel for the content area. `service_navigation_server()` then wires every nav link to its panel in a single call — no per-link `observeEvent` boilerplate:

```{r, eval = FALSE}
# ui.R — shiny tabsetPanel
shiny::tabsetPanel(
  type = "hidden",
  id = "main_panels",
  shiny::tabPanel("Summary",       value = "summary",       "Content"),
  shiny::tabPanel("Detailed data", value = "detailed_data", "Content"),
  shiny::tabPanel("User guide",    value = "user_guide",    "Content")
)

# server.R — auto-wire the nav links to the panels
shinyGovstyle::service_navigation_server(
  session,
  tabset_id = "main_panels",
  link_to_panel = c(
    nav_summary  = "summary",
    nav_detail   = "detailed_data",
    nav_guide    = "user_guide"
  )
)
```

`link_to_panel` is a named character vector — names are nav link inputIds, values are panel values. If your inputIds already match your panel values, pass an unnamed vector instead.

`bslib::navset_hidden()` also responds to `updateTabsetPanel()`, so the same call works whether your tab panel is shiny or bslib.

For navigation that can't be wired declaratively — next / back buttons, modal links, footer shortcuts — use `navigate_to()` to combine the panel switch and the nav-active update in one call:

```{r, eval = FALSE}
# server.R — programmatic navigation in one call
shiny::observeEvent(input$next_btn, {
  shinyGovstyle::navigate_to(
    session, "main_panels",
    inputId = "nav_detail", panel = "detailed_data"
  )
})
```

`navigate_to()` defaults to using the nav link `inputId` as the target panel value. Here `nav_detail`'s panel value is `detailed_data` (per the `link_to_panel` mapping above), so pass `panel` explicitly whenever the inputId and panel value differ. See `?navigate_to` for full details.

### Keeping the page title in sync

`service_navigation()` syncs the browser tab title with the active nav link by default, both for link clicks and for programmatic navigation via `update_service_navigation()`. Pass `page_title_suffix` to get the recommended `"<page> | <service>"` format, or `auto_page_title = FALSE` to opt out. For pages where the heading differs from the nav label (or for pages not in the nav), call `update_page_title()` explicitly. See the [Page titles section of the Headings and text vignette](headings-and-text.html#page-titles) for the full rationale and examples.

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

Footer-only pages also fall outside the `service_navigation()` auto page-title sync — there is no nav link for the JavaScript binding to read. Call `update_page_title()` so the browser tab title reflects the new page:

```{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")
  shinyGovstyle::update_page_title(
    session,
    page_title = "Accessibility statement",
    service_name = "My dashboard"
  )
})
```

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

```{r, eval = FALSE}
library(shiny)
library(shinyGovstyle)

ui <- gov_page(
  title = "Summary | My dashboard",
  skip_to_main(),
  header(
    org_name = "My department",
    service_name = "My dashboard"
  ),
  service_navigation(
    c(
      "Summary"  = "nav_summary",
      "About"    = "nav_about"
    ),
    page_title_suffix = "My dashboard"
  ),
  phase_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",
        # Text-only page: two-thirds keeps it readable even though the
        # app itself uses gov_page(width = "full"). See "Content width
        # vs page width" above.
        shiny::tags$div(
          class = "govuk-!-width-two-thirds",
          heading_text("About this dashboard", size = "l"),
          gov_text("This page describes the dashboard.")
        )
      ),

      shiny::tabPanel(
        "Accessibility statement", value = "accessibility_panel",
        shiny::tags$div(
          class = "govuk-!-width-two-thirds",
          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) {
  service_navigation_server(
    session,
    tabset_id = "main_panels",
    link_to_panel = c("nav_summary", "nav_about")
  )

  shiny::observeEvent(input$accessibility_footer_link, {
    shiny::updateTabsetPanel(session, "main_panels",
                             selected = "accessibility_panel")
    update_page_title(
      session,
      page_title = "Accessibility statement",
      service_name = "My dashboard"
    )
  })
}

shiny::shinyApp(ui, server)
```
