diff --git a/core/composer.json b/core/composer.json
index 3109649..c7ca429 100644
--- a/core/composer.json
+++ b/core/composer.json
@@ -111,6 +111,7 @@
         "drupal/migrate_drupal_ui": "self.version",
         "drupal/node": "self.version",
         "drupal/options": "self.version",
+        "drupal/outside_in": "self.version",
         "drupal/page_cache": "self.version",
         "drupal/path": "self.version",
         "drupal/quickedit": "self.version",
diff --git a/core/modules/outside_in/css/outside_in.base.css b/core/modules/outside_in/css/outside_in.base.css
new file mode 100644
index 0000000..e7e60f0
--- /dev/null
+++ b/core/modules/outside_in/css/outside_in.base.css
@@ -0,0 +1,184 @@
+/**
+ * @file
+ * Resets for Outside-In module.
+ */
+
+/**
+ * Resets
+ * inside the wrapper override font styling the page provides, this may look a bit extreme
+ * but any module may place any new form element in this tray therefore CSS from any
+ * front-end-theme needs to be overridden
+ */
+#offcanvas abbr,
+#offcanvas acronym,
+#offcanvas address,
+#offcanvas article,
+#offcanvas aside,
+#offcanvas audio,
+#offcanvas b,
+#offcanvas caption,
+#offcanvas center,
+#offcanvas cite,
+#offcanvas code,
+#offcanvas dd,
+#offcanvas del,
+#offcanvas details,
+#offcanvas dfn,
+#offcanvas dialog,
+#offcanvas div,
+#offcanvas dl,
+#offcanvas dt,
+#offcanvas em,
+#offcanvas embed,
+#offcanvas fieldset,
+#offcanvas figcaption,
+#offcanvas figure,
+#offcanvas font,
+#offcanvas footer,
+#offcanvas form,
+#offcanvas h1,
+#offcanvas h2,
+#offcanvas h3,
+#offcanvas h4,
+#offcanvas h5,
+#offcanvas h6,
+#offcanvas header,
+#offcanvas hgroup,
+#offcanvas i,
+#offcanvas img,
+#offcanvas ins,
+#offcanvas kbd,
+#offcanvas label,
+#offcanvas legend,
+#offcanvas li,
+#offcanvas mark,
+#offcanvas menu,
+#offcanvas meter,
+#offcanvas nav,
+#offcanvas object,
+#offcanvas ol,
+#offcanvas output,
+#offcanvas p,
+#offcanvas pre,
+#offcanvas progress,
+#offcanvas q,
+#offcanvas rp,
+#offcanvas rt,
+#offcanvas ruby,
+#offcanvas s,
+#offcanvas samp,
+#offcanvas section,
+#offcanvas small,
+#offcanvas span,
+#offcanvas strike,
+#offcanvas strong,
+#offcanvas sub,
+#offcanvas summary,
+#offcanvas sup,
+#offcanvas table,
+#offcanvas tbody,
+#offcanvas td,
+#offcanvas tfoot,
+#offcanvas th,
+#offcanvas thead,
+#offcanvas time,
+#offcanvas tr,
+#offcanvas tt,
+#offcanvas u,
+#offcanvas ul,
+#offcanvas var,
+#offcanvas video {
+  font-family: 'Lucida Grande','Lucida Sans Unicode','Liberation Sans',sans-serif;
+  color: #eee;
+  font-size: 13px;
+  font-weight: normal;
+  font-style: normal;
+  line-height: 1.5em;
+  text-transform: none;
+  text-align: left; /* LTR */
+  text-indent: 0;
+  padding:0;
+}
+/* Reset font family rules from formalize to match Drupal (seven theme) */
+#offcanvas ::-webkit-validation-bubble-message,
+#offcanvas optgroup,
+#offcanvas .ie6_input,
+#offcanvas * html textarea,
+#offcanvas * html select,
+#offcanvas textarea,
+#offcanvas select,
+#offcanvas button,
+#offcanvas a.button,
+#offcanvas input[type="date"],
+#offcanvas input[type="datetime"],
+#offcanvas input[type="datetime-local"],
+#offcanvas input[type="email"],
+#offcanvas input[type="month"],
+#offcanvas input[type="number"],
+#offcanvas input[type="password"],
+#offcanvas input[type="search"],
+#offcanvas input[type="tel"],
+#offcanvas input[type="text"],
+#offcanvas input[type="time"],
+#offcanvas input[type="url"],
+#offcanvas input[type="week"],
+#offcanvas input[type="reset"],
+#offcanvas input[type="submit"],
+#offcanvas input[type="button"]  {
+  font-family: 'Lucida Grande','Lucida Sans Unicode','Liberation Sans',sans-serif;
+}
+/* Reset the link color to one that stands out on dark background */
+#offcanvas a {
+  color: #33aaff;
+  text-decoration: none;
+}
+/* Reset to sensible vertical rhythm */
+#offcanvas .form-item,
+#offcanvas details,
+#offcanvas button,
+.form-type-checkbox,
+.form-type-radio {
+  margin-top: 0;
+  margin-bottom: 1.25em;
+  padding-right: .5em; /* LTR */
+  display: inline-block;
+}
+/* Re-set defaults for fieldsets */
+#offcanvas .fieldset-wrapper {
+  margin: 0;
+  padding: 0;
+  border: 0;
+}
+/* Override formalize label styling */
+#offcanvas label {
+  font-size: 85%;
+  font-weight: bold;
+  line-height: 1.5em;
+}
+/* Override formalize button styling */
+#offcanvas button,
+#offcanvas .button,
+#offcanvas input[type="button"],
+#offcanvas input[type="reset"] {
+  font-weight: normal;
+  text-shadow: none;
+  width: auto;
+  max-width: 50%;
+  color: #fff;
+  border-radius: 30px;
+  border: none;
+  background-color: #777;
+  background-image: none;
+  background-image: none;
+}
+ /* Remove browser and formalize focus effects for dark background */
+#offcanvas input:focus,
+#offcanvas button:focus,
+#offcanvas a.button:focus,
+#offcanvas select:focus,
+#offcanvas textarea:focus {
+  -webkit-box-shadow: none;
+  -moz-box-shadow: none;
+  box-shadow: none;
+  outline: none;
+}
diff --git a/core/modules/outside_in/css/outside_in.module.css b/core/modules/outside_in/css/outside_in.module.css
new file mode 100644
index 0000000..1261992
--- /dev/null
+++ b/core/modules/outside_in/css/outside_in.module.css
@@ -0,0 +1,232 @@
+/**
+ * @file
+ * Styling for Outside-In module.
+ */
+
+/* Style the offcanvas container */
+#offcanvas {
+  box-sizing: border-box;
+  position: fixed;
+  height: 100%;
+  overflow-y: auto;
+  box-shadow: -2px 3px 1px 1px rgba(0, 0, 0, 0.3333); /* LTR */
+  display: inline-block;
+  width: 25%;
+  -webkit-transform: translateX(100%);
+  -moz-transform: translateX(100%);
+  -o-transform: translateX(100%);
+  -ms-transform: translateX(100%);
+  transform: translateX(100%);
+  /* 2s delay matches transition speed to transform speed */
+  transition: ease-out all 1s 2s;
+  -webkit-transition: ease-out all 1s 2s;
+  -moz-transition: ease-out all 1s 2s;
+  z-index: 1000;
+}
+[dir="rtl"] #offcanvas {
+  text-align: right;
+  -webkit-transform: translateX(-100%);
+  -moz-transform: translateX(-100%);
+  -o-transform: translateX(-100%);
+  -ms-transform: translateX(-100%);
+  transform: translateX(-100%);
+}
+#canvas-tray-wrapper.js-tray-open #offcanvas {
+  -webkit-transform: translateX(0);
+  -moz-transform: translateX(0);
+  -o-transform: translateX(0);
+  -ms-transform: translateX(0);
+  transform: translateX(0);
+}
+#canvas-tray-wrapper #canvas-tray {
+  display: inline-block;
+  width: 100%;
+  transition: all 1s;
+  -webkit-transition: all 1s;
+  -moz-transition: all 1s;
+}
+#canvas-tray-wrapper.js-tray-open #canvas-tray {
+  left: 0; /* LTR */
+  width: 75%;
+  transition: all 1s;
+  -webkit-transition: all 1s;
+  -moz-transition: all 1s;
+}
+[dir="rtl"] #canvas-tray-wrapper.js-tray-open #canvas-tray {
+  right: 0;
+}
+/* Button that closes the offcanvas tray */
+#offcanvas > button.offcanvasClose,
+#offcanvas > button.offcanvasClose:hover {  /* @todo add hover state */
+  position: absolute;
+  right: 15px; /* LTR */
+  top: 15px;
+  height: 20px;
+  width: 20px;
+  border: 0;
+  border-radius: 0;
+  background: url(/core/misc/icons/ffffff/ex.svg) center center no-repeat;
+  color: transparent;
+  cursor: pointer;
+}
+[dir="rtl"] #offcanvas > button.offcanvasClose {
+  left: 15px;
+  right: auto;
+}
+#offcanvas h1 {
+  font-size: 120%;
+}
+[dir="rtl"] #offcanvas h1 {
+  text-align: right;
+}
+#offcanvas > .content {
+  height: 100%;
+  padding: 15px;
+}
+[dir="rtl"] #offcanvas .content{
+  text-align: right;
+}
+#canvas-tray.offCanvasDisplayInProgress {
+  position: fixed;
+  display: inline-block;
+}
+#canvas-tray.offCanvasDisplayed {
+  display: inline-block;
+}
+
+/**
+ * Buttons.
+ */
+
+/* Style for primary button */
+#offcanvas .button--primary {
+  width: auto;
+  max-width: 50%;
+  border-radius: 30px;
+  border: none;
+  background-image: -webkit-linear-gradient(top,#007bc6,#0071b8);
+  background-image: linear-gradient(to bottom,#007bc6,#0071b8);
+}
+#offcanvas .button--primary:hover,
+#offcanvas .button--primary:active {
+  background-color: inherit;
+}
+/* Style for delete button */
+#offcanvas .button--danger {
+  width: auto;
+  max-width: 50%;
+  border-radius: 0;
+  border: none;
+  background: transparent;
+}
+/* Special edit link to flow inline with form items that link to more configuration */
+#offcanvas .edit-link { /* @todo add markup and class to template */
+  display: inline-block;
+  font-size: 85%;
+  font-weight: bold;
+}
+
+/**
+ * Inputs.
+ */
+#offcanvas textarea,
+#offcanvas select,
+#offcanvas input[type="date"],
+#offcanvas input[type="datetime"],
+#offcanvas input[type="datetime-local"],
+#offcanvas input[type="email"],
+#offcanvas input[type="month"],
+#offcanvas input[type="number"],
+#offcanvas input[type="password"],
+#offcanvas input[type="search"],
+#offcanvas input[type="tel"],
+#offcanvas input[type="text"],
+#offcanvas input[type="time"],
+#offcanvas input[type="url"],
+#offcanvas input[type="week"],
+#offcanvas input[type="range"] {
+  display: block;
+  width: 100%;
+}
+/* Reduce the size of descriptions and other elements for scannability */
+#offcanvas .description,
+#offcanvas .machine-name-label,
+#offcanvas .machine-name-value {
+  font-size: 85%;
+  font-weight: normal;
+  padding-top: .25em;
+}
+
+/* Add a background to checkboxes, radios for contrast */
+/*
+#offcanvas .radio-wrapper {
+  display: inline-block;
+  padding: 1px;
+  border-radius: 20px;
+}
+#offcanvas .checkbox-wrapper {
+  display: inline-block;
+  padding: 1px;
+  border-radius: 3px;
+}
+*/
+
+/**
+ * Fieldsets.
+ */
+#offcanvas fieldset {
+  border: 0 solid transparent;
+  margin-left: -1em; /* Push background to the left edge of the parent */
+  margin-right: -1em; /* Push background to the right edge of the parent */
+  padding-top: 2.5em;
+  padding-left: 1em;
+  padding-bottom: 1em;
+  position: relative;
+}
+#offcanvas fieldset .form-item {
+  display: block;
+}
+/* Style fieldset legend similar to Drupal (seven theme) */
+#offcanvas span.fieldset-legend {
+  text-transform: uppercase;
+  letter-spacing: 0.08em;
+  font-size: 85%;
+  position: absolute;
+  top: 10px;
+}
+
+/**
+ * Admin label.
+ */
+#offcanvas .form-item-settings-admin-label {
+  font-size: 1.25em;
+  width: 100%;
+  padding: 1em;
+  padding-top: 0;
+  margin-top: 0;
+  margin-left: -1em;
+  margin-right: -1em;
+  margin-bottom: 1em;
+  border-bottom-width: 1px;
+  border-bottom-style: solid;
+}
+#offcanvas .form-item-settings-admin-label label {
+  font-size: 10px;
+  display: block;
+  margin-bottom: 0;
+}
+
+#offcanvas span.field-suffix small { /* Don't know why it's there. @todo figure out why it's rendering empty and taking space */
+  display: none;
+}
+
+/**
+ * Form actions.
+ */
+#offcanvas .form-actions {
+  padding: 1em;
+  margin-left: -1em;
+  margin-right: -1em;
+  border-bottom-width: 1px;
+  border-bottom-style: solid;
+}
diff --git a/core/modules/outside_in/css/outside_in.theme.css b/core/modules/outside_in/css/outside_in.theme.css
new file mode 100644
index 0000000..8cfd66e
--- /dev/null
+++ b/core/modules/outside_in/css/outside_in.theme.css
@@ -0,0 +1,128 @@
+/**
+ * @file
+ * Visual styling for Outside-In module.
+ *
+ * Since visual styling here differs from styling elsewhere in core,
+ * font and color styles are put here so they can be more easily changed.
+ *
+ */
+
+/* style the offcanvas container */
+#offcanvas {
+  color: #eee;
+  border-left: 1px solid #000;
+  background: #444;
+  font-family: 'Lucida Grande','Lucida Sans Unicode','Liberation Sans',sans-serif;
+}
+#offcanvas .content {
+  color: #eee;
+}
+#offcanvas h1 {
+  margin: 0 0 15px 0;
+  padding: 15px;
+  color: #eee;
+  border-bottom: 1px solid #777;
+}
+
+/**
+ * Buttons.
+ */
+#offcanvas button:hover,
+#offcanvas button:active,
+#offcanvas .button:hover,
+#offcanvas .button:active {
+  background-color: inherit;
+  background-image: inherit;
+  background-image: inherit;
+}
+/* style for primary button */
+#offcanvas .button--primary {
+  background-image: -webkit-linear-gradient(top,#007bc6,#0071b8);
+  background-image: linear-gradient(to bottom,#007bc6,#0071b8);
+  font-weight: bold;
+  text-shadow: none;
+  color: #fff;
+}
+#offcanvas .button--primary:hover,
+#offcanvas .button--primary:active {
+  background-color: inherit;
+  background-image: -webkit-linear-gradient(top,#007bc6,#0071b8); /* todo: make the hover gradient different from normal */
+  background-image: linear-gradient(to bottom,#007bc6,#0071b8);  /* todo: make the hover gradient different from normal */
+}
+/* Style for delete button */
+#offcanvas .button--danger {
+  font-weight: bold;
+  text-shadow: none;
+  color: #ff3333;
+}
+
+/**
+ * Inputs.
+ */
+#offcanvas textarea,
+#offcanvas select,
+#offcanvas input[type="date"],
+#offcanvas input[type="datetime"],
+#offcanvas input[type="datetime-local"],
+#offcanvas input[type="email"],
+#offcanvas input[type="month"],
+#offcanvas input[type="number"],
+#offcanvas input[type="password"],
+#offcanvas input[type="search"],
+#offcanvas input[type="tel"],
+#offcanvas input[type="text"],
+#offcanvas input[type="time"],
+#offcanvas input[type="url"],
+#offcanvas input[type="week"],
+#offcanvas input[type="range"] {
+  background-color: #ddd; /* darken inputs to reduce contrast on dark background */
+  border: 1px solid #333;
+}
+/* add focus effect to fields for dark background */
+#offcanvas textarea:focus,
+#offcanvas select:focus,
+#offcanvas input:focus {
+  background-color: #fff;
+}
+/* reduce the size of descriptions and other elements for scannability */
+#offcanvas .description,
+#offcanvas .machine-name-label,
+#offcanvas .machine-name-value {
+  font-family: 'Lucida Grande','Lucida Sans Unicode','Liberation Sans',sans-serif;
+  color: #bbb;
+}
+/* add a background to radio buttons for contrast */
+/*
+#offcanvas .radio-wrapper {
+  background-color: #000;
+}
+#offcanvas .checkbox-wrapper {
+  background-color: #000;
+}
+*/
+
+/**
+ * Fieldsets.
+ */
+#offcanvas fieldset {
+  background-color: #333;
+}
+
+/**
+ * Admin label.
+ */
+#offcanvas .form-item-settings-admin-label {
+  border-bottom-color: #777;
+}
+#offcanvas .form-item-settings-admin-label label {
+  color: #bbb;
+}
+
+/**
+ * Form actions.
+ */
+#offcanvas .form-actions {
+  border-bottom-color: #777;
+}
+
+
diff --git a/core/modules/outside_in/js/offcanvas.js b/core/modules/outside_in/js/offcanvas.js
new file mode 100644
index 0000000..de6f0c7
--- /dev/null
+++ b/core/modules/outside_in/js/offcanvas.js
@@ -0,0 +1,120 @@
+/**
+ * @file
+ * Drupal's off canvas library.
+ */
+
+(function ($, Drupal) {
+  'use strict';
+
+  /**
+   * Create a wrapper container for the off canvas element.
+   * @param  {number} pageWidth
+   *   The width of #page-wrapper.
+   * @return {object}
+   *   jQuery object that is the off canvas wrapper element.
+   */
+  var createOffCanvasWrapper = function (pageWidth) {
+    return $('<div />', {
+      'id': 'offcanvas',
+      'role': 'region',
+      'aria-labelledby': 'offcanvas-header'
+    });
+  };
+
+  /**
+   * Create the title element for the off canvas element.
+   * @param  {string} title
+   *   The title string.
+   * @return {object}
+   *   jQuery object that is the off canvas title element.
+   */
+  var createTitle = function (title) {
+    return $('<h1 />', {text: title, id: 'offcanvas-header'});
+  };
+
+  /**
+   * Create the actual off canvas content.
+   * @param  {string} data
+   *   This is fully rendered html from Drupal.
+   * @return {object}
+   *   jQuery object that is the off canvas content element.
+   */
+  var createOffCanvasContent = function (data) {
+    return $('<div />', {class: 'content', html: data});
+  };
+
+  /**
+   * Create the off canvas close element.
+   * @param  {object} offCanvasWrapper
+   *   The jQuery off canvas wrapper element
+   * @param  {object} pageWrapper
+   *   The jQuery off page wrapper element
+   * @return {object}
+   *   jQuery object that is the off canvas close element.
+   */
+  var createOffCanvasClose = function (offCanvasWrapper, pageWrapper) {
+    return $('<button />', {
+      'class': 'offcanvasClose',
+      'aria-label': Drupal.t('Close configuration tray.'),
+      'html': '<span class="visually-hidden">' + Drupal.t('Close') + '</span>'
+    }).click(function () {
+      pageWrapper
+        .removeClass('js-tray-open')
+        .one('webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend', function (e) {
+          Drupal.offCanvas.visible = false;
+          offCanvasWrapper.remove();
+          Drupal.announce(Drupal.t('Configuration tray closed.'));
+        }
+      );
+    });
+  };
+
+
+  /**
+   * Command to open an off canvas element.
+   *
+   * @param {Drupal.Ajax} ajax
+   *   The Drupal Ajax object.
+   * @param {object} response
+   *   Object holding the server response.
+   * @param {number} [status]
+   *   The HTTP status code.
+   */
+  Drupal.AjaxCommands.prototype.openOffCanvas = function (ajax, response, status) {
+    // Discover display/viewport size.  TODO:  work in breakpoints for tray size.
+    var $pageWrapper = $('#canvas-tray-wrapper');
+    var pageWidth = $pageWrapper.width();
+
+    // Set the initial state of the off canvas element.
+    // If the state has been set previously, use it.
+    Drupal.offCanvas = {
+      visible: (Drupal.offCanvas ? Drupal.offCanvas.visible : false)
+    };
+
+    // Construct off canvas wrapper
+    var $offcanvasWrapper = createOffCanvasWrapper(pageWidth);
+
+    // Construct off canvas internal elements.
+    var $offcanvasClose = createOffCanvasClose($offcanvasWrapper, $pageWrapper);
+    var $title = createTitle(response.dialogOptions.title);
+    var $offcanvasContent = createOffCanvasContent(response.data);
+
+    // Put everything together.
+    $offcanvasWrapper.append([$offcanvasClose, $title, $offcanvasContent]);
+
+    // Handle opening or updating tray with content.
+    if (!Drupal.offCanvas.visible) {
+      // Append content then open tray.
+      $pageWrapper.append($offcanvasWrapper);
+      Drupal.offCanvas.visible = true;
+      $pageWrapper.addClass('js-tray-open');
+      Drupal.announce(Drupal.t('Configuration tray opened.'));
+    } else {
+      // Remove previous content then append new content.
+      $pageWrapper.find('#offcanvas').remove();
+      $pageWrapper.append($offcanvasWrapper);
+      Drupal.announce(Drupal.t('Configuration tray content has been updated.'));
+    }
+  };
+
+})(jQuery, Drupal);
diff --git a/core/modules/outside_in/js/outside_in.js b/core/modules/outside_in/js/outside_in.js
new file mode 100644
index 0000000..ed01e73
--- /dev/null
+++ b/core/modules/outside_in/js/outside_in.js
@@ -0,0 +1,101 @@
+/**
+ * @file
+ * Drupal's Outside In library.
+ */
+
+(function ($, Drupal) {
+  'use strict';
+
+  // Bind a listener to the 'edit' button
+  // Toggle the js-outside-edit-mode class on items that we want
+  // to disable while in edit mode.
+  $('div.contextual-toolbar-tab.toolbar-tab button').click(function (e) {
+    $('.outside-in-editable a, .outside-in-editable button')
+      .not('div.contextual a, div.contextual button')
+      .toggleClass('js-outsidein-edit-mode');
+    $('.outside-in-editable').toggleClass('focus');
+  });
+
+  // Bind an event listener to the .outside-in-editable div
+  // This listen for click events and stops default actions of those elements.
+  $('.outside-in-editable').on('click', '.js-outsidein-edit-mode', function (e) {
+    if (localStorage.getItem('Drupal.contextualToolbar.isViewing') === 'false') {
+      e.preventDefault();
+    }
+  });
+
+  // Bind an event listener to the .outside-in-editable div
+  // When a click occurs try and find the outside-in edit link
+  // and click it.
+  $('.outside-in-editable')
+    .not('div.contextual a, div.contextual button')
+    .click(function (e) {
+      if ($(e.target.offsetParent).hasClass('contextual')) {
+        return;
+      }
+      var editLink = $(e.target).find('li.outside-inblock-configure a')[0];
+
+      (editLink ? editLink : $(e.target).parents('.outside-in-editable')
+        .find('li.outside-inblock-configure a')[0])
+        .click();
+    });
+
+  /**
+   * Add Ajax behaviours to links added by contextual links
+   *
+   * @todo Fix contextual links to work with use-ajax links.
+   *   @see https://www.drupal.org/node/2764931
+   *
+   * @param {jQuery.Event} event
+   *   The `drupalContextualLinkAdded` event.
+   * @param {object} data
+   *   An object containing the data relevant to the event.
+   *
+   * @listens event:drupalContextualLinkAdded
+   */
+  $(document).on('drupalContextualLinkAdded', function (event, data) {
+    // Bind Ajax behaviors to all items showing the class.
+    data.$el.find('.use-ajax').once('ajax').each(function () {
+      // Below is copied directly from ajax.js to keep behavior the same.
+      var element_settings = {};
+      // Clicked links look better with the throbber than the progress bar.
+      element_settings.progress = {type: 'throbber'};
+
+      // For anchor tags, these will go to the target of the anchor rather
+      // than the usual location.
+      var href = $(this).attr('href');
+      if (href) {
+        element_settings.url = href;
+        element_settings.event = 'click';
+      }
+      element_settings.dialogType = $(this).data('dialog-type');
+      element_settings.dialog = $(this).data('dialog-options');
+      element_settings.base = $(this).attr('id');
+      element_settings.element = this;
+      Drupal.ajax(element_settings);
+    });
+  });
+
+  /**
+   * Attaches contextual's edit toolbar tab behavior.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches contextual toolbar behavior on a contextualToolbar-init event.
+   */
+  Drupal.behaviors.outsideinedit = {
+    attach: function (context) {
+      var editMode = localStorage.getItem('Drupal.contextualToolbar.isViewing') === 'false';
+      if (editMode) {
+        $('.outside-in-editable').addClass('focus');
+        var itemsToDisable = $('.outside-in-editable a, .outside-in-editable button')
+          .not('div.contextual a, div.contextual button');
+
+        itemsToDisable
+          .addClass('js-outsidein-edit-mode');
+      }
+    }
+  };
+
+})(jQuery, Drupal);
diff --git a/core/modules/outside_in/outside_in.info.yml b/core/modules/outside_in/outside_in.info.yml
new file mode 100644
index 0000000..8fcb964
--- /dev/null
+++ b/core/modules/outside_in/outside_in.info.yml
@@ -0,0 +1,10 @@
+name: 'Outside In'
+type: module
+description: 'Provides the ability to access useful configuration from the Drupal front-end.'
+package: Core (Experimental)
+version: VERSION
+core: 8.x
+dependencies:
+  - block
+  - toolbar
+  - contextual
diff --git a/core/modules/outside_in/outside_in.libraries.yml b/core/modules/outside_in/outside_in.libraries.yml
new file mode 100644
index 0000000..337ebbc
--- /dev/null
+++ b/core/modules/outside_in/outside_in.libraries.yml
@@ -0,0 +1,23 @@
+drupal.outside_in:
+  version: VERSION
+  js:
+    js/outside_in.js: {}
+  dependencies:
+    - core/jquery
+    - core/drupal
+drupal.off_canvas:
+  version: VERSION
+  js:
+    js/offcanvas.js: {}
+  css:
+    component:
+      css/outside_in.base.css: {}
+      css/outside_in.module.css: {}
+      css/outside_in.theme.css: {}
+  dependencies:
+    - core/jquery
+    - core/drupal
+    - core/drupal.ajax
+    - core/drupal.announce
+    - core/drupal.dialog
+    - core/drupal.dialog.ajax
diff --git a/core/modules/outside_in/outside_in.links.contextual.yml b/core/modules/outside_in/outside_in.links.contextual.yml
new file mode 100644
index 0000000..05455f3
--- /dev/null
+++ b/core/modules/outside_in/outside_in.links.contextual.yml
@@ -0,0 +1,4 @@
+outside_in.block_configure:
+  title: 'Quick Edit'
+  route_name: 'entity.block.offcanvas_form'
+  group: 'block'
diff --git a/core/modules/outside_in/outside_in.module b/core/modules/outside_in/outside_in.module
new file mode 100644
index 0000000..9667a57
--- /dev/null
+++ b/core/modules/outside_in/outside_in.module
@@ -0,0 +1,120 @@
+<?php
+
+/**
+ * @file
+ * Allows configuring blocks and other configuration from the front-end of the site.
+ */
+
+use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\outside_in\Block\BlockEntityOffCanvasForm;
+
+/**
+ * Implements hook_help().
+ */
+function outside_in_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    case 'help.page.outside_in':
+      $output = '<h3>' . t('About') . '</h3>';
+      // @todo Update help text.
+      $output .= '<p>' . t('The Outside In module is something that we should have help for. For more information, see the <a href=":outside-in-documentation">online documentation for the Outside In module</a>.', [':outside-in-documentation' => 'https://www.drupal.org/documentation/modules/outside_in']) . '</p>';
+      return $output;
+  }
+}
+
+/**
+ * Implements hook_contextual_links_view_alter().
+ *
+ * Change Configure Blocks into offcanvas links.
+ */
+function outside_in_contextual_links_view_alter(&$element, $items) {
+  if (isset($element['#links']['outside-inblock-configure'])) {
+    $element['#links']['outside-inblock-configure']['attributes'] = [
+      'class' => ['use-ajax'],
+      'data-dialog-type' => 'offcanvas',
+    ];
+
+    $element['#attached'] = [
+      'library' => [
+        'outside_in/drupal.off_canvas',
+      ],
+    ];
+  }
+}
+
+/**
+ * Implements hook_page_top().
+ *
+ * Opens a div for consistent wrapping to all {{ page }} render in all themes.
+ */
+function outside_in_page_top(array &$page_top) {
+  $page_top['outside_in_tray_open'] = [
+    '#markup' => '<div id="canvas-tray-wrapper"><div id="canvas-tray">',
+    '#weight' => 1000
+    ];
+}
+
+/**
+ * Implements hook_page_bottom().
+ *
+ * Closes a div for consistent wrapping to all {{ page }} render in all themes.
+ */
+function outside_in_page_bottom(array &$page_bottom) {
+  $page_bottom['outside_in_tray_close'] = [
+    '#markup' => '</div></div>',
+    '#weight' => -1000
+  ];
+}
+
+/**
+ * Implements hook_block_alter().
+ */
+function outside_in_block_alter(&$definitions) {
+  foreach ($definitions as &$definition) {
+    // If no default form is defined and this plugin implements
+    // \Drupal\Core\Plugin\PluginFormInterface, use that for the default form.
+    if (!isset($definition['form']['default']) && is_subclass_of($definition['class'], PluginFormInterface::class)) {
+      $definition['form']['default'] = $definition['class'];
+    }
+  }
+}
+
+/**
+ * Implements hook_entity_type_build().
+ */
+function outside_in_entity_type_build(array &$entity_types) {
+  /** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */
+  $entity_types['block']
+    ->setFormClass('offcanvas', BlockEntityOffCanvasForm::class)
+    ->setLinkTemplate('offcanvas-form', '/admin/structure/block/manage/{block}/offcanvas');
+}
+
+/**
+ * Implements hook_preprocess_HOOK() for block templates.
+ *
+ * Adds 'outside-in-editable' class to all blocks to allow Javascript to target.
+ */
+function outside_in_preprocess_block(&$variables) {
+  // Remove on Admin routes.
+  $admin_route = \Drupal::service('router.admin_context')->isAdminRoute();
+  // @todo Check if there is actually different admin theme.
+  // Remove on Block Demo page.
+  $admin_demo = \Drupal::routeMatch()->getRouteName() === 'block.admin_demo';
+  $access = (\Drupal::currentUser()->hasPermission('administer blocks') && !$admin_route && !$admin_demo);
+
+  if (!$access) {
+    return;
+  }
+  $variables['attributes']['class'][] = 'outside-in-editable';
+}
+
+/**
+ * Implements hook_toolbar_alter().
+ *
+ * Includes outside_library if Edit link is in toolbar.
+ */
+function outside_in_toolbar_alter(&$items) {
+  if (isset($items['contextual']['tab'])) {
+    $items['contextual']['#attached']['library'][] = 'outside_in/drupal.outside_in';
+  }
+}
diff --git a/core/modules/outside_in/outside_in.routing.yml b/core/modules/outside_in/outside_in.routing.yml
new file mode 100644
index 0000000..5c81541
--- /dev/null
+++ b/core/modules/outside_in/outside_in.routing.yml
@@ -0,0 +1,7 @@
+entity.block.offcanvas_form:
+  path: '/admin/structure/block/manage/{block}/offcanvas'
+  defaults:
+    _entity_form: 'block.offcanvas'
+    _title: 'Configure block'
+  requirements:
+    _permission: 'administer blocks'
diff --git a/core/modules/outside_in/outside_in.services.yml b/core/modules/outside_in/outside_in.services.yml
new file mode 100644
index 0000000..9a785e8
--- /dev/null
+++ b/core/modules/outside_in/outside_in.services.yml
@@ -0,0 +1,9 @@
+services:
+  main_content_renderer.off_canvas:
+      class: Drupal\outside_in\Render\MainContent\OffCanvasRender
+      arguments: ['@title_resolver', '@renderer']
+      tags:
+        - { name: render.main_content_renderer, format: drupal_offcanvas }
+  outside_in.block.manager:
+    class: Drupal\outside_in\Block\OutsideInBlockManager
+    arguments: ['@class_resolver']
diff --git a/core/modules/outside_in/src/Ajax/OpenOffCanvasDialogCommand.php b/core/modules/outside_in/src/Ajax/OpenOffCanvasDialogCommand.php
new file mode 100644
index 0000000..ef6f272
--- /dev/null
+++ b/core/modules/outside_in/src/Ajax/OpenOffCanvasDialogCommand.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\outside_in\Ajax;
+
+use Drupal\Core\Ajax\OpenDialogCommand;
+
+/**
+ * Defines an AJAX command to open content in a dialog in a off canvas tray.
+ *
+ * @ingroup ajax
+ */
+class OpenOffCanvasDialogCommand extends OpenDialogCommand {
+
+  /**
+   * Constructs an OpenOffCanvasDialogCommand object.
+   *
+   * Drupal provides a built-in offcanvas tray for
+   * this purpose, so no selector needs to be provided.
+   *
+   * @todo Do we need a selector? Or act the same as modal?
+   *
+   * @param string $title
+   *   The title of the dialog.
+   * @param string|array $content
+   *   The content that will be placed in the dialog, either a render array
+   *   or an HTML string.
+   * @param array $dialog_options
+   *   (optional) Settings to be passed to the dialog implementation. Any
+   *   jQuery UI option can be used. See http://api.jqueryui.com/dialog.
+   * @param array|null $settings
+   *   (optional) Custom settings that will be passed to the Drupal behaviors
+   *   on the content of the dialog. If left empty, the settings will be
+   *   populated automatically from the current request.
+   */
+  public function __construct($title, $content, array $dialog_options = [], $settings = NULL) {
+    $dialog_options['modal'] = FALSE;
+    parent::__construct('#drupal-offcanvas', $title, $content, $dialog_options, $settings);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render() {
+    $this->dialogOptions['modal'] = FALSE;
+    return [
+      'command' => 'openOffCanvas',
+      'selector' => $this->selector,
+      'settings' => $this->settings,
+      'data' => $this->getRenderedContent(),
+      'dialogOptions' => $this->dialogOptions,
+    ];
+  }
+
+}
diff --git a/core/modules/outside_in/src/Block/BlockEntityOffcanvasForm.php b/core/modules/outside_in/src/Block/BlockEntityOffcanvasForm.php
new file mode 100644
index 0000000..2c5ad58
--- /dev/null
+++ b/core/modules/outside_in/src/Block/BlockEntityOffcanvasForm.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Drupal\outside_in\Block;
+
+use Drupal\Core\Entity\EntityForm;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * @todo.
+ */
+class BlockEntityOffCanvasForm extends EntityForm {
+
+  /**
+   * The block entity.
+   *
+   * @var \Drupal\block\BlockInterface
+   */
+  protected $entity;
+
+  /**
+   * The block manager.
+   *
+   * @var \Drupal\outside_in\Block\OutsideInBlockManagerInterface
+   */
+  protected $blockManager;
+
+  /**
+   * The context repository service.
+   *
+   * @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface
+   */
+  protected $contextRepository;
+
+  /**
+   * BlockEntityOffCanvasForm constructor.
+   *
+   * @param \Drupal\outside_in\Block\OutsideInBlockManagerInterface $block_manager
+   *   The block manager.
+   * @param \Drupal\Core\Plugin\Context\ContextRepositoryInterface $context_repository
+   *   The lazy context repository service.
+   */
+  public function __construct(OutsideInBlockManagerInterface $block_manager, ContextRepositoryInterface $context_repository) {
+    $this->contextRepository = $context_repository;
+    $this->blockManager = $block_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('outside_in.block.manager'),
+      $container->get('context.repository')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    // Store theme settings in $form_state for use below.
+    $form_state->set('block_theme', $this->entity->getTheme());
+
+    // Store the gathered contexts in the form state for other objects to use
+    // during form building.
+    $form_state->setTemporaryValue('gathered_contexts', $this->contextRepository->getAvailableContexts());
+
+    $form['#tree'] = TRUE;
+    $form['#attached']['library'][] = 'block/drupal.block.admin';
+
+    $form['settings'] = $this->blockManager->getFormObject($this->entity->getPlugin(), 'offcavnas')->buildConfigurationForm([], $form_state);
+
+    return $form;
+  }
+
+}
diff --git a/core/modules/outside_in/src/Block/OutsideInBlockManager.php b/core/modules/outside_in/src/Block/OutsideInBlockManager.php
new file mode 100644
index 0000000..474f8a0
--- /dev/null
+++ b/core/modules/outside_in/src/Block/OutsideInBlockManager.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Drupal\outside_in\Block;
+
+use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
+use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\Core\DependencyInjection\ClassResolverInterface;
+use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\outside_in\OperationAwareFormInterface;
+
+/**
+ * Provides form discovery capabilities for block plugins.
+ */
+class OutsideInBlockManager implements OutsideInBlockManagerInterface {
+
+  /**
+   * The class resolver.
+   *
+   * @var \Drupal\Core\DependencyInjection\ClassResolverInterface
+   */
+  protected $classResolver;
+
+  /**
+   * OutsideInBlockManager constructor.
+   *
+   * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
+   */
+  public function __construct(ClassResolverInterface $class_resolver) {
+    $this->classResolver = $class_resolver;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormObject(PluginInspectionInterface $plugin, $operation) {
+    $definition = $plugin->getPluginDefinition();
+
+    if (!isset($definition['form'][$operation])) {
+      // Use the default form class if no form is specified for this operation.
+      if (isset($definition['form']['default'])) {
+        $operation = 'default';
+      }
+      else {
+        throw new InvalidPluginDefinitionException($plugin->getPluginId(), sprintf('The "%s" plugin did not specify a "%s" form class', $plugin->getPluginId(), $operation));
+      }
+    }
+
+    // If the form specified is the plugin itself, use it directly.
+    if (get_class($plugin) === $definition['form'][$operation]) {
+      $form_object = $plugin;
+    }
+    else {
+      $form_object = $this->classResolver->getInstanceFromDefinition($definition['form'][$operation]);
+    }
+
+    // Ensure the resulting object is a plugin form.
+    if (!$form_object instanceof PluginFormInterface) {
+      throw new InvalidPluginDefinitionException($plugin->getPluginId(), sprintf('The "%s" plugin did not specify a valid "%s" form class, must implement \Drupal\Core\Plugin\PluginFormInterface', $plugin->getPluginId(), $operation));
+    }
+
+    if ($form_object instanceof OperationAwareFormInterface) {
+      $form_object->setOperation($operation);
+    }
+
+    return $form_object;
+  }
+
+}
diff --git a/core/modules/outside_in/src/Block/OutsideInBlockManagerInterface.php b/core/modules/outside_in/src/Block/OutsideInBlockManagerInterface.php
new file mode 100644
index 0000000..4fbed53
--- /dev/null
+++ b/core/modules/outside_in/src/Block/OutsideInBlockManagerInterface.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\outside_in\Block;
+
+use Drupal\Component\Plugin\PluginInspectionInterface;
+
+/**
+ * Provides form discovery capabilities for block plugins.
+ */
+interface OutsideInBlockManagerInterface {
+
+  /**
+   * Creates a new form instance.
+   *
+   * @param \Drupal\Component\Plugin\PluginInspectionInterface $plugin
+   *   The plugin the form is for.
+   * @param string $operation
+   *   The name of the operation to use, e.g., 'default'.
+   *
+   * @return \Drupal\Core\Plugin\PluginFormInterface
+   *   A plugin form instance.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+   */
+  public function getFormObject(PluginInspectionInterface $plugin, $operation);
+
+}
diff --git a/core/modules/outside_in/src/OperationAwareFormInterface.php b/core/modules/outside_in/src/OperationAwareFormInterface.php
new file mode 100644
index 0000000..cf31c58
--- /dev/null
+++ b/core/modules/outside_in/src/OperationAwareFormInterface.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Drupal\outside_in;
+
+/**
+ * Interface for forms that are aware of what operation they are performing.
+ */
+interface OperationAwareFormInterface {
+
+  /**
+   * Sets the operation for this form.
+   *
+   * @param string $operation
+   *   The name of the current operation.
+   *
+   * @return $this
+   */
+  public function setOperation($operation);
+
+}
diff --git a/core/modules/outside_in/src/Render/MainContent/OffCanvasRender.php b/core/modules/outside_in/src/Render/MainContent/OffCanvasRender.php
new file mode 100644
index 0000000..5202f40
--- /dev/null
+++ b/core/modules/outside_in/src/Render/MainContent/OffCanvasRender.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Drupal\outside_in\Render\MainContent;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Controller\TitleResolverInterface;
+use Drupal\Core\Render\MainContent\DialogRenderer;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\outside_in\Ajax\OpenOffCanvasDialogCommand;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Default main content renderer for offcanvas dialog requests.
+ */
+class OffCanvasRender extends DialogRenderer {
+
+  /**
+   * The renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * Constructs a new DialogRenderer.
+   *
+   * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver
+   *   The title resolver.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
+   */
+  public function __construct(TitleResolverInterface $title_resolver, RendererInterface $renderer) {
+    parent::__construct($title_resolver);
+    $this->renderer = $renderer;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) {
+    $response = new AjaxResponse();
+
+    // First render the main content, because it might provide a title.
+    $content = $this->renderer->renderRoot($main_content);
+
+    // Attach the library necessary for using the OpenModalDialogCommand and set
+    // the attachments for this Ajax response.
+    $main_content['#attached']['library'][] = 'outside_in/drupal.off_canvas';
+    $response->setAttachments($main_content['#attached']);
+
+    // If the main content doesn't provide a title, use the title resolver.
+    $title = isset($main_content['#title']) ? $main_content['#title'] : $this->titleResolver->getTitle($request, $route_match->getRouteObject());
+
+    // Determine the title: use the title provided by the main content if any,
+    // otherwise get it from the routing information.
+    $options = $request->request->get('dialogOptions', []);
+
+    $response->addCommand(new OpenOffCanvasDialogCommand($title, $content, $options));
+    return $response;
+  }
+
+}
diff --git a/core/modules/outside_in/src/Tests/Ajax/OffCanvasDialogTest.php b/core/modules/outside_in/src/Tests/Ajax/OffCanvasDialogTest.php
new file mode 100644
index 0000000..2df1e71
--- /dev/null
+++ b/core/modules/outside_in/src/Tests/Ajax/OffCanvasDialogTest.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Drupal\outside_in\Tests\Ajax;
+
+use Drupal\ajax_test\Controller\AjaxTestController;
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
+use Drupal\system\Tests\Ajax\AjaxTestBase;
+
+/**
+ * Performs tests on opening and manipulating dialogs via AJAX commands.
+ *
+ * @group Outside In
+ */
+class OffCanvasDialogTest extends AjaxTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['outside_in'];
+
+  /**
+   * Test sending AJAX requests to open and manipulate offcanvas dialog.
+   */
+  public function testDialog() {
+    $this->drupalLogin($this->drupalCreateUser(['administer contact forms']));
+    // Ensure the elements render without notices or exceptions.
+    $this->drupalGet('ajax-test/dialog');
+
+    // Set up variables for this test.
+    $dialog_renderable = AjaxTestController::dialogContents();
+    $dialog_contents = \Drupal::service('renderer')->renderRoot($dialog_renderable);
+
+    $offcanvas_expected_response = [
+      'command' => 'openOffCanvas',
+      'selector' => '#drupal-offcanvas',
+      'settings' => NULL,
+      'data' => $dialog_contents,
+      'dialogOptions' => [
+        'modal' => FALSE,
+        'title' => 'AJAX Dialog contents',
+      ],
+    ];
+
+    // Emulate going to the JS version of the page and check the JSON response.
+    $ajax_result = $this->drupalGetAjax('ajax-test/dialog-contents', ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_offcanvas']]);
+    $this->assertEqual($offcanvas_expected_response, $ajax_result[3], 'Off-canvas dialog JSON response matches.');
+  }
+
+}
diff --git a/core/modules/outside_in/tests/modules/offcanvas_test/offcanvas_test.info.yml b/core/modules/outside_in/tests/modules/offcanvas_test/offcanvas_test.info.yml
new file mode 100644
index 0000000..8c6cc80
--- /dev/null
+++ b/core/modules/outside_in/tests/modules/offcanvas_test/offcanvas_test.info.yml
@@ -0,0 +1,9 @@
+name: 'Off-canvas tests'
+type: module
+description: 'Provides off-canvas test links.'
+package: Testing
+version: VERSION
+core: 8.x
+dependencies:
+  - block
+  - outside_in
diff --git a/core/modules/outside_in/tests/modules/offcanvas_test/offcanvas_test.routing.yml b/core/modules/outside_in/tests/modules/offcanvas_test/offcanvas_test.routing.yml
new file mode 100644
index 0000000..7bfd52b
--- /dev/null
+++ b/core/modules/outside_in/tests/modules/offcanvas_test/offcanvas_test.routing.yml
@@ -0,0 +1,23 @@
+offcanvas_test.links:
+  path: '/offcanvas-test-links'
+  defaults:
+    _controller: '\Drupal\offcanvas_test\Controller\TestController::linksDisplay'
+    _title: 'Links'
+  requirements:
+    _access: 'TRUE'
+
+offcanvas_test.thing1:
+  path: '/offcanvas-thing1'
+  defaults:
+    _controller: '\Drupal\offcanvas_test\Controller\TestController::thing1'
+    _title: 'Thing 1'
+  requirements:
+    _access: 'TRUE'
+
+offcanvas_test.thing2:
+  path: '/offcanvas-thing2'
+  defaults:
+    _controller: '\Drupal\offcanvas_test\Controller\TestController::thing2'
+    _title: 'Thing 2'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/outside_in/tests/modules/offcanvas_test/src/Controller/TestController.php b/core/modules/outside_in/tests/modules/offcanvas_test/src/Controller/TestController.php
new file mode 100644
index 0000000..8bf28ad
--- /dev/null
+++ b/core/modules/outside_in/tests/modules/offcanvas_test/src/Controller/TestController.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Drupal\offcanvas_test\Controller;
+use Drupal\Core\Url;
+
+/**
+ * Test controller for 2 different responses.
+ */
+class TestController {
+
+  /**
+   * Thing1.
+   *
+   * @return string
+   *   Return Hello string.
+   */
+  public function thing1() {
+    return [
+      '#type' => 'markup',
+      '#markup' => 'Thing 1 says hello',
+    ];
+  }
+
+  /**
+   * Thing2.
+   *
+   * @return string
+   *   Return Hello string.
+   */
+  public function thing2() {
+    return [
+      '#type' => 'markup',
+      '#markup' => 'Thing 2 says hello',
+    ];
+  }
+
+  /**
+   * Display test links that will open in offcanvas tray.
+   *
+   * @return array
+   *   Render array with links.
+   */
+  public function linksDisplay() {
+    return [
+      'offcanvas_link_1' => [
+        '#title' => 'Click Me 1!',
+        '#type' => 'link',
+        '#url' => Url::fromRoute('offcanvas_test.thing1'),
+        '#attributes' => [
+          'class' => ['use-ajax'],
+          'data-dialog-type' => 'offcanvas',
+        ],
+        '#attached' => [
+          'library' => [
+            'outside_in/drupal.off_canvas',
+          ],
+        ],
+      ],
+      'offcanvas_link_2' => [
+        '#title' => 'Click Me 2!',
+        '#type' => 'link',
+        '#url' => Url::fromRoute('offcanvas_test.thing2'),
+        '#attributes' => [
+          'class' => ['use-ajax'],
+          'data-dialog-type' => 'offcanvas',
+        ],
+        '#attached' => [
+          'library' => [
+            'outside_in/drupal.off_canvas',
+          ],
+        ],
+      ],
+
+    ];
+  }
+
+}
diff --git a/core/modules/outside_in/tests/modules/offcanvas_test/src/Form/OffCanvasForm.php b/core/modules/outside_in/tests/modules/offcanvas_test/src/Form/OffCanvasForm.php
new file mode 100644
index 0000000..ecca616
--- /dev/null
+++ b/core/modules/outside_in/tests/modules/offcanvas_test/src/Form/OffCanvasForm.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Drupal\offcanvas_test\Form;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\outside_in\OperationAwareFormInterface;
+
+/**
+ * @todo.
+ */
+class OffCanvasForm implements PluginFormInterface, OperationAwareFormInterface {
+
+  /**
+   * @var string
+   */
+  protected $operation;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setOperation($operation) {
+    $this->operation = $operation;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+    // Intentionally empty.
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    // Intentionally empty.
+  }
+
+}
diff --git a/core/modules/outside_in/tests/modules/offcanvas_test/src/Plugin/Block/TestBlock.php b/core/modules/outside_in/tests/modules/offcanvas_test/src/Plugin/Block/TestBlock.php
new file mode 100644
index 0000000..2866311
--- /dev/null
+++ b/core/modules/outside_in/tests/modules/offcanvas_test/src/Plugin/Block/TestBlock.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace Drupal\offcanvas_test\Plugin\Block;
+
+use Drupal\Core\Block\BlockBase;
+use Drupal\Core\Url;
+
+/**
+ * Provides a 'Powered by Drupal' block.
+ *
+ * @Block(
+ *   id = "offcanvas_links_block",
+ *   form = {
+ *     "offcanvas" = "\Drupal\offcanvas_test\Form\OffCanvasForm"
+ *   },
+ *   admin_label = @Translation("Off-canvas test block")
+ * )
+ */
+class TestBlock extends BlockBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build() {
+    return [
+      'offcanvas_link_1' => [
+        '#title' => $this->t('Click Me 1!'),
+        '#type' => 'link',
+        '#url' => Url::fromRoute('offcanvas_test.thing1'),
+        '#attributes' => [
+          'class' => ['use-ajax'],
+          'data-dialog-type' => 'offcanvas',
+        ],
+      ],
+      'offcanvas_link_2' => [
+        '#title' => $this->t('Click Me 2!'),
+        '#type' => 'link',
+        '#url' => Url::fromRoute('offcanvas_test.thing2'),
+        '#attributes' => [
+          'class' => ['use-ajax'],
+          'data-dialog-type' => 'offcanvas',
+        ],
+      ],
+      '#attached' => [
+        'library' => [
+          'outside_in/drupal.off_canvas',
+        ],
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/outside_in/tests/src/FunctionalJavascript/OffCanvasTest.php b/core/modules/outside_in/tests/src/FunctionalJavascript/OffCanvasTest.php
new file mode 100644
index 0000000..37d05e6
--- /dev/null
+++ b/core/modules/outside_in/tests/src/FunctionalJavascript/OffCanvasTest.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace Drupal\Tests\outside_in\FunctionalJavascript;
+
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
+
+/**
+ * Tests the off-canvas tray functionality.
+ *
+ * @group outside_in
+ */
+class OffCanvasTest extends JavascriptTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'block',
+    'system',
+    'toolbar',
+    'outside_in',
+    'offcanvas_test',
+  ];
+
+  /**
+   * Tests that regular non-contextual links will work with the off-canvas tray.
+   */
+  public function testOffCanvasLinks() {
+    // @todo Add other themes to test against.
+    $themes = ['bartik', 'stark'];
+    // @todo Add RTL Language test for each theme.
+    // Test the same functionality on multiple themes
+    foreach ($themes as $theme) {
+      // Enable the theme.
+      \Drupal::service('theme_installer')->install([$theme]);
+      $theme_config = \Drupal::configFactory()->getEditable('system.theme');
+      $theme_config->set('default', $theme);
+      $theme_config->save();
+      $this->drupalGet('/offcanvas-test-links');
+
+      $page = $this->getSession()->getPage();
+      $web_assert = $this->assertSession();
+
+      // Make sure off-canvas tray is on page when first loaded.
+      $web_assert->elementNotExists('css', '#offcanvas');
+
+      // Check opening and closing with two separate links.
+      // Make sure tray updates to new content.
+      foreach (['1', '2'] as $link_index) {
+        // Click the first test like that should open the page.
+        $page->clickLink("Click Me $link_index!");
+        $this->waitForOffCanvasToOpen();
+
+        // Check that the canvas is not on the page.
+        $web_assert->elementExists('css', '#offcanvas');
+        // Check that response text is on page.
+        $web_assert->pageTextContains("Thing $link_index says hello");
+        $offcanvas_tray = $page->findById('offcanvas');
+
+        // Check that tray is visible.
+        $this->assertEquals(TRUE,$offcanvas_tray->isVisible());
+        $header_text = $offcanvas_tray->findById('offcanvas-header')->getText();
+
+        // Check that header is correct.
+        $this->assertEquals("Thing $link_index", $header_text);
+        $tray_text = $offcanvas_tray->find('css', '.content')->getText();
+        $this->assertEquals("Thing $link_index says hello", $tray_text);
+
+        // Close the tray.
+        // @todo Should the close have an id?
+        $offcanvas_tray->find('css', '.offcanvasClose')->press();
+        // Wait for animation to be done.
+        $this->waitForOffCanvasToClose();
+        // Make sure canvas doesn't exist after closing.
+        $web_assert->elementNotExists('css', '#offcanvas');
+      }
+
+      $this->verbose('Test theme: ' . $theme);
+    }
+  }
+
+  /**
+   * Waits for Off-canvas tray to close.
+   */
+  protected function waitForOffCanvasToClose() {
+    $condition = "(jQuery('#offcanvas').length == 0)";
+    $this->assertJsCondition($condition, 5000);
+  }
+
+  /**
+   * Waits for Off-canvas tray to open.
+   */
+  protected function waitForOffCanvasToOpen() {
+    $condition = "(jQuery('#offcanvas').length > 0)";
+    $this->assertJsCondition($condition, 5000);
+  }
+
+}
diff --git a/core/modules/outside_in/tests/src/Kernel/MultipleBlockFormTest.php b/core/modules/outside_in/tests/src/Kernel/MultipleBlockFormTest.php
new file mode 100644
index 0000000..0899585
--- /dev/null
+++ b/core/modules/outside_in/tests/src/Kernel/MultipleBlockFormTest.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\Tests\outside_in\Kernel;
+
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\offcanvas_test\Form\OffCanvasForm;
+
+/**
+ * Tests that blocks can have multiple forms.
+ *
+ * @group outside_in
+ */
+class MultipleBlockFormTest extends KernelTestBase {
+
+  /**
+   * @var array
+   */
+  public static $modules = ['system', 'block', 'outside_in', 'offcanvas_test'];
+
+  /**
+   * Tests that blocks can have multiple forms.
+   */
+  public function testMultipleForms() {
+    $block = \Drupal::service('plugin.manager.block')->createInstance('offcanvas_links_block');
+
+    $form_object1 = \Drupal::service('outside_in.block.manager')->getFormObject($block, 'default');
+    $form_object2 = \Drupal::service('outside_in.block.manager')->getFormObject($block, 'offcanvas');
+
+    // Assert that the block itself is used for the default form.
+    $this->assertSame($block, $form_object1);
+
+    $expected_offcanvas = new OffCanvasForm();
+    $expected_offcanvas->setOperation('offcanvas');
+    $this->assertEquals($expected_offcanvas, $form_object2);
+  }
+
+}
diff --git a/core/modules/outside_in/tests/src/Unit/Ajax/OpenOffCanvasDialogCommandTest.php b/core/modules/outside_in/tests/src/Unit/Ajax/OpenOffCanvasDialogCommandTest.php
new file mode 100644
index 0000000..45ff2a1
--- /dev/null
+++ b/core/modules/outside_in/tests/src/Unit/Ajax/OpenOffCanvasDialogCommandTest.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Drupal\Tests\outside_in\Unit\Ajax;
+
+use Drupal\outside_in\Ajax\OpenOffCanvasDialogCommand;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\outside_in\Ajax\OpenOffCanvasDialogCommand
+ * @group outside_in
+ */
+class OpenOffCanvasDialogCommandTest extends UnitTestCase {
+
+  /**
+   * @covers ::render
+   */
+  public function testRender() {
+    $command = new OpenOffCanvasDialogCommand('Title', '<p>Text!</p>', ['url' => 'example']);
+
+    $expected = [
+      'command' => 'openOffCanvas',
+      'selector' => '#drupal-offcanvas',
+      'settings' => NULL,
+      'data' => '<p>Text!</p>',
+      'dialogOptions' => [
+        'url' => 'example',
+        'title' => 'Title',
+        'modal' => FALSE,
+      ],
+    ];
+    $this->assertEquals($expected, $command->render());
+  }
+
+}
diff --git a/core/modules/outside_in/tests/src/Unit/OutsideInBlockManagerTest.php b/core/modules/outside_in/tests/src/Unit/OutsideInBlockManagerTest.php
new file mode 100644
index 0000000..1158516
--- /dev/null
+++ b/core/modules/outside_in/tests/src/Unit/OutsideInBlockManagerTest.php
@@ -0,0 +1,155 @@
+<?php
+
+namespace Drupal\Tests\outside_in\Unit;
+
+use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
+use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\Core\DependencyInjection\ClassResolverInterface;
+use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\outside_in\OperationAwareFormInterface;
+use Drupal\outside_in\Block\OutsideInBlockManager;
+use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
+
+/**
+ * @coversDefaultClass \Drupal\outside_in\Block\OutsideInBlockManager
+ * @group outside_in
+ */
+class OutsideInBlockManagerTest extends UnitTestCase {
+
+  /**
+   * The class resolver.
+   *
+   * @var \Drupal\Core\DependencyInjection\ClassResolverInterface|\Prophecy\Prophecy\ProphecyInterface
+   */
+  protected $classResolver;
+
+  /**
+   * The manager being tested.
+   *
+   * @var \Drupal\outside_in\Block\OutsideInBlockManager
+   */
+  protected $manager;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->classResolver = $this->prophesize(ClassResolverInterface::class);
+    $this->manager = new OutsideInBlockManager($this->classResolver->reveal());
+  }
+
+  /**
+   * @covers ::getFormObject
+   */
+  public function testGetFormObject() {
+    $plugin_form = $this->prophesize(PluginFormInterface::class);
+    $expected = $plugin_form->reveal();
+
+    $this->classResolver->getInstanceFromDefinition(get_class($expected))->willReturn($expected);
+
+    $plugin = $this->prophesize(PluginInspectionInterface::class);
+    $plugin->getPluginDefinition()->willReturn([
+      'form' => [
+        'standard_class' => get_class($expected),
+      ],
+    ]);
+
+    $form_object = $this->manager->getFormObject($plugin->reveal(), 'standard_class');
+    $this->assertSame($expected, $form_object);
+  }
+
+  /**
+   * @covers ::getFormObject
+   */
+  public function testGetFormObjectUsingPlugin() {
+    $this->classResolver->getInstanceFromDefinition(Argument::cetera())->shouldNotBeCalled();
+
+    $plugin = $this->prophesize(PluginInspectionInterface::class)->willImplement(PluginFormInterface::class);
+    $plugin->getPluginDefinition()->willReturn([
+      'form' => [
+        'default' => get_class($plugin->reveal()),
+      ],
+    ]);
+
+    $form_object = $this->manager->getFormObject($plugin->reveal(), 'default');
+    $this->assertSame($plugin->reveal(), $form_object);
+  }
+
+  /**
+   * @covers ::getFormObject
+   */
+  public function testGetFormObjectDefaultFallback() {
+    $this->classResolver->getInstanceFromDefinition(Argument::cetera())->shouldNotBeCalled();
+
+    $plugin = $this->prophesize(PluginInspectionInterface::class)->willImplement(PluginFormInterface::class);
+    $plugin->getPluginDefinition()->willReturn([
+      'form' => [
+        'default' => get_class($plugin->reveal()),
+      ],
+    ]);
+
+    $form_object = $this->manager->getFormObject($plugin->reveal(), 'missing');
+    $this->assertSame($plugin->reveal(), $form_object);
+  }
+
+  /**
+   * @covers ::getFormObject
+   */
+  public function testGetFormObjectOperationAware() {
+    $plugin_form = $this->prophesize(PluginFormInterface::class)->willImplement(OperationAwareFormInterface::class);
+    $plugin_form->setOperation('operation_aware')->shouldBeCalled();
+
+    $expected = $plugin_form->reveal();
+
+    $this->classResolver->getInstanceFromDefinition(get_class($expected))->willReturn($expected);
+
+    $plugin = $this->prophesize(PluginInspectionInterface::class);
+    $plugin->getPluginDefinition()->willReturn([
+      'form' => [
+        'operation_aware' => get_class($expected),
+      ],
+    ]);
+
+    $form_object = $this->manager->getFormObject($plugin->reveal(), 'operation_aware');
+    $this->assertSame($expected, $form_object);
+  }
+
+  /**
+   * @covers ::getFormObject
+   */
+  public function testGetFormObjectDefinitionException() {
+    $this->setExpectedException(InvalidPluginDefinitionException::class, 'The "the_plugin_id" plugin did not specify a "anything" form class');
+
+    $plugin = $this->prophesize(PluginInspectionInterface::class);
+    $plugin->getPluginId()->willReturn('the_plugin_id');
+    $plugin->getPluginDefinition()->willReturn([]);
+
+    $form_object = $this->manager->getFormObject($plugin->reveal(), 'anything');
+    $this->assertSame(NULL, $form_object);
+  }
+
+  /**
+   * @covers ::getFormObject
+   */
+  public function testGetFormObjectInvalidException() {
+    $this->setExpectedException(InvalidPluginDefinitionException::class, 'The "the_plugin_id" plugin did not specify a valid "invalid" form class, must implement \Drupal\Core\Plugin\PluginFormInterface');
+
+    $expected = new \stdClass();
+    $this->classResolver->getInstanceFromDefinition(get_class($expected))->willReturn($expected);
+
+    $plugin = $this->prophesize(PluginInspectionInterface::class);
+    $plugin->getPluginId()->willReturn('the_plugin_id');
+    $plugin->getPluginDefinition()->willReturn([
+      'form' => [
+        'invalid' => get_class($expected),
+      ],
+    ]);
+
+    $form_object = $this->manager->getFormObject($plugin->reveal(), 'invalid');
+    $this->assertSame(NULL, $form_object);
+  }
+
+}
