# Getting Started with a11yShiny

## Overview

`a11yShiny` provides accessible drop-in replacements for popular Shiny UI functions. Every component enforces ARIA attributes, visible labels, and semantic HTML according to BITV 2.0 (the German implementation of WCAG 2.1).

The package covers four areas:

1. Page layout – `a11y_fluidPage`, `a11y_fluidRow`, `a11y_column`
2. Input controls – `a11y_actionButton`, `a11y_selectInput`, `a11y_numericInput`, `a11y_textInput`, `a11y_radioButtons`, `a11y_dateInput`
3. Composite widgets – `a11y_textButtonGroup`, `a11y_textInputsGroup`, `a11y_highContrastButton`
4. Data display – `a11y_renderDataTable`, `a11y_ggplot2_line`, `a11y_ggplot2_bar`

## Installation

    # Install from source
    devtools::install()

## Accessible Page Layout

`a11y_fluidPage` wraps `shiny::fluidPage` and enforces two accessibility essentials: a page title and a language attribute on the `<html>` element. It also creates proper landmark regions (`<main>`, `<header>`, `<nav>`, `<footer>`, `<aside>`).

    library(shiny)
    library(a11yShiny)

    ui <- a11y_fluidPage(
      title = "My Accessible App",
      lang = "en",
      header = tags$header(tags$h1("Dashboard")),
      nav = tags$nav(tags$a(href = "#", "Home")),
      footer = tags$footer("Footer content"),

      # Everything passed via ... goes into <main>
      a11y_fluidRow(
        a11y_column(6, tags$p("Left column")),
        a11y_column(6, tags$p("Right column"))
      )
    )

`a11y_fluidRow` validates that all direct children are `a11y_column` elements and that their widths (plus offsets) sum to 12, preventing broken grid layouts.

## Input Controls

All input wrappers share a common set of accessibility features:

- Visible label is required. Omitting it causes an error.
- `describedby_text` creates a screen-reader-only description linked via `aria-describedby`.
- `heading_level` optionally marks the label as a heading (`role="heading"` + `aria-level`), useful for sectioned forms.

### Action Button

    # Button with visible label
    a11y_actionButton("go", label = "Submit")

    # Icon-only button -- aria_label is required when label is missing
    a11y_actionButton(
      "search",
      icon       = icon("search"),
      aria_label = "Search"
    )

### Select Input

    a11y_selectInput(
      inputId = "dataset",
      label = "Choose a dataset",
      choices = c("iris", "mtcars", "faithful"),
      describedby_text = "Select one of the built-in R datasets."
    )

### Numeric Input

    a11y_numericInput(
      inputId = "n",
      label   = "Number of observations",
      value   = 100,
      min     = 1,
      max     = 1000,
      step    = 10
    )

### Text Input

    a11y_textInput(
      inputId     = "name",
      label       = "Your name",
      placeholder = "e.g. Jane Doe"
    )

### Radio Buttons

    a11y_radioButtons(
      inputId = "color",
      label   = "Favourite colour",
      choices = c("Red", "Green", "Blue")
    )

### Date Input

    a11y_dateInput(
      inputId  = "start",
      label    = "Start date",
      value    = Sys.Date(),
      language = "en"
    )

## Composite Widgets

### Text + Button Group

`a11y_textButtonGroup` pairs a text input with an action button. The button automatically receives `aria-controls` pointing to the text input.

    a11y_textButtonGroup(
      textId             = "query",
      buttonId           = "run_query",
      label              = "Search term",
      placeholder        = "Enter a keyword",
      button_icon        = icon("search"),
      button_aria_label  = "Run search",
      layout             = "inline"
    )

### Grouped Text Inputs

`a11y_textInputsGroup` wraps multiple related text fields in a `<fieldset>` / `<legend>` structure, which screen readers announce as a single group.

    a11y_textInputsGroup(
      groupId = "address",
      legend = "Postal address",
      inputs = list(
        list(inputId = "street", label = "Street"),
        list(inputId = "city", label = "City"),
        list(inputId = "zip", label = "ZIP code", width = "120px")
      )
    )

### High-Contrast Toggle

`a11y_highContrastButton` adds a button that toggles a `.high-contrast` CSS class on the `<body>` element and manages its own `aria-pressed` state.

    a11y_highContrastButton(
      inputId    = "contrast",
      label      = "High Contrast",
      aria_label = "Toggle high-contrast mode"
    )

## Accessible Data Tables

`a11y_renderDataTable` wraps `DT::renderDataTable`. It enables the KeyTable extension for keyboard navigation by default and warns when inaccessible features (copy/print/pdf buttons, column filters) are used.

    server <- function(input, output, session) {
      output$table <- a11y_renderDataTable(
        expr = iris,
        lang = "en"
      )
    }

German translations are built in – set `lang = "de"` and the table interface is fully translated. For other languages, pass a custom list via `dt_language`.

## Accessible Charts (ggplot2)

Both chart helpers use a high-contrast, WCAG-compliant colour palette by default. The line chart also distinguishes series by marker shape, so the information is not conveyed by colour alone.

### Line Chart

    df <- data.frame(
      year  = rep(2020:2024, 2),
      value = c(10, 14, 13, 17, 20, 8, 9, 11, 12, 15),
      group = rep(c("A", "B"), each = 5)
    )

    a11y_ggplot2_line(
      data  = df,
      x     = year,
      y     = value,
      group = group,
      title = "Trend by Group",
      x     = "Year",
      y     = "Value"
    )

### Bar Chart

    df <- data.frame(
      category = c("Alpha", "Beta", "Gamma"),
      count    = c(23, 17, 31)
    )

    a11y_ggplot2_bar(
      data  = df,
      x     = category,
      y     = count,
      title = "Counts by Category"
    )

## Key Design Decisions

- Fail fast. Missing labels or invalid configurations stop with a clear error message instead of producing inaccessible output silently.
- Drop-in API. Every function mirrors the signature of its Shiny counterpart and adds only the extra parameters needed for accessibility.
- No runtime overhead. ARIA attributes are set at render time in R; the shipped JavaScript is minimal (contrast toggle, focus management).

## Demo App Walkthrough

The package ships with a complete Shiny application that places standard Shiny components side-by-side with their accessible `a11y_*` counterparts. You can launch it with:

    shiny::runApp(system.file("examples/demo", package = "a11yShiny"))

The rest of this section walks through the demo and highlights what the standard component gets wrong and how the `a11y_*` wrapper fixes it.

### Page structure and landmarks

The demo wraps its entire UI in `a11y_fluidPage`, which enforces a page `title`, a `lang` attribute on the `<html>` element, and semantic landmark regions:

    ui <- a11y_fluidPage(
      lang = "de",
      title = "Demo",
      header = tags$header(
        class = "page-header",
        tags$h1("Demo Dashboard"),
        tags$h2("A dashboard with a11yShiny components")
      ),
      aside = tags$aside(
        class = "help-panel",
        tags$h2("Help"),
        tags$p("Supplementary information goes here.")
      ),
      footer = tags$footer(tags$p("Copyright 2025")),

      # Everything below becomes <main>
      a11y_fluidRow(
        a11y_column(8, tags$p("Main content")),
        a11y_column(4, a11y_highContrastButton())
      )
    )

Screen readers use these landmarks (`<header>`, `<main>`, `<aside>`, `<footer>`) for quick navigation. A standard `fluidPage` produces none of them.

### Input controls: standard vs. accessible

The demo shows each input type twice – first the standard Shiny version, then the accessible wrapper. The key problems with the standard versions and how `a11yShiny` resolves them are outlined below.

#### Select input

Standard (inaccessible): The label is set to `NULL`, so screen readers cannot announce what the control is for.

    # Standard -- no visible label, no ARIA description
    selectInput("n_breaks", label = NULL, choices = c(10, 20, 35, 50))

Accessible: A visible label is required (the function errors otherwise). Optional `heading_level` promotes the label to a heading for form sections, and `describedby_text` adds a screen-reader-only description.

    a11y_selectInput(
      inputId = "n_breaks_1",
      label = "Number of bins",
      choices = c(10, 20, 35, 50),
      selected = 20,
      heading_level = 3
    )

    a11y_selectInput(
      inputId          = "n_breaks_2",
      label            = "Number of bins",
      choices          = c(10, 20, 35, 50),
      selected         = 20,
      describedby_text = "Select the number of histogram bins."
    )

#### Numeric input

Standard (inaccessible): Again `label = NULL`, and no ARIA role or value attributes on the `<input>` element.

    numericInput("seed", label = NULL, value = 123)

Accessible: Adds `role="spinbutton"`, `aria-valuemin`, `aria-valuemax`, and `aria-valuenow` on the control. The `describedby` parameter can link to an existing help-text element by its ID instead of creating a new one.

    # With auto-generated sr-only description
    a11y_numericInput(
      inputId          = "seed_3",
      label            = "Seed",
      value            = 123,
      heading_level    = 6,
      describedby_text = "Choose the seed for the random number generator."
    )

    # Linking to an existing help-text element
    a11y_numericInput(
      inputId = "seed_1",
      label = "Seed",
      value = 123,
      describedby = "seed_help"
    )

#### Date input

Standard: `dateInput` provides a label, but the datepicker widget has no heading-level annotation and no `aria-describedby` support.

    dateInput("mydate", "Choose a date:")

Accessible: Adds the `language` parameter for locale-aware rendering and `heading_level` to integrate the label into the page’s heading hierarchy.

    a11y_dateInput(
      "mydate_acc",
      "Choose a date:",
      language      = "de",
      heading_level = 2
    )

#### Search bar (text + button composite)

Standard (inaccessible): A `textInput` with `label = NULL` and an `actionButton` with `label = NULL` are placed in a raw `<div>`. There is no ARIA linkage between the two, and neither element has an accessible name.

    div(
      textInput("searchbox",
        label = NULL,
        placeholder = "Enter your query:", width = "100%"
      ),
      actionButton("do_search", label = NULL, icon = icon("search"))
    )

Accessible: `a11y_textButtonGroup` enforces a visible label on the text field, requires `button_aria_label` when the button has no visible text, and automatically wires `aria-controls` from the button to the text input.

    a11y_textButtonGroup(
      textId            = "text-acc",
      buttonId          = "btn-acc",
      label             = "Enter your query:",
      button_icon       = icon("search"),
      button_aria_label = "Search",
      layout            = "inline"
    )

#### Address form (grouped text inputs)

Standard (inaccessible): Four separate `textInput` fields under a plain `<h3>` heading. Screen readers see no relationship between the fields.

    div(h3("Address"))
    textInput("adr_street", "Street and number")
    textInput("adr_postcode", "ZIP code")
    textInput("adr_city", "City")
    textInput("adr_country", "Country")

Accessible: `a11y_textInputsGroup` wraps the fields in a `<fieldset>` / `<legend>` structure with `role="group"` and `aria-labelledby`. The legend can be promoted to a heading via `legend_heading_level`, and a group-level `describedby_text` adds a screen-reader description for the entire group.

    a11y_textInputsGroup(
      groupId = "address_group",
      legend = "Address",
      inputs = list(
        list(inputId = "adr_street_acc", label = "Street and number"),
        list(inputId = "adr_postcode_acc", label = "ZIP code"),
        list(inputId = "adr_city_acc", label = "City"),
        list(inputId = "adr_country_acc", label = "Country")
      ),
      describedby_text = "Please enter your full postal address.",
      legend_heading_level = 3
    )

#### Action buttons

Standard (inaccessible): Icon-only buttons with `label = NULL` produce a `<button>` with no accessible name at all.

    # Icon-only button -- no accessible name
    actionButton("refresh", label = NULL, icon = icon("refresh"))

    # Empty button -- no label, no icon, no aria-label
    actionButton("refresh_0", label = NULL)

Accessible: `a11y_actionButton` requires either a visible `label` or an `aria_label`. Both can be combined to provide a richer screen-reader announcement (e.g., a short visible label plus a longer `aria_label`).

    # Visible label + icon
    a11y_actionButton("refresh_1",
      label = "Refresh",
      icon = icon("refresh")
    )

    # Icon-only with aria_label
    a11y_actionButton("refresh_2",
      icon = icon("refresh"),
      aria_label = "Click to refresh"
    )

    # Both visible label and aria_label
    a11y_actionButton("refresh_3",
      label = "Refresh",
      aria_label = "Click to refresh data"
    )

### Charts: colour alone is not enough

The demo renders the same data as both a standard `ggplot2` chart and an accessible version. The standard chart uses a manually chosen palette (`#A8A8A8`, `#FEF843`, `#6E787F`) whose contrast ratios are too low, particularly against a white background. The colours are also the only distinguishing feature between series.

    # Standard -- insufficient contrast, no shape distinction
    ggplot(df, aes(x = time, y = value, color = group)) +
      geom_line() +
      geom_point() +
      scale_color_manual(
        values = c("A" = "#A8A8A8", "B" = "#FEF843", "C" = "#6E787F")
      ) +
      theme_minimal()

Accessible: `a11y_ggplot2_line` applies a WCAG-compliant palette (minimum 3:1 contrast ratio to the background) and maps each series to a different marker shape, so colour is never the sole information carrier. The result is a standard `ggplot2` object that can be extended with additional layers.

    p <- a11y_ggplot2_line(
      data = df,
      x = time,
      y = value,
      group = group,
      legend_title = "Group",
      title = "Simulated time series by group"
    )

    # The result is a regular ggplot2 object -- add layers as usual
    p <- p +
      ggplot2::geom_hline(yintercept = 0, linetype = "dashed") +
      ggplot2::labs(x = "Date", y = "Measurement")

The same principle applies to `a11y_ggplot2_bar`, which adds black bar outlines so bars remain distinguishable even without colour perception.

### Data tables: keyboard navigation and language

The demo shows a standard `DT::datatable` with column filters and copy/print/PDF buttons alongside the accessible version.

Standard (problems): Column filters (especially the numeric range slider) are not keyboard-accessible. The copy, print, and PDF buttons open modal dialogs or browser tabs that are difficult for screen-reader and keyboard users to operate.

    output$tbl <- DT::renderDataTable({
      DT::datatable(
        head(iris[, 1:4], 10),
        filter = "top", selection = "none",
        options = list(
          pageLength = 5,
          dom = "Bfrtip",
          buttons = c("excel", "copy", "csv", "pdf", "print")
        )
      )
    })

Accessible: `a11y_renderDataTable` enables the KeyTable extension by default for full keyboard navigation between cells. It warns at render time when inaccessible features are requested (column filters, copy/print/PDF buttons). Setting `lang = "de"` activates built-in German translations for all interface strings.

    output$tbl_acc <- a11y_renderDataTable(
      expr = head(iris[, 1:4], 10),
      lang = "de",
      selection = "none",
      extensions = c("Buttons"),
      options = list(
        pageLength = 5,
        dom        = "Bfrtip",
        buttons    = c("excel", "csv")
      )
    )

Wrap the output in a `<div class="a11y-dt">` on the UI side to apply the accessible focus and contrast styles shipped with the package:

    div(class = "a11y-dt", dataTableOutput("tbl_acc"))

### Running the demo yourself

Use the demo to compare both versions with accessibility testing tools: inspect the rendered HTML in your browser’s developer tools, navigate with the keyboard only (Tab / Shift-Tab / Arrow keys), or test with a screen reader such as NVDA, JAWS, or VoiceOver.