diff --git a/core/composer.json b/core/composer.json
index 7ae7f4f..390c1ee 100644
--- a/core/composer.json
+++ b/core/composer.json
@@ -112,6 +112,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/block/src/BlockForm.php b/core/modules/block/src/BlockForm.php
index 03448b9..3641b87 100644
--- a/core/modules/block/src/BlockForm.php
+++ b/core/modules/block/src/BlockForm.php
@@ -14,6 +14,7 @@
 use Drupal\Core\Language\LanguageManagerInterface;
 use Drupal\Core\Plugin\ContextAwarePluginInterface;
 use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
+use Drupal\Core\Plugin\PluginWithFormsInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -71,7 +72,7 @@ class BlockForm extends EntityForm {
   protected $contextRepository;
 
   /**
-   * The plugin form manager.
+   * The plugin form factory.
    *
    * @var \Drupal\Core\Plugin\PluginFormFactoryInterface
    */
@@ -90,16 +91,16 @@ class BlockForm extends EntityForm {
    *   The language manager.
    * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
    *   The theme handler.
-   * @param \Drupal\Core\Plugin\PluginFormFactoryInterface $plugin_form_manager
-   *   The plugin form manager.
+   * @param \Drupal\Core\Plugin\PluginFormFactoryInterface $plugin_form_factory
+   *   The plugin form factory.
    */
-  public function __construct(EntityManagerInterface $entity_manager, ExecutableManagerInterface $manager, ContextRepositoryInterface $context_repository, LanguageManagerInterface $language, ThemeHandlerInterface $theme_handler, PluginFormFactoryInterface $plugin_form_manager) {
+  public function __construct(EntityManagerInterface $entity_manager, ExecutableManagerInterface $manager, ContextRepositoryInterface $context_repository, LanguageManagerInterface $language, ThemeHandlerInterface $theme_handler, PluginFormFactoryInterface $plugin_form_factory) {
     $this->storage = $entity_manager->getStorage('block');
     $this->manager = $manager;
     $this->contextRepository = $context_repository;
     $this->language = $language;
     $this->themeHandler = $theme_handler;
-    $this->pluginFormFactory = $plugin_form_manager;
+    $this->pluginFormFactory = $plugin_form_factory;
   }
 
   /**
@@ -352,7 +353,30 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
     // Update the original form values.
     $form_state->setValue('settings', $settings->getValues());
 
-    // Submit visibility condition settings.
+    $this->submitVisibility($form, $form_state);
+
+    // Save the settings of the plugin.
+    $entity->save();
+
+    drupal_set_message($this->t('The block configuration has been saved.'));
+    $form_state->setRedirect(
+      'block.admin_display_theme',
+      array(
+        'theme' => $form_state->getValue('theme'),
+      ),
+      array('query' => array('block-placement' => Html::getClass($this->entity->id())))
+    );
+  }
+
+  /**
+   * Helper function to independently submit the visibility UI.
+   *
+   * @param array $form
+   *   A nested array form elements comprising the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  protected function submitVisibility(array $form, FormStateInterface $form_state) {
     foreach ($form_state->getValue('visibility') as $condition_id => $values) {
       // Allow the condition to submit the form.
       $condition = $form_state->get(['conditions', $condition_id]);
@@ -367,20 +391,8 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
       $condition_configuration = $condition->getConfiguration();
       $form_state->setValue(['visibility', $condition_id], $condition_configuration);
       // Update the visibility conditions on the block.
-      $entity->getVisibilityConditions()->addInstanceId($condition_id, $condition_configuration);
+      $this->entity->getVisibilityConditions()->addInstanceId($condition_id, $condition_configuration);
     }
-
-    // Save the settings of the plugin.
-    $entity->save();
-
-    drupal_set_message($this->t('The block configuration has been saved.'));
-    $form_state->setRedirect(
-      'block.admin_display_theme',
-      array(
-        'theme' => $form_state->getValue('theme'),
-      ),
-      array('query' => array('block-placement' => Html::getClass($this->entity->id())))
-    );
   }
 
   /**
@@ -425,7 +437,10 @@ public function getUniqueMachineName(BlockInterface $block) {
    *   The plugin form for the block.
    */
   protected function getPluginForm(BlockPluginInterface $block) {
-    return $this->pluginFormFactory->createInstance($block, 'configure');
+    if ($block instanceof PluginWithFormsInterface) {
+      return $this->pluginFormFactory->createInstance($block, 'configure');
+    }
+    return $block;
   }
 
 }
diff --git a/core/modules/block/tests/modules/block_test/src/Form/EmptyBlockForm.php b/core/modules/block/tests/modules/block_test/src/Form/EmptyBlockForm.php
new file mode 100644
index 0000000..98c6fc5
--- /dev/null
+++ b/core/modules/block/tests/modules/block_test/src/Form/EmptyBlockForm.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\block_test\Form;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\PluginFormBase;
+
+/**
+ * Provides a form for a block that is empty.
+ */
+class EmptyBlockForm extends PluginFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    // Intentionally empty.
+  }
+
+}
diff --git a/core/modules/block/tests/src/Unit/BlockFormTest.php b/core/modules/block/tests/src/Unit/BlockFormTest.php
index d8efe2b..9d81555 100644
--- a/core/modules/block/tests/src/Unit/BlockFormTest.php
+++ b/core/modules/block/tests/src/Unit/BlockFormTest.php
@@ -56,7 +56,7 @@ class BlockFormTest extends UnitTestCase {
   protected $contextRepository;
 
   /**
-   * The plugin form manager.
+   * The plugin form factory.
    *
    * @var \Drupal\Core\Plugin\PluginFormFactoryInterface|\Prophecy\Prophecy\ProphecyInterface
    */
diff --git a/core/modules/menu_ui/src/MenuForm.php b/core/modules/menu_ui/src/MenuForm.php
index c23e514..7e09fbf 100644
--- a/core/modules/menu_ui/src/MenuForm.php
+++ b/core/modules/menu_ui/src/MenuForm.php
@@ -173,12 +173,7 @@ public function menuNameExists($value) {
    */
   public function save(array $form, FormStateInterface $form_state) {
     $menu = $this->entity;
-    if (!$menu->isNew() || $menu->isLocked()) {
-      $this->submitOverviewForm($form, $form_state);
-    }
-
     $status = $menu->save();
-
     $edit_link = $this->entity->link($this->t('Edit'));
     if ($status == SAVED_UPDATED) {
       drupal_set_message($this->t('Menu %label has been updated.', array('%label' => $menu->label())));
@@ -193,6 +188,17 @@ public function save(array $form, FormStateInterface $form_state) {
   }
 
   /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    parent::submitForm($form, $form_state);
+
+    if (!$this->entity->isNew() || $this->entity->isLocked()) {
+      $this->submitOverviewForm($form, $form_state);
+    }
+  }
+
+  /**
    * Form constructor to edit an entire menu tree at once.
    *
    * Shows for one menu the menu links accessible to the current user and
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..a3e2ea5
--- /dev/null
+++ b/core/modules/outside_in/css/outside_in.module.css
@@ -0,0 +1,200 @@
+/**
+ * @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(/core/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 toolbar module when outside-in is not experimental.
+ */
+.toolbar .toolbar-bar .contextual-toolbar-tab.toolbar-tab {
+  float: left;
+}
+
+/* Media queries. */
+@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 it's 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 it's 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 it's 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 it's 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.
+ */
+#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..3aedbcd
--- /dev/null
+++ b/core/modules/outside_in/css/outside_in.motion.css
@@ -0,0 +1,64 @@
+/**
+ * @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 toolbar module and add a configuration option to performance to disable this file.
+ */
+
+/* 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..c263ba0
--- /dev/null
+++ b/core/modules/outside_in/css/outside_in.theme.css
@@ -0,0 +1,104 @@
+/**
+ * @file
+ * Visual styling for Outside-In module.
+ */
+
+/* Style the edit tab in the toolbar. */
+/* @todo Move this into core when module is not experimental. */
+
+/* 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(../../images/core/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(../../images/core/icons/ffffff/pencil.svg);
+}
+#toolbar-bar.button.toolbar-icon.toolbar-icon.toolbar-icon-edit:before {
+  background-image: url(../../images/core/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.
+ */
+div#offcanvas {
+  background: #fff;
+  border-left: 1px solid #ddd; /* LTR */
+  box-shadow: -2px 2px 1px 1px rgba(0, 0, 0, 0.1); /* LTR */
+}
+[dir="rtl"] div#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..ed3bd20
--- /dev/null
+++ b/core/modules/outside_in/js/offcanvas.js
@@ -0,0 +1,125 @@
+/**
+ * @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: 'offcanvas-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 = $('#main-canvas-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.
+    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..6d72247
--- /dev/null
+++ b/core/modules/outside_in/js/outside_in.js
@@ -0,0 +1,139 @@
+/**
+ * @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) {
+    setToggleActiveMode();
+  });
+
+  // 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-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.
+  $('.outside-in-editable')
+    .not('div.contextual a, div.contextual button')
+    .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 parents = $(e.target).parents('.outside-in-editable');
+        editLink = parents.find('li.outside-inblock-configure a')[0];
+      }
+      editLink.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);
+    });
+
+    // 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').click(function (e) {
+      if (!isActiveMode()) {
+        $('div.contextual-toolbar-tab.toolbar-tab button').click();
+      }
+    });
+  });
+
+  /**
+   * Gets all items that should be toggled with class during edit mode.
+   *
+   * @returns {*}
+   *   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 (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 (context) {
+      var editMode = localStorage.getItem('Drupal.contextualToolbar.isViewing') === 'false';
+      if (editMode) {
+        setToggleActiveMode(true);
+      }
+    }
+  };
+
+})(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..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..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..c757e59
--- /dev/null
+++ b/core/modules/outside_in/outside_in.module
@@ -0,0 +1,140 @@
+<?php
+
+/**
+ * @file
+ * Allows configuring blocks and other configuration from the front-end of the site.
+ */
+
+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.
+      $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) {
+  if (Drupal::service('outside_in.info')->useOutsideIn()) {
+    $page_top['outside_in_tray_open'] = [
+      '#markup' => '<div id="main-canvas-wrapper"><div id="main-canvas">',
+      '#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) {
+  if (Drupal::service('outside_in.info')->useOutsideIn()) {
+    $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.
+ *
+ * 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);
+
+  // The main system block does not contain the block contextual links.
+  if (!$access || $variables['plugin_id'] == 'system_main_block') {
+    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 (Drupal::service('outside_in.info')->useOutsideIn() && 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.
+    $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..c240a18
--- /dev/null
+++ b/core/modules/outside_in/outside_in.services.yml
@@ -0,0 +1,10 @@
+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.info:
+    class: Drupal\outside_in\PageInfo
+    arguments: ['@current_route_match', '@theme.negotiator.admin_theme']
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..313fbe1
--- /dev/null
+++ b/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php
@@ -0,0 +1,81 @@
+<?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;
+use Drupal\Core\Url;
+
+/**
+ * Provides form for block instance forms when used in the off-canvas tray.
+ *
+ * This form will remove 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 = [];
+    $advance_url = Url::fromRoute(
+      'entity.block.edit_form',
+      [
+        'block' => $this->entity->id(),
+      ]
+    );
+
+    if ($destination = $this->getRequest()->query->has('destination')) {
+      $query['destination'] = $this->getRequest()->query->get('destination');
+      $advance_url->setOption('query', $query);
+    }
+    $form['advanced_link'] = [
+      '#type' => 'link',
+      '#title' => $this->t('Advanced Options'),
+      '#url' => $advance_url,
+      '#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/Form/SystemBrandingOffCanvasForm.php b/core/modules/outside_in/src/Form/SystemBrandingOffCanvasForm.php
new file mode 100644
index 0000000..6e71c76
--- /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 OffCanvas form handler for the SystemBrandingBlock.
+ *
+ * @see outside_in_block_alter()
+ */
+class SystemBrandingOffCanvasForm extends PluginFormBase implements ContainerInjectionInterface {
+
+  /**
+   * The 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..627d42e
--- /dev/null
+++ b/core/modules/outside_in/src/Form/SystemMenuOffCanvasForm.php
@@ -0,0 +1,159 @@
+<?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 OffCanvas 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) {
+    $this->ensureFormState($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);
+    unset($form['entity_form']['label'], $form['entity_form']['id'], $form['entity_form']['description'], $form['entity_form']['actions']);
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+    $this->ensureFormState($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->ensureFormState($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;
+  }
+
+  /**
+   * Ensures the form state is set up correctly.
+   *
+   * @todo Remove this once https://www.drupal.org/node/2537732 is fixed.
+   *
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  protected function ensureFormState(FormStateInterface $form_state) {
+    // Prepare $form_state for \Drupal\menu_ui\MenuForm::submitOverviewForm().
+    $input = &$form_state->getUserInput();
+    if (is_null($input)) {
+      $input = [];
+    }
+
+    if (!$this->entity->isNew() || $this->entity->isLocked()) {
+      $form_state->set('menu_overview_form_parents', ['settings', 'entity_form', 'links']);
+    }
+  }
+
+  /**
+   * {@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/PageInfo.php b/core/modules/outside_in/src/PageInfo.php
new file mode 100644
index 0000000..649439a
--- /dev/null
+++ b/core/modules/outside_in/src/PageInfo.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\outside_in;
+
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Theme\ThemeNegotiatorInterface;
+
+/**
+ * Outside In Page Info service.
+ */
+class PageInfo {
+
+  /**
+   * The admin theme negotiator.
+   *
+   * @var \Drupal\Core\Theme\ThemeNegotiatorInterface
+   */
+  protected $adminThemeNegotiator;
+
+  /**
+   * The current route match.
+   *
+   * @var \Drupal\Core\Routing\RouteMatchInterface
+   */
+  protected $routeMatch;
+
+  /**
+   * PageInfo constructor.
+   *
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The current route match.
+   * @param \Drupal\Core\Theme\ThemeNegotiatorInterface $admin_theme_negotiator
+   *   The admin theme negotiator.
+   */
+  public function __construct(RouteMatchInterface $route_match, ThemeNegotiatorInterface $admin_theme_negotiator) {
+    $this->adminThemeNegotiator = $admin_theme_negotiator;
+    $this->routeMatch = $route_match;
+  }
+
+  /**
+   * Determines whether outside should be use in the current requests.
+   *
+   * @return bool
+   *   True if Outside In should be applied to current request.
+   */
+  public function useOutsideIn() {
+    return !$this->adminThemeNegotiator->applies($this->routeMatch);
+  }
+
+}
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/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..63bf09e
--- /dev/null
+++ b/core/modules/outside_in/tests/src/FunctionalJavascript/OffCanvasTest.php
@@ -0,0 +1,67 @@
+<?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() {
+    // @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) {
+
+      $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..8cc826b
--- /dev/null
+++ b/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php
@@ -0,0 +1,117 @@
+<?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 Our test should not require Bartik. Fix targeting markup.
+    $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);
+  }
+
+  /**
+   * Enable Editing mode by pressing "Edit" button in the toolbar.
+   */
+  protected function enableEditingMode() {
+    $this->waitForElement('div[data-contextual-id="block:block=powered: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..dda013c
--- /dev/null
+++ b/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInJavascriptTestBase.php
@@ -0,0 +1,64 @@
+<?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);
+  }
+
+  /**
+   * Wait 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());
+  }
+
+}
