diff --git a/core/composer.json b/core/composer.json
index 3109649..c7ca429 100644
--- a/core/composer.json
+++ b/core/composer.json
@@ -111,6 +111,7 @@
         "drupal/migrate_drupal_ui": "self.version",
         "drupal/node": "self.version",
         "drupal/options": "self.version",
+        "drupal/outside_in": "self.version",
         "drupal/page_cache": "self.version",
         "drupal/path": "self.version",
         "drupal/quickedit": "self.version",
diff --git a/core/core.services.yml b/core/core.services.yml
index 2f7da56..d2aa505 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -569,6 +569,9 @@ services:
   entity.autocomplete_matcher:
     class: Drupal\Core\Entity\EntityAutocompleteMatcher
     arguments: ['@plugin.manager.entity_reference_selection']
+  block_form.manager:
+    class: Drupal\Core\Block\BlockFormManager
+    arguments: ['@class_resolver']
   plugin.manager.entity_reference_selection:
     class: Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManager
     parent: default_plugin_manager
diff --git a/core/lib/Drupal/Core/Block/BlockFormManager.php b/core/lib/Drupal/Core/Block/BlockFormManager.php
new file mode 100644
index 0000000..f866706
--- /dev/null
+++ b/core/lib/Drupal/Core/Block/BlockFormManager.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Drupal\Core\Block;
+
+use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
+use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\Core\DependencyInjection\ClassResolverInterface;
+use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\Core\Form\OperationAwareFormInterface;
+
+/**
+ * Provides form discovery capabilities for block plugins.
+ */
+class BlockFormManager implements BlockFormManagerInterface {
+
+  /**
+   * The class resolver.
+   *
+   * @var \Drupal\Core\DependencyInjection\ClassResolverInterface
+   */
+  protected $classResolver;
+
+  /**
+   * BlockFormManager constructor.
+   *
+   * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
+   */
+  public function __construct(ClassResolverInterface $class_resolver) {
+    $this->classResolver = $class_resolver;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormObject(PluginInspectionInterface $plugin, $operation) {
+    $definition = $plugin->getPluginDefinition();
+
+    if (!isset($definition['form'][$operation])) {
+      // Use the default form class if no form is specified for this operation.
+      if (isset($definition['form']['default'])) {
+        $operation = 'default';
+      }
+      else {
+        throw new InvalidPluginDefinitionException($plugin->getPluginId(), sprintf('The "%s" plugin did not specify a "%s" form class', $plugin->getPluginId(), $operation));
+      }
+    }
+
+    // If the form specified is the plugin itself, use it directly.
+    if (get_class($plugin) === $definition['form'][$operation]) {
+      $form_object = $plugin;
+    }
+    else {
+      $form_object = $this->classResolver->getInstanceFromDefinition($definition['form'][$operation]);
+    }
+
+    // Ensure the resulting object is a plugin form.
+    if (!$form_object instanceof PluginFormInterface) {
+      throw new InvalidPluginDefinitionException($plugin->getPluginId(), sprintf('The "%s" plugin did not specify a valid "%s" form class, must implement \Drupal\Core\Plugin\PluginFormInterface', $plugin->getPluginId(), $operation));
+    }
+
+    if ($form_object instanceof OperationAwareFormInterface) {
+      $form_object->setOperation($operation);
+    }
+
+    return $form_object;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Block/BlockFormManagerInterface.php b/core/lib/Drupal/Core/Block/BlockFormManagerInterface.php
new file mode 100644
index 0000000..2135128
--- /dev/null
+++ b/core/lib/Drupal/Core/Block/BlockFormManagerInterface.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\Core\Block;
+
+use Drupal\Component\Plugin\PluginInspectionInterface;
+
+/**
+ * Provides form discovery capabilities for block plugins.
+ */
+interface BlockFormManagerInterface {
+
+  /**
+   * Creates a new form instance.
+   *
+   * @param \Drupal\Component\Plugin\PluginInspectionInterface $plugin
+   *   The plugin the form is for.
+   * @param string $operation
+   *   The name of the operation to use, e.g., 'default'.
+   *
+   * @return \Drupal\Core\Plugin\PluginFormInterface
+   *   A plugin form instance.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+   */
+  public function getFormObject(PluginInspectionInterface $plugin, $operation);
+
+}
diff --git a/core/lib/Drupal/Core/Block/BlockManager.php b/core/lib/Drupal/Core/Block/BlockManager.php
index 3c88bdc..036933f 100644
--- a/core/lib/Drupal/Core/Block/BlockManager.php
+++ b/core/lib/Drupal/Core/Block/BlockManager.php
@@ -8,6 +8,7 @@
 use Drupal\Core\Plugin\CategorizingPluginManagerTrait;
 use Drupal\Core\Plugin\Context\ContextAwarePluginManagerTrait;
 use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\Core\Plugin\PluginFormInterface;
 
 /**
  * Manages discovery and instantiation of block plugins.
@@ -48,6 +49,12 @@ public function __construct(\Traversable $namespaces, CacheBackendInterface $cac
   public function processDefinition(&$definition, $plugin_id) {
     parent::processDefinition($definition, $plugin_id);
     $this->processDefinitionCategory($definition);
+
+    // If no default form is defined and this plugin implements
+    // \Drupal\Core\Plugin\PluginFormInterface, use that for the default form.
+    if (!isset($definition['form']['default']) && is_subclass_of($definition['class'], PluginFormInterface::class)) {
+      $definition['form']['default'] = $definition['class'];
+    }
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Form/OperationAwareFormInterface.php b/core/lib/Drupal/Core/Form/OperationAwareFormInterface.php
new file mode 100644
index 0000000..8172bd7
--- /dev/null
+++ b/core/lib/Drupal/Core/Form/OperationAwareFormInterface.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Drupal\Core\Form;
+
+/**
+ * Interface for forms that are aware of what operation they are performing.
+ */
+interface OperationAwareFormInterface {
+
+  /**
+   * Sets the operation for this form.
+   *
+   * @param string $operation
+   *   The name of the current operation.
+   *
+   * @return $this
+   */
+  public function setOperation($operation);
+
+}
diff --git a/core/modules/block/src/BlockForm.php b/core/modules/block/src/BlockForm.php
index eeec4d3..38272bd 100644
--- a/core/modules/block/src/BlockForm.php
+++ b/core/modules/block/src/BlockForm.php
@@ -3,6 +3,8 @@
 namespace Drupal\block;
 
 use Drupal\Component\Utility\Html;
+use Drupal\Core\Block\BlockFormManagerInterface;
+use Drupal\Core\Block\BlockPluginInterface;
 use Drupal\Core\Entity\EntityForm;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Executable\ExecutableManagerInterface;
@@ -69,6 +71,13 @@ class BlockForm extends EntityForm {
   protected $contextRepository;
 
   /**
+   * The block form manager.
+   *
+   * @var \Drupal\Core\Block\BlockFormManagerInterface
+   */
+  protected $blockFormManager;
+
+  /**
    * Constructs a BlockForm object.
    *
    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
@@ -81,13 +90,16 @@ class BlockForm extends EntityForm {
    *   The language manager.
    * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
    *   The theme handler.
+   * @param \Drupal\Core\Block\BlockFormManagerInterface $block_form_manager
+   *   The block form manager.
    */
-  public function __construct(EntityManagerInterface $entity_manager, ExecutableManagerInterface $manager, ContextRepositoryInterface $context_repository, LanguageManagerInterface $language, ThemeHandlerInterface $theme_handler) {
+  public function __construct(EntityManagerInterface $entity_manager, ExecutableManagerInterface $manager, ContextRepositoryInterface $context_repository, LanguageManagerInterface $language, ThemeHandlerInterface $theme_handler, BlockFormManagerInterface $block_form_manager) {
     $this->storage = $entity_manager->getStorage('block');
     $this->manager = $manager;
     $this->contextRepository = $context_repository;
     $this->language = $language;
     $this->themeHandler = $theme_handler;
+    $this->blockFormManager = $block_form_manager;
   }
 
   /**
@@ -99,7 +111,8 @@ public static function create(ContainerInterface $container) {
       $container->get('plugin.manager.condition'),
       $container->get('context.repository'),
       $container->get('language_manager'),
-      $container->get('theme_handler')
+      $container->get('theme_handler'),
+      $container->get('block_form.manager')
     );
   }
 
@@ -120,7 +133,7 @@ public function form(array $form, FormStateInterface $form_state) {
     $form_state->setTemporaryValue('gathered_contexts', $this->contextRepository->getAvailableContexts());
 
     $form['#tree'] = TRUE;
-    $form['settings'] = $entity->getPlugin()->buildConfigurationForm(array(), $form_state);
+    $form['settings'] = $this->getPluginForm($this->entity->getPlugin())->buildConfigurationForm(array(), $form_state);
     $form['visibility'] = $this->buildVisibilityInterface([], $form_state);
 
     // If creating a new block, calculate a safe default machine name.
@@ -282,7 +295,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
     // settings form element, so just pass that to the block for validation.
     $settings = (new FormState())->setValues($form_state->getValue('settings'));
     // Call the plugin validate handler.
-    $this->entity->getPlugin()->validateConfigurationForm($form, $settings);
+    $this->getPluginForm($this->entity->getPlugin())->validateConfigurationForm($form, $settings);
     // Update the original form values.
     $form_state->setValue('settings', $settings->getValues());
     $this->validateVisibility($form, $form_state);
@@ -322,15 +335,14 @@ protected function validateVisibility(array $form, FormStateInterface $form_stat
   public function submitForm(array &$form, FormStateInterface $form_state) {
     parent::submitForm($form, $form_state);
 
-    $entity = $this->entity;
     // The Block Entity form puts all block plugin form elements in the
     // settings form element, so just pass that to the block for submission.
     // @todo Find a way to avoid this manipulation.
     $settings = (new FormState())->setValues($form_state->getValue('settings'));
 
     // Call the plugin submit handler.
-    $entity->getPlugin()->submitConfigurationForm($form, $settings);
-    $block = $entity->getPlugin();
+    $block = $this->entity->getPlugin();
+    $this->getPluginForm($block)->submitConfigurationForm($form, $settings);
     // If this block is context-aware, set the context mapping.
     if ($block instanceof ContextAwarePluginInterface && $block->getContextDefinitions()) {
       $context_mapping = $settings->getValue('context_mapping', []);
@@ -339,7 +351,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.
+    $this->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]);
@@ -354,20 +389,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())))
-    );
   }
 
   /**
@@ -402,4 +425,17 @@ public function getUniqueMachineName(BlockInterface $block) {
     return $machine_default;
   }
 
+  /**
+   * Retrieves the plugin form for a given block and operation.
+   *
+   * @param \Drupal\Core\Block\BlockPluginInterface $block
+   *   The block plugin.
+   *
+   * @return \Drupal\Core\Plugin\PluginFormInterface
+   *   The plugin form for the block.
+   */
+  protected function getPluginForm(BlockPluginInterface $block) {
+    return $this->blockFormManager->getFormObject($block, $this->operation);
+  }
+
 }
diff --git a/core/modules/block/tests/modules/block_test/src/Form/SecondaryBlockForm.php b/core/modules/block/tests/modules/block_test/src/Form/SecondaryBlockForm.php
new file mode 100644
index 0000000..3095963
--- /dev/null
+++ b/core/modules/block/tests/modules/block_test/src/Form/SecondaryBlockForm.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Drupal\block_test\Form;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Form\OperationAwareFormInterface;
+use Drupal\Core\Plugin\PluginFormInterface;
+
+/**
+ * @todo.
+ */
+class SecondaryBlockForm implements PluginFormInterface, OperationAwareFormInterface {
+
+  /**
+   * @var string
+   */
+  protected $operation;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setOperation($operation) {
+    $this->operation = $operation;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+    // Intentionally empty.
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    // Intentionally empty.
+  }
+
+}
diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestMultipleFormsBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestMultipleFormsBlock.php
new file mode 100644
index 0000000..ff5ced3
--- /dev/null
+++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestMultipleFormsBlock.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\block_test\Plugin\Block;
+
+use Drupal\Core\Block\BlockBase;
+
+/**
+ * @todo.
+ *
+ * @Block(
+ *   id = "test_multiple_forms_block",
+ *   form = {
+ *     "secondary" = "\Drupal\block_test\Form\SecondaryBlockForm"
+ *   },
+ *   admin_label = @Translation("Multiple forms test block")
+ * )
+ */
+class TestMultipleFormsBlock extends BlockBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build() {
+    return [];
+  }
+
+}
diff --git a/core/modules/block/tests/src/Unit/BlockFormTest.php b/core/modules/block/tests/src/Unit/BlockFormTest.php
index 09f450b..a27014a 100644
--- a/core/modules/block/tests/src/Unit/BlockFormTest.php
+++ b/core/modules/block/tests/src/Unit/BlockFormTest.php
@@ -3,6 +3,7 @@
 namespace Drupal\Tests\block\Unit;
 
 use Drupal\block\BlockForm;
+use Drupal\Core\Block\BlockFormManagerInterface;
 use Drupal\Tests\UnitTestCase;
 
 /**
@@ -55,6 +56,13 @@ class BlockFormTest extends UnitTestCase {
   protected $contextRepository;
 
   /**
+   * The block form manager.
+   *
+   * @var \Drupal\Core\Block\BlockFormManagerInterface|\Prophecy\Prophecy\ProphecyInterface
+   */
+  protected $blockFormManager;
+
+  /**
    * {@inheritdoc}
    */
   protected function setUp() {
@@ -71,6 +79,7 @@ protected function setUp() {
       ->method('getStorage')
       ->will($this->returnValue($this->storage));
 
+    $this->blockFormManager = $this->prophesize(BlockFormManagerInterface::class);
   }
 
   /**
@@ -99,7 +108,7 @@ public function testGetUniqueMachineName() {
       ->method('getQuery')
       ->will($this->returnValue($query));
 
-    $block_form_controller = new BlockForm($this->entityManager, $this->conditionManager, $this->contextRepository, $this->language, $this->themeHandler);
+    $block_form_controller = new BlockForm($this->entityManager, $this->conditionManager, $this->contextRepository, $this->language, $this->themeHandler, $this->blockFormManager->reveal());
 
     // Ensure that the block with just one other instance gets the next available
     // name suggestion.
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..77745ba
--- /dev/null
+++ b/core/modules/outside_in/css/outside_in.module.css
@@ -0,0 +1,128 @@
+/**
+ * @file
+ * Styling for Outside-In module.
+ */
+
+/* Style the offcanvas container. */
+#offcanvas {
+  box-sizing: border-box;
+  position: fixed;
+  height: 100%;
+  overflow-y: auto;
+  display: inline-block;
+  width: 25%;
+  -webkit-transform: translateX(100%);
+  -moz-transform: translateX(100%);
+  -o-transform: translateX(100%);
+  -ms-transform: translateX(100%);
+  transform: translateX(100%);
+  /* 2s delay matches transition speed to transform speed. */
+  -webkit-transition: all .7s ease-in-out 2s;
+  -moz-transition: all .7s ease-in-out 2s;
+  transition: all .7s ease-in-out 2s;
+  z-index: 500;
+}
+[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%);
+}
+#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);
+}
+#main-canvas-wrapper #main-canvas {
+  display: inline-block;
+  width: 100%;
+  -webkit-transition: all .7s ease-in-out;
+  -moz-transition: all .7s ease-in-out;
+  transition: all .7s ease-in-out;
+}
+#main-canvas-wrapper.js-tray-open #main-canvas {
+  left: 0; /* LTR */
+  width: 75%;
+  -webkit-transition: all .7s ease-in-out;
+  -moz-transition: all .7s ease-in-out;
+  transition: all .7s ease-in-out;
+}
+[dir="rtl"] #main-canvas-wrapper.js-tray-open #main-canvas {
+  right: 0;
+}
+/* Button that closes the offcanvas tray. */
+/* @todo Add hover state. */
+#offcanvas > button.offcanvasClose,
+#offcanvas > button.offcanvasClose:hover {
+  position: absolute;
+  right: 15px; /* LTR */
+  top: 15px;
+  height: 20px;
+  width: 20px;
+  border: 0;
+  border-radius: 0;
+  background: url(/core/misc/icons/bebebe/ex.svg) center center no-repeat;
+  color: transparent;
+  cursor: pointer;
+}
+#offcanvas > button.offcanvasClose:focus {
+  outline: none;
+}
+[dir="rtl"] #offcanvas > button.offcanvasClose {
+  left: 15px;
+  right: auto;
+}
+#offcanvas h1 {
+  padding: 15px;
+  margin-top: 0;
+  margin-bottom: 0;
+  font-size: 120%;
+}
+[dir="rtl"] #offcanvas h1 {
+  text-align: right;
+}
+#offcanvas > .offcanvas-content {
+  height: 10000px;
+  padding-top: 0;
+  padding-right: 15px;
+  padding-left: 15px;
+  padding-bottom: 0;
+}
+[dir="rtl"] #offcanvas .offcanvas-content{
+  text-align: right;
+}
+#offcanvas .form-item {
+  width: 100%;
+}
+/**
+ * Change background when in edit mode an over editable item.
+ * Focus not taking effect because offcanvas load blurs element.
+ */
+/* @todo Add .is-active. */
+#main-canvas.js-outside-in-edit-mode .outside-in-editable:hover,
+#main-canvas.js-outside-in-edit-mode .outside-in-editable:focus {
+  background-color: rgba(0, 0, 0, 0.1);
+}
+
+/* Highlight editable regions in edit mode. */
+/* @todo Clean up this css. */
+#main-canvas.js-outside-in-edit-mode .outside-in-editable {
+  outline: -webkit-focus-ring-color auto 2px;
+  outline-color: -webkit-focus-ring-color;
+  outline-style: dashed;
+  outline-width: 1px;
+}
+
+/* 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;
+}
+/* Always hide the toolbar tray in edit mode. */
+#toolbar-bar.js-outside-in-edit-mode .toolbar-tray .is-active {
+  display: none;
+}
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..1329fa9
--- /dev/null
+++ b/core/modules/outside_in/css/outside_in.theme.css
@@ -0,0 +1,81 @@
+/**
+ * @file
+ * Visual styling for Outside-In module.
+ */
+
+/* Style the edit tab in the toolbar. */
+/* @todo Move this into core when module is not experimental. */
+
+/* Smooth transitions between all toolbar states. */
+.toolbar-tab,
+.toolbar-item,
+.toolbar-icon:before {
+  -webkit-transition: all .7s ease-in-out;
+  -moz-transition: all .7s ease-in-out;
+  transition: all .7s ease-in-out;
+}
+/* Style for both 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);
+}
+
+/* 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 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..43dfce9
--- /dev/null
+++ b/core/modules/outside_in/js/offcanvas.js
@@ -0,0 +1,127 @@
+/**
+ * @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',
+     'css': {
+       'background': '#fff',
+       'background-color': '#fff',
+       'border-left': '1px solid #ddd',
+       'box-shadow': '-2px 3px 1px 1px rgba(0, 0, 0, 0.2)'
+     }
+   });
+ };
+
+  /**
+   * 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.
+    if (!Drupal.offCanvas.visible) {
+      // Append content then open tray.
+      $pageWrapper.append($offcanvasWrapper);
+      Drupal.offCanvas.visible = true;
+      $pageWrapper.addClass('js-tray-open');
+      Drupal.announce(Drupal.t('Configuration tray opened.'));
+    } else {
+      // Remove previous content then append new content.
+      $pageWrapper.find('#offcanvas').remove();
+      $pageWrapper.append($offcanvasWrapper);
+      Drupal.announce(Drupal.t('Configuration tray content has been updated.'));
+    }
+  };
+
+})(jQuery, Drupal);
diff --git a/core/modules/outside_in/js/outside_in.js b/core/modules/outside_in/js/outside_in.js
new file mode 100644
index 0000000..8948780
--- /dev/null
+++ b/core/modules/outside_in/js/outside_in.js
@@ -0,0 +1,138 @@
+/**
+ * @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];
+
+      (editLink ? editLink : $(e.target).parents('.outside-in-editable')
+        .find('li.outside-inblock-configure a')[0])
+        .click();
+    });
+
+  /**
+   * Add Ajax behaviours to links added by contextual links
+   *
+   * @todo Fix contextual links to work with use-ajax links.
+   *   @see https://www.drupal.org/node/2764931
+   *
+   * @param {jQuery.Event} event
+   *   The `drupalContextualLinkAdded` event.
+   * @param {object} data
+   *   An object containing the data relevant to the event.
+   *
+   * @listens event:drupalContextualLinkAdded
+   */
+  $(document).on('drupalContextualLinkAdded', function (event, data) {
+    // Bind Ajax behaviors to all items showing the class.
+    data.$el.find('.use-ajax').once('ajax').each(function () {
+      // Below is copied directly from ajax.js to keep behavior the same.
+      var element_settings = {};
+      // Clicked links look better with the throbber than the progress bar.
+      element_settings.progress = {type: 'throbber'};
+
+      // For anchor tags, these will go to the target of the anchor rather
+      // than the usual location.
+      var href = $(this).attr('href');
+      if (href) {
+        element_settings.url = href;
+        element_settings.event = 'click';
+      }
+      element_settings.dialogType = $(this).data('dialog-type');
+      element_settings.dialog = $(this).data('dialog-options');
+      element_settings.base = $(this).attr('id');
+      element_settings.element = this;
+      Drupal.ajax(element_settings);
+    });
+
+    // 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..7784954
--- /dev/null
+++ b/core/modules/outside_in/outside_in.libraries.yml
@@ -0,0 +1,22 @@
+drupal.outside_in:
+  version: VERSION
+  js:
+    js/outside_in.js: {}
+  css:
+    component:
+      css/outside_in.module.css: {}
+      css/outside_in.theme.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..488f3a6
--- /dev/null
+++ b/core/modules/outside_in/outside_in.module
@@ -0,0 +1,121 @@
+<?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;
+
+/**
+ * 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);
+
+  if (!$access) {
+    return;
+  }
+  $variables['attributes']['class'][] = 'outside-in-editable';
+}
+
+/**
+ * Implements hook_toolbar_alter().
+ *
+ * Includes outside_library if Edit link is in toolbar.
+ */
+function outside_in_toolbar_alter(&$items) {
+  if (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)
+          && !in_array('hidden',$items[$key]['#wrapper_attributes']['class'])) {
+        $items[$key]['#wrapper_attributes']['class'][] = 'edit-mode-inactive';
+      }
+    }
+  }
+}
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..0445f0e
--- /dev/null
+++ b/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Drupal\outside_in\Block;
+
+use Drupal\block\BlockForm;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * @todo.
+ */
+class BlockEntityOffCanvasForm extends BlockForm {
+
+  /**
+   * {@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.
+  }
+
+}
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..5e940d5
--- /dev/null
+++ b/core/modules/outside_in/tests/src/FunctionalJavascript/OffCanvasTest.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace Drupal\Tests\outside_in\FunctionalJavascript;
+
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
+
+/**
+ * Tests the off-canvas tray functionality.
+ *
+ * @group outside_in
+ */
+class OffCanvasTest extends JavascriptTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'block',
+    'system',
+    'toolbar',
+    'outside_in',
+    'offcanvas_test',
+  ];
+
+  /**
+   * Tests that regular non-contextual links will work with the off-canvas tray.
+   */
+  public function testOffCanvasLinks() {
+    // @todo Add other themes to test against.
+    $themes = ['bartik', 'stark'];
+    // @todo Add RTL Language test for each theme.
+    // Test the same functionality on multiple themes
+    foreach ($themes as $theme) {
+      // Enable the theme.
+      \Drupal::service('theme_installer')->install([$theme]);
+      $theme_config = \Drupal::configFactory()->getEditable('system.theme');
+      $theme_config->set('default', $theme);
+      $theme_config->save();
+      $this->drupalGet('/offcanvas-test-links');
+
+      $page = $this->getSession()->getPage();
+      $web_assert = $this->assertSession();
+
+      // Make sure off-canvas tray is on page when first loaded.
+      $web_assert->elementNotExists('css', '#offcanvas');
+
+      // Check opening and closing with two separate links.
+      // Make sure tray updates to new content.
+      foreach (['1', '2'] as $link_index) {
+        // Click the first test like that should open the page.
+        $page->clickLink("Click Me $link_index!");
+        $this->waitForOffCanvasToOpen();
+
+        // Check that the canvas is not on the page.
+        $web_assert->elementExists('css', '#offcanvas');
+        // Check that response text is on page.
+        $web_assert->pageTextContains("Thing $link_index says hello");
+        $offcanvas_tray = $page->findById('offcanvas');
+
+        // Check that tray is visible.
+        $this->assertEquals(TRUE,$offcanvas_tray->isVisible());
+        $header_text = $offcanvas_tray->findById('offcanvas-header')->getText();
+
+        // Check that header is correct.
+        $this->assertEquals("Thing $link_index", $header_text);
+        $tray_text = $offcanvas_tray->find('css', '.offcanvas-content')->getText();
+        $this->assertEquals("Thing $link_index says hello", $tray_text);
+
+        // Close the tray.
+        // @todo Should the close have an id?
+        $offcanvas_tray->find('css', '.offcanvasClose')->press();
+        // Wait for animation to be done.
+        $this->waitForOffCanvasToClose();
+        // Make sure canvas doesn't exist after closing.
+        $web_assert->elementNotExists('css', '#offcanvas');
+      }
+
+      $this->verbose('Test theme: ' . $theme);
+    }
+  }
+
+  /**
+   * Waits for Off-canvas tray to close.
+   */
+  protected function waitForOffCanvasToClose() {
+    $condition = "(jQuery('#offcanvas').length == 0)";
+    $this->assertJsCondition($condition, 5000);
+  }
+
+  /**
+   * Waits for Off-canvas tray to open.
+   */
+  protected function waitForOffCanvasToOpen() {
+    $condition = "(jQuery('#offcanvas').length > 0)";
+    $this->assertJsCondition($condition, 5000);
+  }
+
+}
diff --git a/core/modules/outside_in/tests/src/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/tests/Drupal/KernelTests/Core/Block/MultipleBlockFormTest.php b/core/tests/Drupal/KernelTests/Core/Block/MultipleBlockFormTest.php
new file mode 100644
index 0000000..ad13628
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Block/MultipleBlockFormTest.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Block;
+
+use Drupal\block_test\Form\SecondaryBlockForm;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests that blocks can have multiple forms.
+ *
+ * @group block
+ */
+class MultipleBlockFormTest extends KernelTestBase {
+
+  /**
+   * @var array
+   */
+  public static $modules = ['system', 'block', 'block_test'];
+
+  /**
+   * Tests that blocks can have multiple forms.
+   */
+  public function testMultipleForms() {
+    $block = \Drupal::service('plugin.manager.block')->createInstance('test_multiple_forms_block');
+
+    $form_object1 = \Drupal::service('block_form.manager')->getFormObject($block, 'default');
+    $form_object2 = \Drupal::service('block_form.manager')->getFormObject($block, 'secondary');
+
+    // Assert that the block itself is used for the default form.
+    $this->assertSame($block, $form_object1);
+
+    $expected_secondary = new SecondaryBlockForm();
+    $expected_secondary->setOperation('secondary');
+    $this->assertEquals($expected_secondary, $form_object2);
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Block/BlockFormManagerTest.php b/core/tests/Drupal/Tests/Core/Block/BlockFormManagerTest.php
new file mode 100644
index 0000000..7ea0e06
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Block/BlockFormManagerTest.php
@@ -0,0 +1,155 @@
+<?php
+
+namespace Drupal\Tests\Core\Block;
+
+use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
+use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\Core\DependencyInjection\ClassResolverInterface;
+use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\Core\Form\OperationAwareFormInterface;
+use Drupal\Core\Block\BlockFormManager;
+use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Block\BlockFormManager
+ * @group block
+ */
+class BlockFormManagerTest extends UnitTestCase {
+
+  /**
+   * The class resolver.
+   *
+   * @var \Drupal\Core\DependencyInjection\ClassResolverInterface|\Prophecy\Prophecy\ProphecyInterface
+   */
+  protected $classResolver;
+
+  /**
+   * The manager being tested.
+   *
+   * @var \Drupal\Core\Block\BlockFormManager
+   */
+  protected $manager;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->classResolver = $this->prophesize(ClassResolverInterface::class);
+    $this->manager = new BlockFormManager($this->classResolver->reveal());
+  }
+
+  /**
+   * @covers ::getFormObject
+   */
+  public function testGetFormObject() {
+    $plugin_form = $this->prophesize(PluginFormInterface::class);
+    $expected = $plugin_form->reveal();
+
+    $this->classResolver->getInstanceFromDefinition(get_class($expected))->willReturn($expected);
+
+    $plugin = $this->prophesize(PluginInspectionInterface::class);
+    $plugin->getPluginDefinition()->willReturn([
+      'form' => [
+        'standard_class' => get_class($expected),
+      ],
+    ]);
+
+    $form_object = $this->manager->getFormObject($plugin->reveal(), 'standard_class');
+    $this->assertSame($expected, $form_object);
+  }
+
+  /**
+   * @covers ::getFormObject
+   */
+  public function testGetFormObjectUsingPlugin() {
+    $this->classResolver->getInstanceFromDefinition(Argument::cetera())->shouldNotBeCalled();
+
+    $plugin = $this->prophesize(PluginInspectionInterface::class)->willImplement(PluginFormInterface::class);
+    $plugin->getPluginDefinition()->willReturn([
+      'form' => [
+        'default' => get_class($plugin->reveal()),
+      ],
+    ]);
+
+    $form_object = $this->manager->getFormObject($plugin->reveal(), 'default');
+    $this->assertSame($plugin->reveal(), $form_object);
+  }
+
+  /**
+   * @covers ::getFormObject
+   */
+  public function testGetFormObjectDefaultFallback() {
+    $this->classResolver->getInstanceFromDefinition(Argument::cetera())->shouldNotBeCalled();
+
+    $plugin = $this->prophesize(PluginInspectionInterface::class)->willImplement(PluginFormInterface::class);
+    $plugin->getPluginDefinition()->willReturn([
+      'form' => [
+        'default' => get_class($plugin->reveal()),
+      ],
+    ]);
+
+    $form_object = $this->manager->getFormObject($plugin->reveal(), 'missing');
+    $this->assertSame($plugin->reveal(), $form_object);
+  }
+
+  /**
+   * @covers ::getFormObject
+   */
+  public function testGetFormObjectOperationAware() {
+    $plugin_form = $this->prophesize(PluginFormInterface::class)->willImplement(OperationAwareFormInterface::class);
+    $plugin_form->setOperation('operation_aware')->shouldBeCalled();
+
+    $expected = $plugin_form->reveal();
+
+    $this->classResolver->getInstanceFromDefinition(get_class($expected))->willReturn($expected);
+
+    $plugin = $this->prophesize(PluginInspectionInterface::class);
+    $plugin->getPluginDefinition()->willReturn([
+      'form' => [
+        'operation_aware' => get_class($expected),
+      ],
+    ]);
+
+    $form_object = $this->manager->getFormObject($plugin->reveal(), 'operation_aware');
+    $this->assertSame($expected, $form_object);
+  }
+
+  /**
+   * @covers ::getFormObject
+   */
+  public function testGetFormObjectDefinitionException() {
+    $this->setExpectedException(InvalidPluginDefinitionException::class, 'The "the_plugin_id" plugin did not specify a "anything" form class');
+
+    $plugin = $this->prophesize(PluginInspectionInterface::class);
+    $plugin->getPluginId()->willReturn('the_plugin_id');
+    $plugin->getPluginDefinition()->willReturn([]);
+
+    $form_object = $this->manager->getFormObject($plugin->reveal(), 'anything');
+    $this->assertSame(NULL, $form_object);
+  }
+
+  /**
+   * @covers ::getFormObject
+   */
+  public function testGetFormObjectInvalidException() {
+    $this->setExpectedException(InvalidPluginDefinitionException::class, 'The "the_plugin_id" plugin did not specify a valid "invalid" form class, must implement \Drupal\Core\Plugin\PluginFormInterface');
+
+    $expected = new \stdClass();
+    $this->classResolver->getInstanceFromDefinition(get_class($expected))->willReturn($expected);
+
+    $plugin = $this->prophesize(PluginInspectionInterface::class);
+    $plugin->getPluginId()->willReturn('the_plugin_id');
+    $plugin->getPluginDefinition()->willReturn([
+      'form' => [
+        'invalid' => get_class($expected),
+      ],
+    ]);
+
+    $form_object = $this->manager->getFormObject($plugin->reveal(), 'invalid');
+    $this->assertSame(NULL, $form_object);
+  }
+
+}
