diff --git a/DESCRIPTION b/DESCRIPTION
index 2d1a5a66f7..d7cf54ea92 100644
--- a/DESCRIPTION
+++ b/DESCRIPTION
@@ -75,3 +75,42 @@ LazyData: true
 RoxygenNote: 6.1.1
 Encoding: UTF-8
 Roxygen: list(markdown = TRUE)
+Collate: 
+    'add.R'
+    'animate.R'
+    'api.R'
+    'api_exports.R'
+    'data.R'
+    'deprecated.R'
+    'dev.R'
+    'embed.R'
+    'export.R'
+    'ggplotly.R'
+    'gginteractive.R'
+    'group2NA.R'
+    'helpers.R'
+    'highlight.R'
+    'imports.R'
+    'last_plot.R'
+    'layers2layout.R'
+    'layers2traces.R'
+    'layout.R'
+    'mathjax.R'
+    'orca.R'
+    'partial_bundles.R'
+    'pipe.R'
+    'plotly.R'
+    'plotly_IMAGE.R'
+    'plotly_build.R'
+    'plotly_data.R'
+    'plotly_example.R'
+    'print.R'
+    'process.R'
+    'proxy.R'
+    'sf.R'
+    'shiny.R'
+    'signup.R'
+    'style.R'
+    'subplots.R'
+    'toRGB.R'
+    'utils.R'
diff --git a/NAMESPACE b/NAMESPACE
index dd80145f23..55650c84c3 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -21,6 +21,7 @@ S3method(geom2trace,GeomText)
 S3method(geom2trace,GeomTile)
 S3method(geom2trace,default)
 S3method(ggplot,plotly)
+S3method(ggplot_add,gginteractive)
 S3method(ggplotly,"NULL")
 S3method(ggplotly,ggmatrix)
 S3method(ggplotly,ggplot)
@@ -136,6 +137,7 @@ export(filter_)
 export(geom2trace)
 export(get_figure)
 export(gg2list)
+export(gginteractive)
 export(ggplotly)
 export(group2NA)
 export(group_by)
@@ -222,6 +224,7 @@ importFrom(dplyr,summarise_)
 importFrom(dplyr,transmute)
 importFrom(dplyr,transmute_)
 importFrom(dplyr,ungroup)
+importFrom(ggplot2,ggplot_add)
 importFrom(grDevices,as.raster)
 importFrom(grDevices,col2rgb)
 importFrom(grDevices,dev.list)
@@ -257,6 +260,7 @@ importFrom(lazyeval,f_new)
 importFrom(lazyeval,is_formula)
 importFrom(lazyeval,is_lang)
 importFrom(magrittr,"%>%")
+importFrom(purrr,partial)
 importFrom(purrr,transpose)
 importFrom(rlang,eval_tidy)
 importFrom(stats,complete.cases)
diff --git a/R/gginteractive.R b/R/gginteractive.R
new file mode 100644
index 0000000000..b61df5dd7c
--- /dev/null
+++ b/R/gginteractive.R
@@ -0,0 +1,30 @@
+#' @include ggplotly.R
+#' @rdname ggplotly
+#' @importFrom purrr partial
+#' 
+#' @param interactive
+#'  If `TRUE` (default), then ggplot object is converted to plotly object.
+#' 
+#' @export
+gginteractive <- function(
+  interactive = TRUE, width = NULL, height = NULL, tooltip = "all",
+  dynamicTicks = FALSE, layerData = 1, originalData = TRUE, source = "A", ...
+) {
+  structure(
+    if (interactive) {
+      partial(
+        ggplotly,
+        width = width, height = height, tooltip = tooltip,
+        dynamicTicks = dynamicTicks, layerData = layerData,
+        originalData = originalData, source = source, ...
+      )
+    } else {
+      identity
+    },
+    class = c("gginteractive", "function")
+  )
+}
+
+#' @importFrom ggplot2 ggplot_add
+#' @export
+ggplot_add.gginteractive <- function(object, plot, object_name) object(plot)
diff --git a/R/ggplotly.R b/R/ggplotly.R
index 6f3e638e6d..e2aff2ff13 100644
--- a/R/ggplotly.R
+++ b/R/ggplotly.R
@@ -1,7 +1,6 @@
 #' Convert ggplot2 to plotly
 #'
-#' This function converts a [ggplot2::ggplot()] object to a 
-#' plotly object. 
+#' These functions convert a [ggplot2::ggplot()] object to a plotly object. 
 #' 
 #' @details Conversion of relative sizes depends on the size of the current 
 #' graphics device (if no device is open, width/height of a new (off-screen) 
@@ -38,6 +37,7 @@
 #' # simple example
 #' ggiris <- qplot(Petal.Width, Sepal.Length, data = iris, color = Species)
 #' ggplotly(ggiris)
+#' ggiris + gginteractive()
 #'
 #' data(canada.cities, package = "maps")
 #' viz <- ggplot(canada.cities, aes(long, lat)) +
diff --git a/man/ggplotly.Rd b/man/ggplotly.Rd
index d5f3ec205e..e3b3c6a4ec 100644
--- a/man/ggplotly.Rd
+++ b/man/ggplotly.Rd
@@ -1,12 +1,17 @@
 % Generated by roxygen2: do not edit by hand
-% Please edit documentation in R/ggplotly.R
+% Please edit documentation in R/ggplotly.R, R/gginteractive.R
 \name{ggplotly}
 \alias{ggplotly}
+\alias{gginteractive}
 \title{Convert ggplot2 to plotly}
 \usage{
 ggplotly(p = ggplot2::last_plot(), width = NULL, height = NULL,
   tooltip = "all", dynamicTicks = FALSE, layerData = 1,
   originalData = TRUE, source = "A", ...)
+
+gginteractive(interactive = TRUE, width = NULL, height = NULL,
+  tooltip = "all", dynamicTicks = FALSE, layerData = 1,
+  originalData = TRUE, source = "A", ...)
 }
 \arguments{
 \item{p}{a ggplot object.}
@@ -36,10 +41,11 @@ with the source argument in \code{\link[=event_data]{event_data()}} to retrieve
 event data corresponding to a specific plot (shiny apps can have multiple plots).}
 
 \item{...}{arguments passed onto methods.}
+
+\item{interactive}{If \code{TRUE} (default), then ggplot object is converted to plotly object.}
 }
 \description{
-This function converts a \code{\link[ggplot2:ggplot]{ggplot2::ggplot()}} object to a
-plotly object.
+These functions convert a \code{\link[ggplot2:ggplot]{ggplot2::ggplot()}} object to a plotly object.
 }
 \details{
 Conversion of relative sizes depends on the size of the current
@@ -54,6 +60,7 @@ shiny app, see \code{plotly_example("shiny", "ggplotly_sizing")}.
 # simple example
 ggiris <- qplot(Petal.Width, Sepal.Length, data = iris, color = Species)
 ggplotly(ggiris)
+ggiris + gginteractive()
 
 data(canada.cities, package = "maps")
 viz <- ggplot(canada.cities, aes(long, lat)) +
diff --git a/tests/testthat/test-ggplot-ggplotly.R b/tests/testthat/test-ggplot-ggplotly.R
index d2c5276c33..a0994abc32 100644
--- a/tests/testthat/test-ggplot-ggplotly.R
+++ b/tests/testthat/test-ggplot-ggplotly.R
@@ -3,29 +3,53 @@ context("ggplotly+plotly")
 p <- ggplot(txhousing, aes(x = date, y = median, group = city)) +
   geom_line(alpha = 0.3)
 
+expect_identical_plotly <- function(object, expected, ...) {
+  expect_identical(names(object$x), names(expected$x))
+  
+  exceptions <- c("attrs", "cur_data", "visdat", "layoutAttrs")
+  object$x <- object$x[setdiff(names(object$x), exceptions)]
+  expected$x <- expected$x[setdiff(names(expected$x), exceptions)]
+  expect_identical(object, expected, ...)
+}
+
 test_that("ggplotly returns original data with special attributes", {
   dat <- ggplotly(p) %>% plotly_data()
   expect_equivalent(dat, p$data)
   expect_equivalent(as.character(dplyr::groups(dat)), "city")
+  expect_equivalent(dat, plotly_data(p + gginteractive()))
 })
 
 test_that("can filter data returned by ggplotly", {
   dat <- ggplotly(p) %>% filter(city == "Houston") %>% plotly_data()
   expect_equivalent(dat, subset(p$data, city == "Houston"))
   expect_equivalent(as.character(dplyr::groups(dat)), "city")
+  expect_equivalent(
+    dat, (p + gginteractive()) %>% filter(city == "Houston") %>% plotly_data()
+  )
 })
 
 test_that("can add traces with original _and_ scaled data", {
   l1 <- ggplotly(p) %>% add_lines() %>% plotly_build()
   expect_equivalent(length(l1$x$data), 2)
+  expect_identical_plotly(
+    l1, (p + gginteractive()) %>% add_lines() %>% plotly_build()
+  )
   l2 <- ggplotly(p, originalData = FALSE) %>% 
     add_lines() %>% plotly_build()
   # ideally we'd test that the two plots have the same data, but 
   # for some reason R CMD check throws an error which I can't replicate :(
   expect_equivalent(length(l2$x$data), 2)
+  expect_identical_plotly(
+    l2, (p + gginteractive(originalData = FALSE)) %>% add_lines() %>% plotly_build()
+  )
 })
 
 test_that("can access ggplot data in layout()", {
   l <- ggplotly(p) %>% layout(title = ~range(date))
   expect_equivalent(plotly_build(l)$x$layout$title, range(txhousing$date))
+  expect_identical_plotly(l, (p + gginteractive()) %>% layout(title = ~range(date)))
+})
+
+test_that("can suppress conversion of ggplot to plotly", {
+  expect_s3_class(p + gginteractive(interactive = FALSE), class(p))
 })