
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
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.
- 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 withshinyApp(). - 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.
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:
- Data — one or more datasets, wrapped so teal can track and filter them.
- Modules — a list of the analysis views you want in the app.
- 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.
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 |
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:
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.clinicalgives 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.
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.
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
- teal package documentation — the framework:
init(),teal_data(), the filter panel, and how to write your own module. - teal on CRAN — “Exploratory Web Apps for Analyzing Clinical Trials Data.”
- teal.modules.clinical documentation — the pre-built clinical modules (
tm_t_summary,tm_g_km,tm_t_events,tm_g_forest_tte,tm_a_mmrm, and more). - teal.modules.clinical on CRAN — the clinical modules package.
- pharmaverse — the ecosystem of open-source R packages for clinical reporting that teal and teal.modules.clinical belong to.
- Basic teal demo app — a public teal instance showing the app shell on the
iris/mtcarsexample data.
Reuse
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}
}