diff --git a/.Rbuildignore b/.Rbuildignore
index 48fa4c9..5a5d0df 100644
--- a/.Rbuildignore
+++ b/.Rbuildignore
@@ -10,3 +10,4 @@
 ^LICENSE\.md$
 ^\.Rproj\.user$
 ^docs$
+^derivatives$
diff --git a/.gitignore b/.gitignore
index 94ece8e..f4b0c97 100644
--- a/.gitignore
+++ b/.gitignore
@@ -52,3 +52,5 @@ po/*~
 rsconnect/
 inst/doc
 #docs
+
+derivatives
diff --git a/NAMESPACE b/NAMESPACE
index e28ff03..09e5551 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -15,6 +15,7 @@ importFrom(dplyr,across)
 importFrom(dplyr,all_of)
 importFrom(grDevices,dev.off)
 importFrom(grDevices,jpeg)
+importFrom(grDevices,png)
 importFrom(grDevices,rainbow)
 importFrom(graphics,legend)
 importFrom(graphics,lines)
diff --git a/R/bidsify.R b/R/bidsify.R
index 8afeb3c..7fee484 100644
--- a/R/bidsify.R
+++ b/R/bidsify.R
@@ -31,6 +31,12 @@
 #' must already be installed. Defaults to FALSE.
 #' @param report_seed Random seed for the plots that will appear in the report.
 #' Defaults to 0. See [eyeris::plot()] for a more detailed description.
+#' @param report_epoch_grouping_var_col String name of grouping column to use
+#' for epoch-by-epoch diagnostic plots in an interactive rendered HTML report.
+#' Column name must exist (i.e., be a custom grouping variable name set within
+#' the metadata template of your `epoch()` call). Defaults to `"matched_event"`,
+#' which all epoched dataframes have as a valid column name. To disable these
+#' epoch-level diagnostic plots, set to `NULL`.
 #'
 #' @examples
 #' # Bleed around blink periods just long enough to remove majority of
@@ -62,7 +68,8 @@ bidsify <- function(eyeris, save_all = TRUE, epochs_list = NULL,
                     merge_epochs = FALSE, bids_dir = NULL,
                     participant_id = NULL, session_num = NULL,
                     task_name = NULL, run_num = NULL, save_raw = TRUE,
-                    html_report = TRUE, pdf_report = FALSE, report_seed = 0) {
+                    html_report = TRUE, pdf_report = FALSE, report_seed = 0,
+                    report_epoch_grouping_var_col = "matched_event") {
   sub <- participant_id
   ses <- session_num
   task <- task_name
@@ -221,6 +228,7 @@ bidsify <- function(eyeris, save_all = TRUE, epochs_list = NULL,
 
     for (i in seq_along(pupil_steps)) {
       fig_paths[i] <- file.path(figs_out, paste0("fig", i, ".jpg"))
+
       jpeg(file.path(fig_paths[i]),
         width = 12, height = 7, units = "in",
         res = 300, pointsize = 14
@@ -237,9 +245,7 @@ bidsify <- function(eyeris, save_all = TRUE, epochs_list = NULL,
     jpeg(file.path(fig_paths[length(fig_paths)]),
       width = 12, height = 7, units = "in", res = 300, pointsize = 18
     )
-
     plot(eyeris, steps = 1, preview_window = c(0, nrow(eyeris$timeseries)))
-
     dev.off()
 
     fig_paths <- c(
@@ -250,15 +256,84 @@ bidsify <- function(eyeris, save_all = TRUE, epochs_list = NULL,
     jpeg(file.path(fig_paths[length(fig_paths)]),
       width = 12, height = 7, units = "in", res = 300, pointsize = 18
     )
-
     plot(eyeris,
       steps = length(pupil_steps),
       preview_window = c(0, nrow(eyeris$timeseries))
     )
-
     dev.off()
 
-    report_output <- make_report(eyeris, report_path, fig_paths,
+    if (!is.null(report_epoch_grouping_var_col)) {
+      for (i in seq_along(epochs_to_save)) {
+        tryCatch(
+          {
+            check_column(epochs_to_save[[i]], report_epoch_grouping_var_col)
+          },
+          error = function(e) {
+            error_handler(e, "column_doesnt_exist_in_df_error")
+          }
+        )
+
+        epochs_out <- file.path(figs_out, names(epochs_to_save)[i])
+        check_and_create_dir(epochs_out)
+
+        epoch_groups <- as.vector(
+          unique(epochs_to_save[[i]][report_epoch_grouping_var_col])[[1]]
+        )
+
+        for (group in epoch_groups) {
+          group_df <- epochs_to_save[[i]]
+          group_df <- group_df[
+            group_df[[report_epoch_grouping_var_col]] == group,
+          ]
+
+          for (pstep in seq_along(pupil_steps)) {
+            if (grepl("z", pupil_steps[pstep])) {
+              y_units <- "(z)"
+            } else {
+              y_units <- "(a.u.)"
+            }
+
+            colors <- c("black", rainbow(length(pupil_steps) - 1))
+
+            y_label <- paste("pupil size", y_units)
+
+            file_out <- file.path(epochs_out, paste0(group, "_", pstep, ".png"))
+
+            png(file_out,
+              width = 3.25,
+              height = 2.5,
+              units = "in",
+              res = 600,
+              pointsize = 6
+            )
+            plot(group_df$timebin, group_df[[pupil_steps[pstep]]],
+              type = "l", xlab = "time (s)", ylab = y_label,
+              col = colors[pstep],
+              main = paste0(group, "\n", pupil_steps[pstep])
+            )
+            dev.off()
+          }
+        }
+
+        epochs <- list.files(epochs_out,
+          full.names = FALSE,
+          pattern = "\\.(jpg|jpeg|png|gif)$",
+          ignore.case = TRUE
+        )
+
+        epochs <- file.path("source", "figures",
+                            names(epochs_to_save)[i], epochs)
+
+        make_gallery(eyeris, epochs, report_path, names(epochs_to_save)[i],
+          sub = sub, ses = ses, task = task, run = run
+        )
+      }
+    }
+
+    report_output <- make_report(
+      eyeris,
+      report_path,
+      fig_paths,
       sub = sub, ses = ses, task = task, run = run
     )
 
diff --git a/R/checks.R b/R/checks.R
index 099b68e..288b865 100644
--- a/R/checks.R
+++ b/R/checks.R
@@ -72,6 +72,14 @@ check_baseline_inputs <- function(events, limits) {
   }
 }
 
+check_column <- function(df, col_name) {
+  if (!col_name %in% colnames(df)) {
+    err_c <- "column_doesnt_exist_in_df_error"
+    err_m <- paste0("No grouping variable '", col_name, "' in the epoched df.")
+    stop(structure(list(message = err_m, call = match.call()), class = err_c))
+  }
+}
+
 check_data <- function(eyeris, fun) {
   err_m <- sprintf(paste(
     "The provided object to `eyeris::%s()` is of type",
diff --git a/R/eyeris-package.R b/R/eyeris-package.R
index 2b9eec8..42cbcdc 100644
--- a/R/eyeris-package.R
+++ b/R/eyeris-package.R
@@ -10,6 +10,7 @@
 #' @importFrom graphics text
 #' @importFrom grDevices dev.off
 #' @importFrom grDevices jpeg
+#' @importFrom grDevices png
 #' @importFrom grDevices rainbow
 #' @importFrom rlang :=
 #' @importFrom stats lm
diff --git a/R/gallery.R b/R/gallery.R
new file mode 100644
index 0000000..e751058
--- /dev/null
+++ b/R/gallery.R
@@ -0,0 +1,84 @@
+make_gallery <- function(eyeris, epochs, out, epoch_name, ...) {
+  params <- list(...)
+
+  epoch_name_corrected <- sub("^epoch_", "epoch-", epoch_name)
+
+  rmd_f <- file.path(out, paste0(
+    "sub-", params$sub, "_",
+    "run-", params$run, "_",
+    epoch_name_corrected, ".Rmd"
+  ))
+
+  report_date <- format(Sys.time(), "%B %d, %Y | %H:%M:%OS3")
+  package_version <- as.character(
+    utils::packageVersion("eyeris")
+  )
+
+  css <- system.file(
+    file.path("rmarkdown", "css", "report.css"),
+    package = "eyeris"
+  )
+
+  sticker_path <- system.file("figures", "sticker.png", package = "eyeris")
+
+  epoch_lightbox_html <- print_lightbox_img_html(epochs)
+
+  content <- paste0(
+    "---\n",
+    "title: '`eyeris` epoch previewer'\n",
+    "date: '", report_date, "'\n",
+    "output:\n",
+    "  html_document:\n",
+    "    df_print: paged\n",
+    "    css: '", css, "'\n",
+    "---\n\n",
+    "\n\n<img src='", sticker_path, "' class='top-right-image'>",
+    "\n\n---\n\n## Summary\n",
+    " - Subject ID: ", params$sub, "\n",
+    " - Session: ", params$ses, "\n",
+    " - Task: ", params$task, "\n",
+    " - Run: ", params$run, "\n",
+    " - BIDS Directory: ", out, "\n",
+    " - Source `.asc` file: ", eyeris$file, "\n",
+    " - [`eyeris` version](https://github.com/shawntz/eyeris): ",
+    package_version, "\n",
+    "\n\n<style type='text/css'>\n",
+    "@import url('http://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/",
+    "bootstrap.min.css');\n",
+    "@import url('https://cdn.jsdelivr.net/npm/lightbox2/dist/css/",
+    "lightbox.min.css');\n</style>\n",
+    "<script src='https://cdn.jsdelivr.net/npm/lightbox2/dist/js/",
+    "lightbox.min.js'></script>\n<script>document.addEventListener(",
+    "'DOMContentLoaded', function() {lightbox.option({'imageFadeDuration' : 0,",
+    "'resizeDuration': 25,'wrapAround': false});});</script>\n\n\n",
+    "\n# Preprocessed Data Preview\n\n",
+    "\n## ", epoch_name, "\n\n",
+    epoch_lightbox_html,
+    "\n",
+    "\n\n---\n\n### Citation\n\n",
+    "```{r citation, echo=FALSE, comment=NA}\n",
+    "citation('eyeris')\n",
+    "```\n\n"
+  )
+
+  writeLines(content, con = rmd_f)
+
+  rmarkdown::render(rmd_f, output_format = "html_document")
+
+  unlink(rmd_f)
+}
+
+print_lightbox_img_html <- function(images) {
+  html_out <- ""
+
+  for (i in images) {
+    html_out <- paste0(
+      html_out,
+      '<a href="', i, '" data-lightbox="gallery" data-title="Image 1">',
+      '<img src="', i,
+      '" alt="Thumbnail 1" style="margin: 5px; width: 150px;"></a>'
+    )
+  }
+
+  return(html_out)
+}
diff --git a/R/report.R b/R/report.R
index fb27b73..f779408 100644
--- a/R/report.R
+++ b/R/report.R
@@ -37,9 +37,15 @@ make_report <- function(eyeris, out, plots, ...) {
     " - Run: ", params$run, "\n",
     " - BIDS Directory: ", out, "\n",
     " - Source `.asc` file: ", eyeris$file, "\n",
-    " - [`eyeris` version](https://github.com/shawntschwartz/eyeris): ",
+    " - [`eyeris` version](https://github.com/shawntz/eyeris): ",
     package_version, "\n",
 
+    "\n\n<style type='text/css'>\n",
+    "@import url('http://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/",
+    "bootstrap.min.css');\n",
+    "@import url('https://cdn.jsdelivr.net/npm/lightbox2/dist/css/",
+    "lightbox.min.css');\n</style>\n",
+
     "\n## Preprocessed Data Preview\n\n",
     print_plots(plots), "\n",
 
diff --git a/man/bidsify.Rd b/man/bidsify.Rd
index ef853fa..02b29c0 100644
--- a/man/bidsify.Rd
+++ b/man/bidsify.Rd
@@ -17,7 +17,8 @@ bidsify(
   save_raw = TRUE,
   html_report = TRUE,
   pdf_report = FALSE,
-  report_seed = 0
+  report_seed = 0,
+  report_epoch_grouping_var_col = "matched_event"
 )
 }
 \arguments{
@@ -53,6 +54,13 @@ must already be installed. Defaults to FALSE.}
 
 \item{report_seed}{Random seed for the plots that will appear in the report.
 Defaults to 0. See \code{\link[=plot]{plot()}} for a more detailed description.}
+
+\item{report_epoch_grouping_var_col}{String name of grouping column to use
+for epoch-by-epoch diagnostic plots in an interactive rendered HTML report.
+Column name must exist (i.e., be a custom grouping variable name set within
+the metadata template of your \code{epoch()} call). Defaults to \code{"matched_event"},
+which all epoched dataframes have as a valid column name. To disable these
+epoch-level diagnostic plots, set to \code{NULL}.}
 }
 \description{
 This method provides a structured way to save out pupil data in a BIDS-like