Interactive Clinical Review with teal: Build Clinical Trial Data Apps in R

Assemble an interactive review app for clinical-trial data from pre-built modules instead of hand-writing Shiny — the teal mental model (data + modules + a shared filter panel), the init() code, and teal.modules.clinical

Pharma & Clinical

teal is the Roche/NEST framework for building interactive review and exploration apps for clinical-trial data. This lesson explains what teal is, its mental model — data plus a list of modules plus a shared filter panel, assembled with init() — and how teal.modules.clinical gives you pre-built clinical outputs (demographics, Kaplan-Meier, adverse events, forest, MMRM). It walks the code that assembles an app, shows what a running app looks like with an architecture schematic and a live demo, and places teal in the clinical-review workflow versus writing a bespoke Shiny app by hand.

Published

July 1, 2026

Modified

July 7, 2026

TipKey takeaways
  • teal lets you assemble a clinical review app, not hand-write one. teal (lowercase — the R package) is a Shiny-based framework from Roche’s NEST (a suite of R packages for clinical reporting) project — part of pharmaverse, the open-source clinical-reporting R ecosystem — for building interactive apps that explore clinical-trial data.
  • Every teal app is three things: data + modules + a filter panel. You give init() your datasets, a list of modules (self-contained analysis views), and it wires up a shared filter panel — the sidebar that subsets every module at once — then you launch it with shinyApp().
  • teal.modules.clinical ships the standard clinical outputs pre-built. Demographics (tm_t_summary), Kaplan-Meier (tm_g_km), adverse events (tm_t_events), forest plots (tm_g_forest_tte), MMRM (tm_a_mmrm) — you configure them, you don’t code them from scratch.
  • A running teal app gives you reproducibility for free. Every view has a Show R code button that emits the exact R that produced it — the review app is auditable, not a black box.
  • teal complements your static tables, listings, and figures (TLFs). The submission outputs stay fixed; teal is where a medical monitor explores the data interactively before and around those outputs.

Introduction

You have built the ADaM (Analysis Data Model) datasets for a study, and now a medical monitor wants to look around in them: subset to the treatment arm, see the Kaplan-Meier curve for that subgroup, pull up the adverse-event table, filter to patients over 65, and check the demographics again. Every one of those is a reasonable request, and every one of them is a small, bespoke Shiny app if you build it by hand — a new UI, a new reactive server, a new filter, for each analyst, for each study.

teal exists so you don’t do that. teal is a Shiny-based framework — its CRAN description is “Exploratory Web Apps for Analyzing Clinical Trials Data” — open-sourced by F. Hoffmann-La Roche / Genentech as the NEST project and now part of pharmaverse. Instead of writing raw Shiny, you assemble an app from pre-built pieces: hand it your data, pick the analysis modules you want, and teal gives you a full application with a shared filter panel and reproducible output.

This lesson explains what teal is, its mental model, the code that assembles an app with teal.modules.clinical, what a running app looks like, and where it fits the clinical-review workflow.

A schematic of the teal application shell, drawn as a browser window with four labeled regions. Across the top, a dark navy toolbar labelled 'teal app' carries three chips on the right: a 'Dataset ADSL' chooser, a 'Show R code' button, and an '+ Add to Report' button. Down the left side, a light-azure 'Filter Data panel' contains an 'Active Data Summary' box reading 'ADSL  95 / 306 rows' and two stacked filter cards, 'ARM' and 'SEX' — the shared sidebar that subsets every module at once. The large main pane on the right has a row of three module tabs, 'Demographics', 'Kaplan-Meier', and 'Adverse Events', above an output area labelled 'Module output — reactive to the filter panel'. The visual point is that a teal app is data plus a list of modules plus one shared filter panel: filtering on the left updates every module tab, and Show R code makes each view reproducible.

What teal is

teal is a framework for building interactive web applications on top of Shiny (R’s web-application framework), aimed specifically at exploring clinical-trial data. Two ideas make it worth learning:

  • You assemble, you don’t author. A raw Shiny app makes you write the UI layout, the reactive server logic, and the data-filtering yourself. teal gives you all of that as reusable parts — you pick the pieces and connect them. The unit you connect is a module: a self-contained analysis view (one table, one plot, one listing) that knows how to render itself from the app’s data.
  • It is built for clinical data. teal understands CDISC (Clinical Data Interchange Standards Consortium)-shaped data — it can take ADaM datasets keyed by subject, wire up a filter panel that respects that structure, and (via the companion package teal.modules.clinical) drop in modules that produce the standard clinical outputs a reviewer expects.

teal was open-sourced by F. Hoffmann-La Roche / Genentech, built by their Insights Engineering team as the NEST (a suite of R packages for clinical reporting) project, and is part of the pharmaverse family of open-source clinical R packages. Because it is a Roche framework with no equivalent in the base R stack, it is the right tool to reach for here — there is no need to hand-roll a review app when teal exists. Full documentation lives at the teal package site and on CRAN.

The mental model: data + modules + a filter panel

Every teal app is the same three ingredients, combined by one function:

  1. Data — one or more datasets, wrapped so teal can track and filter them.
  2. Modules — a list of the analysis views you want in the app.
  3. A filter panel — the shared sidebar teal builds for you; filtering there subsets every module at once.

You combine them with teal::init(data =, modules =), which returns a Shiny app object, and you launch it with shinyApp(). Here is the smallest possible teal app — the getting-started shape (this is exactly what the public demo below runs):

library(teal)

app <- init(
  data = teal_data(IRIS = iris, MTCARS = mtcars),
  modules = modules(example_module())
)

shinyApp(app$ui, app$server)

Read it top to bottom. teal_data() registers two datasets (IRIS and MTCARS) so teal can filter them and track their provenance. modules() holds the list of views — here a single example_module(), a built-in demo module. init() assembles the application, and shinyApp() runs it. That is a complete, working teal app in five lines: no UI code, no reactive server, no hand-written filter logic. teal supplied all of it.

The pieces you swap to make a real app are the data wrapper and the module list. For clinical data you swap teal_data() for cdisc_data() (which understands CDISC keys), and you swap the demo module for the clinical modules in the next section.

Note

The teal code on this page is illustrative and API-correct, but it is not executed here — teal is a full Shiny application, not something that runs inside a static page. To see it live, install teal and run these snippets in a local R session, or open the demo app in the next section.

teal.modules.clinical: the pre-built clinical modules

The teal.modules.clinical package is a library of ready-made teal modules for standard clinical outputs, built on the same rtables/tern engine that produces submission tables. You don’t write these views — you configure them. The most common ones:

Module What it produces
tm_t_summary Summary / demographics table (a Table 1)
tm_g_km Kaplan-Meier (KM) survival plot
tm_t_events Adverse-events (AE) table — counts of adverse events
tm_t_events_summary Adverse-events summary table
tm_g_forest_tte Forest plot for a time-to-event endpoint
tm_a_mmrm MMRM (mixed model for repeated measures) analysis
tm_t_tte Time-to-event table (survival estimates)
tm_t_coxreg Cox regression table
Important

tm_t_events is the adverse-events table, not a time-to-event module. The time-to-event table is tm_t_tte and the Kaplan-Meier plot is tm_g_km. The _events in the name means adverse events — keep the two straight when you pick a module.

Here is a clinical app: one module, a demographics table, built on a real example ADaM subject-level dataset (tmc_ex_adsl, shipped with the package). Note the shape of the configuration — clinical modules take choices_selected() specs (a set of allowed columns plus the default selection), not bare column names, so the reviewer can change the variables from inside the running app:

library(teal.modules.clinical)

ADSL <- tmc_ex_adsl   # an example subject-level ADaM dataset shipped with the package

app <- init(
  data = cdisc_data(
    ADSL = ADSL,
    code = "ADSL <- tmc_ex_adsl"   # the code that recreates the data — teal reports it
  ),
  modules = modules(
    tm_t_summary(
      label    = "Demographic Table",
      dataname = "ADSL",
      arm_var  = choices_selected(choices = c("ARM", "ARMCD"), selected = "ARM"),
      summarize_vars = choices_selected(
        choices  = c("SEX", "RACE", "BMRKR2", "EOSDY", "DCSREAS", "AGE"),
        selected = c("SEX", "RACE")
      )
    )
  )
)

shinyApp(app$ui, app$server)

Two things to notice. cdisc_data() replaces teal_data() — it registers the ADaM dataset with its CDISC keys so the filter panel subsets by subject correctly. And the code = argument records how the data was made: teal keeps that in the app’s reproducible trail, so the Show R code button can hand a reviewer the full script that recreates every view.

To grow the app, you add more modules to the modules() list — a tm_g_km() for the Kaplan-Meier curve, a tm_t_events() for the AE table, a tm_g_forest_tte() for a forest plot — each configured with the same choices_selected() pattern. Each becomes a tab in the running app, and all of them respond to the one shared filter panel.

What a running teal app looks like

The schematic at the top of this lesson is the app shell. Walk its four regions, because they are the whole interface:

  • The filter panel (left). teal builds this for you from the data. Its Active Data Summary shows how many rows survive the current filters versus the total (e.g. ADSL 95 / 306), and below it sits one filter card per variable you expose — drag a slider or tick a category and every module updates at once. This shared filtering is the feature you’d otherwise write by hand in raw Shiny.
  • The module tabs (main pane). Each module in your modules() list is a tab — Demographics, Kaplan-Meier, Adverse Events. Click a tab to see that analysis, rendered from the currently-filtered data.
  • The dataset chooser + variable selectors. Because you passed choices_selected() specs, the reviewer can change the treatment variable or the summarized variables from inside the app — no code, no re-run.
  • Show R code / Add to Report. Every view carries a Show R code button that emits the exact R that produced it (the reproducibility guarantee), and an + Add to Report button that collects views into a downloadable report via teal.reporter.

The fastest way to feel the model is to open a live one:

NoteLaunch a live teal app

Open the basic teal demo ▸ — a public teal instance running the getting-started app on the iris and mtcars example data. It is a basic demo, not a clinical app, but it shows the exact shell described here: the filter panel on the left, the module tabs in the main pane, the dataset chooser, and the Show R code button. Filter on the left and watch the module update; click Show R code to see the reproducible script teal generated.

Where teal fits the clinical-review workflow

Your submission outputs — the tables, listings, and figures (TLFs) — are fixed: a defined set of programmed, validated, versioned outputs. teal is the interactive layer around them. A medical monitor or statistician uses a teal app to explore — slice by subgroup, check a curve for a filtered population, look up a patient’s adverse events — before, during, and after the formal outputs are produced. The two are complementary: the static TLFs are the record; teal is the sandbox.

Against hand-written Shiny, teal earns its place on three counts:

  • Pre-built modules. teal.modules.clinical gives you demographics, KM, AE, forest, and MMRM views out of the box — you configure, you don’t code them.
  • A shared filter panel. teal generates the data-filtering sidebar and applies it across every module. In raw Shiny you would write that reactivity yourself, per app.
  • Reproducibility built in. Show R code makes every view auditable — essential when a regulated reviewer needs to trust what an app is showing them.

The trade-off is that a teal app is a running R/Shiny session, not a static artifact like the rest of this pillar’s outputs. It needs a Shiny server (or a local R session) to run — which is exactly why this lesson ships the code as illustrative snippets plus a hosted demo, rather than an embedded live app.

🟢 With an AI agent

Ask Prova “Assemble a teal app for my ADaM data with a demographics table and a Kaplan-Meier module — show me the init() call, the cdisc_data() wrapper, and the tm_t_summary and tm_g_km configuration.” — it answers grounded in this pillar’s lessons, with API-correct teal code you can run in a local R session. The runtime is the judge. Ask Prova →

Common issues

Trying to hand-write Shiny inside teal. teal is an assembly framework — you pass a list of modules to init(), you don’t write ui/server functions the way you would for a raw Shiny app. If you find yourself writing renderPlot() and reactive() blocks, step back: reach for an existing module from teal.modules.clinical (or the general teal.modules.general), or wrap your custom logic in a module with teal::module() — don’t bypass the framework.

Mislabeling tm_t_events as time-to-event. The _events in tm_t_events means adverse events, and the module builds an AE (adverse-events) table. The time-to-event table is tm_t_tte, and the Kaplan-Meier survival plot is tm_g_km. Picking tm_t_events when you wanted a survival analysis is a common first mistake — check the module reference before you wire it in.

Expecting a teal app to be static like the rest of the package. Unlike an rtables table or a survminer figure, a teal app is a live Shiny application: it needs a running R session or a Shiny server. You can’t embed it in a static report or a crawlable page — you deploy it (shinyapps.io, Posit Connect, or an internal Shiny server) or run it locally. Plan for a hosting target, not a file.

Frequently asked questions

teal is an R package from Roche’s NEST project (part of pharmaverse) for building interactive web applications that explore clinical-trial data. Its CRAN title is “Exploratory Web Apps for Analyzing Clinical Trials Data.” You build a teal app by giving init() your data and a list of modules (pre-built analysis views); teal supplies the shared filter panel, the UI, and a reproducible Show R code trail. It is built on Shiny but saves you from writing raw Shiny.

Use teal with teal.modules.clinical. Wrap your ADaM datasets with cdisc_data(), list the clinical modules you want — for example tm_t_summary() for demographics and tm_g_km() for a Kaplan-Meier curve — pass them to init(data =, modules = modules(...)), and launch with shinyApp(app$ui, app$server). Configure each module with choices_selected() so reviewers can change variables from inside the running app. You assemble the app from modules rather than coding the UI and server by hand.

Shiny is the general R web-application framework — you write the UI and the reactive server yourself. teal is a framework built on Shiny for a specific job: exploring clinical-trial data. teal gives you pre-built analysis modules, a shared filter panel, CDISC-aware data handling, and a Show R code reproducibility trail, all assembled by init(). Choose raw Shiny for a fully custom app; choose teal when you want standard clinical review views assembled quickly and reproducibly.

It is the companion R package that ships pre-built clinical teal modules, built on the rtables/tern table engine. It includes demographics (tm_t_summary), Kaplan-Meier (tm_g_km), adverse-events tables (tm_t_events), forest plots (tm_g_forest_tte), MMRM (tm_a_mmrm), time-to-event tables (tm_t_tte), Cox regression (tm_t_coxreg), and more. You add these modules to a teal app’s modules() list and configure them with choices_selected() specs — you don’t program the outputs yourself.

Yes. A teal app is a standard Shiny application object, so it deploys anywhere Shiny does: shinyapps.io, Posit Connect, Shiny Server, or an internal container. init() returns an app with $ui and $server, and shinyApp(app$ui, app$server) runs it — the same object you deploy. Because a teal app needs a live R session, it is hosted and run, not embedded as a static file.

Test your understanding

Starting from the demographics app above, add a Kaplan-Meier (KM) module for a time-to-event dataset. The KM module is tm_g_km(); it needs a dataname pointing at a time-to-event ADaM dataset (an ADTTE-style dataset — one analysis record per subject per endpoint) and an arm_var. Add it to the modules() list so the running app gains a second tab.

Modules go in the modules() list passed to init() — separate them with commas. Register the second dataset in cdisc_data() alongside ADSL, then add tm_g_km(label = ..., dataname = "ADTTE", arm_var = choices_selected(...)) as a second element of modules(). Like every clinical module, tm_g_km() takes choices_selected() specs, not bare column names.

library(teal.modules.clinical)

ADSL  <- tmc_ex_adsl
ADTTE <- tmc_ex_adtte   # an example time-to-event ADaM dataset shipped with the package

app <- init(
  data = cdisc_data(
    ADSL  = ADSL,
    ADTTE = ADTTE,
    code  = "ADSL <- tmc_ex_adsl\nADTTE <- tmc_ex_adtte"
  ),
  modules = modules(
    tm_t_summary(
      label    = "Demographic Table",
      dataname = "ADSL",
      arm_var  = choices_selected(choices = c("ARM", "ARMCD"), selected = "ARM"),
      summarize_vars = choices_selected(
        choices  = c("SEX", "RACE", "AGE"),
        selected = c("SEX", "RACE")
      )
    ),
    tm_g_km(
      label    = "Kaplan-Meier Plot",
      dataname = "ADTTE",
      arm_var  = choices_selected(choices = c("ARM", "ARMCD"), selected = "ARM")
    )
  )
)

shinyApp(app$ui, app$server)

The app now has two tabs — the demographics table and the Kaplan-Meier curve — and both respond to the one shared filter panel. Adding an analysis view means adding one more element to modules(); you never touch a UI or server function.

A. A UI function, a server function, and a reactive() block you write by hand. B. Data (via teal_data()/cdisc_data()), a list of modules, and a shared filter panel — assembled by init(). C. A static .qmd file, an rtables object, and a Define-XML.

B. A teal app is data + modules + a filter panel, combined by init() and launched with shinyApp(). A describes raw Shiny — the code teal writes for you. C describes static submission artifacts, which are a different job: teal is the interactive exploration layer, not a static output.

Conclusion

teal turns “build a review app for this study” from a bespoke Shiny project into an assembly job: hand init() your data, list the modules you want, and teal supplies the shared filter panel, the UI, and a reproducible Show R code trail. With teal.modules.clinical those modules are the standard clinical outputs already built — demographics, Kaplan-Meier, adverse events, forest, MMRM — configured with choices_selected() specs rather than coded from scratch. It sits around your fixed tables, listings, and figures as the interactive exploration layer, and because it is a Roche/pharmaverse framework with no base-stack equivalent, it is the right tool to reach for when a reviewer needs to explore clinical-trial data.

Note

The teal code on this page is illustrative and API-correct — it is not executed in this static lesson, because a teal app is a full Shiny application rather than a runnable code chunk. To see it work, install teal and teal.modules.clinical and run these snippets in a local R session, or open the live demo above.

References

Reuse

Citation

BibTeX citation:
@online{2026,
  author = {},
  title = {Interactive {Clinical} {Review} with Teal: {Build} {Clinical}
    {Trial} {Data} {Apps} in {R}},
  date = {2026-07-01},
  url = {https://www.datanovia.com/learn/pharma-clinical/06-teal-review-apps/interactive-clinical-review-teal},
  langid = {en}
}
For attribution, please cite this work as:
“Interactive Clinical Review with Teal: Build Clinical Trial Data Apps in R.” 2026. July 1. https://www.datanovia.com/learn/pharma-clinical/06-teal-review-apps/interactive-clinical-review-teal.