Cookies and analytics

Overview

Public services need to tell users about cookies and, where they set non-essential cookies (such as analytics), let users accept or reject them. shinyGovstyle gives you the building blocks for this:

  • cookieBanner(): the GOV.UK styled banner that appears at the top of the page, with accept / reject / hide message states wired up for you.
  • radio_button_Input(): a GOV.UK styled radio group, useful for a “change your cookie settings” control on a dedicated cookies page.
  • update_radio_button_Input(): the server-side companion that lets you change the selected option (or the choices, or the label) of a radio group from your server code.

shinyGovstyle is deliberately agnostic about analytics. It does not set, read, or send any analytics cookies itself. It gives you the consent UI and the reactive plumbing; you decide what to do with the user’s choice. The Extending for analytics section points to a worked example from DfE using Google Analytics.

A cookies settings page

A common pattern is a dedicated cookies page with a radio group that lets the user change their analytics choice, a button to save it, and a confirmation message. The example app that ships with this package builds exactly this as a Shiny module (see inst/example_app/modules/mod_cookies.R), and the rest of this section walks through that pattern.

The module UI

Build the control with radio_button_Input(), add a save button, and leave a uiOutput() placeholder for the confirmation message. Use shiny::NS(id, ...) so the ids are namespaced to the module:

mod_cookies_ui <- function(id) {
  shiny::tagList(
    shinyGovstyle::heading_text("Cookies", size = "l", level = 1),
    shinyGovstyle::gov_text(
      "We use analytics cookies to measure how the service is used so we",
      "can improve it."
    ),
    shiny::uiOutput(shiny::NS(id, "cookie_saved")),
    shinyGovstyle::radio_button_Input(
      inputId = shiny::NS(id, "cookies_analytics"),
      label = "Do you want to accept analytics cookies?",
      choices = c("Yes" = "yes", "No" = "no"),
      selected = "no",
      inline = TRUE
    ),
    shinyGovstyle::button_Input(
      shiny::NS(id, "save_cookies"),
      "Save cookie settings"
    )
  )
}

The module server

The server takes the banner choices and the active tab as reactive arguments. The banner choices keep the radio in sync via update_radio_button_Input(), saving shows a success noti_banner(), and leaving the tab clears any stale message:

mod_cookies_server <- function(id, cookie_accept, cookie_reject, active_tab) {
  shiny::moduleServer(id, function(input, output, session) {
    # Banner choices drive the settings radio without the user touching it.
    # ignoreInit = TRUE stops these firing on startup.
    shiny::observeEvent(
      cookie_accept(),
      shinyGovstyle::update_radio_button_Input(
        session,
        inputId = "cookies_analytics",
        selected = "yes"
      ),
      ignoreInit = TRUE
    )

    shiny::observeEvent(
      cookie_reject(),
      shinyGovstyle::update_radio_button_Input(
        session,
        inputId = "cookies_analytics",
        selected = "no"
      ),
      ignoreInit = TRUE
    )

    # Saving shows a success banner reflecting the live radio value.
    saved_choice <- shiny::reactiveVal(NULL)

    shiny::observeEvent(input$save_cookies, {
      saved_choice(
        if (identical(input$cookies_analytics, "yes")) "accept" else "reject"
      )
    })

    # Clear the success banner when the user leaves the cookies tab, so a
    # stale message isn't waiting for them when they come back.
    shiny::observeEvent(
      active_tab(),
      {
        if (!identical(active_tab(), "panel-cookies")) {
          saved_choice(NULL)
        }
      },
      ignoreInit = TRUE
    )

    output$cookie_saved <- shiny::renderUI({
      choice <- saved_choice()
      if (is.null(choice)) {
        return(NULL)
      }
      shinyGovstyle::noti_banner(
        session$ns("saved_banner"),
        title_txt = "Success",
        body_txt = paste0(
          "You've updated your cookie preferences. You chose to ",
          choice,
          " analytics cookies."
        ),
        type = "success"
      )
    })
  })
}

update_radio_button_Input() mirrors shiny::updateRadioButtons(): only the arguments you pass are sent to the client, and the inputId is namespaced automatically when you call it inside a Shiny module. That is why the module server can use the plain id "cookies_analytics" even though the rendered input is namespaced.

Wiring it together at the top level

The banner observers and the cookieLink navigation stay at the top level (because the banner ids are global), and the banner inputs are passed into the module as reactives:

server <- function(input, output, session) {
  mod_cookies_server(
    "cookies",
    cookie_accept = shiny::reactive(input$cookieAccept),
    cookie_reject = shiny::reactive(input$cookieReject),
    active_tab = shiny::reactive(input[["tab-container"]])
  )

  # Banner show/hide handlers (as in the banner example above)
  observeEvent(input$cookieAccept, {
    shinyjs::show(id = "cookieAcceptDiv")
    shinyjs::hide(id = "cookieMain")
  })
  observeEvent(input$cookieReject, {
    shinyjs::show(id = "cookieRejectDiv")
    shinyjs::hide(id = "cookieMain")
  })
  observeEvent(input$hideAccept, shinyjs::toggle(id = "cookieDiv"))
  observeEvent(input$hideReject, shinyjs::toggle(id = "cookieDiv"))

  # The "View cookies" link in the banner opens the cookies tab
  observeEvent(input$cookieLink, {
    updateTabsetPanel(session, "tab-container", selected = "panel-cookies")
  })
}

You can run this end to end with shiny::runApp(system.file("example_app", package = "shinyGovstyle")) and open the Cookies tab to see the radio stay in sync with the banner and the save confirmation appear.

Extending for analytics

shinyGovstyle stops at the consent UI on purpose. To actually act on the user’s choice (for example to load Google Analytics only after consent, store the decision in a cookie, and respond to it across sessions), you layer your own logic (or another package’s) on top of these components.

dfeshiny is a worked example of exactly this. It provides a cookies module (cookies_banner_ui() / cookies_banner_server() and cookies_panel_ui()) that builds on shinyGovstyle’s components and adds Google Analytics consent handling: reading and writing the consent cookie, and toggling analytics on or off based on the user’s choice. If you need a ready-made analytics consent flow rather than the building blocks, that module is a good place to start, and a good template if you are writing your own.