diff --git a/core/modules/contextual/contextual.toolbar.js b/core/modules/contextual/contextual.toolbar.js
index 8a1ece7..f253ebd 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.ipad.yml b/core/modules/responsive_preview/config/responsive_preview.device.ipad.yml
new file mode 100644
index 0000000..6795ad8
--- /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: 5
+status: 0
+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..07ca7b7
--- /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: 4
+status: 0
+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..e38d0f0
--- /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: 3
+status: 0
+langcode: en
diff --git a/core/modules/responsive_preview/config/responsive_preview.device.large.yml b/core/modules/responsive_preview/config/responsive_preview.device.large.yml
new file mode 100644
index 0000000..97bab7f
--- /dev/null
+++ b/core/modules/responsive_preview/config/responsive_preview.device.large.yml
@@ -0,0 +1,10 @@
+id: large
+label: Typical desktop
+dimensions:
+  width: 1366
+  height: 768
+  dppx: 1
+orientation: landscape
+weight: 2
+status: 0
+langcode: en
diff --git a/core/modules/responsive_preview/config/responsive_preview.device.medium.yml b/core/modules/responsive_preview/config/responsive_preview.device.medium.yml
new file mode 100644
index 0000000..467ba82
--- /dev/null
+++ b/core/modules/responsive_preview/config/responsive_preview.device.medium.yml
@@ -0,0 +1,10 @@
+id: medium
+label: Tablet
+dimensions:
+  width: 800
+  height: 1280
+  dppx: 1.325
+orientation: portrait
+weight: 1
+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..2f90b24
--- /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: 6
+status: 0
+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..2affe9b
--- /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: 7
+status: 0
+langcode: en
diff --git a/core/modules/responsive_preview/config/responsive_preview.device.small.yml b/core/modules/responsive_preview/config/responsive_preview.device.small.yml
new file mode 100644
index 0000000..3e63537
--- /dev/null
+++ b/core/modules/responsive_preview/config/responsive_preview.device.small.yml
@@ -0,0 +1,10 @@
+id: small
+label: Smart phone
+dimensions:
+  width: 768
+  height: 1280
+  dppx: 2
+orientation: portrait
+weight: 0
+status: 1
+langcode: en
diff --git a/core/modules/responsive_preview/config/schema/responsive_preview.schema.yml b/core/modules/responsive_preview/config/schema/responsive_preview.schema.yml
new file mode 100644
index 0000000..2b1a23d
--- /dev/null
+++ b/core/modules/responsive_preview/config/schema/responsive_preview.schema.yml
@@ -0,0 +1,40 @@
+# Schema for the configuration files of the Responsive preview module.
+
+responsive_preview.device.*:
+  type: mapping
+  label: 'Responsive preview device'
+  mapping:
+    id:
+      type: string
+      label: 'Device ID'
+    uuid:
+      type: string
+      label: 'UUID'
+    label:
+      type: label
+      label: 'Device name'
+    weight:
+      type: integer
+      label: 'Device weight'
+    status:
+      type: integer
+      label: 'Show in preview list'
+    orientation:
+      type: string
+      label: 'Default orientation'
+    dimensions:
+      type: mapping
+      label: 'Dimensions'
+      mapping:
+        width:
+          type: integer
+          label: 'Width'
+        height:
+          type: integer
+          label: 'Height'
+        dppx:
+          type: float
+          label: 'Dots per pixel (dppx)'
+    langcode:
+      type: string
+      label: 'Default language'
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..5060d1d
--- /dev/null
+++ b/core/modules/responsive_preview/css/responsive-preview.icons.css
@@ -0,0 +1,130 @@
+/**
+ * @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;
+}
+[dir="rtl"] .toolbar .bar .toolbar-tab-responsive-preview .icon-responsive-preview:before {
+  left: auto; /* LTR */
+  right: 1em;
+}
+.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;
+  height: 14px;
+  left: 3px; /* LTR */
+  top: 0.5em;
+  width: 13px;
+}
+[dir="rtl"] .toolbar .toolbar-tab-responsive-preview.tab .options .device.icon-active:before {
+  left: auto;
+  right: 6px;
+}
+.toolbar .toolbar-tab-responsive-preview.tab .options .device.icon-active.active:before {
+  background-position: center -116px;
+}
+@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; /* LTR */
+    text-indent: 0;
+    width: auto;
+  }
+  [dir="rtl"] .toolbar .toolbar-tab-responsive-preview.tab .options .device.icon-active {
+    padding: 0.5em  2.25em 0.5em 1.3333em;
+    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 */
+}
+[dir="rtl"] .responsive-preview .icon-close:before {
+  left: 9px;
+  right: auto;
+}
+.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; /* LTR */
+  left: 9px; /* LTR */
+}
+[dir="rtl"] .responsive-preview .icon-orientation:before {
+  background-position: left -155px;
+  left: auto;
+  right: 9px;
+}
+.responsive-preview .icon-orientation:hover:before {
+  background-position: left -104px; /* LTR */
+}
+[dir="rtl"] .responsive-preview .icon-orientation:hover:before {
+  background-position: left -167px;
+}
+.responsive-preview .icon-orientation.rotated:before {
+  background-position: left -68px; /* LTR */
+}
+[dir="rtl"] .responsive-preview .icon-orientation.rotated:before {
+  background-position: left -131px;
+}
+.responsive-preview .icon-orientation.rotated:hover:before {
+  background-position: left -80px; /* LTR */
+}
+[dir="rtl"] .responsive-preview .icon-orientation.rotated:hover:before {
+  background-position: left -143px;
+}
diff --git a/core/modules/responsive_preview/css/responsive-preview.module.css b/core/modules/responsive_preview/css/responsive-preview.module.css
new file mode 100644
index 0000000..9b20d70
--- /dev/null
+++ b/core/modules/responsive_preview/css/responsive-preview.module.css
@@ -0,0 +1,116 @@
+/**
+ * @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;
+}
+[dir="rtl"].js .toolbar .bar .toolbar-tab-responsive-preview.tab {
+  float: left;
+}
+.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;
+}
+[dir="rtl"] .responsive-preview {
+  left: auto;
+  right: -200%;
+}
+.responsive-preview.active {
+  left: 0; /* LTR */
+  position: fixed;
+}
+[dir="rtl"] .responsive-preview.active {
+  left: auto;
+  right: 0;
+}
+.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.theme.css b/core/modules/responsive_preview/css/responsive-preview.theme.css
new file mode 100644
index 0000000..637e4bd
--- /dev/null
+++ b/core/modules/responsive_preview/css/responsive-preview.theme.css
@@ -0,0 +1,228 @@
+/**
+ * @file
+ * Styling for responsive preview.
+ */
+
+/**
+ * Toolbar tab.
+ */
+.toolbar-tab-responsive-preview .options {
+  background-color: white;
+}
+/* Device preview options. */
+.toolbar-tab-responsive-preview .options {
+  box-shadow: 0 0.8em 2.5em -0.8em rgba(0, 0, 0, 0.75);
+}
+/* [dir] is needed to override Bartik's .item-list li padding */
+[dir] .toolbar-tab-responsive-preview .options li {
+  margin: 0;
+  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;
+}
+/* Configuration link. */
+.toolbar-tab-responsive-preview.tab .configure {
+  border-top: 1px solid #000;
+  color: #000;
+  margin-top: 0.5em;
+}
+
+/* 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
+}
+[dir="rtl"] .toolbar-tab-responsive-preview .trigger:after {
+  left: 1em;
+  right: auto;
+}
+.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;
+}
+[dir="rtl"] .toolbar-tab-responsive-preview.open:before {
+  left: 0;
+  right: auto;
+}
+.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;
+}
+[dir="rtl"] .toolbar-tab-responsive-preview.open .trigger:after {
+  left: 0.7em;
+  right: auto;
+}
+.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, rgb(20,20,20),rgb(50,50,50) 25%, rgb(100,100,100) 40%, rgb(100,100,100) 60%, rgb(50,50,50) 85%, rgb(20,20,20));
+  background-image: linear-gradient(left, rgb(20,20,20),rgb(50,50,50) 25%, rgb(100,100,100) 40%, rgb(100,100,100) 60%, rgb(50,50,50) 85%, rgb(20,20,20));
+}
+
+/**
+ * Responsive preview control placement.
+ */
+.responsive-preview .control {
+  cursor: pointer;
+  height: 40px;
+  position: absolute;
+  top: 0;
+  width: 40px;
+}
+.responsive-preview .control.close {
+  right: 0; /* LTR */
+}
+[dir="rtl"] .responsive-preview .control.close {
+  left: 0;
+  right: auto;
+}
+.responsive-preview .control.orientation {
+  left: 0; /* LTR */
+}
+[dir="rtl"] .responsive-preview .control.orientation {
+  left: auto;
+  right: 0;
+}
+.responsive-preview .device-label {
+  color: #bbbbbb;
+  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: #212121;
+  border-radius: 20px;
+  box-shadow:
+    0 0 0px 1px #777,
+    1px 1px 60px 0px #000;
+  -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 1px #808080;
+  -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..b30de7e
--- /dev/null
+++ b/core/modules/responsive_preview/images/responsive-preview-icons.png
@@ -0,0 +1,13 @@
+PNG
+
+   IHDR         R+y  nIDATh͘[J+A{	,a%d	YB|(DDD1,!K5h.驮=~B&Mj9;;YA)ԕ999Q\{||(DGGG@{xx(D@(D{{{@(D Pbwvv (v{{Pb (vssPb766 (v}}Pb (vuu}`TXgw
+nGVPC# SwM'ԕ f)v>JQ;4žJQ+4ž JQ34>==JQ#4><< JQl6{@i [@i k@i 2hns0[gws~~>*u ,=>1O	 ȁ (TFcR^k֩S>!3JiLVF{p"uJ{ξ>P5ϴhL+b-:u8(>M28؊.Q`2%3DT1&Ĵ0vC)11:X=TjA1&hTЯJ؉Uso/s1bU.'Js,[bZ!Ɣ:0#;{cmFW	˽FW܋]+P̕Ju%m7M]ɇԮ0O$օX*|[AR%l,{+!x(JQHr*p%RSJ1H='qBVܰU{BYCvMOd1P$Ӊukc
+U,N)VX$bAr
+IXr
+QPn g1Od屍=k/Un
+n)옼=kcP.nS
+ql^n(vWThNXXi,	ݤ+
+n*Ie;Z
+A,S
+15Qpo(Q@RU
+MR@J.Db%!hJ.$H#,cǣrǣJSئ]H.@\2QѨ),	pxQ4㑘{oΏ*    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..fe6083c
--- /dev/null
+++ b/core/modules/responsive_preview/js/responsive-preview.js
@@ -0,0 +1,904 @@
+/**
+ * @file
+ * Provides a component that previews the page in various device dimensions.
+ */
+
+(function ($, Backbone, Drupal, drupalSettings, undefined) {
+
+"use strict";
+
+var options = $.extend({
+  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')
+  }
+}, drupalSettings.responsivePreview || {});
+
+var currentPath;
+
+/**
+ * Attaches behaviors to the toolbar tab and preview containers.
+ */
+Drupal.behaviors.responsivePreview = {
+  attach: function (context) {
+    // jQuery.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) {
+      // 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 = Drupal.responsivePreview.models.envModel = new Drupal.responsivePreview.EnvironmentModel({
+        dir: document.getElementsByTagName('html')[0].getAttribute('dir')
+      });
+      var tabModel = Drupal.responsivePreview.models.tabModel = new Drupal.responsivePreview.TabStateModel();
+      var previewModel = Drupal.responsivePreview.models.previewModel = new Drupal.responsivePreview.PreviewStateModel();
+
+      // Manages the PreviewView.
+      Drupal.responsivePreview.views.appView = new Drupal.responsivePreview.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) {
+        Drupal.responsivePreview.views.tabView = new Drupal.responsivePreview.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) {
+        Drupal.responsivePreview.views.blockView = new Drupal.responsivePreview.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.
+      Drupal.responsivePreview.views.keyboardView = new Drupal.responsivePreview.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);
+          }
+        })
+        // Close the preview if the overlay is opened.
+        .on('drupalOverlayOpen.responsivepreview', function () {
+          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) {
+    /**
+     * Loops through object properties; applies a callback function.
+     */
+    function looper (obj, iterator) {
+      for (var prop in obj) {
+        if (obj.hasOwnProperty(prop)) {
+          iterator.call(null, prop, obj[prop]);
+        }
+      }
+    }
+    // Remove listeners on the window and document.
+    $(window).add(document).off('.responsivepreview');
+    // Set the preview to an inactive state.
+    Drupal.responsivePreview.models.previewModel.set('isActive', false);
+    // Remove and delete the views.
+    looper(Drupal.responsivePreview.views, function (label, view) {
+      Drupal.responsivePreview.views[label].remove();
+      Drupal.responsivePreview.views[label] = undefined;
+    });
+    // Remove listeners and delete the models.
+    looper(Drupal.responsivePreview.models, function (label, model) {
+      Drupal.responsivePreview.models[label].off();
+      Drupal.responsivePreview.models[label] = undefined;
+    });
+  }
+};
+
+Drupal.responsivePreview = Drupal.responsivePreview || {
+
+  // Storage for view instances.
+  views: {},
+
+  // Storage for model instances.
+  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
+      }
+    }
+  }),
+
+  /**
+   * 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.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 .trigger': '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 jQuery.Event event
+     */
+    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 jQuery.Event event
+     */
+    onClose: function (event) {
+      this.model.set('isActive', false);
+    },
+
+    /**
+     * Responds to rotation button presses.
+     *
+     * @param jQuery.Event event
+     */
+    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 jQuery.Event event
+ */
+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="visually-hidden"></span></button>'
+      + '<button id="responsive-preview-orientation" role="button" class="icon icon-orientation control orientation" aria-pressed="false"><span class="visually-hidden"></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/DeviceAccessController.php b/core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceAccessController.php
new file mode 100644
index 0000000..eacbc49
--- /dev/null
+++ b/core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceAccessController.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\responsive_preview\CommentAccessController
+ */
+
+namespace Drupal\responsive_preview;
+
+use Drupal\Core\Entity\EntityAccessController;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Access controller for the Device entity.
+ *
+ * @see \Drupal\responsive_preview\Plugin\Core\Entity\Device.
+ */
+class DeviceAccessController extends EntityAccessController {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) {
+    switch ($operation) {
+      case 'update':
+        return user_access('administer site configuration', $account);
+        break;
+      case 'delete':
+        return user_access('administer site configuration', $account);
+        break;
+    }
+  }
+
+}
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..4ef993b
--- /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..41cce61
--- /dev/null
+++ b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Form/DeviceDelete.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\responsive_preview\Form\DeviceDelete.
+ */
+
+namespace Drupal\responsive_preview\Form;
+
+use Drupal\Core\Entity\EntityConfirmFormBase;
+use Drupal\responsive_preview\DeviceInterface;
+
+/**
+ * Provides a deletion confirmation form for a device entity.
+ */
+class DeviceDelete extends EntityConfirmFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    return t('Are you sure you want to delete the device %name?', array('%name' => $this->entity->label()));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelPath() {
+    return 'admin/config/content/responsive-preview';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return t('Delete');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submit(array $form, array &$form_state) {
+    $this->entity->delete();
+    watchdog('responsive_preview', 'Device %name has been deleted.', array('%name' => $this->entity->label()));
+    drupal_set_message(t('Device %name has been deleted.', array('%name' => $this->entity->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..7e20a51
--- /dev/null
+++ b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Plugin/Core/Entity/Device.php
@@ -0,0 +1,99 @@
+<?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 = {
+ *     "access" = "Drupal\responsive_preview\DeviceAccessController",
+ *     "storage" = "Drupal\Core\Config\Entity\ConfigStorageController",
+ *     "list" = "Drupal\responsive_preview\DeviceListController",
+ *     "form" = {
+ *       "edit" = "Drupal\responsive_preview\DeviceFormController",
+ *       "add" = "Drupal\responsive_preview\DeviceFormController",
+ *       "delete" = "Drupal\responsive_preview\Form\DeviceDelete"
+ *     }
+ *   },
+ *   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/lib/Drupal/responsive_preview/Tests/DeviceCRUDTest.php b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Tests/DeviceCRUDTest.php
new file mode 100644
index 0000000..d789b2d
--- /dev/null
+++ b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Tests/DeviceCRUDTest.php
@@ -0,0 +1,90 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\responsive_preview\Tests\DeviceCRUDTest.
+ */
+
+namespace Drupal\responsive_preview\Tests;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests the device listing.
+ */
+class DeviceCRUDTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('toolbar', 'responsive_preview');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Responsive preview',
+      'description' => 'Tests device management functionality.',
+      'group' => 'Responsive preview',
+    );
+  }
+
+  /**
+   * Tests configuring devices.
+   */
+  function testDeviceConfiguration() {
+    // Create and login administrative user.
+    $admin_user = $this->drupalCreateUser(array(
+      'administer site configuration',
+      'access toolbar',
+    ));
+    $this->drupalLogin($admin_user);
+
+    $this->drupalGet('admin/config/content/responsive-preview');
+
+    // Some default devices exist.
+    $this->assertLinkByHref('admin/config/content/responsive-preview/manage/large/delete');
+    $this->assertLinkByHref('admin/config/content/responsive-preview/manage/ipad/delete');
+
+    // Generic devices are shown by default.
+    $this->drupalGet('');
+    $this->checkDevices(array('medium', 'small'));
+
+    // Delete one of the predefined generic items.
+    $this->drupalPost('admin/config/content/responsive-preview/manage/medium/delete', array(), t('Delete'));
+    $this->assertRaw(t('Device %name has been deleted.', array('%name' => 'Tablet')));
+
+    // Make iPad appear in the list.
+    $this->drupalPost('admin/config/content/responsive-preview/manage/ipad', array('status' => 1), t('Save'));
+    $this->assertRaw(t('Device %name has been updated.', array('%name' => 'iPad')));
+
+    // Add a new device as well.
+    $edit = array(
+      'label' => 'Smartwatch',
+      'id' => 'smartwatch',
+      'dimensions[width]' => '200',
+      'dimensions[height]' => '350',
+      'dimensions[dppx]' => '3',
+      'status' => 1,
+    );
+    $this->drupalPost('admin/config/content/responsive-preview/add', $edit, t('Save'));
+    $this->assertRaw(t('Device %name has been added.', array('%name' => 'Smartwatch')));
+
+    // Check updated device list applied.
+    $this->drupalGet('');
+    $this->checkDevices(array('smartwatch', 'ipad', 'small'));
+  }
+
+  /**
+   * Tests exposed devices in the responsive preview list.
+   */
+  private function checkDevices(array $devices) {
+    foreach ($devices as $name) {
+      $device_button = $this->xpath('//button[@data-responsive-preview-name=:name]', array(
+        ':name' => $name
+      ));
+      $this->assertTrue(!empty($device_button), format_string('%name device shown by default', array('%name' => $name)));
+    }
+  }
+
+}
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..5f9292e
--- /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..f92f483
--- /dev/null
+++ b/core/modules/responsive_preview/responsive_preview.module
@@ -0,0 +1,236 @@
+<?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',
+        ),
+      );
+    }
+  }
+  // Add a configuration link.
+  $links['configure_link'] = array(
+    '#type' => 'link',
+    '#title' => t('Configure devices'),
+    '#href' => url('admin/config/content/responsive-preview'),
+    '#options' => array(
+      'attributes' => array(
+        'class' => array('configure'),
+      ),
+    ),
+  );
+
+  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="visually-hidden">',
+        '#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.module.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..dcf7e63
--- /dev/null
+++ b/core/modules/responsive_preview/responsive_preview.routing.yml
@@ -0,0 +1,28 @@
+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_delete:
+  pattern: '/admin/config/content/responsive-preview/manage/{responsive_preview_device}/delete'
+  defaults:
+    _entity_form: responsive_preview_device.delete
+  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);
