diff --git a/core/modules/contextual/contextual.toolbar.js b/core/modules/contextual/contextual.toolbar.js
index feea0cf..295ebb7 100644
--- a/core/modules/contextual/contextual.toolbar.js
+++ b/core/modules/contextual/contextual.toolbar.js
@@ -32,13 +32,21 @@ function initContextualToolbar (context) {
   new contextualToolbar.VisualView(viewOptions);
   new contextualToolbar.AuralView(viewOptions);
 
-  // Update the model based on overlay events.
   $(document).on({
+    // Update the model based on overlay events.
     'drupalOverlayOpen.contextualToolbar': function () {
       model.set('overlayIsOpen', true);
     },
     'drupalOverlayClose.contextualToolbar': function () {
       model.set('overlayIsOpen', false);
+      model.set('isVisible', true);
+    },
+    // Update the model based on Responsive Preview events.
+    'drupalResponsivePreviewStarted.contextualToolbar': function () {
+      model.set('isVisible', false);
+    },
+    'drupalResponsivePreviewStopped.contextualToolbar': function () {
+      model.set('isVisible', true);
     }
   });
 
diff --git a/core/modules/responsive_preview/config/responsive_preview.device.desktop.yml b/core/modules/responsive_preview/config/responsive_preview.device.desktop.yml
new file mode 100644
index 0000000..14e5ba9
--- /dev/null
+++ b/core/modules/responsive_preview/config/responsive_preview.device.desktop.yml
@@ -0,0 +1,10 @@
+id: desktop
+label: Typical desktop
+dimensions:
+  width: 1366
+  height: 768
+  dppx: 1
+orientation: landscape
+weight: 5
+status: 1
+langcode: en
diff --git a/core/modules/responsive_preview/config/responsive_preview.device.ipad.yml b/core/modules/responsive_preview/config/responsive_preview.device.ipad.yml
new file mode 100644
index 0000000..c14ecac
--- /dev/null
+++ b/core/modules/responsive_preview/config/responsive_preview.device.ipad.yml
@@ -0,0 +1,10 @@
+id: ipad
+label: iPad
+dimensions:
+  width: 1536
+  height: 2048
+  dppx: 2
+orientation: portrait
+weight: 2
+status: 1
+langcode: en
diff --git a/core/modules/responsive_preview/config/responsive_preview.device.iphone4.yml b/core/modules/responsive_preview/config/responsive_preview.device.iphone4.yml
new file mode 100644
index 0000000..c57d380
--- /dev/null
+++ b/core/modules/responsive_preview/config/responsive_preview.device.iphone4.yml
@@ -0,0 +1,10 @@
+id: iphone4
+label: iPhone 4
+dimensions:
+  width: 640
+  height: 960
+  dppx: 2
+orientation: portrait
+weight: 1
+status: 1
+langcode: en
diff --git a/core/modules/responsive_preview/config/responsive_preview.device.iphone5.yml b/core/modules/responsive_preview/config/responsive_preview.device.iphone5.yml
new file mode 100644
index 0000000..d076c8d
--- /dev/null
+++ b/core/modules/responsive_preview/config/responsive_preview.device.iphone5.yml
@@ -0,0 +1,10 @@
+id: iphone5
+label: iPhone 5
+dimensions:
+  width: 640
+  height: 1136
+  dppx: 2
+orientation: portrait
+weight: 0
+status: 1
+langcode: en
diff --git a/core/modules/responsive_preview/config/responsive_preview.device.nexus4.yml b/core/modules/responsive_preview/config/responsive_preview.device.nexus4.yml
new file mode 100644
index 0000000..d43ffea
--- /dev/null
+++ b/core/modules/responsive_preview/config/responsive_preview.device.nexus4.yml
@@ -0,0 +1,10 @@
+id: nexus4
+label: Nexus 4
+dimensions:
+  width: 768
+  height: 1280
+  dppx: 2
+orientation: portrait
+weight: 3
+status: 1
+langcode: en
diff --git a/core/modules/responsive_preview/config/responsive_preview.device.nexus7.yml b/core/modules/responsive_preview/config/responsive_preview.device.nexus7.yml
new file mode 100644
index 0000000..bf17615
--- /dev/null
+++ b/core/modules/responsive_preview/config/responsive_preview.device.nexus7.yml
@@ -0,0 +1,10 @@
+id: nexus7
+label: Nexus 7
+dimensions:
+  width: 800
+  height: 1280
+  dppx: 1.325
+orientation: portrait
+weight: 4
+status: 1
+langcode: en
diff --git a/core/modules/responsive_preview/css/responsive-preview.base-rtl.css b/core/modules/responsive_preview/css/responsive-preview.base-rtl.css
new file mode 100644
index 0000000..790cc0a
--- /dev/null
+++ b/core/modules/responsive_preview/css/responsive-preview.base-rtl.css
@@ -0,0 +1,32 @@
+/**
+ * @file
+ * RTL base styling for responsive preview.
+ */
+
+/**
+ * Toolbar tab.
+ */
+
+/* At narrow screen widths, float the tab to the right so it falls in line with
+ * the rest of the toolbar tabs. */
+.js .toolbar .bar .toolbar-tab-responsive-preview.tab {
+  float: left;
+}
+.toolbar-tab-responsive-preview .responsive-preview-options {
+  left: 0.3em;
+  right: auto;
+}
+
+/**
+ * Preview container.
+ *
+ * The container is kept offscreen after it is built and has been disabled.
+ */
+.responsive-preview {
+  left: auto;
+  right: -200%;
+}
+.responsive-preview.active {
+  left: auto;
+  right: 0;
+}
diff --git a/core/modules/responsive_preview/css/responsive-preview.base.css b/core/modules/responsive_preview/css/responsive-preview.base.css
new file mode 100644
index 0000000..cd82f1b
--- /dev/null
+++ b/core/modules/responsive_preview/css/responsive-preview.base.css
@@ -0,0 +1,105 @@
+/**
+ * @file
+ * Base styling for responsive preview.
+ */
+
+/**
+ * Constrain the window height to the client height when the preview is active.
+ */
+.responsive-preview-active {
+  height: 100%;
+  overflow: hidden;
+}
+
+/**
+ * Toolbar tab.
+ */
+.toolbar-tab-responsive-preview {
+  display: none;
+}
+/* At narrow screen widths, float the tab to the left so it falls in line with
+ * the rest of the toolbar tabs. */
+.js .toolbar .bar .toolbar-tab-responsive-preview.tab {
+  display: block;
+  float: right; /* LTR */
+  position: relative;
+}
+.toolbar-tab-responsive-preview .trigger {
+  display: block;
+}
+/* Device preview options. */
+.toolbar-tab-responsive-preview .item-list {
+  display: none;
+  position: absolute;
+  white-space: nowrap;
+  z-index: 1;
+}
+.toolbar-tab-responsive-preview.open .item-list {
+  display: block;
+}
+.js .toolbar-tab-responsive-preview.tab .options li {
+  float: none;
+}
+
+/**
+ * Preview container.
+ *
+ * The container is kept offscreen after it is built and has been disabled.
+ */
+.responsive-preview {
+  bottom: 0;
+  height: 100%;
+  left: -200%; /* LTR */
+  position: relative;
+  top: 0;
+  width: 100%;
+  z-index: 1050;
+}
+.responsive-preview.active {
+  left: 0; /* LTR */
+  position: fixed;
+}
+.responsive-preview .control {
+  position: absolute;
+}
+.responsive-preview .modal-background {
+  bottom: 0;
+  height: 100%;
+  left: 0;
+  position: static;
+  right: 0;
+  top: 0;
+  width: 100%;
+  z-index: 1;
+}
+.responsive-preview.active .modal-background {
+  position: fixed;
+}
+
+/**
+ * Preview iframe.
+ */
+.responsive-preview .frame-container {
+  position: absolute;
+  z-index: 100;
+}
+.responsive-preview .frame-container iframe {
+  position: relative;
+}
+
+/**
+ * Override Toolbar styling in the preview iframe.
+ */
+body.toolbar-tray-open.responsive-preview-frame {
+  margin-left: 0 !important;
+  margin-right: 0 !important;
+}
+.responsive-preview-frame {
+  overflow-x: hidden !important;
+}
+.responsive-preview-frame #toolbar-administration {
+  display: none !important;
+}
+.responsive-preview-frame .contextual {
+  display: none !important;
+}
diff --git a/core/modules/responsive_preview/css/responsive-preview.icons-rtl.css b/core/modules/responsive_preview/css/responsive-preview.icons-rtl.css
new file mode 100644
index 0000000..0c8d2b6
--- /dev/null
+++ b/core/modules/responsive_preview/css/responsive-preview.icons-rtl.css
@@ -0,0 +1,21 @@
+/**
+ * @file
+ * RTL icon styling for responsive preview.
+ */
+
+.toolbar .bar .toolbar-tab-responsive-preview .icon-responsive-preview:before {
+  left: auto; /* LTR */
+  right: 1em;
+}
+
+/**
+ * Responsive preview controls icons.
+ */
+.responsive-preview .icon-close:before {
+  left: 9px;
+  right: auto;
+}
+.responsive-preview .icon-orientation:before {
+  left: auto;
+  right: 9px;
+}
diff --git a/core/modules/responsive_preview/css/responsive-preview.icons.css b/core/modules/responsive_preview/css/responsive-preview.icons.css
new file mode 100644
index 0000000..6ed041e
--- /dev/null
+++ b/core/modules/responsive_preview/css/responsive-preview.icons.css
@@ -0,0 +1,95 @@
+/**
+ * @file
+ * Responsive preview icon styling.
+ */
+.toolbar-tab-responsive-preview .icon:before,
+.responsive-preview .icon:before {
+  background-attachment: scroll;
+  background-color: transparent;
+  background-image: url("../images/responsive-preview-icons.png");
+  background-repeat: no-repeat;
+  content: '';
+  display: block;
+  position: absolute;
+  z-index: 1;
+}
+.toolbar .bar .toolbar-tab-responsive-preview .icon:before {
+  width: 13px;
+}
+.responsive-preview button.icon {
+  background-color: transparent;
+  border: 0;
+  font-size: 1em;
+}
+
+/* Toolbar icon. */
+.toolbar .bar .icon.icon-responsive-preview {
+  margin-left: 0;
+  margin-right: 0;
+  padding-left: 0;
+  padding-right: 0;
+  width: 5em;
+}
+.toolbar-tab-responsive-preview .icon.icon-responsive-preview:before {
+  background-position: center top;
+}
+.toolbar-tab-responsive-preview.open .icon-responsive-preview:before,
+.toolbar-tab-responsive-preview .icon-responsive-preview.active:before,
+.toolbar-tab-responsive-preview .icon-responsive-preview:hover:before {
+  background-position: center -22px;
+}
+.toolbar .bar .toolbar-tab-responsive-preview .icon-responsive-preview:before {
+  left: 1em; /* LTR */
+  height: 22px;
+  top: 0.6667em;
+}
+.toolbar .toolbar-tab-responsive-preview.tab .options .device.icon-active {
+  padding-left: 2.25em;
+}
+.toolbar .toolbar-tab-responsive-preview.tab .options .device.icon-active:before {
+  background-position: -999px -999px;
+}
+.toolbar .toolbar-tab-responsive-preview.tab .options .device.icon-active.active:before {
+  background-position: center -110px;
+}
+@media only screen and (min-width: 16.5em) {
+  .toolbar .toolbar-tab-responsive-preview.tab .options .device.icon-active {
+    padding: 0.5em 1.3333em 0.5em 2.25em;
+    text-indent: 0;
+    width: auto;
+  }
+  .toolbar .toolbar-tab-responsive-preview.tab .options .device.icon-active:before {
+    left: 0.667em;
+  }
+}
+
+/**
+ * Responsive preview controls icons.
+ */
+.responsive-preview .control.icon:before {
+  height: 12px;
+  width: 12px;
+  top: 12px;
+}
+.responsive-preview .icon-close:before {
+  background-position: left -44px;
+  right: 9px; /* LTR */
+}
+.responsive-preview .icon-close:active:before,
+.responsive-preview .icon-close.active:before,
+.responsive-preview .icon-close:hover:before {
+  background-position: left -56px;
+}
+.responsive-preview .icon-orientation:before {
+  background-position: left -92px;
+  left: 9px; /* LTR */
+}
+.responsive-preview .icon-orientation:hover:before {
+  background-position: left -104px;
+}
+.responsive-preview .icon-orientation.rotated:before {
+  background-position: left -68px;
+}
+.responsive-preview .icon-orientation.rotated:hover:before {
+  background-position: left -80px;
+}
diff --git a/core/modules/responsive_preview/css/responsive-preview.theme-rtl.css b/core/modules/responsive_preview/css/responsive-preview.theme-rtl.css
new file mode 100644
index 0000000..b8892ab
--- /dev/null
+++ b/core/modules/responsive_preview/css/responsive-preview.theme-rtl.css
@@ -0,0 +1,30 @@
+/**
+ * @file
+ * RTL styling for responsive preview.
+ */
+
+/**
+ * Toolbar tab.
+ */
+
+/* Toolbar tab triangle toggle. */
+.toolbar-tab-responsive-preview .trigger:after {
+  left: 1em;
+  right: auto;
+}
+.toolbar-tab-responsive-preview.open:before {
+  left: 0;
+  right: auto;
+}
+.toolbar-tab-responsive-preview.open .trigger:after {
+  left: 0.7em;
+  right: auto;
+}
+.responsive-preview .control.close {
+  left: 0;
+  right: auto;
+}
+.responsive-preview .control.orientation {
+  left: auto;
+  right: 0;
+}
diff --git a/core/modules/responsive_preview/css/responsive-preview.theme.css b/core/modules/responsive_preview/css/responsive-preview.theme.css
new file mode 100644
index 0000000..d276aa4
--- /dev/null
+++ b/core/modules/responsive_preview/css/responsive-preview.theme.css
@@ -0,0 +1,203 @@
+/**
+ * @file
+ * Styling for responsive preview.
+ */
+
+/**
+ * Toolbar tab.
+ */
+.toolbar-tab-responsive-preview .options {
+  background-color: #0f0f0f;
+}
+/* Device preview options. */
+.toolbar-tab-responsive-preview .options {
+  box-shadow: 0 0.8em 2.5em -0.8em rgba(0, 0, 0, 0.75);
+}
+.toolbar-tab-responsive-preview .options li {
+  background-color: white;
+  padding: 0;
+}
+.toolbar-tab-responsive-preview .trigger {
+  cursor: pointer;
+  line-height: 1;
+  height: 3em;
+}
+.toolbar-tab-responsive-preview .trigger:hover {
+  background-image: -webkit-linear-gradient(rgba(255, 255, 255, 0.125) 20%, transparent 200%);
+  background-image: linear-gradient(rgba(255, 255, 255, 0.125) 20%, transparent 200%);
+}
+.toolbar-tab-responsive-preview .trigger.active,
+.toolbar-tab-responsive-preview .trigger.active:hover {
+  background-image: -webkit-linear-gradient(top, rgb(78,159,234) 0%, rgb(69,132,221) 100%);
+  background-image: linear-gradient(rgb(78,159,234) 0%,rgb(69,132,221) 100%);
+}
+.toolbar-tab-responsive-preview .trigger,
+.toolbar-tab-responsive-preview .options .device {
+  padding-bottom: 1em;
+  padding-top: 1em;
+}
+.toolbar-tab-responsive-preview .options .device {
+  background: none;
+  border: none;
+  cursor: pointer;
+  font-family: inherit;
+  font-size: 1em;
+  padding: 0.5em 1.3333em
+}
+.toolbar .toolbar-tab-responsive-preview.tab .options .device {
+  color: #777;
+}
+.toolbar .toolbar-tab-responsive-preview.tab .options .device:hover,
+.toolbar .toolbar-tab-responsive-preview.tab .options .device.active {
+  color: black;
+}
+.toolbar .toolbar-tab-responsive-preview.tab .options .device[disabled] {
+  color: #ccc;
+  cursor: default;
+}
+
+/* Toolbar tab triangle toggle. */
+.toolbar-tab-responsive-preview .trigger:after {
+  border-bottom-color: transparent;
+  border-left-color: transparent;
+  border-right-color: transparent;
+  border-style: solid;
+  border-width: 0.4545em 0.4em 0;
+  color: #a0a0a0;
+  content: ' ';
+  display: block;
+  height: 0;
+  line-height: 0;
+  overflow: hidden;
+  position: absolute;
+  right: 1.6667em; /* LTR */
+  top: 50%;
+  margin-top: -0.1666em;
+  width: 0;
+  z-index: 1
+}
+.toolbar-tab-responsive-preview.open:before {
+  background-color: white;
+  bottom: 0;
+  content: ' ';
+  display: block;
+  position: absolute;
+  right: 0; /* LTR */
+  top: 0;
+  width: 2em;
+  z-index: 1;
+}
+.toolbar-tab-responsive-preview.open .trigger:after {
+  border-bottom: 0.4545em solid;
+  border-top-color: transparent;
+  color: black;
+  right: 0.7em; /* LTR */
+  top: 1.25em;
+}
+.toolbar-tab-responsive-preview:hover .trigger:after,
+.toolbar-tab-responsive-preview .trigger.active:after,
+.toolbar-tab-responsive-preview:hover .trigger.active:after {
+  color: white;
+}
+.toolbar-tab-responsive-preview.open:hover .trigger:after {
+  color: black;
+}
+
+/**
+ * Preview container.
+ */
+.responsive-preview {
+  box-shadow: 0 0 10px 0 black;
+}
+.responsive-preview .frame-container {
+  opacity: 0;
+  -moz-transition: all 450ms;
+  -webkit-transition: all 450ms;
+  transition: all 450ms;
+}
+.responsive-preview.active .frame-container {
+  opacity: 1;
+}
+.responsive-preview .modal-background {
+  background-color: black;
+  background-color: rgba(0,0,0,0.92);
+  background-image: -webkit-linear-gradient(left, black,rgb(20,20,20) 15%, rgb(45,45,45) 40%, rgb(45,45,45) 60%, rgb(20,20,20) 85%, black 100%);
+  background-image: linear-gradient(left, black, rgb(20,20,20) 15%, rgb(45,45,45) 40%, rgb(45,45,45) 60%, rgb(20,20,20) 85%, black 100%);
+}
+
+/**
+ * Responsive preview control placement.
+ */
+.responsive-preview .control {
+  cursor: pointer;
+  height: 40px;
+  position: absolute;
+  top: 0;
+  width: 40px;
+}
+.responsive-preview .control.close {
+  right: 0; /* LTR */
+}
+.responsive-preview .control.orientation {
+  left: 0; /* LTR */
+}
+.responsive-preview .device-label {
+  color: #e0e0e0;
+  font-family: sans-serif;
+  font-size: 0.75em;
+  font-weight: normal;
+  left: 40px;
+  letter-spacing: 0.25ex;
+  line-height: 2.6667;
+  margin: 0;
+  overflow: hidden;
+  position: absolute;
+  right: 40px;
+  text-overflow: ellipsis;
+  top: 0;
+  white-space: nowrap;
+  width: auto;
+}
+
+/**
+ * Responsive preview frame.
+ */
+.responsive-preview .frame-container {
+  background-color: #343434;
+  border-radius: 20px;
+  box-shadow:
+    0 0 0px 1px #404040,
+    2px 2px 0 0px black;
+  -webkit-transition: all 150ms ease-out;
+  -moz-transition: all 150ms ease-out;
+  -o-transition: all 150ms ease-out;
+  transition: all 150ms ease-out;
+  margin-top: 2em;
+}
+.responsive-preview .frame-container iframe {
+  box-shadow:
+    0 0 0 2px black,
+    0 0 0 3px #404040;
+  -webkit-transition: all 150ms ease-out;
+  -moz-transition: all 150ms ease-out;
+  -o-transition: all 150ms ease-out;
+  transition: all 150ms ease-out;
+}
+
+/**
+ * Control block styling.
+ */
+#block-responsive-preview-controls .content .device {
+  background: none;
+  border: none;
+  color: inherit;
+  cursor: pointer;
+  font: inherit;
+  line-height: 1;
+  margin: 0;
+  padding: 0.25em 0;
+}
+#block-responsive-preview-controls .content .device[disabled] {
+  color: #ccc;
+  cursor: default;
+}
diff --git a/core/modules/responsive_preview/images/responsive-preview-icons.png b/core/modules/responsive_preview/images/responsive-preview-icons.png
new file mode 100644
index 0000000..0ce06e7
--- /dev/null
+++ b/core/modules/responsive_preview/images/responsive-preview-icons.png
@@ -0,0 +1,11 @@
+PNG
+
+   IHDR         Bq   tEXtSoftware Adobe ImageReadyqe<  IDATxYMv0$uWn'{
+v!KO } 9Ae;~
+ȝeI<^7,FDߏ<ϻۃ djD`0Huri'I"YzMȷۭ3t6W$F@>57_=j! L"+a KIYS
+V!R]lNV+XiDbZBrvNNN
+A{@lMb6>&6rחa#1NKӉ3if`<}{{;̬	T*"⼧6@7rk<Z
+ b-͵`C?REH_cuEKp	lT&ABe<z4I@9TH -$2yKZC&&K>E*Ud ׯ8<c#"M')H0u&
+yK @0Wl2c)z$\l޳*];wr?~edr n&(ɷ=_p hkNa4H([S2PY6zcneK<J&e4^(RJړkcz/J<`R[ {άdzkXYI .&l Wg Z-|%iĬ,Ęg|DTnjX)_P9TJF꽡+3+AgV2+B,ub[[WֺSWz/RNp
+x
+zy?.*pt~ޮ8Eߌ[T	W Ц}ɨmz-{|G+@ת[/ ׌e{w.Kz}-	%bu 3 >(>]    IENDB`
\ No newline at end of file
diff --git a/core/modules/responsive_preview/js/responsive-preview.js b/core/modules/responsive_preview/js/responsive-preview.js
new file mode 100644
index 0000000..2948f81
--- /dev/null
+++ b/core/modules/responsive_preview/js/responsive-preview.js
@@ -0,0 +1,892 @@
+/**
+ * @file
+ * Provides a component that previews the page in various device dimensions.
+ */
+
+(function ($, Backbone, Drupal, drupalSettings) {
+
+"use strict";
+
+var previewModel, tabModel, appView, tabView, blockView, keyboardView;
+
+var currentPath;
+
+/**
+ * Attaches behaviors to the toolbar tab and preview containers.
+ */
+Drupal.behaviors.responsivePreview = {
+  attach: function (context) {
+    // once() returns a jQuery set. It will be empty if no unprocessed
+    // elements are found. window and window.parent are equivalent unless the
+    // Drupal page is itself wrapped in an iframe.
+    var $body = $(window.parent.document.body).once('responsive-preview');
+    // Store the current path. The drupalSettings.currentPath changes whenever
+    // an AJAX request is sent, so we save it on the first process of attach.
+    currentPath = currentPath || drupalSettings.currentPath;
+
+    if ($body.length) {
+      var options = $.extend(this.defaults, drupalSettings.responsivePreview || {});
+      // If this window is itself in an iframe it must be marked as processed.
+      // Its parent window will have been processed above.
+      // When attach() is called again for the preview iframe, it will check
+      // its parent window and find it has been processed. In most cases, the
+      // following code will have no effect.
+      $(window.document.body).once('responsive-preview');
+
+      var envModel = new Drupal.responsivePreview.models.EnvironmentModel({
+        dir: document.getElementsByTagName('html')[0].getAttribute('dir')
+      });
+      tabModel = new Drupal.responsivePreview.models.TabStateModel();
+      previewModel = new Drupal.responsivePreview.models.PreviewStateModel();
+
+      // Manages the PreviewView.
+      appView = new Drupal.responsivePreview.views.AppView({
+        // The previewView model.
+        model: previewModel,
+        envModel: envModel,
+        // Gutter size around preview frame.
+        gutter: options.gutter,
+        // Preview device frame width.
+        bleed: options.bleed,
+        strings: options.strings
+      });
+
+      // The toolbar tab view.
+      var $tab = $(context).find('#responsive-preview-toolbar-tab');
+      if ($tab.length > 0) {
+        tabView = new Drupal.responsivePreview.views.TabView({
+          el: $tab.get(),
+          model: previewModel,
+          tabModel: tabModel,
+          envModel: envModel,
+          // Gutter size around preview frame.
+          gutter: options.gutter,
+          // Preview device frame width.
+          bleed: options.bleed
+        });
+      }
+      // The control block view.
+      var $block = $(context).find('#block-responsive-preview-controls');
+      if ($block.length > 0) {
+        blockView = new Drupal.responsivePreview.views.BlockView({
+          el: $block.get(),
+          model: previewModel,
+          envModel: envModel,
+          // Gutter size around preview frame.
+          gutter: options.gutter,
+          // Preview device frame width.
+          bleed: options.bleed
+        });
+      }
+
+      // Keyboard controls view.
+      keyboardView = new Drupal.responsivePreview.views.KeyboardView({
+        el: $block.get(),
+        model: previewModel
+      });
+
+      var setViewportWidth = function() {
+        envModel.set('viewportWidth', document.documentElement.clientWidth);
+      };
+
+      $(window)
+        // Update the viewport width whenever it is resized, but max 4 times/s.
+        .on('resize.responsivepreview', Drupal.debounce(setViewportWidth, 250));
+
+      $(document)
+        // Respond to viewport offsetting elements like the Toolbar.
+        .on('drupalViewportOffsetChange.responsivepreview', function (event, offsets) {
+          envModel.set('offsets', offsets);
+        })
+        .on('keyup.responsivepreview', function (event) {
+          // Close the preview if the Esc key is pressed.
+          if (event.keyCode === 27) {
+            previewModel.set('isActive', false);
+          }
+        });
+
+      // Allow other scripts to respond to responsive preview mode changes.
+      tabModel.on('change:isActive', function (model, isActive) {
+        $(document).trigger((isActive) ? 'drupalResponsivePreviewStarted' : 'drupalResponsivePreviewStopped');
+      });
+
+      // Initialization: set the current viewport width.
+      setViewportWidth();
+    }
+    // The main window is equivalent to window.parent and window.self. Inside,
+    // an iframe, these objects are not equivalent. If the parent window is
+    // itself in an iframe, check that the parent window has been processed.
+    // If it has been, this invocation of attach() is being called on the
+    // preview iframe, not its parent.
+    if ((window.parent !== window.self) && !$body.length) {
+      var $frameBody = $(window.self.document.body).once('responsive-preview');
+      if ($frameBody.length > 0) {
+        $frameBody.get(0).className += ' responsive-preview-frame';
+        // Call Drupal.displace in the next process frame to relayout the page
+        // in the iframe. This will ensure that no gaps in the presentation
+        // exist from elements that are hidden, such as the toolbar.
+        var win = window;
+        window.setTimeout(function () {
+          win.Drupal.displace();
+        }, 0);
+      }
+    }
+  },
+  detach: function (context) {
+    // Remove listeners on the window and document.
+    $(window).add(document).off('.responsivepreview');
+    // Set the preview to an inactive state.
+    previewModel.set('isActive', false);
+    // Remove listener on the tabModel.
+    tabModel.off();
+    // Remove any views
+    (appView && appView.remove());
+    (blockView && blockView.remove());
+    (tabView && tabView.remove());
+    (keyboardView && keyboardView.remove());
+    // Set the scope variables to an undefined value to remove references to
+    // the views.
+    previewModel = tabModel = appView = blockView = tabView = keyboardView = this.undef;
+  },
+  defaults: {
+    gutter: 60,
+    // The width of the device border around the iframe. This value is critical
+    // to determine the size and placement of the preview iframe container,
+    // therefore it must be defined here instead of in the CSS file.
+    bleed: 30,
+    strings: {
+      close: Drupal.t('close'),
+      orientation: Drupal.t('Change orientation'),
+      portrait: Drupal.t('portrait'),
+      landscape: Drupal.t('landscape')
+    }
+  }
+};
+
+Drupal.responsivePreview = Drupal.responsivePreview || {
+  models: {
+    /**
+     * Backbone Model for the environment in which the Responsive Preview operates.
+     */
+    EnvironmentModel: Backbone.Model.extend({
+      defaults: {
+        // The viewport width, within which the preview will have to fit.
+        viewportWidth: null,
+        // Text direction of the document, affects some positioning.
+        dir: 'ltr',
+        // Viewport offset values.
+        offsets: {
+          top: 0,
+          right: 0,
+          bottom: 0,
+          left: 0
+        }
+      }
+    }),
+
+    /**
+     * Backbone Model for the Responsive Preview toolbar tab state.
+     */
+    TabStateModel: Backbone.Model.extend({
+      defaults: {
+        // The state of toolbar list of available device previews.
+        isDeviceListOpen: false
+      }
+    }),
+
+    /**
+     * Backbone Model for the Responsive Preview preview state.
+     */
+    PreviewStateModel: Backbone.Model.extend({
+      defaults: {
+        // The state of the preview.
+        isActive: false,
+        // Indicates whether the preview iframe has been built.
+        isBuilt: false,
+        // Indicates whether the device is portrait (false) or landscape (true).
+        isRotated: false,
+        // The number of devices that fit the current viewport (i.e. previewable).
+        fittingDeviceCount: 0,
+        // Currently selected device link.
+        activeDevice: null,
+        // Dimensions of the currently selected device to preview.
+        dimensions: {
+          // The width of the device to preview.
+          width: null,
+          // The height of the device to preview.
+          height: null,
+          // The dots per pixel of the device to preview.
+          dppx: null
+        }
+      }
+    })
+  },
+  views: {
+    /**
+     * Manages the PreviewView.
+     */
+    AppView: Backbone.View.extend({
+
+      /**
+       * {@inheritdoc}
+       */
+      initialize: function () {
+        this.envModel = this.options.envModel;
+        // Listen to changes on the previewModel.
+        this.model.on('change:isActive', this.render, this);
+      },
+
+      /**
+       * {@inheritdoc}
+       */
+      render: function (previewModel, isActive, options) {
+        // The preview container view.
+        if (isActive && !this.previewView) {
+          // Holds the Backbone View of the preview. This view is created and destroyed
+          // when the preview is enabled or disabled respectively.
+          this.previewView = new Drupal.responsivePreview.views.PreviewView({
+            el: Drupal.theme('responsivePreviewContainer'),
+            // The previewView model.
+            model: this.model,
+            envModel: this.envModel,
+            // Gutter size around preview frame.
+            gutter: this.options.gutter,
+            // Preview device frame width.
+            bleed: this.options.bleed,
+            strings: this.options.strings
+          });
+        }
+        else if (!isActive && this.previewView) {
+          this.previewView.remove();
+          delete this.previewView;
+        }
+      },
+
+      /**
+       * {@inheritdoc}
+       */
+      remove: function () {
+        // Remove the previewView if it exists.
+        (this.previewView && this.previewView.remove());
+        // Call the parent remove method on this view.
+        Backbone.View.prototype.remove.call(this);
+      }
+    }),
+
+    /**
+     * Handles responsive preview toolbar tab interactions.
+     */
+    TabView: Backbone.View.extend({
+
+      events: {
+        'click': 'toggleDeviceList',
+        'mouseleave': 'toggleDeviceList',
+      },
+
+      /**
+       * {@inheritdoc}
+       */
+      initialize: function () {
+        this.gutter = this.options.gutter;
+        this.bleed = this.options.bleed;
+        this.tabModel = this.options.tabModel;
+        this.envModel = this.options.envModel;
+
+        // The selectDevice function is declared outside of the view because it is
+        // shared among views. It must be bound to this for the correct context
+        // to obtain.
+        this.$el.on('click.responsivepreview', '.device', $.proxy(selectDevice, this));
+
+        this.model.on('change:isActive change:dimensions change:activeDevice change:fittingDeviceCount', this.render, this);
+
+        this.tabModel.on('change:isDeviceListOpen', this.render, this);
+
+        this.envModel.on('change:viewportWidth', updateDeviceList, this);
+        this.envModel.on('change:viewportWidth', this.correctDeviceListEdgeCollision, this);
+      },
+
+      /**
+       * {@inheritdoc}
+       */
+      render: function () {
+        var $deviceLink = $(this.model.get('activeDevice'));
+        var name = $deviceLink.data('responsive-preview-name');
+        var isActive = this.model.get('isActive');
+        var isDeviceListOpen = this.tabModel.get('isDeviceListOpen');
+        this.$el
+          // Render the visibility of the toolbar tab.
+          .toggle(this.model.get('fittingDeviceCount') > 0)
+          // Toggle the display of the device list.
+          .toggleClass('open', isDeviceListOpen);
+
+        // Render the state of the toolbar tab button.
+        this.$el
+          .find('> button')
+          .toggleClass('active', isActive)
+          .attr('aria-pressed', isActive);
+
+        // Clean the active class from the device list.
+        this.$el
+          .find('.device.active')
+          .removeClass('active');
+
+        this.$el
+          .find('[data-responsive-preview-name="' + name + '"]')
+          .toggleClass('active', isActive);
+        // When the preview is active, a class on the body is necessary to impose
+        // styling to aid in the display of the preview element.
+        $('body').toggleClass('responsive-preview-active', isActive);
+        // The list of devices might render outside the window.
+        if (isDeviceListOpen) {
+          this.correctDeviceListEdgeCollision();
+        }
+        return this;
+      },
+
+      /**
+       * Toggles the list of devices available to preview from the toolbar tab.
+       *
+       * @param Object event
+       *   jQuery Event object.
+       */
+      toggleDeviceList: function (event) {
+        // Force the options list closed on mouseleave.
+        if (event.type === 'mouseleave') {
+          this.tabModel.set('isDeviceListOpen', false);
+        }
+        else {
+          this.tabModel.set('isDeviceListOpen', !this.tabModel.get('isDeviceListOpen'));
+        }
+
+        event.preventDefault();
+        event.stopPropagation();
+      },
+
+      /**
+       * Model change handler; corrects possible device list window edge collision.
+       */
+      correctDeviceListEdgeCollision: function () {
+        // The position of the dropdown depends on the language direction.
+        var dir = this.envModel.get('dir');
+        var edge = (dir === 'rtl') ? 'left' : 'right';
+        this.$el
+          .find('.item-list')
+          .position({
+            'my': edge +' top',
+            'at': edge + ' bottom',
+            'of': this.$el,
+            'collision': 'flip fit'
+          });
+      }
+    }),
+
+    /**
+     * Handles responsive preview control block interactions.
+     */
+    BlockView: Backbone.View.extend({
+
+      /**
+       * {@inheritdoc}
+       */
+      initialize: function () {
+        this.gutter = this.options.gutter;
+        this.bleed = this.options.bleed;
+        this.envModel = this.options.envModel;
+
+        // The selectDevice function is declared outside of the view because it is
+        // shared among views. It must be bound to this for the correct context
+        // to obtain.
+        this.$el.on('click.responsivepreview', '.device', $.proxy(selectDevice, this));
+
+        this.model.on('change:isActive change:dimensions change:activeDevice change:fittingDeviceCount', this.render, this);
+
+        this.envModel.on('change:viewportWidth', updateDeviceList, this);
+      },
+
+      /**
+       * {@inheritdoc}
+       */
+      render: function () {
+        var $deviceLink = $(this.model.get('activeDevice'));
+        var name = $deviceLink.data('responsive-preview-name');
+        var isActive = this.model.get('isActive');
+        this.$el
+          // Render the visibility of the toolbar block.
+          .toggle(this.model.get('fittingDeviceCount') > 0)
+          .find('.device.active')
+          .removeClass('active');
+
+        this.$el
+          .find('[data-responsive-preview-name="' + name + '"]')
+          .addClass('active');
+        // When the preview is active, a class on the body is necessary to impose
+        // styling to aid in the display of the preview element.
+        $('body').toggleClass('responsive-preview-active', isActive);
+        return this;
+      }
+    }),
+
+    /**
+     * Handles keyboard input.
+     */
+    KeyboardView: Backbone.View.extend({
+
+      /*
+       * {@inheritdoc}
+       */
+      initialize: function () {
+        $(document).on('keyup.responsivepreview', _.bind(this.onKeypress, this));
+      },
+
+      /**
+       * Responds to esc key press events.
+       *
+       * @param jQuery.Event event
+       */
+      onKeypress: function (event) {
+        if (event.keyCode === 27) {
+          this.model.set('isActive', false);
+        }
+      },
+
+      /**
+       * Removes a listener on the document; calls the standard Backbone remove.
+       */
+      remove: function () {
+        // Unbind the keyup listener.
+        $(document).off('keyup.responsivepreview');
+        // Call the standard remove method on this.
+        Backbone.View.prototype.remove.call(this);
+      }
+    }),
+
+    /**
+     * Handles the responsive preview element interactions.
+     */
+    PreviewView: Backbone.View.extend({
+
+      events: {
+        'click #responsive-preview-close': 'onClose',
+        'click #responsive-preview-orientation': 'onRotate'
+      },
+
+      /**
+       * {@inheritdoc}
+       */
+      initialize: function () {
+        this.gutter = this.options.gutter;
+        this.bleed = this.options.bleed;
+        this.strings = this.options.strings;
+        this.envModel = this.options.envModel;
+
+        this.model.on('change:isRotated change:dimensions change:activeDevice', this.render, this);
+
+        // Recalculate the size of the preview container when the window resizes.
+        this.envModel.on('change:viewportWidth change:offsets', this.render, this);
+
+        // Build the preview.
+        this._build();
+
+        // Call an initial render.
+        this.render();
+      },
+
+      /**
+       * {@inheritdoc}
+       */
+      render: function () {
+        // Refresh the preview.
+        this._refresh();
+        Drupal.displace();
+
+        // Render the state of the preview.
+        var that = this;
+        // Wrap the call in a setTimeout so that it invokes in the next compute
+        // cycle, causing the CSS animations to render in the first pass.
+        window.setTimeout(function () {
+          that.$el.toggleClass('active', that.model.get('isActive'));
+        }, 0);
+
+        return this;
+      },
+
+      /**
+       * Closes the preview.
+       *
+       * @param Object event
+       *   A jQuery event object.
+       */
+      onClose: function (event) {
+        this.model.set('isActive', false);
+      },
+
+      /**
+       * Responds to rotation button presses.
+       *
+       * @param Object event
+       *   A jQuery event object.
+       */
+      onRotate: function (event) {
+        this.model.set('isRotated', !this.model.get('isRotated'));
+      },
+
+      /**
+       * Builds the preview iframe.
+       */
+      _build: function () {
+        var offsets = this.envModel.get('offsets');
+        var $frameContainer = $(Drupal.theme('responsivePreviewFrameContainer'))
+          .find('#responsive-preview-close span')
+          .text(this.strings.close)
+          .end()
+          .find('#responsive-preview-orientation span')
+          .text(this.strings.orientation)
+          .end()
+          // The padding around the frame must be known in order to position it
+          // correctly, so the style property is defined in JavaScript rather than
+          // CSS.
+          .css('padding', this.bleed);
+        // Attach the iframe that will hold the preview.
+        var $frame = $(Drupal.theme('responsivePreviewFrame'))
+          .attr({
+            'data-loading': true,
+            src: drupalSettings.basePath + Drupal.encodePath(currentPath),
+            width: '100%',
+            height: '100%'
+          })
+          // Load the current page URI into the preview iframe.
+          .on('load.responsivepreview', $.proxy(this._refresh, this))
+          // Add the frame to the preview container.
+          .appendTo($frameContainer);
+        // Insert the container into the DOM.
+        this.$el
+          .css({
+            'top': offsets.top,
+            'right': offsets.right,
+            'left': offsets.left
+          })
+          // Apend the frame container.
+          .append($frameContainer)
+          // Append the container to the body to initialize the iframe document.
+          .appendTo('body');
+        // Mark the preview element processed.
+        this.model.set('isBuilt', true);
+      },
+
+      /**
+       * Refreshes the preview based on the current state (device & viewport width).
+       */
+      _refresh: function () {
+        var isRotated = this.model.get('isRotated');
+        var $deviceLink = $(this.model.get('activeDevice'));
+        var $container = this.$el.find('#responsive-preview-frame-container');
+        var $frame = $container.find('> iframe');
+        var offsets = this.envModel.get('offsets');
+
+        // Get the static state.
+        var edge = (this.envModel.get('dir') === 'rtl') ? 'right' : 'left';
+        var minGutter = this.gutter;
+
+        // Get current (dynamic) state.
+        var dimensions = this.model.get('dimensions');
+        var isRotated = this.model.get('isRotated');
+        var viewportWidth = this.envModel.get('viewportWidth') - (offsets.left + offsets.right);
+
+        // Calculate preview width & height. If the preview is rotated, swap width
+        // and height.
+        var displayWidth = dimensions[(isRotated) ? 'height' : 'width'];
+        var displayHeight = dimensions[(isRotated) ? 'width' : 'height'];
+        var width = displayWidth / dimensions.dppx;
+        var height = displayHeight / dimensions.dppx;
+
+        // Get the container padding and border width for the left and right.
+        var bleed = this.bleed;
+        var spread = width + (bleed * 2);
+
+        // Calculate gutter.
+        var gutterPercent = (1 - (spread / viewportWidth)) / 2;
+        var gutter = gutterPercent * viewportWidth;
+        gutter = (gutter < minGutter) ? minGutter : gutter;
+
+        // The preview width plus gutters must fit within the viewport width.
+        width = (viewportWidth - (gutter * 2) < spread) ? viewportWidth - (gutter * 2) - (bleed * 2) : width;
+
+        // Updated the state of the rotated icon.
+        this.$el.find('.control.orientation').toggleClass('rotated', isRotated);
+
+        // Resize & reposition the iframe.
+        this.$el.css({
+          'top': offsets.top,
+          'right': offsets.right,
+          'left': offsets.left
+        });
+        var position = {};
+        position[edge] = (gutter > minGutter) ? gutter : minGutter; // Depends on text direction.
+        $frame
+          .css({
+            width: width,
+            height: height
+          });
+        $container
+          .css(position);
+
+        // Scale if not responsive.
+        this._scaleIfNotResponsive();
+
+        // Update the device label.
+        $container.find('.device-label').text(Drupal.t('@label (@widthpx by @heightpx, @dpidppx, @orientation)', {
+          '@label': $deviceLink.text(),
+          '@width': Math.ceil(displayWidth),
+          '@height': Math.ceil(displayHeight),
+          '@dpi': dimensions.dppx,
+          '@orientation': (isRotated) ? this.strings.landscape : this.strings.portrait
+        }));
+
+        // Update the positioning of the modal background.
+        this.$el.find('.modal-background').css(offsets);
+      },
+
+      /**
+       * Applies scaling in order to better approximate content display on a device.
+       */
+      _scaleIfNotResponsive: function () {
+        var scalingCSS = this._calculateScalingCSS();
+        if (scalingCSS === false) {
+          return;
+        }
+
+        // Step 0: find DOM nodes we'll need to modify.
+        var $frame = this.$el.find('#responsive-preview-frame');
+        var $html = $($frame[0].contentDocument || $frame[0].contentWindow.document).find('html');
+
+        // Step 1: When scaling (as we're about to do), the background (color and
+        // image) doesn't scale along. Fortunately, we can fix things in case of
+        // background color.
+        // @todo: figure out a work-around for background images, or somehow
+        // document this explicitly.
+        function isTransparent (color) {
+          // TRICKY: edge case for Firefox' "transparent" here; this is a
+          // browser bug: https://bugzilla.mozilla.org/show_bug.cgi?id=635724
+          return (color === 'rgba(0, 0, 0, 0)' || color === 'transparent');
+        }
+        var htmlBgColor = $html.css('background-color');
+        var bodyBgColor = $html.find('body').css('background-color');
+        if (!isTransparent(htmlBgColor) || !isTransparent(bodyBgColor)) {
+          var bgColor = isTransparent(htmlBgColor) ? bodyBgColor : htmlBgColor;
+          $frame.css('background-color', bgColor);
+        }
+
+        // Step 2: apply scaling.
+        $html.css(scalingCSS);
+      },
+
+      /**
+       * Calculates scaling based on device dimensions and <meta name="viewport" />.
+       *
+       * Websites that don't indicate via <meta name="viewport" /> that their width
+       * is identical to the device width will be rendered at a larger size: at the
+       * layout viewport's default width. This width exceeds the visual viewport on
+       * the device, and causes it to scale it down.
+       *
+       * This function checks whether the underlying web page is responsive, and if
+       * it's not, then it will calculate a CSS scaling transformation, to closely
+       * approximate how an actual mobile device would render the web page.
+       *
+       * We assume all mobile devices' layout viewport's default width is 980px. It
+       * is the value used on all iOS and Android >=4.0 devices.
+       *
+       * Related reading:
+       *  - http://www.quirksmode.org/mobile/viewports.html
+       *  - http://www.quirksmode.org/mobile/viewports2.html
+       *  - https://developer.apple.com/library/safari/#documentation/AppleApplications/Reference/SafariWebContent/UsingtheViewport/UsingtheViewport.html
+       *  - http://tripleodeon.com/2011/12/first-understand-your-screen/
+       *  - http://tripleodeon.com/wp-content/uploads/2011/12/table.html?r=android40window.innerw&c=980
+       */
+      _calculateScalingCSS: function () {
+        var isRotated = this.model.get('isRotated');
+        var settings = this._parseViewportMetaTag();
+        var defaultLayoutWidth = 980, initialScale = 1;
+        var layoutViewportWidth, layoutViewportHeight;
+        var visualViewPortWidth; // The visual viewport width === the preview width.
+
+        if (settings.width) {
+          if (settings.width === 'device-width') {
+            // Don't scale if the page is marked to be as wide as the device.
+            return false;
+          }
+          else {
+            layoutViewportWidth = parseInt(settings.width, 10);
+          }
+        }
+        else {
+          layoutViewportWidth = defaultLayoutWidth;
+        }
+
+        if (settings.height && settings.height !== 'device-height') {
+          layoutViewportHeight = parseInt(settings.height, 10);
+        }
+
+        if (settings['initial-scale']) {
+          initialScale = parseFloat(settings['initial-scale'], 10);
+          if (initialScale < 1) {
+            layoutViewportWidth = defaultLayoutWidth;
+          }
+        }
+
+        // Calculate the scale, prevent excesses (ensure the (0.25, 1) range).
+        var dimensions = this.model.get('dimensions');
+        // If the preview is rotated, width and height are swapped.
+        visualViewPortWidth = dimensions[(isRotated) ? 'height' : 'width'] / dimensions.dppx;
+        var scale = initialScale * (100 / layoutViewportWidth) * (visualViewPortWidth / 100);
+        scale = Math.min(scale, 1);
+        scale = Math.max(scale, 0.25);
+
+        var transform = "scale(" + scale + ")";
+        var origin = "0 0";
+        return {
+            'min-width': layoutViewportWidth + 'px',
+            'min-height': layoutViewportHeight + 'px',
+            '-webkit-transform': transform,
+                '-ms-transform': transform,
+                    'transform': transform,
+            '-webkit-transform-origin': origin,
+                '-ms-transform-origin': origin,
+                    'transform-origin': origin
+        };
+      },
+
+      /**
+       * Parses <meta name="viewport" /> tag's "content" attribute, if any.
+       *
+       * Parses something like this:
+       *   <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5, minimum-scale=1, user-scalable=yes">
+       * into this:
+       *   {
+       *     width: 'device-width',
+       *     initial-scale: '1',
+       *     maximum-scale: '5',
+       *     minimum-scale: '1',
+       *     user-scalable: 'yes'
+       *   }
+       *
+       * @return Object
+       *   Parsed viewport settings, or {}.
+       */
+      _parseViewportMetaTag: function () {
+        var settings = {};
+        var $viewportMeta = $(document).find('meta[name=viewport][content]');
+        if ($viewportMeta.length > 0) {
+          $viewportMeta
+            .attr('content')
+            // Reduce multiple parts of whitespace to a single space.
+            .replace(/\s+/g, '')
+            // Split on comma (which separates the different settings).
+            .split(',')
+            .map(function (setting) {
+              setting = setting.split('=');
+              settings[setting[0]] = setting[1];
+            });
+        }
+        return settings;
+      }
+    })
+  }
+};
+
+/**
+ * Functions that are common to both the TabView and BlockView.
+ */
+
+/**
+ * Model change handler; hides devices that don't fit the current viewport.
+ */
+function updateDeviceList () {
+  var gutter = this.gutter;
+  var bleed = this.bleed;
+  var viewportWidth = this.envModel.get('viewportWidth');
+  var $devices = this.$el.find('.device');
+  var fittingDeviceCount = $devices.length;
+
+  // Remove devices whose previews won't fit the current viewport.
+  $devices.each(function (index, element) {
+    var $this = $(this);
+    var width = parseInt($this.data('responsive-preview-width'), 10);
+    var dppx = parseFloat($this.data('responsive-preview-dppx'), 10);
+    var previewWidth = width / dppx;
+    var fits = ((previewWidth + (gutter * 2) + (bleed * 2)) <= viewportWidth);
+    if (!fits) {
+      fittingDeviceCount--;
+    }
+    // Set the button to disabled if the device doesn't fit in the current
+    // viewport.
+    // Toggle between the prop() and removeProp() methods.
+    $this.prop('disabled', !fits)
+      .attr('aria-disabled', !fits);
+  });
+  // Set the number of devices that fit the current viewport.
+  this.model.set('fittingDeviceCount', fittingDeviceCount);
+}
+
+/**
+ * Updates the model to reflect the properties of the chosen device.
+ *
+ * @param Object event
+ *   A jQuery event object.
+ */
+function selectDevice (event) {
+  var $link = $(event.target);
+  // Update the device dimensions.
+  this.model.set({
+    'activeDevice': $link.get(0),
+    'dimensions': {
+      'width': parseInt($link.data('responsive-preview-width'), 10),
+      'height': parseInt($link.data('responsive-preview-height'), 10),
+      'dppx': parseFloat($link.data('responsive-preview-dppx'), 10)
+    }
+  });
+  // Toggle the preview on.
+  this.model.set('isActive', true);
+
+  event.preventDefault();
+}
+
+/**
+ * Registers theme templates with Drupal.theme().
+ */
+$.extend(Drupal.theme, {
+  /**
+   * Theme function for the preview container element.
+   *
+   * @return
+   *   The corresponding HTML.
+   */
+  responsivePreviewContainer: function () {
+    return '<div id="responsive-preview" class="responsive-preview"><div class="modal-background"></div></div>';
+  },
+
+  /**
+   * Theme function for the close button for the preview container.
+   *
+   * @return
+   *   The corresponding HTML.
+   */
+  responsivePreviewFrameContainer: function () {
+    return '<div id="responsive-preview-frame-container" class="frame-container">'
+      + '<label id="responsive-preview-frame-label" class="device-label"></label>'
+      + '<button id="responsive-preview-close" role="button" class="icon icon-close control close" aria-pressed="false"><span class="element-invisible"></span></button>'
+      + '<button id="responsive-preview-orientation" role="button" class="icon icon-orientation control orientation" aria-pressed="false"><span class="element-invisible"></span></button>'
+      + '</div>';
+  },
+
+  /**
+   * Theme function for a responsive preview iframe element.
+   *
+   * @return
+   *   The corresponding HTML.
+   */
+  responsivePreviewFrame: function (url) {
+    return '<iframe id="responsive-preview-frame" frameborder="0" scrolling="auto" allowtransparency="true"></iframe>';
+  }
+});
+
+}(jQuery, Backbone, Drupal, drupalSettings));
diff --git a/core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceFormController.php b/core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceFormController.php
new file mode 100644
index 0000000..9c982d2
--- /dev/null
+++ b/core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceFormController.php
@@ -0,0 +1,118 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\responsive_preview\DeviceFormController.
+ */
+
+namespace Drupal\responsive_preview;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityFormController;
+
+/**
+ * Form controller for the device entity edit forms.
+ */
+class DeviceFormController extends EntityFormController {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, array &$form_state, EntityInterface $responsive_preview_device = NULL) {
+    $entity = $this->entity;
+    $form['label'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Device name'),
+      '#default_value' => $entity->label(),
+      '#size' => 30,
+      '#required' => TRUE,
+      '#maxlength' => 64,
+      '#description' => t('The name for this device. Example: "Small", "Medium", "HTC One", "Google Glass", "Smart TV".'),
+    );
+    $form['id'] = array(
+      '#type' => 'machine_name',
+      '#default_value' => $entity->id(),
+      '#required' => TRUE,
+      '#disabled' => !$entity->isNew(),
+      '#size' => 30,
+      '#maxlength' => 64,
+      '#machine_name' => array(
+        'exists' => 'responsive_preview_device_load',
+      ),
+    );
+    $dimensions = $entity->get('dimensions');
+    $form['dimensions'] = array(
+      '#type' => 'details',
+      '#title' => t('Dimensions'),
+      '#tree' => TRUE,
+    );
+    $form['dimensions']['width'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Width'),
+      '#default_value' => $dimensions['width'],
+      '#field_suffix' => 'px',
+      '#size' => 6,
+      '#required' => TRUE,
+    );
+    $form['dimensions']['height'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Height'),
+      '#default_value' => $dimensions['height'],
+      '#field_suffix' => 'px',
+      '#size' => 6,
+      '#required' => TRUE,
+    );
+    $form['dimensions']['dppx'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Dots per pixel (dppx)'),
+      '#default_value' => $dimensions['dppx'],
+      '#size' => 4,
+      '#required' => TRUE,
+    );
+    $form['orientation'] = array(
+      '#type' => 'select',
+      '#title' => t('Default orientation'),
+      '#default_value' => $entity->get('orientation'),
+      '#options' => array('portrait' => t('Portrait'), 'landscape' => t('Landscape')),
+    );
+    $form['status'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Show in preview list'),
+      '#default_value' => $entity->get('status'),
+    );
+    $form['weight'] = array(
+      '#type' => 'value',
+      '#value' => $entity->get('weight'),
+    );
+
+    return parent::form($form, $form_state, $entity);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(array $form, array &$form_state) {
+    $entity = $this->entity;
+
+    // Prevent leading and trailing spaces in device names.
+    $entity->set('label', trim($entity->label()));
+    $uri = $entity->uri();
+    if ($entity->save() == SAVED_UPDATED) {
+      drupal_set_message(t('Device %label has been updated.', array('%label' => $entity->label())));
+      watchdog('responsive_preview', 'Device %label has been updated.', array('%label' => $entity->label()), WATCHDOG_NOTICE, l(t('Edit'), $uri['path']));
+    }
+    else {
+      drupal_set_message(t('Device %label has been added.', array('%label' => $entity->label())));
+      watchdog('responsive_preview', 'Device %label has been added.', array('%label' => $entity->label()), WATCHDOG_NOTICE, l(t('Edit'), $uri['path']));
+    }
+    $form_state['redirect'] = 'admin/config/content/responsive_preview';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function delete(array $form, array &$form_state) {
+    $form_state['redirect'] = 'admin/config/content/responsive_preview/manage/' . $this->entity->id() . '/delete';
+  }
+
+}
diff --git a/core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceInterface.php b/core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceInterface.php
new file mode 100644
index 0000000..7b01572
--- /dev/null
+++ b/core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceInterface.php
@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\responsive_preview\DeviceInterface.
+ */
+
+namespace Drupal\responsive_preview;
+
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+
+/**
+ * Provides an interface defining a device entity.
+ */
+interface DeviceInterface extends ConfigEntityInterface {
+
+}
diff --git a/core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceListController.php b/core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceListController.php
new file mode 100644
index 0000000..ab76894
--- /dev/null
+++ b/core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceListController.php
@@ -0,0 +1,138 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\responsive_preview\DeviceListController.
+ */
+
+namespace Drupal\responsive_preview;
+
+use Drupal\Core\Config\Entity\ConfigEntityListController;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\FormInterface;
+
+/**
+ * Provides a listing of responsive preview devices.
+ */
+class DeviceListController extends ConfigEntityListController implements FormInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormID() {
+    return 'responsive_preview_admin_devices_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildHeader() {
+    $row = parent::buildHeader();
+    unset($row['operations']);
+    $row['label'] = t('Name');
+    $row['dimensions'] = t('Dimensions');
+    $row['orientation'] = t('Default orientation');
+    $row['status'] = t('Show in list');
+    unset($row['id']);
+    $row['weight'] = t('Weight');
+    $row['operations'] = t('Operations');
+    return $row;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildRow(EntityInterface $entity) {
+    $row = parent::buildRow($entity);
+    $operations = $row['operations'];
+    unset($row['operations']);
+
+    // Override default values to markup elements.
+    $row['#attributes']['class'][] = 'draggable';
+    unset($row['id']);
+
+    $row['label'] = array(
+      '#markup' => check_plain($row['label']),
+    );
+    $dimensions = $entity->get('dimensions');
+    $row['dimensions'] = array(
+      '#markup' => check_plain($dimensions['width'] . 'x' . $dimensions['height'] . ' (' . $dimensions['dppx'] . ' dppx)'),
+    );
+    $row['orientation'] = array(
+      '#markup' => $entity->get('orientation') == 'landscape' ? t('Landscape') : t('Portrait'),
+    );
+    $row['status'] = array(
+      '#markup' => $entity->get('status') ? t('yes') : t('no'),
+    );
+    $row['#weight'] = $entity->get('weight');
+    // Add weight column.
+    $row['weight'] = array(
+      '#type' => 'weight',
+      '#title' => t('Weight for @title', array('@title' => $entity->label())),
+      '#title_display' => 'invisible',
+      '#default_value' => $entity->get('weight'),
+      '#attributes' => array('class' => array('weight')),
+    );
+    $row['operations'] = $operations;
+    return $row;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render() {
+    return drupal_get_form($this);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, array &$form_state) {
+    $form['entities'] = array(
+      '#type' => 'table',
+      '#header' => $this->buildHeader(),
+      '#empty' => t('There is no @label yet.', array('@label' => $this->entityInfo['label'])),
+      '#tabledrag' => array(
+        array('order', 'sibling', 'weight'),
+      ),
+    );
+
+    foreach ($this->load() as $entity) {
+      $form['entities'][$entity->id()] = $this->buildRow($entity);
+    }
+
+    $form['actions']['#type'] = 'actions';
+    $form['actions']['submit'] = array(
+      '#type' => 'submit',
+      '#value' => t('Save order'),
+      '#button_type' => 'primary',
+    );
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, array &$form_state) {
+    // No validation.
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, array &$form_state) {
+    $values = $form_state['values']['entities'];
+
+    $entities = entity_load_multiple($this->entityType, array_keys($values));
+    foreach ($values as $id => $value) {
+      if (isset($entities[$id]) && $value['weight'] != $entities[$id]->get('weight')) {
+        // Update changed weight.
+        $entities[$id]->set('weight', $value['weight']);
+        $entities[$id]->save();
+      }
+    }
+
+    drupal_set_message(t('The device settings have been updated.'));
+  }
+}
diff --git a/core/modules/responsive_preview/lib/Drupal/responsive_preview/Form/DeviceDelete.php b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Form/DeviceDelete.php
new file mode 100644
index 0000000..0413b09
--- /dev/null
+++ b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Form/DeviceDelete.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\responsive_preview\Form\DeviceDelete.
+ */
+
+namespace Drupal\responsive_preview\Form;
+
+use Drupal\Core\Form\ConfirmFormBase;
+use Drupal\responsive_preview\DeviceInterface;
+
+/**
+ * Provides a deletion confirmation form for a device entity.
+ */
+class DeviceDelete extends ConfirmFormBase {
+
+  /**
+   * The device being deleted.
+   *
+   * @var \Drupal\responsive_preview\DeviceInterface
+   */
+  protected $device;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormID() {
+    return 'responsive_preview_admin_device_delete_confirm';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getQuestion() {
+    return t('Are you sure you want to delete the device %name?', array('%name' => $this->device->label()));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getCancelPath() {
+    return 'admin/config/content/responsive_preview';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getConfirmText() {
+    return t('Delete');
+  }
+
+  /**
+   * {@inheritdoc}
+   * @param \Drupal\responsive_preview\DeviceInterface $device
+   *   The device being deleted.
+   */
+  public function buildForm(array $form, array &$form_state, DeviceInterface $device = NULL) {
+    $this->device = $device;
+    return parent::buildForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, array &$form_state) {
+    $this->device->delete();
+    watchdog('responsive_preview', 'Device %name has been deleted.', array('%name' => $this->device->label()));
+    drupal_set_message(t('Device %name has been deleted.', array('%name' => $this->device->label())));
+    $form_state['redirect'] = 'admin/config/content/responsive_preview';
+  }
+
+}
diff --git a/core/modules/responsive_preview/lib/Drupal/responsive_preview/Plugin/Block/ResponsivePreviewControlBlock.php b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Plugin/Block/ResponsivePreviewControlBlock.php
new file mode 100644
index 0000000..748a8bf
--- /dev/null
+++ b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Plugin/Block/ResponsivePreviewControlBlock.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\responsive_preview\Plugin\Block\ResponsivePreviewControlBlock.
+ */
+
+namespace Drupal\responsive_preview\Plugin\Block;
+
+use Drupal\block\BlockBase;
+use Drupal\Component\Annotation\Plugin;
+use Drupal\Core\Annotation\Translation;
+
+/**
+ * Provides a 'Responsive preview controls' block.
+ *
+ * @Plugin(
+ *   id = "responsive-preview-controls",
+ *   admin_label = @Translation("Responsive preview controls"),
+ *   module = "responsive_preview"
+ * )
+ */
+class ResponsivePreviewControlBlock extends BlockBase {
+
+  /**
+   * Implements \Drupal\block\BlockBase::blockBuild().
+   */
+  public function blockBuild() {
+    $block = array(
+      'device_options' => array(
+        '#theme' => 'item_list',
+        '#items' => responsive_preview_get_devices_list(),
+        '#attributes' => array(
+          'class' => array('options'),
+        ),
+        '#attached' => array(
+          'library' => array(
+            array('responsive_preview', 'responsive-preview'),
+          ),
+        ),
+      ),
+    );
+
+    return $block;
+  }
+}
diff --git a/core/modules/responsive_preview/lib/Drupal/responsive_preview/Plugin/Core/Entity/Device.php b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Plugin/Core/Entity/Device.php
new file mode 100644
index 0000000..5abbde9
--- /dev/null
+++ b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Plugin/Core/Entity/Device.php
@@ -0,0 +1,97 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\responsive_preview\Plugin\Core\Entity\Device.
+ */
+
+namespace Drupal\responsive_preview\Plugin\Core\Entity;
+
+use Drupal\Core\Entity\Annotation\EntityType;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Config\Entity\ConfigEntityBase;
+
+/**
+ * Defines the responsive preview device entity class.
+ *
+ * @EntityType(
+ *   id = "responsive_preview_device",
+ *   label = @Translation("Responsive preview device"),
+ *   module = "responsive_preview",
+ *   controllers = {
+ *     "storage" = "Drupal\Core\Config\Entity\ConfigStorageController",
+ *     "list" = "Drupal\responsive_preview\DeviceListController",
+ *     "form" = {
+ *       "edit" = "Drupal\responsive_preview\DeviceFormController",
+ *       "add" = "Drupal\responsive_preview\DeviceFormController"
+ *     }
+ *   },
+ *   config_prefix = "responsive_preview.device",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "uuid" = "uuid",
+ *     "label" = "label"
+ *   }
+ * )
+ */
+class Device extends ConfigEntityBase {
+
+  /**
+   * The machine name of this device.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The UUID of this device.
+   *
+   * @var string
+   */
+  public $uuid;
+
+  /**
+   * The human-readable label of this device.
+   *
+   * @var string
+   */
+  public $label;
+
+  /**
+   * The weight of this device in listings.
+   *
+   * @var int
+   */
+  public $weight;
+
+  /**
+   * Default orientation.
+   *
+   * @var string
+   *   Either 'landscape' or 'portrait'.
+   */
+  public $orientation;
+
+  /**
+   * Dimension information.
+   *
+   * @var array
+   *   Associative array with keys 'weight' (int), 'height' (int)
+   *   and 'dppx' (int).
+   */
+  public $dimensions;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function uri() {
+    return array(
+      'path' => 'admin/config/content/responsive_preview/manage/' . $this->id(),
+      'options' => array(
+        'entity_type' => $this->entityType,
+        'entity' => $this,
+      ),
+    );
+  }
+
+}
diff --git a/core/modules/responsive_preview/responsive_preview.info.yml b/core/modules/responsive_preview/responsive_preview.info.yml
new file mode 100644
index 0000000..291176a
--- /dev/null
+++ b/core/modules/responsive_preview/responsive_preview.info.yml
@@ -0,0 +1,7 @@
+name: 'Responsive Preview'
+type: module
+description: 'Provides a component that previews a page in various device dimensions.'
+package: Core
+version: VERSION
+core: 8.x
+configure: admin/config/content/responsive_preview
diff --git a/core/modules/responsive_preview/responsive_preview.module b/core/modules/responsive_preview/responsive_preview.module
new file mode 100644
index 0000000..ddaa9e5
--- /dev/null
+++ b/core/modules/responsive_preview/responsive_preview.module
@@ -0,0 +1,225 @@
+<?php
+
+/**
+ * @file
+ * Provides a component that previews the a page in various device dimensions.
+ */
+
+/**
+ * Implements hook_help().
+ */
+function responsive_preview_help($path, $arg) {
+
+  switch ($path) {
+    case 'admin/help#responsive_preview':
+      $output = '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('The Responsive Preview module provides a quick way to preview a page on your site within the dimensions of many popular device and screen sizes.') . '</p>';
+      $output .= '<h3>' . t('Uses') . '</h3>';
+      $output .= '<p>' . t('To launch a preview, first click the toolbar tab with the small device icon. The tab has the title "@title". A list of devices will appear. Selecting a device name will launch a preview of the current page within the dimensions of that device.', array('@title' => t('Preview page layout'))) . '</p>';
+      $output .= '<p>' . t('To close the preview, click the close button signified visually by an x.') . '</p>';
+      return $output;
+    case 'admin/config/content/responsive_preview':
+      $output = '<p>' . t('Configure the set and order of available devices on this page for responsive site preview. The list of devices is shown in a dropdown accessible from the toolbar tab with a small device icon.') . '</p>';
+      return $output;
+  }
+}
+
+/**
+ * Implements hook_menu().
+ */
+function responsive_preview_menu() {
+  $items['admin/config/content/responsive_preview'] = array(
+    'title' => 'Responsive preview',
+    'description' => 'Configure device listings for content preview.',
+    'route_name' => 'responsive_preview_device_list',
+  );
+  $items['admin/config/content/responsive_preview/add'] = array(
+    'title' => 'Add preview device',
+    'route_name' => 'responsive_preview_device_add',
+    'type' => MENU_LOCAL_ACTION,
+    'weight' => 1,
+  );
+  $items['admin/config/content/responsive_preview/manage/%responsive_preview_device'] = array(
+    'title' => 'Edit device',
+    'route_name' => 'responsive_preview_device_edit',
+  );
+  $items['admin/config/content/responsive_preview/manage/%responsive_preview_device/edit'] = array(
+    'title' => 'Edit',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+  );
+  $items['admin/config/content/responsive_preview/manage/%responsive_preview_device/delete'] = array(
+    'title' => 'Delete',
+    'route_name' => 'responsive_preview_device_delete',
+    'type' => MENU_LOCAL_TASK,
+    'weight' => 10,
+  );
+  return $items;
+}
+/**
+ * Returns a list of devices and their properties from configuration.
+ */
+function responsive_preview_get_devices_list() {
+  $devices = entity_load_multiple('responsive_preview_device');
+  uasort($devices, array('Drupal\responsive_preview\Plugin\Core\Entity\Device', 'sort'));
+
+  $links = array();
+  foreach ($devices as $device) {
+    if ($device->status) {
+      $dimensions = $device->get('dimensions');
+      $links[$device->id()] = array(
+        '#type' => 'html_tag',
+        '#tag' => 'button',
+        '#value' => $device->label(),
+        '#attributes' => array(
+          'class' => array('device', 'icon', 'icon-active'),
+          'data-responsive-preview-name' => $device->id(),
+          'data-responsive-preview-width' => (!empty($dimensions['width'])) ? $dimensions['width'] : '',
+          'data-responsive-preview-height' => (!empty($dimensions['height'])) ? $dimensions['height'] : '',
+          'data-responsive-preview-dppx' => (!empty($dimensions['dppx'])) ? $dimensions['dppx'] : '1',
+        ),
+      );
+    }
+  }
+
+  return $links;
+}
+
+/**
+ * Fetches a responsive preview device by ID.
+ *
+ * @param string $id
+ *   A string representing the device ID (machine name).
+ *
+ * @return
+ *   A fully-loaded device object if a device with the given ID exists,
+ *   or FALSE otherwise.
+ */
+function responsive_preview_device_load($id) {
+  return entity_load('responsive_preview_device', $id);
+}
+
+/**
+ * Prevents the preview tab from rendering on administration pages.
+ */
+function responsive_preview_access() {
+  return !path_is_admin(current_path());
+}
+
+/**
+ * Implements hook_toolbar().
+ */
+function responsive_preview_toolbar() {
+  $items['responsive_preview'] = array(
+    '#type' => 'toolbar_item',
+    'tab' => array(
+      'trigger' => array(
+        '#type' => 'html_tag',
+        '#tag' => 'button',
+        '#value' => t('Layout preview'),
+        '#value_prefix' => '<span class="element-invisible">',
+        '#value_suffix' => '</span>',
+        '#attributes' => array(
+          'title' => t('Preview page layout'),
+          'class' => array('icon', 'icon-responsive-preview', 'trigger'),
+        ),
+      ),
+      'device_options' => array(
+        '#theme' => 'item_list',
+        '#items' => responsive_preview_get_devices_list(),
+        '#attributes' => array(
+          'class' => array('options'),
+        ),
+      ),
+    ),
+    '#wrapper_attributes' => array(
+      'id' => 'responsive-preview-toolbar-tab',
+      'class' => array('toolbar-tab-responsive-preview'),
+    ),
+    '#attached' => array(
+      'library' => array(
+        array('responsive_preview', 'responsive-preview'),
+      ),
+    ),
+    '#weight' => 200,
+    '#access' => responsive_preview_access(),
+  );
+
+  return $items;
+}
+
+/**
+ * Implements hook_library().
+ */
+function responsive_preview_library_info() {
+  $path = drupal_get_path('module', 'responsive_preview');
+  $options = array(
+    'scope' => 'footer',
+    'attributes' => array('defer' => TRUE),
+  );
+
+  $libraries['responsive-preview'] = array(
+    'title' => 'Preview layouts',
+    'version' => VERSION,
+    'css' => array(
+      $path . '/css/responsive-preview.base.css',
+      $path . '/css/responsive-preview.theme.css',
+      $path . '/css/responsive-preview.icons.css',
+    ),
+    'js' => array(
+      $path . '/js/responsive-preview.js' => $options,
+    ),
+    'dependencies' => array(
+      array('system', 'jquery'),
+      array('system', 'drupal'),
+      array('system', 'backbone'),
+      array('system', 'jquery.ui.position'),
+    ),
+  );
+
+  return $libraries;
+}
+
+/**
+ * Implements hook_testswarm_tests().
+ */
+function responsive_preview_testswarm_tests() {
+
+  $path = drupal_get_path('module', 'responsive_preview');
+
+  return array(
+    'responsivePreview' => array(
+      'module' => 'responsive_preview',
+      'description' => 'Test the responsive preview module.',
+      'js' => array(
+        $path . '/tests/testswarm/responsive_preview.tests.js' => array(),
+        array(
+          'data' => array(
+            'responsive_preview' => array(
+              'devices' => config('responsive_preview.devices')->get()
+            ),
+          ),
+          'type' => 'setting',
+        ),
+      ),
+      'dependencies' => array(
+        array('system', 'jquery'),
+        array('system', 'drupalSettings'),
+        array('testswarm', 'jquery.simulate'),
+      ),
+      'path' => '',
+      'permissions' => array()
+    ),
+    'responsivePreviewAdmin' => array(
+      'module' => 'responsive_preview',
+      'description' => 'Test the responsive preview module admin.',
+      'js' => array(
+        $path . '/tests/testswarm/responsive_preview.admin.tests.js' => array(),
+      ),
+      'dependencies' => array(
+        array('system', 'jquery'),
+      ),
+      'path' => 'admin',
+      'permissions' => array()
+    ),
+  );
+}
diff --git a/core/modules/responsive_preview/responsive_preview.routing.yml b/core/modules/responsive_preview/responsive_preview.routing.yml
new file mode 100644
index 0000000..6a89138
--- /dev/null
+++ b/core/modules/responsive_preview/responsive_preview.routing.yml
@@ -0,0 +1,35 @@
+responsive_preview_device_list:
+  pattern: '/admin/config/content/responsive_preview'
+  defaults:
+    _content: '\Drupal\Core\Entity\Controller\EntityListController::listing'
+    entity_type: 'responsive_preview_device'
+  requirements:
+    _permission: 'administer site configuration'
+
+responsive_preview_device_add:
+  pattern: '/admin/config/content/responsive_preview/add'
+  defaults:
+    _entity_form: responsive_preview_device.add
+  requirements:
+    _permission: 'administer site configuration'
+
+responsive_preview_device_edit:
+  pattern: '/admin/config/content/responsive_preview/manage/{responsive_preview_device}'
+  defaults:
+    _entity_form: responsive_preview_device.edit
+  requirements:
+    _permission: 'administer site configuration'
+
+responsive_preview_device_edit_tab:
+  pattern: '/admin/config/content/responsive_preview/manage/{responsive_preview_device}/edit'
+  defaults:
+    _entity_form: responsive_preview_device.edit
+  requirements:
+    _permission: 'administer site configuration'
+
+responsive_preview_device_delete:
+  pattern: '/admin/config/content/responsive_preview/manage/{responsive_preview_device}/delete'
+  defaults:
+    _form: '\Drupal\responsive_preview\Form\DeviceDelete'
+  requirements:
+    _permission: 'administer site configuration'
diff --git a/core/modules/responsive_preview/tests/testswarm/responsive_preview.admin.tests.js b/core/modules/responsive_preview/tests/testswarm/responsive_preview.admin.tests.js
new file mode 100644
index 0000000..b0466e9
--- /dev/null
+++ b/core/modules/responsive_preview/tests/testswarm/responsive_preview.admin.tests.js
@@ -0,0 +1,27 @@
+/*jshint strict:true, browser:true, curly:true, eqeqeq:true, expr:true, forin:true, latedef:true, newcap:true, noarg:true, trailing: true, undef:true, unused:true */
+/*global Drupal: true, jQuery: true, QUnit:true*/
+(function ($, Drupal, drupalSettings, window, document, undefined) {
+  "use strict";
+  Drupal.tests.responsivePreviewAdmin = {
+    getInfo: function() {
+      return {
+        name: 'Responsive Preview',
+        description: 'Tests for the responsive preview admin.',
+        group: 'Core'
+      };
+    },
+    setup: function () {},
+    teardown: function () {},
+    tests: {
+      toolbarTab: function ($, Drupal, window, document, undefined) {
+        return function() {
+          QUnit.expect(1);
+
+          // The toolbar tab should not be present on an admin path.
+          var $tab = $('.toolbar .toolbar-tab-responsive-preview');
+          QUnit.equal($tab.length, 0, Drupal.t('The tab is not present.'));
+        };
+      }
+    }
+  };
+})(jQuery, Drupal, drupalSettings, this, this.document);
diff --git a/core/modules/responsive_preview/tests/testswarm/responsive_preview.tests.js b/core/modules/responsive_preview/tests/testswarm/responsive_preview.tests.js
new file mode 100644
index 0000000..f3f66cc
--- /dev/null
+++ b/core/modules/responsive_preview/tests/testswarm/responsive_preview.tests.js
@@ -0,0 +1,69 @@
+/*jshint strict:true, browser:true, curly:true, eqeqeq:true, expr:true, forin:true, latedef:true, newcap:true, noarg:true, trailing: true, undef:true, unused:true */
+/*global Drupal: true, jQuery: true, QUnit:true*/
+(function ($, Drupal, drupalSettings, window, document, undefined) {
+  "use strict";
+  Drupal.tests.responsivePreview = {
+    getInfo: function() {
+      return {
+        name: 'Responsive Preview',
+        description: 'Tests for the responsive preview feature.',
+        group: 'Core'
+      };
+    },
+    setup: function () {},
+    teardown: function () {
+      // Close the preview container.
+      $('#responsive-preview-close').trigger('click');
+    },
+    tests: {
+      toolbarTab: function ($, Drupal, window, document, undefined) {
+        return function() {
+          QUnit.expect(3);
+
+          // Find the toolbar tab.
+          var $tab = $('.toolbar .toolbar-tab-responsive-preview');
+          QUnit.equal($tab.length, 1, Drupal.t('The tab is present.'));
+
+          // Verify the tab dropdown click functionality.
+          $tab.find('> .trigger').trigger('click');
+          QUnit.ok($tab.hasClass('open'), Drupal.t('The tab dropdown list opens.'));
+
+          // Verify the number of devices in the list.
+          var devices = drupalSettings.responsive_preview.devices;
+          var count = 0;
+          for (var device in devices) {
+            if (devices.hasOwnProperty(device)) {
+              count++;
+            }
+          }
+          var $devices = $tab.find('.options .device');
+          QUnit.equal($devices.length, count, Drupal.t('The correct number of devices are listed.'));
+        };
+      },
+      previewLaunch: function ($, Drupal, window, document, undefined) {
+        return function () {
+          QUnit.expect(3);
+
+          // Find the toolbar tab.
+          var $tab = $('.toolbar .toolbar-tab-responsive-preview');
+          // Verify that the responsive preview container is not been built yet.
+          var $container = $('#responsive-preview');
+          QUnit.equal($container.length, 0, Drupal.t('The preview container does not exist yet.'));
+
+          // Verify that clicking a device link activates the preview container.
+          $tab.find('.options .device').first().trigger('click');
+          QUnit.stop();
+          window.setTimeout(function () {
+            $container = $('#responsive-preview');
+            // Verify that the preview container exists.
+            QUnit.equal($container.length, 1, Drupal.t('The preview container exists.'));
+
+            // Verify that preview container is active.
+            QUnit.ok($container.hasClass('active'), Drupal.t('The preview container is active.'));
+            QUnit.start();
+          }, 500);
+        };
+      }
+    }
+  };
+})(jQuery, Drupal, drupalSettings, this, this.document);
