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