diff --git a/core/composer.json b/core/composer.json
index e38b665..de2feb8 100644
--- a/core/composer.json
+++ b/core/composer.json
@@ -113,6 +113,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.module.css b/core/modules/outside_in/css/outside_in.module.css
new file mode 100644
index 0000000..6ac1deb
--- /dev/null
+++ b/core/modules/outside_in/css/outside_in.module.css
@@ -0,0 +1,203 @@
+/**
+ * @file
+ * Styling for Outside-In module.
+ */
+
+/* Position the offcanvas tray container outside the right of the viewport. */
+#offcanvas {
+  box-sizing: border-box;
+  height: 100%;
+  overflow-y: auto;
+  z-index: 501;
+}
+
+/* Shift the main canvas to the right for right-to-left languages. */
+[dir="rtl"] #main-canvas-wrapper.js-tray-open #main-canvas {
+  right: 0;
+}
+
+/* Position the button that closes the offcanvas tray. */
+#offcanvas > button.offcanvasClose {
+  position: static;
+  float: right; /* LTR */
+  height: 52px;
+  width: 40px;
+  border: 0;
+  border-radius: 0;
+  background: url(../../../misc/icons/bebebe/ex.svg) center center no-repeat;
+  color: transparent;
+  cursor: pointer;
+  z-index: 501;
+}
+#offcanvas > button.offcanvasClose:focus {
+  outline: none;
+}
+[dir="rtl"] #offcanvas > button.offcanvasClose {
+  float: left;
+}
+
+/* Create a place to name the tray. */
+#offcanvas h1 {
+  padding: 15px 25% 15px 15px; /* LTR */
+  margin-top: 0;
+  margin-bottom: 0;
+  font-size: 120%;
+}
+[dir="rtl"] #offcanvas h1 {
+  text-align: right;
+  padding-right: 0;
+  padding-left: 25%;
+}
+
+/* Wrap the form that's inside the offcanvas tray. */
+#offcanvas > .offcanvas-content {
+  height: 10000px;
+  padding: 0 15px;
+}
+[dir="rtl"] #offcanvas .offcanvas-content {
+  text-align: right;
+}
+#offcanvas > .form-item,
+#offcanvas > .form-item .form-item {
+  width: 100%;
+}
+
+/*
+ * Position the edit toolbar tab.
+ * @todo Move changes into contextual module when outside-in is not experimental.
+ *   https://www.drupal.org/node/2784591
+ */
+.toolbar .toolbar-bar .contextual-toolbar-tab.toolbar-tab {
+  float: left;
+}
+
+/* Media queries. */
+/* @todo Rework breakpoints. https://www.drupal.org/node/2784599 */
+@media (max-width: 700px) {
+  #offcanvas {
+    position: absolute;
+    display: block;
+    right: 0;
+    top: 0;
+    width: 300px;
+    margin-right: -300px;
+    padding-top: 39px;
+  }
+  /* Wrap the rest of the site so we can control its width. */
+  #main-canvas-wrapper #main-canvas {
+    display: inline-block;
+    width: 100%;
+  }
+  #main-canvas-wrapper.js-tray-open #offcanvas {
+    margin-right: 0;
+    right: 0;
+    top: 0;
+  }
+  #main-canvas-wrapper.js-tray-open #main-canvas {
+    position: static;
+    width: 100%;
+  }
+}
+@media (min-width: 700px) {
+  /* Position the offcanvas tray container outside the right of the viewport. */
+  #offcanvas {
+    position: fixed;
+    display: inline-block;
+    width: 35%;
+    -webkit-transform: translateX(100%);
+    -moz-transform: translateX(100%);
+    -o-transform: translateX(100%);
+    -ms-transform: translateX(100%);
+    transform: translateX(100%);
+  }
+  [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%);
+  }
+  /* Wrap the rest of the site so we can control its width. */
+  #main-canvas-wrapper #main-canvas {
+    display: inline-block;
+    width: 100%;
+  }
+  /* Move the offcanvas tray on canvas. */
+  #main-canvas-wrapper.js-tray-open #offcanvas {
+    -webkit-transform: translateX(0);
+    -moz-transform: translateX(0);
+    -o-transform: translateX(0);
+    -ms-transform: translateX(0);
+    transform: translateX(0);
+  }
+  /* Reduce the width of the main canvas to provide space for the offcanvas tray. */
+  #main-canvas-wrapper.js-tray-open #main-canvas {
+    width: 65%;
+  }
+}
+@media (min-width: 900px) {
+  /* Position the offcanvas tray container outside the right of the viewport. */
+  #offcanvas {
+    position: fixed;
+    display: inline-block;
+    width: 30%;
+  }
+  /* Wrap the rest of the site so we can control its width. */
+  #main-canvas-wrapper #main-canvas {
+    display: inline-block;
+    width: 100%;
+  }
+  /* Reduce the width of the main canvas to provide space for the offcanvas tray. */
+  #main-canvas-wrapper.js-tray-open #main-canvas {
+    width: 70%;
+  }
+}
+@media (min-width: 1000px) {
+  /* Position the offcanvas tray container outside the right of the viewport. */
+  #offcanvas {
+    position: fixed;
+    display: inline-block;
+    width: 25%;
+  }
+  /* Wrap the rest of the site so we can control its width. */
+  #main-canvas-wrapper #main-canvas {
+    display: inline-block;
+    width: 100%;
+  }
+  /* Reduce the width of the main canvas to provide space for the offcanvas tray. */
+  #main-canvas-wrapper.js-tray-open #main-canvas {
+    width: 75%;
+  }
+}
+
+/*
+ * Form layout changes, mostly specific to Bartik theme and menu.
+ * @todo Remove when more general form styling is done.
+ *    https://www.drupal.org/node/2784437
+ */
+#offcanvas td {
+    width: auto;
+}
+#offcanvas .menu-enabled {
+    width: auto;
+}
+#offcanvas table#menu-overview th {
+  display: none;
+}
+#offcanvas table#menu-overview tr td:first-child {
+  min-width: 110px;
+}
+#offcanvas details > .details-wrapper {
+  padding: 5px;
+  overflow: scroll;
+}
+#offcanvas .tabledrag-toggle-weight {
+  font-size: 80%;
+}
+#offcanvas input:focus,
+#offcanvas summary:focus {
+  outline: none;
+  box-shadow: 2px 2px #ddd;
+}
+
diff --git a/core/modules/outside_in/css/outside_in.motion.css b/core/modules/outside_in/css/outside_in.motion.css
new file mode 100644
index 0000000..e9d0981
--- /dev/null
+++ b/core/modules/outside_in/css/outside_in.motion.css
@@ -0,0 +1,68 @@
+/**
+ * @file
+ * Motion effects for outside-in module.
+ *
+ * Motion effects are in a separate file so that they can be easily turned off
+ * to improve performance if desired.
+ *
+ * @todo Move motion effects file into a core Off-Canvas library and add a configuration
+ *   option for browser rendering performance to disable this file. https://www.drupal.org/node/2784443
+ */
+
+/* Transition the offcanvas tray container, with 2s delay to match main canvas speed. */
+#offcanvas {
+  -webkit-transition: all .7s ease 2s;
+  -moz-transition: all .7s ease 2s;
+  transition: all .7s ease 2s;
+}
+#main-canvas-wrapper #main-canvas,
+#main-canvas-wrapper.js-tray-open #main-canvas {
+  -webkit-transition: all .7s ease;
+  -moz-transition: all .7s ease;
+  transition: all .7s ease;
+}
+
+/* Transition the edit icon in the toolbar. */
+#toolbar-bar.button.toolbar-icon.toolbar-icon.toolbar-icon-edit:before {
+  -webkit-transition: all .7s ease;
+  -moz-transition: all .7s ease;
+  transition: all .7s ease;
+}
+
+/* Transition the editables on the page, their contextual links and their hover states. */
+#main-canvas-wrapper .contextual,
+#main-canvas-wrapper .outside-in-editable,
+#main-canvas-wrapper.js-tray-open .outside-in-editable {
+  -webkit-transition: all .7s ease;
+  -moz-transition: all .7s ease;
+  transition: all .7s ease;
+}
+
+/* Transition the position of the toolbar. */
+.toolbar-fixed,
+.toolbar-tray-open {
+  -webkit-transition: all .5s ease;
+  -moz-transition: all .5s ease;
+  transition: all .5s ease;
+}
+
+@media (max-width: 700px) {
+  #offcanvas {
+    -webkit-transition: all .7s ease;
+    -moz-transition: all .7s ease;
+    transition: all .7s ease;
+  }
+  #main-canvas-wrapper.js-tray-open #offcanvas {
+    -webkit-transition: all .7s ease;
+    -moz-transition: all .7s ease;
+    transition: all .7s ease;
+  }
+}
+
+/* Transition the administration tray.
+#toolbar-administration,
+#toolbar-administration * {
+  -webkit-transition: all .7s ease;
+  -moz-transition: all .7s ease;
+  transition: all .7s ease;
+}*/
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..da7efb1
--- /dev/null
+++ b/core/modules/outside_in/css/outside_in.theme.css
@@ -0,0 +1,103 @@
+/**
+ * @file
+ * Visual styling for Outside-In module.
+ */
+
+/* Style the contextual links toggle tab in the toolbar. */
+/* @todo Move this into toolbar when module is not experimental. https://www.drupal.org/node/2784593 */
+
+/* Style both the edit and editing states. */
+button.toolbar-icon.toolbar-icon-edit.toolbar-item {
+  background: #0e69be;
+  background-image: -webkit-linear-gradient(top, #0094f0, #0e69be);
+  background-image: linear-gradient(to bottom, #0094f0, #0e69be);
+}
+button.toolbar-icon.toolbar-icon-edit.toolbar-item:hover,
+button.toolbar-icon.toolbar-icon-edit.toolbar-item:focus {
+  background-image: -webkit-linear-gradient(top, #0094f0, #0e69be);
+  background-image: linear-gradient(to bottom, #0094f0, #0e69be);
+  color: #fff;
+}
+button.toolbar-icon.toolbar-icon-edit.toolbar-item:before:hover,
+button.toolbar-icon.toolbar-icon-edit.toolbar-item:before:focus {
+  background-image: url(../../../misc/icons/ffffff/pencil.svg);
+}
+button.toolbar-icon.toolbar-icon-edit.toolbar-item:hover,
+button.toolbar-icon.toolbar-icon-edit.toolbar-item:focus {
+  background-image: -webkit-linear-gradient(top, #0094f0, #0e69be);
+  background-image: linear-gradient(to bottom, #0094f0, #0e69be);
+  outline: none;
+}
+button.toolbar-icon.toolbar-icon-edit.toolbar-item:hover > .toolbar-icon-edit:before {
+  background-image: url(../../../misc/icons/ffffff/pencil.svg);
+}
+#toolbar-bar.button.toolbar-icon.toolbar-icon.toolbar-icon-edit:before {
+  background-image: url(../../../misc/icons/ffffff/pencil.svg);
+}
+
+/* Style the toolbar when in edit mode. */
+#toolbar-bar.js-outside-in-edit-mode {
+  background-color: #fff;
+}
+/* Change text color for white background. */
+#toolbar-bar.js-outside-in-edit-mode .toolbar-item {
+  color: #999;
+}
+#toolbar-bar.js-outside-in-edit-mode .toolbar-item .is-active {
+  color: #333;
+}
+/* Set color back to white for 'editing' button only. */
+#toolbar-bar.js-outside-in-edit-mode button.toolbar-icon.toolbar-icon-edit.toolbar-item.is-active {
+ color: #fff;
+}
+#toolbar-bar.js-outside-in-edit-mode button.toolbar-icon.toolbar-icon-edit.toolbar-item.is-active:hover {
+ background-image: -webkit-linear-gradient(top, #0094f0, #0e69be);
+ background-image: linear-gradient(to bottom, #0094f0, #0e69be);
+}
+
+/*
+ * Style the editables while in edit mode.
+ */
+
+/* Highlight editable regions in edit mode. */
+#main-canvas.js-outside-in-edit-mode .outside-in-editable {
+  outline: 1px dashed rgba(0,0,0,0.5);
+  box-shadow: 0 0 0 1px rgba(255,255,255,0.7);
+}
+#main-canvas.js-outside-in-edit-mode .outside-in-editable:hover {
+  outline: 1px dashed rgba(0,0,0,0.5);
+  box-shadow: 0 0 0 1px rgba(255,255,255,0.7);
+  background-color: rgba(0,0,0,0.2);
+}
+/* Turn off the outlines on editables when the tray is open. */
+#main-canvas-wrapper.js-tray-open .outside-in-editable {
+  outline: transparent;
+  outline-color: transparent;
+  box-shadow: none;
+}
+#main-canvas-wrapper.js-tray-open .contextual {
+  opacity: 0;
+}
+#main-canvas-wrapper.js-tray-open .contextual:hover {
+  opacity: 1;
+}
+
+/*
+ * Style the offcanvas container.
+ * @todo Move Off-canvas css into core Off-canvas library. https://www.drupal.org/node/2784443
+ */
+#offcanvas {
+  background: #fff;
+  border-left: 1px solid #ddd; /* LTR */
+  box-shadow: -2px 2px 1px 1px rgba(0, 0, 0, 0.1); /* LTR */
+}
+[dir="rtl"] #offcanvas {
+  border-right: 1px solid #ddd;
+  box-shadow: 2px 2px 1px 1px rgba(0, 0, 0, 0.1);
+}
+
+/* Style the tray header. */
+#offcanvas h1 {
+  font-size: 120%;
+  border-bottom: 1px solid #ddd;
+}
diff --git a/core/modules/outside_in/js/offcanvas.js b/core/modules/outside_in/js/offcanvas.js
new file mode 100644
index 0000000..ddfe38e
--- /dev/null
+++ b/core/modules/outside_in/js/offcanvas.js
@@ -0,0 +1,131 @@
+/**
+ * @file
+ * Drupal's off-canvas library.
+ */
+
+(function ($, Drupal) {
+
+  'use strict';
+
+  // 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)
+  };
+
+  /**
+   * Create a wrapper container for the off-canvas element.
+   *
+   * @return {jQuery}
+   *   jQuery object that is the off-canvas wrapper element.
+   */
+  Drupal.theme.createOffCanvasWrapper = function createOffCanvasWrapper() {
+    return $('<div id="offcanvas" ' + (document.dir === 'ltr' ? 'data-offset-right' : 'data-offset-left') + ' role="region" aria-labelledby="offcanvas-header"></div>');
+  };
+
+  /**
+   * 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.
+   */
+  Drupal.theme.createTitle = function createTitle(title) {
+    return $('<h1 id="offcanvas-header">' + title + '</h1>');
+  };
+
+  /**
+   * 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.
+   */
+  Drupal.theme.createOffCanvasContent = function createOffCanvasContent(data) {
+    return $('<div class="offcanvas-content">' + data + '</div>');
+  };
+
+  /**
+   * 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 {jQuery}
+   *   jQuery object that is the off-canvas close element.
+   */
+  Drupal.theme.createOffCanvasClose = function createOffCanvasClose(offCanvasWrapper, pageWrapper) {
+    return $([
+      '<button class="offcanvasClose" aria-label="',
+      Drupal.t('Close configuration tray.'),
+      '"><span class="visually-hidden">',
+      Drupal.t('Close'),
+      '</span></button>'
+    ].join(''))
+    .on('click', function () {
+      pageWrapper
+        .removeClass('js-tray-open')
+        .one('webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend', function () {
+          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 on breakpoints for tray size. https://www.drupal.org/node/2784599
+    var $pageWrapper = $('#main-canvas-wrapper');
+    // var pageWidth = $pageWrapper.width();
+
+    // Construct off-canvas wrapper
+    var $offcanvasWrapper = Drupal.theme('createOffCanvasWrapper');
+
+    // Construct off-canvas internal elements.
+    var $offcanvasClose = Drupal.theme('createOffCanvasClose', $offcanvasWrapper, $pageWrapper);
+    var $title = Drupal.theme('createTitle', response.dialogOptions.title);
+    var $offcanvasContent = Drupal.theme('createOffCanvasContent', response.data);
+
+    // Put everything together.
+    $offcanvasWrapper.append([$offcanvasClose, $title, $offcanvasContent]);
+
+    // Handle opening or updating tray with content.
+    var existingTray = false;
+    if (Drupal.offCanvas.visible) {
+      // Remove previous content then append new content.
+      $pageWrapper.find('#offcanvas').remove();
+      existingTray = true;
+    }
+    $pageWrapper.addClass('js-tray-open');
+    Drupal.offCanvas.visible = true;
+    $pageWrapper.append($offcanvasWrapper);
+    if (existingTray) {
+      Drupal.announce(Drupal.t('Configuration tray content has been updated.'));
+    }
+    else {
+      Drupal.announce(Drupal.t('Configuration tray opened.'));
+    }
+    Drupal.attachBehaviors(document.querySelector('#offcanvas'), drupalSettings);
+  };
+
+})(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..8648921
--- /dev/null
+++ b/core/modules/outside_in/js/outside_in.js
@@ -0,0 +1,147 @@
+/**
+ * @file
+ * Drupal's Outside-In library.
+ */
+
+(function ($, Drupal) {
+
+  'use strict';
+
+  $('.outside-in-editable')
+    // Bind an event listener to the .outside-in-editable div
+    // This listen for click events and stops default actions of those elements.
+    .on('click', '.js-outside-in-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.
+    .not('div.contextual a, div.contextual button')
+    .on('click', function (e) {
+      if ($(e.target.offsetParent).hasClass('contextual')) {
+        return;
+      }
+      if (!localStorage.getItem('Drupal.contextualToolbar.isViewing')) {
+        return;
+      }
+      var editLink = $(e.target).find('li.outside-inblock-configure a')[0];
+      if (!editLink) {
+        var closest = $(e.target).closest('.outside-in-editable');
+        editLink = closest.find('li.outside-inblock-configure a')[0];
+      }
+      editLink.click();
+    });
+
+  /**
+   * React to contextual links being added.
+   *
+   * @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.
+    // @todo Fix contextual links to work with use-ajax links in
+    //    https://www.drupal.org/node/2764931.
+    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);
+    });
+
+    // Bind a listener to all 'Quick edit' links for blocks
+    // Click "Edit" button in toolbar to force Contextual Edit which starts
+    // Outside-In edit mode also.
+    data.$el.find('.outside-inblock-configure a').on('click', function () {
+      if (!isActiveMode()) {
+        $('div.contextual-toolbar-tab.toolbar-tab button').click();
+      }
+    });
+  });
+
+  /**
+   * Gets all items that should be toggled with class during edit mode.
+   *
+   * @return {*}
+   *   Items that should be toggled.
+   */
+  var getItemsToToggle = function () {
+    return $('#main-canvas, #toolbar-bar, .outside-in-editable a, .outside-in-editable button')
+      .not('div.contextual a, div.contextual button');
+  };
+
+  var isActiveMode = function () {
+    return $('#toolbar-bar').hasClass('js-outside-in-edit-mode');
+  };
+
+  var setToggleActiveMode = function setToggleActiveMode(forceActive) {
+    forceActive = forceActive || false;
+    if (forceActive || !isActiveMode()) {
+      $('#toolbar-bar .contextual-toolbar-tab button').text(Drupal.t('Editing'));
+      // Close the Manage tray if open when entering edit mode.
+      if ($('#toolbar-item-administration-tray').hasClass('is-active')) {
+        $('#toolbar-item-administration').click();
+      }
+      getItemsToToggle().addClass('js-outside-in-edit-mode');
+      $('.edit-mode-inactive').addClass('visually-hidden');
+    }
+    else {
+      $('#toolbar-bar .contextual-toolbar-tab button').text(Drupal.t('Edit'));
+      getItemsToToggle().removeClass('js-outside-in-edit-mode');
+      $('.edit-mode-inactive').removeClass('visually-hidden');
+    }
+  };
+
+  /**
+   * 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 () {
+      var editMode = localStorage.getItem('Drupal.contextualToolbar.isViewing') === 'false';
+      if (editMode) {
+        setToggleActiveMode(true);
+      }
+    }
+  };
+
+  /**
+   * Toggle the js-outside-edit-mode class on items that we want to disable while in edit mode.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Toggle the js-outside-edit-mode class.
+   */
+  Drupal.behaviors.toggleActiveMode = {
+    attach: function () {
+      $('.contextual-toolbar-tab.toolbar-tab button').on('click', function () {
+        setToggleActiveMode();
+      });
+    }
+  };
+
+})(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..2ff1c7a
--- /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 change the most common 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.install b/core/modules/outside_in/outside_in.install
new file mode 100644
index 0000000..d042915
--- /dev/null
+++ b/core/modules/outside_in/outside_in.install
@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the Outside-In module.
+ */
+
+use Drupal\Core\Cache\Cache;
+
+/**
+ * Implements hook_install().
+ */
+function outside_in_install() {
+  // This module affects the rendering of blocks and of the page.
+  // @todo Remove in https://www.drupal.org/node/2783791.
+  Cache::invalidateTags(['rendered']);
+
+  // \Drupal\Core\Menu\ContextualLinkManager caches per-group definitions
+  // without associating the cache tag that would allow them to be cleared
+  // by its clearCachedDefinitions() implementation that is automatically
+  // invoked when modules are installed.
+  // @todo Remove when that is fixed in https://www.drupal.org/node/2773591.
+  \Drupal::service('cache.discovery')->deleteAll();
+}
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..8787d77
--- /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: {}
+  css:
+    component:
+      css/outside_in.module.css: {}
+      css/outside_in.theme.css: {}
+      css/outside_in.motion.css: {}
+  dependencies:
+    - core/jquery
+    - core/drupal
+drupal.off_canvas:
+  version: VERSION
+  js:
+    js/offcanvas.js: {}
+  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..99b36ef
--- /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..3e79d68
--- /dev/null
+++ b/core/modules/outside_in/outside_in.module
@@ -0,0 +1,137 @@
+<?php
+
+/**
+ * @file
+ * Allows configuring blocks and other configuration from the site front-end.
+ */
+
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\outside_in\Block\BlockEntityOffCanvasForm;
+use Drupal\outside_in\Form\SystemBrandingOffCanvasForm;
+use Drupal\outside_in\Form\SystemMenuOffCanvasForm;
+
+/**
+ * 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. https://www.drupal.org/node/2784465
+      $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_block_view_alter().
+ */
+function outside_in_block_view_alter(array &$build) {
+  // Force a new 'data-contextual-id' attribute on blocks when this module is
+  // enabled so as not to reuse stale data cached client-side.
+  // @todo Remove when https://www.drupal.org/node/2773591 is fixed.
+  $build['#contextual_links']['outside_in'] = [
+    'route_parameters' => [],
+  ];
+}
+
+/**
+ * Implements hook_page_top().
+ */
+function outside_in_page_top(array &$page_top) {
+  // Opens a div for consistent wrapping to {{ page }} render in all themes.
+  $page_top['outside_in_tray_open'] = [
+    '#markup' => '<div id="main-canvas-wrapper"><div id="main-canvas">',
+    '#weight' => 1000,
+  ];
+}
+
+/**
+ * Implements hook_page_bottom().
+ */
+function outside_in_page_bottom(array &$page_bottom) {
+  // Closes a div for consistent wrapping to {{ page }} render in all themes.
+  $page_bottom['outside_in_tray_close'] = [
+    '#markup' => '</div></div>',
+    '#weight' => -1000,
+  ];
+}
+
+/**
+ * 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.
+ */
+function outside_in_preprocess_block(&$variables) {
+  // The main system block does not contain the block contextual links.
+  $variables['#cache']['contexts'][] = 'outside_in_is_applied';
+  if (\Drupal::service('outside_in.manager')->isApplicable() && $variables['plugin_id'] !== 'system_main_block') {
+    // Add class to all blocks to allow Javascript to target.
+    $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) {
+  $items['contextual']['#cache']['contexts'][] = 'outside_in_is_applied';
+  if (\Drupal::service('outside_in.manager')->isApplicable() && isset($items['contextual']['tab'])) {
+    $items['contextual']['#weight'] = -1000;
+    $items['contextual']['#attached']['library'][] = 'outside_in/drupal.outside_in';
+
+    // Set a class on items to mark whether they should be active in edit mode.
+    // @todo Create a dynamic method for modules to set their own items.
+    //   https://www.drupal.org/node/2784589
+
+    $edit_mode_items = ['contextual', 'block_place'];
+    foreach ($items as $key => $item) {
+      if (!in_array($key, $edit_mode_items) && (!isset($items[$key]['#wrapper_attributes']['class']) || !in_array('hidden', $items[$key]['#wrapper_attributes']['class']))) {
+        $items[$key]['#wrapper_attributes']['class'][] = 'edit-mode-inactive';
+      }
+    }
+  }
+}
+
+/**
+ * Implements hook_block_alter().
+ */
+function outside_in_block_alter(&$definitions) {
+  if (!empty($definitions['system_branding_block'])) {
+    $definitions['system_branding_block']['forms']['offcanvas'] = SystemBrandingOffCanvasForm::class;
+  }
+
+  // Since menu blocks use derivatives, check the definition ID instead of
+  // relying on the plugin ID.
+  foreach ($definitions as &$definition) {
+    if ($definition['id'] === 'system_menu_block') {
+      $definition['forms']['offcanvas'] = SystemMenuOffCanvasForm::class;
+    }
+  }
+}
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..9ac920a
--- /dev/null
+++ b/core/modules/outside_in/outside_in.services.yml
@@ -0,0 +1,16 @@
+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.manager:
+    class: Drupal\outside_in\OutsideInManager
+    arguments: ['@router.admin_context', '@current_route_match', '@current_user']
+
+  cache_context.outside_in_is_applied:
+    class: Drupal\outside_in\Cache\Context\OutsideInCacheContext
+    arguments: ['@outside_in.manager']
+    tags:
+      - { name: cache.context}
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..7a49505
--- /dev/null
+++ b/core/modules/outside_in/src/Ajax/OpenOffCanvasDialogCommand.php
@@ -0,0 +1,52 @@
+<?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.
+   *
+   * @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..089feae
--- /dev/null
+++ b/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Drupal\outside_in\Block;
+
+use Drupal\block\BlockForm;
+use Drupal\Core\Block\BlockPluginInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\PluginWithFormsInterface;
+
+/**
+ * Provides form for block instance forms when used in the off-canvas tray.
+ *
+ * This form removes advanced sections of regular block form such as the
+ * visibility settings, machine ID and region.
+ */
+class BlockEntityOffCanvasForm extends BlockForm {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form = parent::form($form, $form_state);
+
+    // Create link to full block form.
+    $query = [];
+    if ($destination = $this->getRequest()->query->get('destination')) {
+      $query['destination'] = $destination;
+    }
+    $form['advanced_link'] = [
+      '#type' => 'link',
+      '#title' => $this->t('Advanced options'),
+      '#url' => $this->entity->toUrl('edit-form', ['query' => $query]),
+      '#weight' => 1000,
+    ];
+
+    // Remove the ID and region elements.
+    unset($form['id'], $form['region']);
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildVisibilityInterface(array $form, FormStateInterface $form_state) {
+    // Do not display the visibility.
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function validateVisibility(array $form, FormStateInterface $form_state) {
+    // Intentionally empty.
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function submitVisibility(array $form, FormStateInterface $form_state) {
+    // Intentionally empty.
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getPluginForm(BlockPluginInterface $block) {
+    if ($block instanceof PluginWithFormsInterface) {
+      return $this->pluginFormFactory->createInstance($block, 'offcanvas', 'configure');
+    }
+    return $block;
+  }
+
+}
diff --git a/core/modules/outside_in/src/Cache/Context/OutsideInCacheContext.php b/core/modules/outside_in/src/Cache/Context/OutsideInCacheContext.php
new file mode 100644
index 0000000..193b948
--- /dev/null
+++ b/core/modules/outside_in/src/Cache/Context/OutsideInCacheContext.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\outside_in\Cache\Context;
+
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Cache\Context\CacheContextInterface;
+use Drupal\outside_in\OutsideInManagerInterface;
+
+/**
+ * Defines the OutsideInCacheContext service, for "Outside-In or not" caching.
+ *
+ * Cache context ID: 'outside_in_is_applied'.
+ */
+class OutsideInCacheContext implements CacheContextInterface {
+
+  /**
+   * The Outside-In manager.
+   *
+   * @var \Drupal\outside_in\OutsideInManagerInterface
+   */
+  protected $outsideInManager;
+
+  /**
+   * OutsideInCacheContext constructor.
+   *
+   * @param \Drupal\outside_in\OutsideInManagerInterface $outside_in_manager
+   *   The Outside-In manager.
+   */
+  public function __construct(OutsideInManagerInterface $outside_in_manager) {
+    $this->outsideInManager = $outside_in_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getLabel() {
+    return t('Outside-In');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getContext() {
+    return $this->outsideInManager->isApplicable() ? '1' : '0';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheableMetadata() {
+    return new CacheableMetadata();
+  }
+
+}
diff --git a/core/modules/outside_in/src/Form/SystemBrandingOffCanvasForm.php b/core/modules/outside_in/src/Form/SystemBrandingOffCanvasForm.php
new file mode 100644
index 0000000..65139ec
--- /dev/null
+++ b/core/modules/outside_in/src/Form/SystemBrandingOffCanvasForm.php
@@ -0,0 +1,102 @@
+<?php
+
+namespace Drupal\outside_in\Form;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\PluginFormBase;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * The off-canvas form handler for the SystemBrandingBlock.
+ *
+ * @see outside_in_block_alter()
+ */
+class SystemBrandingOffCanvasForm extends PluginFormBase implements ContainerInjectionInterface {
+
+  /**
+   * The block plugin.
+   *
+   * @var \Drupal\Core\Block\BlockPluginInterface
+   */
+  protected $plugin;
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * SystemBrandingOffCanvasForm constructor.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory) {
+    $this->configFactory = $config_factory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('config.factory')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $form = $this->plugin->buildConfigurationForm($form, $form_state);
+
+    // Unset links to Site Information form, we can make these changes here.
+    unset($form['block_branding']['use_site_name']['#description'], $form['block_branding']['use_site_slogan']['#description']);
+
+    $site_config = $this->configFactory->getEditable('system.site');
+    $form['site_information'] = [
+      '#type' => 'details',
+      '#title' => t('Site details'),
+      '#open' => TRUE,
+      '#weight' => -100,
+    ];
+    $form['site_information']['site_name'] = [
+      '#type' => 'textfield',
+      '#title' => t('Site name'),
+      '#default_value' => $site_config->get('name'),
+      '#required' => TRUE,
+    ];
+    $form['site_information']['site_slogan'] = [
+      '#type' => 'textfield',
+      '#title' => t('Slogan'),
+      '#default_value' => $site_config->get('slogan'),
+      '#description' => t("How this is used depends on your site's theme."),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+    $this->plugin->validateConfigurationForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    $site_info = $form_state->getValue('site_information');
+    $this->configFactory->getEditable('system.site')
+      ->set('name', $site_info['site_name'])
+      ->set('slogan', $site_info['site_slogan'])
+      ->save();
+    $this->plugin->submitConfigurationForm($form, $form_state);
+  }
+
+}
diff --git a/core/modules/outside_in/src/Form/SystemMenuOffCanvasForm.php b/core/modules/outside_in/src/Form/SystemMenuOffCanvasForm.php
new file mode 100644
index 0000000..73cda59
--- /dev/null
+++ b/core/modules/outside_in/src/Form/SystemMenuOffCanvasForm.php
@@ -0,0 +1,137 @@
+<?php
+
+namespace Drupal\outside_in\Form;
+
+use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\PluginFormBase;
+use Drupal\Core\Render\Element;
+use Drupal\Core\Routing\RedirectDestinationTrait;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+use Drupal\system\MenuInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * The off-canvas form handler for the SystemMenuBlock.
+ *
+ * @see outside_in_block_alter()
+ */
+class SystemMenuOffCanvasForm extends PluginFormBase implements ContainerInjectionInterface {
+
+  use StringTranslationTrait;
+  use RedirectDestinationTrait;
+
+  /**
+   * The plugin.
+   *
+   * @var \Drupal\Core\Block\BlockPluginInterface
+   */
+  protected $plugin;
+
+  /**
+   * @var \Drupal\system\MenuInterface
+   */
+  protected $entity;
+
+  /**
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $menuStorage;
+
+  /**
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * SystemMenuOffCanvasForm constructor.
+   *
+   * @param \Drupal\Core\Entity\EntityStorageInterface $menu_storage
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
+   */
+  public function __construct(EntityStorageInterface $menu_storage, EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation) {
+    $this->menuStorage = $menu_storage;
+    $this->entityTypeManager = $entity_type_manager;
+    $this->stringTranslation = $string_translation;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager')->getStorage('menu'),
+      $container->get('entity_type.manager'),
+      $container->get('string_translation')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $form = $this->plugin->buildConfigurationForm([], $form_state);
+    // Move the menu levels section to the bottom.
+    $form['menu_levels']['#weight'] = 100;
+
+    $form['entity_form'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Edit menu %label', array('%label' => $this->entity->label())),
+      '#open' => TRUE,
+    ];
+    $form['entity_form'] += $this->getEntityForm($this->entity)->buildForm([], $form_state);
+    // Remove the label, ID, description, and buttons from the entity form.
+    unset($form['entity_form']['label'], $form['entity_form']['id'], $form['entity_form']['description'], $form['entity_form']['actions']);
+    // Since the overview form is further nested than expected, update the
+    // #parents. See \Drupal\menu_ui\MenuForm::form().
+    $form_state->set('menu_overview_form_parents', ['settings', 'entity_form', 'links']);
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+    $this->plugin->validateConfigurationForm($form, $form_state);
+    $this->getEntityForm($this->entity)->validateForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    $this->plugin->submitConfigurationForm($form, $form_state);
+    $this->getEntityForm($this->entity)->submitForm($form, $form_state);
+    $this->entity->save();
+  }
+
+  /**
+   * Gets the entity form for this menu.
+   *
+   * @param \Drupal\system\MenuInterface $entity
+   *   The menu entity.
+   *
+   * @return \Drupal\Core\Entity\EntityFormInterface
+   *   The entity form.
+   */
+  protected function getEntityForm(MenuInterface $entity) {
+    $entity_form = $this->entityTypeManager->getFormObject('menu', 'edit');
+    $entity_form->setEntity($entity);
+    return $entity_form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setPlugin(PluginInspectionInterface $plugin) {
+    $this->plugin = $plugin;
+    $this->entity = $this->menuStorage->load($this->plugin->getDerivativeId());
+  }
+
+}
diff --git a/core/modules/outside_in/src/OutsideInManager.php b/core/modules/outside_in/src/OutsideInManager.php
new file mode 100644
index 0000000..295655b
--- /dev/null
+++ b/core/modules/outside_in/src/OutsideInManager.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace Drupal\outside_in;
+
+use Drupal\Core\Routing\AdminContext;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Manages information related to Outside-In.
+ */
+class OutsideInManager implements OutsideInManagerInterface {
+
+  /**
+   * The admin context service.
+   *
+   * @var \Drupal\Core\Routing\AdminContext
+   */
+  protected $adminContext;
+
+  /**
+   * The current route match.
+   *
+   * @var \Drupal\Core\Routing\RouteMatchInterface
+   */
+  protected $routeMatch;
+
+  /**
+   * The current account.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $account;
+
+  /**
+   * OutsideInManager constructor.
+   *
+   * @param \Drupal\Core\Routing\AdminContext $admin_context
+   *   The admin context service.
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The current route match.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The current account.
+   */
+  public function __construct(AdminContext $admin_context, RouteMatchInterface $route_match, AccountInterface $account) {
+    $this->adminContext = $admin_context;
+    $this->routeMatch = $route_match;
+    $this->account = $account;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isApplicable() {
+    // Remove on Admin routes.
+    $is_admin_route = $this->adminContext->isAdminRoute();
+
+    // Remove on Block Demo page.
+    $is_admin_demo_route = $this->routeMatch->getRouteName() === 'block.admin_demo';
+
+    // @todo Check if there is actually a different admin theme.
+    //   https://www.drupal.org/node/2784853
+    return $this->account->hasPermission('administer blocks') && !$is_admin_route && !$is_admin_demo_route;
+  }
+
+}
diff --git a/core/modules/outside_in/src/OutsideInManagerInterface.php b/core/modules/outside_in/src/OutsideInManagerInterface.php
new file mode 100644
index 0000000..bb3dae5
--- /dev/null
+++ b/core/modules/outside_in/src/OutsideInManagerInterface.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace Drupal\outside_in;
+
+/**
+ * Provides an interface for managing information related to Outside-In.
+ */
+interface OutsideInManagerInterface {
+
+  /**
+   * Determines if the Outside-In logic should be run on the current page.
+   *
+   * @return bool
+   *   TRUE if the Outside-In logic should be run.
+   */
+  public function isApplicable();
+
+}
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..5d24c09
--- /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 OffCanvasRender.
+   *
+   * @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 OpenOffCanvasDialogCommand 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..2d360f9
--- /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..6295f55
--- /dev/null
+++ b/core/modules/outside_in/tests/modules/offcanvas_test/src/Controller/TestController.php
@@ -0,0 +1,78 @@
+<?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',
+    ];
+  }
+
+  /**
+   * Displays 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/Plugin/Block/TestBlock.php b/core/modules/outside_in/tests/modules/offcanvas_test/src/Plugin/Block/TestBlock.php
new file mode 100644
index 0000000..da72f9b
--- /dev/null
+++ b/core/modules/outside_in/tests/modules/offcanvas_test/src/Plugin/Block/TestBlock.php
@@ -0,0 +1,49 @@
+<?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",
+ *   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..32ccc55
--- /dev/null
+++ b/core/modules/outside_in/tests/src/FunctionalJavascript/OffCanvasTest.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Drupal\Tests\outside_in\FunctionalJavascript;
+
+/**
+ * Tests the off-canvas tray functionality.
+ *
+ * @group outside_in
+ */
+class OffCanvasTest extends OutsideInJavascriptTestBase {
+
+  /**
+   * {@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() {
+    $themes = ['bartik', 'stark'];
+    // Test the same functionality on multiple themes
+    foreach ($themes as $theme) {
+      $this->enableTheme($theme);
+      $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 = $this->getTray();
+
+        // 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', '.offcanvas-content')->getText();
+        $this->assertEquals("Thing $link_index says hello", $tray_text);
+      }
+    }
+  }
+
+}
diff --git a/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php b/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php
new file mode 100644
index 0000000..58b5a7f
--- /dev/null
+++ b/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace Drupal\Tests\outside_in\FunctionalJavascript;
+
+/**
+ * Testing opening and saving block forms in the off-canvas tray.
+ *
+ * @group outside_in
+ */
+class OutsideInBlockFormTest extends OutsideInJavascriptTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['block', 'system', 'breakpoint', 'toolbar', 'contextual', 'outside_in'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    // @todo Ensure that this test class works against bartik and stark. https://www.drupal.org/node/2784881
+    $this->enableTheme('bartik');
+    $user = $this->createUser([
+      'administer blocks',
+      'access contextual links',
+      'access toolbar',
+    ]);
+    $this->drupalLogin($user);
+
+    $this->placeBlock('system_powered_by_block', ['id' => 'powered']);
+    $this->placeBlock('system_branding_block', ['id' => 'branding']);
+  }
+
+  /**
+   * Tests updating the "Powered by Drupal" block in the Off-Canvas tray.
+   */
+  public function testPoweredByBlock() {
+
+    $page = $this->getSession()->getPage();
+    $web_assert = $this->assertSession();
+
+    $this->drupalGet('user');
+    $this->enableEditingMode();
+
+    // Open "Powered by Drupal" block form by clicking div.
+    $page->find('css', '#block-powered')->click();
+    $this->waitForOffCanvasToOpen();
+    $this->assertOffCanvasBlockFormIsValid();
+
+    // Fill out form, save the form.
+    $new_label = 'Can you imagine anyone showing the label on this block?';
+    $page->fillField('settings[label]', $new_label);
+    $page->checkField('settings[label_display]');
+    $this->getTray()->pressButton('Save block');
+    // Make sure the changes are present.
+    $web_assert->pageTextContains($new_label);
+  }
+
+  /**
+   * Tests updating the System Branding block in the Off-Canvas tray.
+   *
+   * Also tests updating the site name.
+   */
+  public function testBrandingBlock() {
+    $web_assert = $this->assertSession();
+    $this->drupalGet('user');
+    $page = $this->getSession()->getPage();
+    $this->enableEditingMode();
+
+    // Open branding block form by clicking div.
+    $page->find('css', '#block-branding')->click();
+    $this->waitForOffCanvasToOpen();
+    $this->assertOffCanvasBlockFormIsValid();
+
+    // Fill out form, save the form.
+    $new_site_name = 'The site that will live a very short life.';
+    $page->fillField('settings[site_information][site_name]', $new_site_name);
+    $this->getTray()->pressButton('Save block');
+
+    // Make sure the changes are present.
+    $web_assert->pageTextContains($new_site_name);
+  }
+
+  /**
+   * Enables Editing mode by pressing "Edit" button in the toolbar.
+   */
+  protected function enableEditingMode() {
+    $this->waitForElement('div[data-contextual-id="block:block=powered:langcode=en|outside_in::langcode=en"] .contextual-links a');
+
+    $this->waitForElement('#toolbar-bar', 3000);
+
+    $edit_button = $this->getSession()->getPage()->find('css', '#toolbar-bar div.contextual-toolbar-tab button');
+
+    $edit_button->press();
+  }
+
+  /**
+   * Asserts that Off-Canvas block form is valid.
+   */
+  protected function assertOffCanvasBlockFormIsValid() {
+    $web_assert = $this->assertSession();
+    // Check that common block form elements exist.
+    $web_assert->elementExists('css', 'input[data-drupal-selector="edit-settings-label"]');
+    $web_assert->elementExists('css', 'input[data-drupal-selector="edit-settings-label-display"]');
+    // Check that advanced block form elements do not exist.
+    $web_assert->elementNotExists('css', 'input[data-drupal-selector="edit-visibility-request-path-pages"]');
+    $web_assert->elementNotExists('css', 'select[data-drupal-selector="edit-region"]');
+  }
+
+}
diff --git a/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInJavascriptTestBase.php b/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInJavascriptTestBase.php
new file mode 100644
index 0000000..5871aef
--- /dev/null
+++ b/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInJavascriptTestBase.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Drupal\Tests\outside_in\FunctionalJavascript;
+
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
+
+/**
+ * Base class contains common test functionality for the Outside-In module.
+ */
+abstract class OutsideInJavascriptTestBase extends JavascriptTestBase {
+
+  /**
+   * Enables a theme.
+   *
+   * @param string $theme
+   *   The theme.
+   */
+  public function enableTheme($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();
+  }
+
+  /**
+   * Waits for Off-canvas tray to open.
+   */
+  protected function waitForOffCanvasToOpen() {
+    $this->waitForElement('#offcanvas');
+  }
+
+  /**
+   * Waits for Off-canvas tray to close.
+   */
+  protected function waitForOffCanvasToClose() {
+    $condition = "(jQuery('#offcanvas').length == 0)";
+    $this->assertJsCondition($condition);
+  }
+
+  /**
+   * Waits for an element to appear on the page.
+   *
+   * @param string $selector
+   *   CSS selector.
+   * @param int $timeout
+   *   (optional) Timeout in milliseconds, defaults to 1000.
+   */
+  protected function waitForElement($selector, $timeout = 1000) {
+    $condition = "(jQuery('$selector').length > 0)";
+    $this->assertJsCondition($condition, $timeout);
+  }
+
+  /**
+   * Gets the Off-Canvas tray element.
+   *
+   * @return \Behat\Mink\Element\NodeElement|null
+   */
+  protected function getTray() {
+    return $this->getSession()->getPage()->findById('offcanvas');
+  }
+
+}
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/OutsideInManagerTest.php b/core/modules/outside_in/tests/src/Unit/OutsideInManagerTest.php
new file mode 100644
index 0000000..53b83e7
--- /dev/null
+++ b/core/modules/outside_in/tests/src/Unit/OutsideInManagerTest.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\Tests\outside_in\Unit;
+
+use Drupal\Core\Routing\AdminContext;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\outside_in\OutsideInManager;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\outside_in\OutsideInManager
+ * @group outside_in
+ */
+class OutsideInManagerTest extends UnitTestCase {
+
+  /**
+   * @covers ::isApplicable
+   * @dataProvider providerTestIsApplicable
+   */
+  public function testIsApplicable($is_admin_route, $route_name, $has_permission, $expected) {
+    $admin_context = $this->prophesize(AdminContext::class);
+    $admin_context->isAdminRoute()->willReturn($is_admin_route);
+
+    $route_match = $this->prophesize(RouteMatchInterface::class);
+    $route_match->getRouteName()->willReturn($route_name);
+
+    $account = $this->prophesize(AccountInterface::class);
+    $account->hasPermission('administer blocks')->willReturn($has_permission);
+
+    $outside_in_manager = new OutsideInManager($admin_context->reveal(), $route_match->reveal(), $account->reveal());
+
+    $this->assertSame($expected, $outside_in_manager->isApplicable());
+  }
+
+  public function providerTestIsApplicable() {
+    $data = [];
+
+    // Passing combination.
+    $data[] = [FALSE, 'the_route_name', TRUE, TRUE];
+
+    // Failing combinations.
+    $data[] = [TRUE, 'the_route_name', TRUE, FALSE];
+    $data[] = [TRUE, 'the_route_name', FALSE, FALSE];
+    $data[] = [TRUE, 'block.admin_demo', TRUE, FALSE];
+    $data[] = [TRUE, 'block.admin_demo', FALSE, FALSE];
+    $data[] = [FALSE, 'the_route_name', FALSE, FALSE];
+    $data[] = [FALSE, 'block.admin_demo', TRUE, FALSE];
+    $data[] = [FALSE, 'block.admin_demo', FALSE, FALSE];
+
+    return $data;
+  }
+
+}
