Teal custom modules

Teal

Notes on how to create custom modules

Author

Chi Zhang

Published

August 26, 2025

The technology underneath is shiny modules.

There are 4 steps:

UI part

Two things to provide: data and variable.

library(teal)

# UI function for the custom histogram module
histogram_module_ui <- function(id) {
  ns <- shiny::NS(id)
  shiny::tagList(
    shiny::selectInput(
      ns("dataset"),
      "Select Dataset",
      choices = c("iris", "mtcars")
    ),
    shiny::selectInput(
      ns("variable"),
      "Select Variable",
      choices = c(names(iris), names(mtcars))
    ),
    shiny::plotOutput(ns("histogram_plot")),
    shiny::verbatimTextOutput(ns("plot_code")) # To display the reactive plot code
  )
}

Server part

Requires teal_data object.

To call the data, use data() - treat it as a function.

# names of data: gives list of datasets inside the teal_data obj
names(data())
# access specific data
data()[[input$dataset]] # might also just use $
# Server function for the custom histogram module with injected variables in within()
histogram_module_server <- function(id, data) {
  
  moduleServer(id, function(input, output, session) {
   
    shiny::observe({
      shiny::updateSelectInput( 
        session,
        "dataset",
        choices = names(data())
      )
    })

    observeEvent(input$dataset, {
      req(input$dataset) # Ensure dataset is selected
      numeric_vars <- names(data()[[input$dataset]])[sapply(data()[[input$dataset]], is.numeric)]
      shiny::updateSelectInput(
        session,  
        "variable", 
        choices = numeric_vars)
    })

    
    
    # Create a reactive `teal_data` object with the histogram plot
    result <- reactive({
      req(input$dataset, input$variable) # Ensure dataset and variable are selected

      # Create a new teal_data object with the histogram plot
      new_data <- within(
        data(),
        {
          my_plot <- hist(
            input_dataset[[input_vars]],
            las = 1,
            main = paste("Histogram of", input_vars),
            xlab = input_vars,
            col = "lightblue",
            border = "black"
          )
        },
        
        input_dataset = as.name(input$dataset),
        input_vars = input$variable
      )
      new_data
    })

    # Render the histogram from the updated teal_data object
    output$histogram_plot <- shiny::renderPlot({
      result()[["my_plot"]] 
    })

    # Reactive expression to get the generated code for the plot
    output$plot_code <- shiny::renderText({
      teal.code::get_code(result())
    })
  })
}
1
Update dataset choices based on available datasets in teal_data. observe is used because it needs to run every time data changes.
2
observeEvent watches the changes in dataset selection. It gets updated when it changes - because different datasets have different variable names.
3
Update variable choices based on selected dataset, only including numeric variables
4
This is a modified version of data(). Injected input$dataset and input$variable into within().
5
This is where the function is positioned
6
Replace input_dataset with input$dataset
7
Replace input_vars with input$variable
8
Access and render the plot stored in new_data
9
Retrieve and display the code for the updated teal_data object

Custom module function

create_histogram_module <- function(label = "Histogram Module") {
  teal::module(
    label = label,
    ui = histogram_module_ui,
    server = histogram_module_server,
    datanames = "all"
  )
}
1
use teal::module() to wrap the UI and server together.
2
the module has access to all datasets in the teal_data object. Datasets with names starting with . are not included

Integration

Finally, embed the module into your app.

# Define datasets in `teal_data`
data_obj <- teal_data(
  iris = iris,
  mtcars = mtcars
)

# Initialize the teal app
app <- init(
  data = data_obj,
  modules = modules(create_histogram_module())
)

# Run the app
if (interactive()) {
  shiny::shinyApp(ui = app$ui, server = app$server)
}
1
call the data object created above
2
call the custom module function created above. Do not forget the ().