diff --git a/core/composer.json b/core/composer.json
index bda527f..37b81fa 100644
--- a/core/composer.json
+++ b/core/composer.json
@@ -111,6 +111,7 @@
         "drupal/image": "self.version",
         "drupal/inline_form_errors": "self.version",
         "drupal/language": "self.version",
+        "drupal/layout_builder": "self.version",
         "drupal/layout_discovery": "self.version",
         "drupal/link": "self.version",
         "drupal/locale": "self.version",
diff --git a/core/misc/ajax.es6.js b/core/misc/ajax.es6.js
index 6248e46..7b95dd2 100644
--- a/core/misc/ajax.es6.js
+++ b/core/misc/ajax.es6.js
@@ -47,25 +47,7 @@ function loadAjaxBehavior(base) {
       }
 
       // Bind Ajax behaviors to all items showing the class.
-      $('.use-ajax').once('ajax').each(function () {
-        const 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.
-        const href = $(this).attr('href');
-        if (href) {
-          element_settings.url = href;
-          element_settings.event = 'click';
-        }
-        element_settings.dialogType = $(this).data('dialog-type');
-        element_settings.dialogRenderer = $(this).data('dialog-renderer');
-        element_settings.dialog = $(this).data('dialog-options');
-        element_settings.base = $(this).attr('id');
-        element_settings.element = this;
-        Drupal.ajax(element_settings);
-      });
+      Drupal.ajax.bindAjaxLinks($('body'));
 
       // This class means to submit the form to the action using Ajax.
       $('.use-ajax-submit').once('ajax').each(function () {
@@ -269,6 +251,28 @@ function loadAjaxBehavior(base) {
   Drupal.ajax.expired = function () {
     return Drupal.ajax.instances.filter(instance => instance && instance.element !== false && !document.body.contains(instance.element));
   };
+  Drupal.ajax.bindAjaxLinks = ($element) => {
+    $element.find('.use-ajax').once('ajax').each(function () {
+      const 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.
+      const href = $(this).attr('href');
+      if (href) {
+        element_settings.url = href;
+        element_settings.event = 'click';
+        console.log(href);
+      }
+      element_settings.dialogType = $(this).data('dialog-type');
+      element_settings.dialogRenderer = $(this).data('dialog-renderer');
+      element_settings.dialog = $(this).data('dialog-options');
+      element_settings.base = $(this).attr('id');
+      element_settings.element = this;
+      Drupal.ajax(element_settings);
+    });
+  };
 
   /**
    * Settings for an Ajax object.
@@ -518,13 +522,15 @@ else if (this.element && element.form) {
     else {
       ajax.options.url += '&';
     }
-    // If this element has a dialog type use if for the wrapper if not use 'ajax'.
-    let wrapper = `drupal_${(element_settings.dialogType || 'ajax')}`;
-    if (element_settings.dialogRenderer) {
-      wrapper += `.${element_settings.dialogRenderer}`;
+    // Add a wrapper format, if none exists.
+    if (ajax.options.url.indexOf(`${Drupal.ajax.WRAPPER_FORMAT}`) === -1) {
+      // If this element has a dialog type use if for the wrapper if not use 'ajax'.
+      let wrapper = `drupal_${(element_settings.dialogType || 'ajax')}`;
+      if (element_settings.dialogRenderer) {
+        wrapper += `.${element_settings.dialogRenderer}`;
+      }
+      ajax.options.url += `${Drupal.ajax.WRAPPER_FORMAT}=${wrapper}`;
     }
-    ajax.options.url += `${Drupal.ajax.WRAPPER_FORMAT}=${wrapper}`;
-
 
     // Bind the ajaxSubmit function to the element event.
     $(ajax.element).on(element_settings.event, function (event) {
@@ -1338,4 +1344,8 @@ else if (effect.showEffect !== 'show') {
       }
     },
   };
+  $(document).on('drupalContextualLinkAdded', (event, data) => {
+    // console.log(data);
+    Drupal.ajax.bindAjaxLinks($(data.$el[0]));
+  });
 }(jQuery, window, Drupal, drupalSettings));
diff --git a/core/misc/ajax.js b/core/misc/ajax.js
index 5ea5242..400bdc4 100644
--- a/core/misc/ajax.js
+++ b/core/misc/ajax.js
@@ -27,23 +27,7 @@ function loadAjaxBehavior(base) {
         }
       }
 
-      $('.use-ajax').once('ajax').each(function () {
-        var element_settings = {};
-
-        element_settings.progress = { type: 'throbber' };
-
-        var href = $(this).attr('href');
-        if (href) {
-          element_settings.url = href;
-          element_settings.event = 'click';
-        }
-        element_settings.dialogType = $(this).data('dialog-type');
-        element_settings.dialogRenderer = $(this).data('dialog-renderer');
-        element_settings.dialog = $(this).data('dialog-options');
-        element_settings.base = $(this).attr('id');
-        element_settings.element = this;
-        Drupal.ajax(element_settings);
-      });
+      Drupal.ajax.bindAjaxLinks($('body'));
 
       $('.use-ajax-submit').once('ajax').each(function () {
         var element_settings = {};
@@ -138,6 +122,26 @@ function loadAjaxBehavior(base) {
       return instance && instance.element !== false && !document.body.contains(instance.element);
     });
   };
+  Drupal.ajax.bindAjaxLinks = function ($element) {
+    $element.find('.use-ajax').once('ajax').each(function () {
+      var element_settings = {};
+
+      element_settings.progress = { type: 'throbber' };
+
+      var href = $(this).attr('href');
+      if (href) {
+        element_settings.url = href;
+        element_settings.event = 'click';
+        console.log(href);
+      }
+      element_settings.dialogType = $(this).data('dialog-type');
+      element_settings.dialogRenderer = $(this).data('dialog-renderer');
+      element_settings.dialog = $(this).data('dialog-options');
+      element_settings.base = $(this).attr('id');
+      element_settings.element = this;
+      Drupal.ajax(element_settings);
+    });
+  };
 
   Drupal.Ajax = function (base, element, element_settings) {
     var defaults = {
@@ -242,11 +246,13 @@ function loadAjaxBehavior(base) {
       ajax.options.url += '&';
     }
 
-    var wrapper = 'drupal_' + (element_settings.dialogType || 'ajax');
-    if (element_settings.dialogRenderer) {
-      wrapper += '.' + element_settings.dialogRenderer;
+    if (ajax.options.url.indexOf('' + Drupal.ajax.WRAPPER_FORMAT) === -1) {
+      var wrapper = 'drupal_' + (element_settings.dialogType || 'ajax');
+      if (element_settings.dialogRenderer) {
+        wrapper += '.' + element_settings.dialogRenderer;
+      }
+      ajax.options.url += Drupal.ajax.WRAPPER_FORMAT + '=' + wrapper;
     }
-    ajax.options.url += Drupal.ajax.WRAPPER_FORMAT + '=' + wrapper;
 
     $(ajax.element).on(element_settings.event, function (event) {
       if (!drupalSettings.ajaxTrustedUrl[ajax.url] && !Drupal.url.isLocal(ajax.url)) {
@@ -588,4 +594,7 @@ function loadAjaxBehavior(base) {
       }
     }
   };
+  $(document).on('drupalContextualLinkAdded', function (event, data) {
+    Drupal.ajax.bindAjaxLinks($(data.$el[0]));
+  });
 })(jQuery, window, Drupal, drupalSettings);
\ No newline at end of file
diff --git a/core/modules/layout_builder/config/schema/layout_builder.schema.yml b/core/modules/layout_builder/config/schema/layout_builder.schema.yml
new file mode 100644
index 0000000..b870007
--- /dev/null
+++ b/core/modules/layout_builder/config/schema/layout_builder.schema.yml
@@ -0,0 +1,7 @@
+core.entity_view_display.*.*.*.third_party.layout_builder:
+  type: mapping
+  label: 'Per-view-mode Layout Builder settings'
+  mapping:
+    allow_custom:
+      type: boolean
+      label: 'Allow a customized layout'
diff --git a/core/modules/layout_builder/css/layout-builder.css b/core/modules/layout_builder/css/layout-builder.css
new file mode 100644
index 0000000..0691f09
--- /dev/null
+++ b/core/modules/layout_builder/css/layout-builder.css
@@ -0,0 +1,59 @@
+.add-section {
+  width: 100%;
+  outline: 2px dashed #979797;
+  padding: 1.5em 0;
+  text-align: center;
+  margin-bottom: 1.5em;
+  transition: visually-hidden 2s ease-out, height 2s ease-in;
+}
+
+.layout-section {
+  margin-bottom: 1.5em;
+}
+
+.layout-section .layout__region {
+  outline: 2px dashed #2f91da;
+  padding: 1.5em 0;
+}
+
+.layout-section .layout__region .add-block {
+  text-align: center;
+}
+
+.layout-section .remove-section {
+  position: relative;
+  background: url(../../../misc/icons/bebebe/ex.svg) #ffffff center center / 16px 16px no-repeat;
+  border: 1px solid #cccccc;
+  box-sizing: border-box;
+  font-size: 1rem;
+  padding: 0;
+  height: 26px;
+  width: 26px;
+  white-space: nowrap;
+  text-indent: -9999px;
+  display: inline-block;
+  border-radius: 26px;
+  margin-left: -10px;
+}
+
+.layout-section .remove-section:hover {
+  background-image: url(../../../misc/icons/787878/ex.svg);
+}
+
+#drupal-off-canvas  details.layout-selection {
+  background-color: transparent;
+}
+
+#drupal-off-canvas  details.layout-selection summary {
+  margin-bottom: 1em;
+}
+
+#drupal-off-canvas  details.layout-selection li {
+  display: block;
+  padding-bottom: 1em;
+}
+
+#drupal-off-canvas  details.layout-selection li a {
+  display: block;
+  padding-top: 0.55em;
+}
diff --git a/core/modules/layout_builder/js/layout-builder.es6.js b/core/modules/layout_builder/js/layout-builder.es6.js
new file mode 100644
index 0000000..56a921c
--- /dev/null
+++ b/core/modules/layout_builder/js/layout-builder.es6.js
@@ -0,0 +1,40 @@
+(function ($, Drupal) {
+
+  Drupal.behaviors.layoutBuilder = {
+
+    attach: function (context) {
+      $(context).find('.layout__region').sortable({
+        items: '> .draggable',
+        connectWith: '.layout__region',
+        update: function (event, ui) {
+          let data = {
+            region_to: $(this).data('region'),
+            block_uuid: ui.item.data('layout-block-uuid'),
+            delta_to: ui.item.closest('[data-layout-delta]').data('layout-delta'),
+            preceding_block_uuid: ui.item.prev('[data-layout-block-uuid]').data('layout-block-uuid')
+          };
+          if (this === ui.item.parent()[0]) {
+            if (ui.sender) {
+              data.region_from = ui.sender.data('region');
+              data.delta_from = ui.sender.closest('[data-layout-delta]').data('layout-delta');
+            }
+            else {
+              data.region_from = data.region_to;
+              data.delta_from = data.delta_to;
+            }
+
+            let url = ui.item.closest('[data-layout-update-url]').data('layout-update-url');
+
+            let ajax = Drupal.ajax({
+              url: url,
+              submit: data
+            });
+            ajax.execute();
+          }
+        }
+      });
+    }
+
+  };
+
+})(jQuery, Drupal);
diff --git a/core/modules/layout_builder/js/layout-builder.js b/core/modules/layout_builder/js/layout-builder.js
new file mode 100644
index 0000000..eca948d
--- /dev/null
+++ b/core/modules/layout_builder/js/layout-builder.js
@@ -0,0 +1,45 @@
+/**
+* DO NOT EDIT THIS FILE.
+* See the following change record for more information,
+* https://www.drupal.org/node/2815083
+* @preserve
+**/
+
+(function ($, Drupal) {
+
+  Drupal.behaviors.layoutBuilder = {
+
+    attach: function attach(context) {
+      $(context).find('.layout__region').sortable({
+        items: '> .draggable',
+        connectWith: '.layout__region',
+        update: function update(event, ui) {
+          var data = {
+            region_to: $(this).data('region'),
+            block_uuid: ui.item.data('layout-block-uuid'),
+            delta_to: ui.item.closest('[data-layout-delta]').data('layout-delta'),
+            preceding_block_uuid: ui.item.prev('[data-layout-block-uuid]').data('layout-block-uuid')
+          };
+          if (this === ui.item.parent()[0]) {
+            if (ui.sender) {
+              data.region_from = ui.sender.data('region');
+              data.delta_from = ui.sender.closest('[data-layout-delta]').data('layout-delta');
+            } else {
+              data.region_from = data.region_to;
+              data.delta_from = data.delta_to;
+            }
+
+            var url = ui.item.closest('[data-layout-update-url]').data('layout-update-url');
+
+            var ajax = Drupal.ajax({
+              url: url,
+              submit: data
+            });
+            ajax.execute();
+          }
+        }
+      });
+    }
+
+  };
+})(jQuery, Drupal);
\ No newline at end of file
diff --git a/core/modules/layout_builder/layout_builder.info.yml b/core/modules/layout_builder/layout_builder.info.yml
new file mode 100644
index 0000000..d7bedde
--- /dev/null
+++ b/core/modules/layout_builder/layout_builder.info.yml
@@ -0,0 +1,9 @@
+name: 'Layout Builder'
+type: module
+description: 'Provides layout building utility.'
+package: Core (Experimental)
+version: VERSION
+core: 8.x
+dependencies:
+  - layout_discovery
+  - settings_tray
diff --git a/core/modules/layout_builder/layout_builder.libraries.yml b/core/modules/layout_builder/layout_builder.libraries.yml
new file mode 100644
index 0000000..9c17391
--- /dev/null
+++ b/core/modules/layout_builder/layout_builder.libraries.yml
@@ -0,0 +1,10 @@
+drupal.layout_builder:
+  version: VERSION
+  css:
+    theme:
+      css/layout-builder.css: {}
+  js:
+    js/layout-builder.js: {}
+  dependencies:
+    - core/jquery.ui.sortable
+    - settings_tray/drupal.off_canvas
diff --git a/core/modules/layout_builder/layout_builder.links.contextual.yml b/core/modules/layout_builder/layout_builder.links.contextual.yml
new file mode 100644
index 0000000..67952a1
--- /dev/null
+++ b/core/modules/layout_builder/layout_builder.links.contextual.yml
@@ -0,0 +1,19 @@
+layout_builder_block_configure:
+  title: 'Configure'
+  route_name: 'layout_builder.configure_block'
+  group: 'layout_builder_block'
+  options:
+    attributes:
+      class: ['use-ajax']
+      data-dialog-type: dialog
+      data-dialog-renderer: off_canvas
+
+layout_builder_block_remove:
+  title: 'Remove block'
+  route_name: 'layout_builder.remove_block'
+  group: 'layout_builder_block'
+  options:
+    attributes:
+      class: ['use-ajax']
+      data-dialog-type: dialog
+      data-dialog-renderer: off_canvas
diff --git a/core/modules/layout_builder/layout_builder.links.task.yml b/core/modules/layout_builder/layout_builder.links.task.yml
new file mode 100644
index 0000000..b003d77
--- /dev/null
+++ b/core/modules/layout_builder/layout_builder.links.task.yml
@@ -0,0 +1,2 @@
+layout_builder_ui:
+  deriver: '\Drupal\layout_builder\Plugin\Derivative\LayoutBuilderLocalTaskDeriver'
diff --git a/core/modules/layout_builder/layout_builder.module b/core/modules/layout_builder/layout_builder.module
new file mode 100644
index 0000000..b9e30a3
--- /dev/null
+++ b/core/modules/layout_builder/layout_builder.module
@@ -0,0 +1,160 @@
+<?php
+
+/**
+ * @file
+ * Provides hook implementations for Layout Builder.
+ */
+
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+
+/**
+ * Implements hook_help().
+ */
+function layout_builder_help($route_name) {
+  switch ($route_name) {
+    case 'help.page.layout_builder':
+      $output = '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('Layout Builder provides layout building utility, surprisingly.') . '</p>';
+      $output .= '<p>' . t('For more information, see the <a href=":layout-builder-documentation">online documentation for the Layout Builder module</a>.', [':layout-builder-documentation' => 'https://www.drupal.org/docs/8/core/modules/layout_builder']) . '</p>';
+      return $output;
+  }
+}
+
+/**
+ * Implements hook_contextual_links_view_alter().
+ *
+ * Change Configure Blocks into off_canvas links.
+ */
+function layout_builder_contextual_links_view_alter(&$element, $items) {
+  // @todo Move this to system_contextual_links_view_alter().
+  // If any items use the off_canvas render, add the corresponding library.
+  foreach ($items as $item) {
+    if (isset($item['localized_options']['attributes']['data-dialog-renderer']) && $item['localized_options']['attributes']['data-dialog-renderer'] === 'off_canvas') {
+      $element['#attached']['library'][] = 'settings_tray/drupal.off_canvas';
+      // After finding one, stop.
+      break;
+    }
+  }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter() for \Drupal\field_ui\Form\EntityViewDisplayEditForm.
+ */
+function layout_builder_form_entity_view_display_edit_form_alter(&$form, FormStateInterface $form_state) {
+  /** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display */
+  $display = $form_state->getFormObject()->getEntity();
+  $entity_type = \Drupal::entityTypeManager()->getDefinition($display->getTargetEntityTypeId());
+
+  // Remove layout_builder__layout both visually and from the #fields handling.
+  // This prevents any interaction with this field. It is manipulated directly
+  // in layout_builder_entity_view_display_alter().
+  unset($form['fields']['layout_builder__layout']);
+  unset($form['#fields'][array_search('layout_builder__layout', $form['#fields'])]);
+
+  $form['layout'] = [
+    '#type' => 'details',
+    '#open' => TRUE,
+    '#title' => t('Layout options'),
+    '#tree' => TRUE,
+  ];
+  $form['layout']['allow_custom'] = [
+    '#type' => 'checkbox',
+    '#title' => t('Allow each @entity to have its layout customized.', [
+      '@entity' => $entity_type->getSingularLabel(),
+    ]),
+    '#default_value' => $display->getThirdPartySetting('layout_builder', 'allow_custom', FALSE),
+  ];
+
+  array_unshift($form['actions']['submit']['#submit'], 'layout_builder_form_entity_view_display_edit_submit');
+}
+
+/**
+ * Form submission handler for layout options on the entity view display form.
+ *
+ * @see layout_builder_form_entity_view_display_edit_form_alter()
+ */
+function layout_builder_form_entity_view_display_edit_submit(&$form, FormStateInterface $form_state) {
+  /** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display */
+  $display = $form_state->getFormObject()->getEntity();
+
+  $original_value = $display->getThirdPartySetting('layout_builder', 'allow_custom', FALSE);
+  $allow_custom = $form_state->getValue(['layout', 'allow_custom'], FALSE);
+
+  // Only continue if the value has changed.
+  if ($original_value !== $allow_custom) {
+    $display->setThirdPartySetting('layout_builder', 'allow_custom', $allow_custom);
+    $entity_type_id = $display->getTargetEntityTypeId();
+    $bundle = $display->getTargetBundle();
+
+    if ($allow_custom) {
+      layout_builder_add_layout_section_field($entity_type_id, $bundle);
+    }
+    elseif ($field = FieldConfig::loadByName($entity_type_id, $bundle, 'layout_builder__layout')) {
+      $field->delete();
+    }
+  }
+}
+
+/**
+ * Adds a layout section field to a given bundle.
+ *
+ * @param string $entity_type_id
+ *   The entity type ID.
+ * @param string $bundle
+ *   The bundle.
+ * @param string $field_name
+ *   (optional) The name for the layout section field. Defaults to
+ *   'layout_builder__layout'.
+ *
+ * @return \Drupal\field\FieldConfigInterface
+ *   A layout section field.
+ */
+function layout_builder_add_layout_section_field($entity_type_id, $bundle, $field_name = 'layout_builder__layout') {
+  $field = FieldConfig::loadByName($entity_type_id, $bundle, $field_name);
+  if (!$field) {
+    $field_storage = FieldStorageConfig::loadByName($entity_type_id, $field_name);
+    if (!$field_storage) {
+      $field_storage = FieldStorageConfig::create([
+        'entity_type' => $entity_type_id,
+        'field_name' => $field_name,
+        'type' => 'layout_section',
+      ]);
+      $field_storage->save();
+    }
+
+    $field = FieldConfig::create([
+      'field_storage' => $field_storage,
+      'bundle' => $bundle,
+      'label' => t('Layout'),
+    ]);
+    $field->save();
+  }
+  return $field;
+}
+
+/**
+ * Implements hook_entity_view_display_alter().
+ */
+function layout_builder_entity_view_display_alter(EntityViewDisplayInterface $display, array $context) {
+  // @todo Expand to work for all view modes.
+  if (!in_array($context['view_mode'], ['full', 'default'])) {
+    return;
+  }
+
+  if ($display->getThirdPartySetting('layout_builder', 'allow_custom', FALSE)) {
+    /** @var \Drupal\Core\Field\FieldDefinitionInterface[] $field_definitions */
+    $field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions($display->getTargetEntityTypeId(), $display->getTargetBundle());
+    // Remove all display-configurable fields.
+    foreach (array_keys($display->getComponents()) as $name) {
+      if (isset($field_definitions[$name]) && $field_definitions[$name]->isDisplayConfigurable('view')) {
+        $display->removeComponent($name);
+      }
+    }
+
+    // Force the layout to render with no label.
+    $display->setComponent('layout_builder__layout', ['label' => 'hidden']);
+  }
+}
diff --git a/core/modules/layout_builder/layout_builder.permissions.yml b/core/modules/layout_builder/layout_builder.permissions.yml
new file mode 100644
index 0000000..0d66577
--- /dev/null
+++ b/core/modules/layout_builder/layout_builder.permissions.yml
@@ -0,0 +1,3 @@
+configure any layout:
+  title: 'Configure any layout'
+  restrict access: true
diff --git a/core/modules/layout_builder/layout_builder.routing.yml b/core/modules/layout_builder/layout_builder.routing.yml
new file mode 100644
index 0000000..0b4c08b
--- /dev/null
+++ b/core/modules/layout_builder/layout_builder.routing.yml
@@ -0,0 +1,75 @@
+layout_builder.choose_section:
+  path: '/layout_builder/choose/section/{entity_type_id}/{entity_id}/{delta}'
+  defaults:
+   _controller: '\Drupal\layout_builder\Controller\LayoutController::chooseSection'
+  requirements:
+    _permission: 'configure any layout'
+  options:
+    _admin_route: TRUE
+
+layout_builder.add_section:
+  path: '/layout_builder/add/section/{entity_type_id}/{entity_id}/{delta}/{plugin_id}'
+  defaults:
+    _controller: '\Drupal\layout_builder\Controller\LayoutController::addSection'
+  requirements:
+    _permission: 'configure any layout'
+  options:
+    _admin_route: TRUE
+
+layout_builder.remove_section:
+  path: '/layout_builder/remove/section/{entity_type_id}/{entity_id}/{delta}'
+  defaults:
+    _form: '\Drupal\layout_builder\Form\RemoveSectionForm'
+  requirements:
+    _permission: 'configure any layout'
+  options:
+    _admin_route: TRUE
+
+layout_builder.choose_block:
+  path: '/layout_builder/choose/block/{entity_type_id}/{entity_id}/{delta}/{region}'
+  defaults:
+    _controller: '\Drupal\layout_builder\Controller\LayoutController::chooseBlock'
+  requirements:
+    _permission: 'configure any layout'
+  options:
+    _admin_route: TRUE
+
+layout_builder.add_block:
+  path: '/layout_builder/add/block/{entity_type_id}/{entity_id}/{delta}/{region}/{plugin_id}'
+  defaults:
+    _form: '\Drupal\layout_builder\Form\ConfigureBlockForm'
+  requirements:
+    _permission: 'configure any layout'
+  options:
+    _admin_route: TRUE
+
+layout_builder.configure_block:
+  path: '/layout_builder/configure/block/{entity_type_id}/{entity_id}/{delta}/{region}/{uuid}'
+  defaults:
+    _form: '\Drupal\layout_builder\Form\ConfigureBlockForm'
+  requirements:
+    _permission: 'configure any layout'
+  options:
+    _admin_route: TRUE
+
+layout_builder.remove_block:
+  path: '/layout_builder/remove/block/{entity_type_id}/{entity_id}/{delta}/{region}/{uuid}'
+  defaults:
+    _form: '\Drupal\layout_builder\Form\RemoveBlockForm'
+  requirements:
+    _permission: 'configure any layout'
+  options:
+    _admin_route: TRUE
+
+layout_builder.move_block:
+  path: '/layout_builder/move/block/{entity_type_id}/{entity_id}'
+  defaults:
+    _controller: '\Drupal\layout_builder\Controller\LayoutController::moveBlock'
+  methods: [POST]
+  requirements:
+    _permission: 'configure any layout'
+  options:
+    _admin_route: TRUE
+
+route_callbacks:
+  - 'layout_builder.routes:getRoutes'
diff --git a/core/modules/layout_builder/layout_builder.services.yml b/core/modules/layout_builder/layout_builder.services.yml
new file mode 100644
index 0000000..dcb254d
--- /dev/null
+++ b/core/modules/layout_builder/layout_builder.services.yml
@@ -0,0 +1,22 @@
+services:
+  layout_builder.builder:
+    class: Drupal\layout_builder\LayoutSectionBuilder
+    arguments: ['@current_user', '@plugin.manager.core.layout', '@plugin.manager.block', '@context.handler', '@context.repository']
+  layout_builder.tempstore_repository:
+    class: Drupal\layout_builder\LayoutTempstoreRepository
+    arguments: ['@user.shared_tempstore', '@entity_type.manager']
+  access_check.entity.layout:
+    class: Drupal\layout_builder\Access\LayoutSectionAccessCheck
+    arguments: ['@entity_type.manager']
+    tags:
+      - { name: access_check, applies_to: _has_layout_selection }
+  layout_builder.routes:
+    class: Drupal\layout_builder\Routing\LayoutBuilderRoutes
+    arguments: ['@entity_type.manager']
+    tags:
+      - { name: event_subscriber }
+  layout_builder.route_enhancer:
+    class: Drupal\layout_builder\Routing\LayoutBuilderRouteEnhancer
+    arguments: ['@entity_type.manager']
+    tags:
+      - { name: route_enhancer }
diff --git a/core/modules/layout_builder/src/Access/LayoutSectionAccessCheck.php b/core/modules/layout_builder/src/Access/LayoutSectionAccessCheck.php
new file mode 100644
index 0000000..e08dd12
--- /dev/null
+++ b/core/modules/layout_builder/src/Access/LayoutSectionAccessCheck.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Drupal\layout_builder\Access;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Routing\Access\AccessInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Provides an access check for the Layout Builder UI.
+ */
+class LayoutSectionAccessCheck implements AccessInterface {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a new LayoutSectionAccessCheck.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * Checks routing access to layout for the entity.
+   *
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The current route match.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The currently logged in account.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  public function access(RouteMatchInterface $route_match, AccountInterface $account) {
+    $entity = $route_match->getParameter('layout_section_entity');
+    // If we don't have an entity, forbid access.
+    if (empty($entity)) {
+      return AccessResult::forbidden()->addCacheContexts(['route']);
+    }
+
+    // If the entity isn't fieldable, forbid access.
+    if (!$entity instanceof FieldableEntityInterface || !$entity->hasField('layout_builder__layout')) {
+      $access = AccessResult::forbidden();
+    }
+    else {
+      $access = AccessResult::allowedIf($account->hasPermission('configure any layout'));
+    }
+
+    return $access->addCacheableDependency($entity);
+  }
+
+}
diff --git a/core/modules/layout_builder/src/Controller/LayoutBuilderController.php b/core/modules/layout_builder/src/Controller/LayoutBuilderController.php
new file mode 100644
index 0000000..76a98a0
--- /dev/null
+++ b/core/modules/layout_builder/src/Controller/LayoutBuilderController.php
@@ -0,0 +1,299 @@
+<?php
+
+namespace Drupal\layout_builder\Controller;
+
+use Drupal\Core\Block\BlockManagerInterface;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Entity\RevisionableInterface;
+use Drupal\Core\Layout\LayoutPluginManagerInterface;
+use Drupal\Core\Link;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Url;
+use Drupal\layout_builder\LayoutSectionBuilder;
+use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Returns responses for Layout Builder routes.
+ */
+class LayoutBuilderController implements ContainerInjectionInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The layout builder.
+   *
+   * @var \Drupal\layout_builder\LayoutSectionBuilder
+   */
+  protected $builder;
+
+  /**
+   * The layout manager.
+   *
+   * @var \Drupal\Core\Layout\LayoutPluginManagerInterface
+   */
+  protected $layoutManager;
+
+  /**
+   * The block manager.
+   *
+   * @var \Drupal\Core\Block\BlockManagerInterface
+   */
+  protected $blockManager;
+
+  /**
+   * The layout tempstore repository.
+   *
+   * @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
+   */
+  protected $layoutTempstoreRepository;
+
+  /**
+   * LayoutController constructor.
+   *
+   * @param \Drupal\layout_builder\LayoutSectionBuilder $builder
+   *   The layout section builder.
+   * @param \Drupal\Core\Layout\LayoutPluginManagerInterface $layout_manager
+   *   The layout manager.
+   * @param \Drupal\Core\Block\BlockManagerInterface $block_manager
+   *   The block manager.
+   * @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
+   *   The layout tempstore repository.
+   */
+  public function __construct(LayoutSectionBuilder $builder, LayoutPluginManagerInterface $layout_manager, BlockManagerInterface $block_manager, LayoutTempstoreRepositoryInterface $layout_tempstore_repository) {
+    $this->builder = $builder;
+    $this->layoutManager = $layout_manager;
+    $this->blockManager = $block_manager;
+    $this->layoutTempstoreRepository = $layout_tempstore_repository;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('layout_builder.builder'),
+      $container->get('plugin.manager.core.layout'),
+      $container->get('plugin.manager.block'),
+      $container->get('layout_builder.tempstore_repository')
+    );
+  }
+
+  /**
+   * Provides a title callback.
+   *
+   * @param \Drupal\Core\Entity\FieldableEntityInterface $layout_section_entity
+   *   The entity.
+   *
+   * @return string
+   *   The title for the layout page.
+   */
+  public function title(FieldableEntityInterface $layout_section_entity) {
+    return $this->t('Edit layout for %label', ['%label' => $layout_section_entity->label()]);
+  }
+
+  /**
+   * Renders the Layout UI.
+   *
+   * @param \Drupal\Core\Entity\FieldableEntityInterface $layout_section_entity
+   *   The entity.
+   *
+   * @return array
+   *   A render array.
+   */
+  public function layout(FieldableEntityInterface $layout_section_entity) {
+    $layout_section_entity = $this->layoutTempstoreRepository->get($layout_section_entity);
+    $entity_id = $layout_section_entity->id();
+    if ($layout_section_entity instanceof RevisionableInterface) {
+      $entity_id = $layout_section_entity->getRevisionId();
+    }
+
+    $entity_type_id = $layout_section_entity->getEntityTypeId();
+
+    $output = [];
+    $count = 0;
+    $output[] = $this->buildAddSectionLink($entity_type_id, $entity_id, $count);
+    $count++;
+    /** @var \Drupal\layout_builder\LayoutSectionItemInterface $item */
+    foreach ($layout_section_entity->layout_builder__layout as $item) {
+      $output[] = $this->buildAdministrativeSection($item->layout, $item->section ?: [], $entity_type_id, $entity_id, $count - 1);
+      $output[] = $this->buildAddSectionLink($entity_type_id, $entity_id, $count);
+      $count++;
+    }
+    $output['#attached']['library'][] = 'layout_builder/drupal.layout_builder';
+    $output['#type'] = 'container';
+    $output['#attributes']['id'] = 'layout-builder';
+    // Mark this UI as uncacheable.
+    $output['#cache']['max-age'] = 0;
+    return $output;
+  }
+
+  /**
+   * Builds a link to add a new section at a given delta.
+   *
+   * @param string $entity_type_id
+   *   The entity type.
+   * @param string $entity_id
+   *   The entity ID.
+   * @param int $delta
+   *   The delta of the section to splice.
+   *
+   * @return array
+   *   A render array for a link.
+   */
+  protected function buildAddSectionLink($entity_type_id, $entity_id, $delta) {
+    $link = Link::createFromRoute($this->t('Add Section'),
+      'layout_builder.choose_section',
+      [
+        'entity_type_id' => $entity_type_id,
+        'entity_id' => $entity_id,
+        'delta' => $delta,
+      ],
+      [
+        'attributes' => [
+          'class' => ['use-ajax'],
+          'data-dialog-type' => 'dialog',
+          'data-dialog-renderer' => 'off_canvas',
+        ],
+      ]
+    );
+    return [
+      'link' => $link->toRenderable(),
+      '#type' => 'container',
+      '#attributes' => [
+        'class' => ['add-section'],
+      ],
+    ];
+  }
+
+  /**
+   * Builds the render array for the layout section while editing.
+   *
+   * @param string $layout_id
+   *   The ID of the layout.
+   * @param array $section
+   *   An array of configuration, keyed first by region and then by block UUID.
+   * @param string $entity_type_id
+   *   The entity type.
+   * @param string $entity_id
+   *   The entity ID.
+   * @param int $delta
+   *   The delta of the section to splice.
+   *
+   * @return array
+   *   The render array for a given section.
+   */
+  protected function buildAdministrativeSection($layout_id, array $section, $entity_type_id, $entity_id, $delta) {
+    $build = $this->builder->buildSection($layout_id, $section);
+    $layout = $this->layoutManager->getDefinition($layout_id);
+    foreach ($layout->getRegions() as $region => $info) {
+      $link = Link::createFromRoute($this->t('Add Block'),
+        'layout_builder.choose_block',
+        [
+          'entity_type_id' => $entity_type_id,
+          'entity_id' => $entity_id,
+          'delta' => $delta,
+          'region' => $region,
+        ],
+        [
+          'attributes' => [
+            'class' => ['use-ajax'],
+            'data-dialog-type' => 'dialog',
+            'data-dialog-renderer' => 'off_canvas',
+          ],
+        ]
+      );
+      $build[$region]['layout_builder_add_block']['link'] = $link->toRenderable();
+      $build[$region]['layout_builder_add_block']['#type'] = 'container';
+      $build[$region]['layout_builder_add_block']['#attributes'] = ['class' => ['add-block']];
+      $build[$region]['#attributes']['data-region'] = $region;
+    }
+    foreach ($section as $region => $blocks) {
+      foreach ($blocks as $uuid => $configuration) {
+        if (isset($build[$region][$uuid])) {
+          $build[$region][$uuid]['#attributes']['class'][] = 'draggable';
+          $build[$region][$uuid]['#attributes']['data-layout-block-uuid'] = $uuid;
+          $build[$region][$uuid]['#contextual_links'] = [
+            'layout_builder_block' => [
+              'route_parameters' => [
+                'entity_type_id' => $entity_type_id,
+                'entity_id' => $entity_id,
+                'delta' => $delta,
+                'region' => $region,
+                'uuid' => $uuid,
+              ],
+            ],
+          ];
+        }
+      }
+    }
+
+    $build['#attributes']['data-layout-update-url'] = Url::fromRoute('layout_builder.move_block', [
+      'entity_type_id' => $entity_type_id,
+      'entity_id' => $entity_id,
+    ])->toString();
+    $build['#attributes']['data-layout-delta'] = $delta;
+
+    return [
+      '#type' => 'container',
+      '#attributes' => [
+        'class' => ['layout-section'],
+      ],
+      'remove' => [
+        '#type' => 'link',
+        '#title' => $this->t('Remove section'),
+        '#url' => Url::fromRoute('layout_builder.remove_section', [
+          'entity_type_id' => $entity_type_id,
+          'entity_id' => $entity_id,
+          'delta' => $delta,
+        ]),
+        '#attributes' => [
+          'class' => ['use-ajax', 'remove-section'],
+          'data-dialog-type' => 'dialog',
+          'data-dialog-renderer' => 'off_canvas',
+        ],
+      ],
+      'layout-section' => $build,
+    ];
+  }
+
+  /**
+   * Saves the layout.
+   *
+   * @param \Drupal\Core\Entity\FieldableEntityInterface $layout_section_entity
+   *   The entity.
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse
+   *   A redirect response.
+   */
+  public function saveLayout(FieldableEntityInterface $layout_section_entity) {
+    $layout_section_entity = $this->layoutTempstoreRepository->get($layout_section_entity);
+
+    // @todo figure out if we should save a new revision.
+    $layout_section_entity->save();
+
+    $this->layoutTempstoreRepository->delete($layout_section_entity);
+
+    // @todo Make trusted redirect instead.
+    return new RedirectResponse($layout_section_entity->toUrl()->setAbsolute()->toString(), Response::HTTP_SEE_OTHER);
+  }
+
+  /**
+   * Cancels the layout.
+   *
+   * @param \Drupal\Core\Entity\FieldableEntityInterface $layout_section_entity
+   *   The entity.
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse
+   *   A redirect response.
+   */
+  public function cancelLayout(FieldableEntityInterface $layout_section_entity) {
+    $this->layoutTempstoreRepository->delete($layout_section_entity);
+    // @todo Make trusted redirect instead.
+    return new RedirectResponse($layout_section_entity->toUrl()->setAbsolute()->toString(), Response::HTTP_SEE_OTHER);
+  }
+
+}
diff --git a/core/modules/layout_builder/src/Controller/LayoutController.php b/core/modules/layout_builder/src/Controller/LayoutController.php
new file mode 100644
index 0000000..02a1b77
--- /dev/null
+++ b/core/modules/layout_builder/src/Controller/LayoutController.php
@@ -0,0 +1,300 @@
+<?php
+
+namespace Drupal\layout_builder\Controller;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Block\BlockManagerInterface;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Layout\LayoutPluginManagerInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Url;
+use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Returns responses for Layout Builder routes.
+ */
+class LayoutController implements ContainerInjectionInterface {
+
+  use LayoutRebuildTrait;
+  use StringTranslationTrait;
+
+  /**
+   * The layout manager.
+   *
+   * @var \Drupal\Core\Layout\LayoutPluginManagerInterface
+   */
+  protected $layoutManager;
+
+  /**
+   * The block manager.
+   *
+   * @var \Drupal\Core\Block\BlockManagerInterface
+   */
+  protected $blockManager;
+
+  /**
+   * The layout tempstore repository.
+   *
+   * @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
+   */
+  protected $layoutTempstoreRepository;
+
+  /**
+   * LayoutController constructor.
+   *
+   * @param \Drupal\Core\Layout\LayoutPluginManagerInterface $layout_manager
+   *   The layout manager.
+   * @param \Drupal\Core\Block\BlockManagerInterface $block_manager
+   *   The block manager.
+   * @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
+   *   The layout tempstore repository.
+   */
+  public function __construct(LayoutPluginManagerInterface $layout_manager, BlockManagerInterface $block_manager, LayoutTempstoreRepositoryInterface $layout_tempstore_repository) {
+    $this->layoutManager = $layout_manager;
+    $this->blockManager = $block_manager;
+    $this->layoutTempstoreRepository = $layout_tempstore_repository;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.core.layout'),
+      $container->get('plugin.manager.block'),
+      $container->get('layout_builder.tempstore_repository')
+    );
+  }
+
+  /**
+   * Choose a layout plugin to add as a section.
+   *
+   * @param string $entity_type_id
+   *   The entity type ID.
+   * @param string $entity_id
+   *   The entity ID.
+   * @param int $delta
+   *   The delta of the section to splice.
+   *
+   * @return array
+   *   The render array.
+   */
+  public function chooseSection($entity_type_id, $entity_id, $delta) {
+    $output = [];
+    $items = [];
+    foreach ($this->layoutManager->getDefinitions() as $plugin_id => $definition) {
+      $icon = $definition->getIconPath();
+      if ($icon) {
+        $icon = [
+          '#theme' => 'image',
+          '#uri' => $icon,
+          '#alt' => $definition->getLabel(),
+        ];
+      }
+
+      $items[] = [
+        'label' => [
+          '#type' => 'link',
+          '#title' => [
+            $icon ?: [],
+            [
+              '#type' => 'container',
+              '#children' => $definition->getLabel(),
+            ],
+          ],
+          '#url' => $this->generateSectionUrl($entity_type_id, $entity_id, $delta, $plugin_id),
+          '#attributes' => [
+            'class' => ['use-ajax'],
+            'data-dialog-type' => 'dialog',
+            'data-dialog-renderer' => 'off_canvas',
+          ],
+        ],
+      ];
+    }
+    $output['layouts'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Basic Layouts'),
+      '#open' => TRUE,
+      '#attributes' => [
+        'class' => [
+          'layout-selection',
+        ],
+      ],
+      'list' => [
+        '#theme' => 'item_list',
+        '#items' => $items,
+        '#attributes' => [
+          'class' => [
+            'layout-list',
+          ],
+        ],
+      ],
+    ];
+
+    return $output;
+  }
+
+  /**
+   * Add the layout to the entity field in a tempstore.
+   *
+   * @param string $entity_type_id
+   *   The entity type ID.
+   * @param string $entity_id
+   *   The entity ID.
+   * @param int $delta
+   *   The delta of the section to splice.
+   * @param string $plugin_id
+   *   The plugin ID of the layout to add.
+   *
+   * @return \Drupal\Core\Ajax\AjaxResponse
+   *   The render array.
+   */
+  public function addSection($entity_type_id, $entity_id, $delta, $plugin_id) {
+    $entity = $this->layoutTempstoreRepository->getFromId($entity_type_id, $entity_id);
+    $values = $entity->layout_builder__layout->getValue();
+    if (isset($values[$delta])) {
+      $start = array_slice($values, 0, $delta);
+      $end = array_slice($values, $delta);
+      $value = [
+        'layout' => $plugin_id,
+        'section' => [],
+      ];
+      $values = array_merge($start, [$value], $end);
+    }
+    else {
+      $values[] = [
+        'layout' => $plugin_id,
+        'section' => [],
+      ];
+    }
+    $entity->layout_builder__layout->setValue($values);
+    $this->layoutTempstoreRepository->set($entity);
+    return $this->rebuildAndClose(new AjaxResponse(), $entity);
+  }
+
+  /**
+   * Provides the UI for choosing a new block.
+   *
+   * @param string $entity_type_id
+   *   The entity type ID.
+   * @param string $entity_id
+   *   The entity ID.
+   * @param int $delta
+   *   The delta of the section to splice.
+   * @param string $region
+   *   The region the block is going in.
+   *
+   * @return array
+   *   A render array.
+   */
+  public function chooseBlock($entity_type_id, $entity_id, $delta, $region) {
+    $build['#type'] = 'container';
+    $build['#attributes']['class'][] = 'block-categories';
+
+    foreach ($this->blockManager->getGroupedDefinitions() as $category => $blocks) {
+      $build[$category]['#type'] = 'details';
+      $build[$category]['#open'] = TRUE;
+      $build[$category]['#title'] = $category;
+      $build[$category]['links'] = [
+        '#type' => 'table',
+      ];
+      foreach ($blocks as $block_id => $block) {
+        $build[$category]['links'][]['data'] = [
+          '#type' => 'link',
+          '#title' => $block['admin_label'],
+          '#url' => Url::fromRoute('layout_builder.add_block',
+            [
+              'entity_type_id' => $entity_type_id,
+              'entity_id' => $entity_id,
+              'delta' => $delta,
+              'region' => $region,
+              'plugin_id' => $block_id,
+            ]
+          ),
+          '#attributes' => [
+            'class' => ['use-ajax'],
+            'data-dialog-type' => 'dialog',
+            'data-dialog-renderer' => 'off_canvas',
+          ],
+        ];
+      }
+    }
+    return $build;
+  }
+
+  /**
+   * Moves a block to another region.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request.
+   * @param string $entity_type_id
+   *   The entity type ID.
+   * @param string $entity_id
+   *   The entity ID.
+   *
+   * @return \Drupal\Core\Ajax\AjaxResponse
+   *   An AJAX response.
+   */
+  public function moveBlock(Request $request, $entity_type_id, $entity_id) {
+    $entity = $this->layoutTempstoreRepository->getFromId($entity_type_id, $entity_id);
+    $data = $request->request->all();
+
+    /** @var \Drupal\layout_builder\LayoutSectionItemInterface $field */
+    $field = $entity->layout_builder__layout->get($data['delta_from']);
+    $values = $field->section ?: [];
+
+    $region_from = $data['region_from'];
+    $region_to = $data['region_to'];
+    $block_uuid = $data['block_uuid'];
+    $configuration = $values[$region_from][$block_uuid];
+    unset($values[$region_from][$block_uuid]);
+    $field->section = array_filter($values);
+
+    /** @var \Drupal\layout_builder\LayoutSectionItemInterface $field */
+    $field = $entity->layout_builder__layout->get($data['delta_to']);
+    $values = $field->section ?: [];
+    if (isset($data['preceding_block_uuid'])) {
+      $slice_id = array_search($data['preceding_block_uuid'], array_keys($values[$region_to]));
+      $before = array_slice($values[$region_to], 0, $slice_id + 1);
+      $after = array_slice($values[$region_to], $slice_id + 1);
+      $values[$region_to] = array_merge($before, [$block_uuid => $configuration], $after);
+    }
+    else {
+      if (empty($values[$region_to])) {
+        $values[$region_to] = [];
+      }
+      $values[$region_to] = array_merge([$block_uuid => $configuration], $values[$region_to]);
+    }
+    $field->section = array_filter($values);
+
+    $this->layoutTempstoreRepository->set($entity);
+    return $this->rebuildLayout(new AjaxResponse(), $entity);
+  }
+
+  /**
+   * A helper function for building Url object to add a section.
+   *
+   * @param string $entity_type_id
+   *   The entity type.
+   * @param string $entity_id
+   *   The entity ID.
+   * @param int $delta
+   *   The delta of the section to splice.
+   * @param string $plugin_id
+   *   The plugin ID of the layout to add.
+   *
+   * @return \Drupal\Core\Url
+   *   The Url object of the add_section route.
+   */
+  protected function generateSectionUrl($entity_type_id, $entity_id, $delta, $plugin_id) {
+    return new Url('layout_builder.add_section', [
+      'entity_type_id' => $entity_type_id,
+      'entity_id' => $entity_id,
+      'delta' => $delta,
+      'plugin_id' => $plugin_id,
+    ]);
+  }
+
+}
diff --git a/core/modules/layout_builder/src/Controller/LayoutRebuildTrait.php b/core/modules/layout_builder/src/Controller/LayoutRebuildTrait.php
new file mode 100644
index 0000000..77e2e67
--- /dev/null
+++ b/core/modules/layout_builder/src/Controller/LayoutRebuildTrait.php
@@ -0,0 +1,155 @@
+<?php
+
+namespace Drupal\layout_builder\Controller;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\CloseDialogCommand;
+use Drupal\Core\Ajax\RedirectCommand;
+use Drupal\Core\Ajax\ReplaceCommand;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+
+/**
+ * Provides AJAX responses to rebuild the Layout Builder.
+ */
+trait LayoutRebuildTrait {
+
+  /**
+   * Submit form dialog #ajax callback.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return \Drupal\Core\Ajax\AjaxResponse
+   *   An AJAX response that display validation error messages or redirects
+   *   to a URL
+   */
+  public function ajaxSubmit(array &$form, FormStateInterface $form_state) {
+    if ($form_state->hasAnyErrors()) {
+      $form['status_messages'] = [
+        '#type' => 'status_messages',
+        '#weight' => -1000,
+      ];
+      $response = new AjaxResponse();
+      $response->addCommand(new ReplaceCommand('[data-drupal-selector="' . $form['#attributes']['data-drupal-selector'] . '"]', $form));
+    }
+    else {
+      $entity = $this->getLayoutTempstoreRepository()->getFromId($this->entityTypeId, $this->entityId);
+      $response = $this->rebuildAndClose(new AjaxResponse(), $entity);
+    }
+    return $response;
+  }
+
+  /**
+   * Gets the layout tempstore repository.
+   *
+   * @return \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
+   *   The layout tempstore repository.
+   */
+  protected function getLayoutTempstoreRepository() {
+    if (!$this->layoutTempstoreRepository) {
+      $this->layoutTempstoreRepository = \Drupal::service('layout_builder.tempstore_repository');
+    }
+    return $this->layoutTempstoreRepository;
+  }
+
+  /**
+   * Rebuilds the layout.
+   *
+   * @param \Drupal\Core\Ajax\AjaxResponse $response
+   *   The AJAX response.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity.
+   *
+   * @return \Drupal\Core\Ajax\AjaxResponse
+   *   An AJAX response to either rebuild the layout and close the dialog, or
+   *   reload the page.
+   */
+  protected function rebuildAndClose(AjaxResponse $response, EntityInterface $entity) {
+    $response = $this->rebuildLayout($response, $entity);
+    $url = Url::fromRoute("entity.{$entity->getEntityTypeId()}.layout", [$entity->getEntityTypeId() => $entity->id()]);
+    return $this->closeLayout($response, $url);
+  }
+
+  /**
+   * Rebuilds the layout.
+   *
+   * @param \Drupal\Core\Ajax\AjaxResponse $response
+   *   The AJAX response.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity.
+   *
+   * @return \Drupal\Core\Ajax\AjaxResponse
+   *   An AJAX response to either rebuild the layout and close the dialog, or
+   *   reload the page.
+   */
+  protected function rebuildLayout(AjaxResponse $response, EntityInterface $entity) {
+    $layout_controller = $this->getClassResolver()->getInstanceFromDefinition(LayoutBuilderController::class);
+    $layout = $layout_controller->layout($entity);
+    $response->addCommand(new ReplaceCommand('#layout-builder', $layout));
+    return $response;
+  }
+
+  /**
+   * Returns to the layout builder.
+   *
+   * @param \Drupal\Core\Ajax\AjaxResponse $response
+   *   The AJAX response.
+   * @param \Drupal\Core\Url $url
+   *   The URL to redirect to if not using a dialog.
+   *
+   * @return \Drupal\Core\Ajax\AjaxResponse
+   *   An AJAX response to either rebuild the layout and close the dialog, or
+   *   reload the page.
+   */
+  protected function closeLayout(AjaxResponse $response, Url $url) {
+    if ($this->isDialog()) {
+      $response->addCommand(new CloseDialogCommand('#drupal-off-canvas'));
+    }
+    else {
+      $response->addCommand(new RedirectCommand($url->setAbsolute()->toString()));
+    }
+    return $response;
+  }
+
+  /**
+   * Determines if the current request is within a dialog.
+   *
+   * @return bool
+   *   TRUE if the current request is within a dialog, FALSE otherwise.
+   */
+  protected function isDialog() {
+    return $this->getRequest()->get(MainContentViewSubscriber::WRAPPER_FORMAT) === 'drupal_dialog.off_canvas';
+  }
+
+  /**
+   * Gets the request object.
+   *
+   * @return \Symfony\Component\HttpFoundation\Request
+   *   The request object.
+   */
+  protected function getRequest() {
+    if (!$this->requestStack) {
+      $this->requestStack = \Drupal::requestStack();
+    }
+    return $this->requestStack->getCurrentRequest();
+  }
+
+  /**
+   * Gets the class resolver.
+   *
+   * @return \Drupal\Core\DependencyInjection\ClassResolver
+   *   The class resolver.
+   */
+  protected function getClassResolver() {
+    if (!$this->classResolver) {
+      $this->classResolver = \Drupal::classResolver();
+    }
+    return $this->classResolver;
+  }
+
+}
diff --git a/core/modules/layout_builder/src/Form/ConfigureBlockForm.php b/core/modules/layout_builder/src/Form/ConfigureBlockForm.php
new file mode 100644
index 0000000..bb60337
--- /dev/null
+++ b/core/modules/layout_builder/src/Form/ConfigureBlockForm.php
@@ -0,0 +1,278 @@
+<?php
+
+namespace Drupal\layout_builder\Form;
+
+use Drupal\Component\Uuid\UuidInterface;
+use Drupal\Core\Block\BlockManagerInterface;
+use Drupal\Core\Block\BlockPluginInterface;
+use Drupal\Core\DependencyInjection\ClassResolverInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Form\SubformState;
+use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
+use Drupal\Core\Plugin\ContextAwarePluginAssignmentTrait;
+use Drupal\Core\Plugin\ContextAwarePluginInterface;
+use Drupal\Core\Plugin\PluginFormFactoryInterface;
+use Drupal\Core\Plugin\PluginWithFormsInterface;
+use Drupal\layout_builder\Controller\LayoutRebuildTrait;
+use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a form to configure a block.
+ */
+class ConfigureBlockForm extends FormBase {
+
+  use ContextAwarePluginAssignmentTrait;
+  use LayoutRebuildTrait;
+
+  /**
+   * The plugin being configured.
+   *
+   * @var \Drupal\Core\Block\BlockPluginInterface
+   */
+  protected $block;
+
+  /**
+   * The context repository.
+   *
+   * @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface
+   */
+  protected $contextRepository;
+
+  /**
+   * The layout tempstore repository.
+   *
+   * @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
+   */
+  protected $layoutTempstoreRepository;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The block manager.
+   *
+   * @var \Drupal\Core\Block\BlockManagerInterface
+   */
+  protected $blockManager;
+
+  /**
+   * The UUID generator.
+   *
+   * @var \Drupal\Component\Uuid\UuidInterface
+   */
+  protected $uuid;
+
+  /**
+   * The class resolver.
+   *
+   * @var \Drupal\Core\DependencyInjection\ClassResolver
+   */
+  protected $classResolver;
+
+  /**
+   * The plugin form manager.
+   *
+   * @var \Drupal\Core\Plugin\PluginFormFactoryInterface
+   */
+  protected $pluginFormFactory;
+
+  /**
+   * The entity type ID.
+   *
+   * @var string
+   */
+  protected $entityTypeId;
+
+  /**
+   * The entity ID.
+   *
+   * @var int
+   */
+  protected $entityId;
+
+  /**
+   * The field delta.
+   *
+   * @var int
+   */
+  protected $delta;
+
+  /**
+   * The current region.
+   *
+   * @var string
+   */
+  protected $region;
+
+  /**
+   * Constructs a new ConfigureBlockForm.
+   *
+   * @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
+   *   The layout tempstore repository.
+   * @param \Drupal\Core\Plugin\Context\ContextRepositoryInterface $context_repository
+   *   The context repository.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Block\BlockManagerInterface $block_manager
+   *   The block manager.
+   * @param \Drupal\Component\Uuid\UuidInterface $uuid
+   *   The UUID generator.
+   * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
+   *   The class resolver.
+   * @param \Drupal\Core\Plugin\PluginFormFactoryInterface $plugin_form_manager
+   *   The plugin form manager.
+   */
+  public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, ContextRepositoryInterface $context_repository, EntityTypeManagerInterface $entity_type_manager, BlockManagerInterface $block_manager, UuidInterface $uuid, ClassResolverInterface $class_resolver, PluginFormFactoryInterface $plugin_form_manager) {
+    $this->layoutTempstoreRepository = $layout_tempstore_repository;
+    $this->contextRepository = $context_repository;
+    $this->entityTypeManager = $entity_type_manager;
+    $this->blockManager = $block_manager;
+    $this->uuid = $uuid;
+    $this->classResolver = $class_resolver;
+    $this->pluginFormFactory = $plugin_form_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('layout_builder.tempstore_repository'),
+      $container->get('context.repository'),
+      $container->get('entity_type.manager'),
+      $container->get('plugin.manager.block'),
+      $container->get('uuid'),
+      $container->get('class_resolver'),
+      $container->get('plugin_form.factory')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'layout_builder_configure_block';
+  }
+
+  /**
+   * Prepares the block plugin based on the block ID.
+   *
+   * @param string $block_id
+   *   Either a block ID, or the plugin ID used to create a new block.
+   * @param array $configuration
+   *   The block configuration.
+   *
+   * @return \Drupal\Core\Block\BlockPluginInterface
+   *   The block plugin.
+   */
+  protected function prepareBlock($block_id, array $configuration) {
+    if (!isset($configuration['uuid'])) {
+      $configuration['uuid'] = $this->uuid->generate();
+    }
+
+    return $this->blockManager->createInstance($block_id, $configuration);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $entity_type_id = NULL, $entity_id = NULL, $delta = NULL, $region = NULL, $plugin_id = NULL, $uuid = NULL) {
+    $this->entityTypeId = $entity_type_id;
+    $this->entityId = $entity_id;
+    $this->delta = $delta;
+    $this->region = $region;
+
+    $configuration = [];
+    if ($uuid) {
+      $entity = $this->layoutTempstoreRepository->getFromId($this->entityTypeId, $this->entityId);
+
+      /** @var \Drupal\layout_builder\LayoutSectionItemInterface $field */
+      $field = $entity->layout_builder__layout->get($this->delta);
+      $plugin_id = $field->section[$region][$uuid]['id'];
+      $configuration = $field->section[$region][$uuid];
+    }
+    $this->block = $this->prepareBlock($plugin_id, $configuration);
+
+    $form_state->setTemporaryValue('gathered_contexts', $this->contextRepository->getAvailableContexts());
+
+    // Some Block Plugins rely on the block_theme value to load theme settings.
+    // @see \Drupal\system\Plugin\Block\SystemBrandingBlock::blockForm().
+    $form_state->set('block_theme', $this->config('system.theme')->get('default'));
+
+    $form['#tree'] = TRUE;
+    $form['settings'] = [];
+    $subform_state = SubformState::createForSubform($form['settings'], $form, $form_state);
+    $form['settings'] = $this->getPluginForm($this->block)->buildConfigurationForm($form['settings'], $subform_state);
+
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $uuid ? $this->t('Update') : $this->t('Add Block'),
+      '#button_type' => 'primary',
+      '#ajax' => [
+        'callback' => '::ajaxSubmit',
+      ],
+    ];
+
+    $form['#attached']['library'][] = 'core/drupal.dialog.ajax';
+    $form['#attributes']['id'] = 'dialog-form';
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    $sub_form_state = SubformState::createForSubform($form['settings'], $form, $form_state);
+    $this->getPluginForm($this->block)->validateConfigurationForm($form['settings'], $sub_form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    // Call the plugin submit handler.
+    $sub_form_state = SubformState::createForSubform($form['settings'], $form, $form_state);
+    $this->getPluginForm($this->block)->submitConfigurationForm($form, $sub_form_state);
+
+    // If this block is context-aware, set the context mapping.
+    if ($this->block instanceof ContextAwarePluginInterface) {
+      $this->block->setContextMapping($sub_form_state->getValue('context_mapping', []));
+    }
+
+    $configuration = $this->block->getConfiguration();
+
+    /** @var \Drupal\layout_builder\LayoutSectionItemInterface $field */
+    $entity = $this->layoutTempstoreRepository->getFromId($this->entityTypeId, $this->entityId);
+    $values = $entity->layout_builder__layout->getValue();
+    $values[$this->delta]['section'][$this->region][$configuration['uuid']] = $configuration;
+    $entity->layout_builder__layout->setValue($values);
+
+    $this->layoutTempstoreRepository->set($entity);
+    $form_state->setRedirect("entity.{$this->entityTypeId}.layout", [$this->entityTypeId => $this->entityId]);
+  }
+
+  /**
+   * 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) {
+    if ($block instanceof PluginWithFormsInterface) {
+      return $this->pluginFormFactory->createInstance($block, 'configure');
+    }
+    return $block;
+  }
+
+}
diff --git a/core/modules/layout_builder/src/Form/LayoutRebuildConfirmFormBase.php b/core/modules/layout_builder/src/Form/LayoutRebuildConfirmFormBase.php
new file mode 100644
index 0000000..3c56206
--- /dev/null
+++ b/core/modules/layout_builder/src/Form/LayoutRebuildConfirmFormBase.php
@@ -0,0 +1,132 @@
+<?php
+
+namespace Drupal\layout_builder\Form;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\ConfirmFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\layout_builder\Controller\LayoutRebuildTrait;
+use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a base class for confirmation forms that rebuild the Layout Builder.
+ */
+abstract class LayoutRebuildConfirmFormBase extends ConfirmFormBase {
+
+  use LayoutRebuildTrait;
+
+  /**
+   * The layout tempstore repository.
+   *
+   * @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
+   */
+  protected $layoutTempstoreRepository;
+
+  /**
+   * The entity type ID.
+   *
+   * @var string
+   */
+  protected $entityTypeId;
+
+  /**
+   * The entity ID.
+   *
+   * @var int
+   */
+  protected $entityId;
+
+  /**
+   * The field delta.
+   *
+   * @var int
+   */
+  protected $delta;
+
+  /**
+   * Constructs a new RemoveSectionForm.
+   *
+   * @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
+   *   The layout tempstore repository.
+   */
+  public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository) {
+    $this->layoutTempstoreRepository = $layout_tempstore_repository;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('layout_builder.tempstore_repository')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelUrl() {
+    $parameters = [
+      $this->entityTypeId => $this->entityId,
+    ];
+    return new Url("entity.{$this->entityTypeId}.layout", $parameters);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $entity_type_id = NULL, $entity_id = NULL, $delta = NULL) {
+    $form = parent::buildForm($form, $form_state);
+
+    $this->entityTypeId = $entity_type_id;
+    $this->entityId = $entity_id;
+    $this->delta = $delta;
+
+    $form['#attached']['library'][] = 'core/drupal.dialog.ajax';
+    $form['actions']['submit']['#ajax']['callback'] = '::ajaxSubmit';
+
+    // @todo Improve the cancel link of ConfirmFormBase to handle AJAX links.
+    $form['actions']['cancel'] = [
+      '#type' => 'button',
+      '#value' => $this->getCancelText(),
+      '#ajax' => [
+        'callback' => '::ajaxCancel',
+      ],
+    ];
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $entity = $this->layoutTempstoreRepository->getFromId($this->entityTypeId, $this->entityId);
+
+    $this->handleEntity($entity, $form_state);
+
+    $this->layoutTempstoreRepository->set($entity);
+
+    $form_state->setRedirect("entity.{$entity->getEntityTypeId()}.layout", [$entity->getEntityTypeId() => $entity->id()]);
+  }
+
+  /**
+   * Performs any actions on the layout entity before saving.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  abstract protected function handleEntity(EntityInterface $entity, FormStateInterface $form_state);
+
+  /**
+   * Ajax callback to close the modal.
+   */
+  public function ajaxCancel(array &$form, FormStateInterface $form_state) {
+    return $this->closeLayout(new AjaxResponse(), $this->getCancelUrl());
+  }
+
+}
diff --git a/core/modules/layout_builder/src/Form/RemoveBlockForm.php b/core/modules/layout_builder/src/Form/RemoveBlockForm.php
new file mode 100644
index 0000000..99018ed
--- /dev/null
+++ b/core/modules/layout_builder/src/Form/RemoveBlockForm.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Drupal\layout_builder\Form;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Provides a form to confirm the removal of a block.
+ */
+class RemoveBlockForm extends LayoutRebuildConfirmFormBase {
+
+  /**
+   * The current region.
+   *
+   * @var string
+   */
+  protected $region;
+
+  /**
+   * The UUID of the block being removed.
+   *
+   * @var string
+   */
+  protected $uuid;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    return $this->t('Are you sure you want to remove this block?');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return $this->t('Remove');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'layout_builder_remove_block';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $entity_type_id = NULL, $entity_id = NULL, $delta = NULL, $region = NULL, $uuid = NULL) {
+    $this->region = $region;
+    $this->uuid = $uuid;
+    return parent::buildForm($form, $form_state, $entity_type_id, $entity_id, $delta);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function handleEntity(EntityInterface $entity, FormStateInterface $form_state) {
+    /** @var \Drupal\layout_builder\LayoutSectionItemInterface $field */
+    $field = $entity->layout_builder__layout->get($this->delta);
+    $values = $field->section;
+    unset($values[$this->region][$this->uuid]);
+    $field->section = $values;
+  }
+
+}
diff --git a/core/modules/layout_builder/src/Form/RemoveSectionForm.php b/core/modules/layout_builder/src/Form/RemoveSectionForm.php
new file mode 100644
index 0000000..7b2f564
--- /dev/null
+++ b/core/modules/layout_builder/src/Form/RemoveSectionForm.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Drupal\layout_builder\Form;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Provides a form to confirm the removal of a section.
+ */
+class RemoveSectionForm extends LayoutRebuildConfirmFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'layout_builder_remove_section';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    return $this->t('Are you sure you want to remove this section?');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return $this->t('Remove');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function handleEntity(EntityInterface $entity, FormStateInterface $form_state) {
+    $entity->layout_builder__layout->removeItem($this->delta);
+  }
+
+}
diff --git a/core/modules/layout_builder/src/LayoutSectionBuilder.php b/core/modules/layout_builder/src/LayoutSectionBuilder.php
new file mode 100644
index 0000000..ea05d48
--- /dev/null
+++ b/core/modules/layout_builder/src/LayoutSectionBuilder.php
@@ -0,0 +1,152 @@
+<?php
+
+namespace Drupal\layout_builder;
+
+use Drupal\Component\Plugin\Exception\PluginException;
+use Drupal\Core\Block\BlockManagerInterface;
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Layout\LayoutPluginManagerInterface;
+use Drupal\Core\Plugin\Context\ContextHandlerInterface;
+use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
+use Drupal\Core\Plugin\ContextAwarePluginInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Builds the UI for layout sections.
+ */
+class LayoutSectionBuilder {
+
+  use StringTranslationTrait;
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $account;
+
+  /**
+   * The layout plugin manager.
+   *
+   * @var \Drupal\Core\Layout\LayoutPluginManagerInterface
+   */
+  protected $layoutPluginManager;
+
+  /**
+   * The block plugin manager.
+   *
+   * @var \Drupal\Core\Block\BlockManagerInterface
+   */
+  protected $blockManager;
+
+  /**
+   * The plugin context handler.
+   *
+   * @var \Drupal\Core\Plugin\Context\ContextHandlerInterface
+   */
+  protected $contextHandler;
+
+  /**
+   * The context manager service.
+   *
+   * @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface
+   */
+  protected $contextRepository;
+
+  /**
+   * Constructs a LayoutSectionFormatter object.
+   *
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The current user.
+   * @param \Drupal\Core\Layout\LayoutPluginManagerInterface $layoutPluginManager
+   *   The layout plugin manager.
+   * @param \Drupal\Core\Block\BlockManagerInterface $blockManager
+   *   THe block plugin manager.
+   * @param \Drupal\Core\Plugin\Context\ContextHandlerInterface $context_handler
+   *   The ContextHandler for applying contexts to conditions properly.
+   * @param \Drupal\Core\Plugin\Context\ContextRepositoryInterface $context_repository
+   *   The lazy context repository service.
+   */
+  public function __construct(AccountInterface $account, LayoutPluginManagerInterface $layoutPluginManager, BlockManagerInterface $blockManager, ContextHandlerInterface $context_handler, ContextRepositoryInterface $context_repository) {
+    $this->account = $account;
+    $this->layoutPluginManager = $layoutPluginManager;
+    $this->blockManager = $blockManager;
+    $this->contextHandler = $context_handler;
+    $this->contextRepository = $context_repository;
+  }
+
+  /**
+   * Builds the render array for the layout section.
+   *
+   * @param string $layout_id
+   *   The ID of the layout.
+   * @param array $section
+   *   An array of configuration, keyed first by region and then by block UUID.
+   *
+   * @return array
+   *   The render array for a given section.
+   */
+  public function buildSection($layout_id, array $section) {
+    $cacheability = CacheableMetadata::createFromRenderArray([]);
+
+    $regions = [];
+    $weight = 0;
+    foreach ($section as $region => $blocks) {
+      // @todo determine if config should at least always be an empty array.
+      foreach ($blocks as $uuid => $configuration) {
+        $block = $this->getBlock($uuid, $configuration);
+
+        $access = $block->access($this->account, TRUE);
+        $cacheability->addCacheableDependency($access);
+
+        if ($access->isAllowed()) {
+          $regions[$region][$uuid] = [
+            '#theme' => 'block',
+            '#weight' => $weight++,
+            '#configuration' => $block->getConfiguration(),
+            '#plugin_id' => $block->getPluginId(),
+            '#base_plugin_id' => $block->getBaseId(),
+            '#derivative_plugin_id' => $block->getDerivativeId(),
+            'content' => $block->build(),
+          ];
+          $cacheability->addCacheableDependency($block);
+        }
+      }
+    }
+
+    $layout = $this->layoutPluginManager->createInstance($layout_id);
+    $section = $layout->build($regions);
+    $cacheability->applyTo($section);
+    return $section;
+  }
+
+  /**
+   * Gets a block instance.
+   *
+   * @param string $uuid
+   *   The UUID of this block instance.
+   * @param array $configuration
+   *   An array of configuration relevant to the block instance. Must contain
+   *   the plugin ID with the key 'id'.
+   *
+   * @return \Drupal\Core\Block\BlockPluginInterface
+   *   The block instance.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   *   Thrown when the configuration parameter does not contain 'id'.
+   */
+  protected function getBlock($uuid, array $configuration) {
+    if (!isset($configuration['id'])) {
+      throw new PluginException(sprintf('No plugin ID specified for block with "%s" UUID', $uuid));
+    }
+
+    $block = $this->blockManager->createInstance($configuration['id'], $configuration);
+    if ($block instanceof ContextAwarePluginInterface) {
+      $contexts = $this->contextRepository->getRuntimeContexts(array_values($block->getContextMapping()));
+      $this->contextHandler->applyContextMapping($block, $contexts);
+    }
+    return $block;
+  }
+
+}
diff --git a/core/modules/layout_builder/src/LayoutSectionItemInterface.php b/core/modules/layout_builder/src/LayoutSectionItemInterface.php
new file mode 100644
index 0000000..1a39cde
--- /dev/null
+++ b/core/modules/layout_builder/src/LayoutSectionItemInterface.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Drupal\layout_builder;
+
+use Drupal\Core\Field\FieldItemInterface;
+
+/**
+ * Defines an interface for the layout section field item.
+ *
+ * @property string layout
+ * @property array[] section
+ */
+interface LayoutSectionItemInterface extends FieldItemInterface {
+
+}
diff --git a/core/modules/layout_builder/src/LayoutTempstoreRepository.php b/core/modules/layout_builder/src/LayoutTempstoreRepository.php
new file mode 100644
index 0000000..abd6067
--- /dev/null
+++ b/core/modules/layout_builder/src/LayoutTempstoreRepository.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace Drupal\layout_builder;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\RevisionableInterface;
+use Drupal\user\SharedTempStoreFactory;
+
+/**
+ * Provides a mechanism for loading layouts from tempstore.
+ */
+class LayoutTempstoreRepository implements LayoutTempstoreRepositoryInterface {
+
+  /**
+   * The shared tempstore factory.
+   *
+   * @var \Drupal\user\SharedTempStoreFactory
+   */
+  protected $tempStoreFactory;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * LayoutTempstoreRepository constructor.
+   *
+   * @param \Drupal\user\SharedTempStoreFactory $temp_store_factory
+   *   The shared tempstore factory.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(SharedTempStoreFactory $temp_store_factory, EntityTypeManagerInterface $entity_type_manager) {
+    $this->tempStoreFactory = $temp_store_factory;
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function get(EntityInterface $entity) {
+    list($collection, $id) = $this->generateTempstoreId($entity);
+    $tempstore = $this->tempStoreFactory->get($collection)->get($id);
+    if (!empty($tempstore['entity'])) {
+      return $tempstore['entity'];
+    }
+    return $entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFromId($entity_type_id, $entity_id) {
+    $entity = $this->entityTypeManager->getStorage($entity_type_id)->loadRevision($entity_id);
+    return $this->get($entity);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function set(EntityInterface $entity) {
+    list($collection, $id) = $this->generateTempstoreId($entity);
+    $this->tempStoreFactory->get($collection)->set($id, ['entity' => $entity]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function delete(EntityInterface $entity) {
+    if ($this->get($entity)) {
+      list($collection, $id) = $this->generateTempstoreId($entity);
+      $this->tempStoreFactory->get($collection)->delete($id);
+    }
+  }
+
+  /**
+   * Generates a collection and ID for putting an entity in tempstore.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity being stored.
+   *
+   * @return array
+   *   An array containing the collection name and the tempstore ID.
+   */
+  protected function generateTempstoreId(EntityInterface $entity) {
+    // @todo Can we make the collection simply the entity type ID?
+    $collection = $entity->getEntityTypeId() . '.layout_builder__layout';
+    $id = "{$entity->id()}.{$entity->language()->getId()}";
+    if ($entity instanceof RevisionableInterface) {
+      $id .= '.' . $entity->getRevisionId();
+    }
+    return [$collection, $id];
+  }
+
+}
diff --git a/core/modules/layout_builder/src/LayoutTempstoreRepositoryInterface.php b/core/modules/layout_builder/src/LayoutTempstoreRepositoryInterface.php
new file mode 100644
index 0000000..8043c84
--- /dev/null
+++ b/core/modules/layout_builder/src/LayoutTempstoreRepositoryInterface.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\layout_builder;
+
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Provides an interface for loading layouts from tempstore.
+ */
+interface LayoutTempstoreRepositoryInterface {
+
+  /**
+   * Gets the tempstore version of an entity, if it exists.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity to check for in tempstore.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface
+   *   Either the version of this entity from tempstore, or the passed entity if
+   *   none exists.
+   */
+  public function get(EntityInterface $entity);
+
+  /**
+   * Loads an entity from tempstore given the entity ID.
+   *
+   * @param string $entity_type_id
+   *   The entity type ID.
+   * @param string $entity_id
+   *   The entity ID (or revision ID).
+   *
+   * @return \Drupal\Core\Entity\EntityInterface
+   *   Either the version of this entity from tempstore, or the entity from
+   *   storage if none exists.
+   */
+  public function getFromId($entity_type_id, $entity_id);
+
+  /**
+   * Stores this entity in tempstore.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity to set in tempstore.
+   */
+  public function set(EntityInterface $entity);
+
+  /**
+   * Removes the tempstore version of an entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity to remove from tempstore.
+   */
+  public function delete(EntityInterface $entity);
+
+}
diff --git a/core/modules/layout_builder/src/Plugin/Derivative/LayoutBuilderLocalTaskDeriver.php b/core/modules/layout_builder/src/Plugin/Derivative/LayoutBuilderLocalTaskDeriver.php
new file mode 100644
index 0000000..9ef0db4
--- /dev/null
+++ b/core/modules/layout_builder/src/Plugin/Derivative/LayoutBuilderLocalTaskDeriver.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace Drupal\layout_builder\Plugin\Derivative;
+
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\layout_builder\Plugin\Menu\LayoutBuilderLocalTask;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides local task definitions for the layout builder user interface.
+ */
+class LayoutBuilderLocalTaskDeriver extends DeriverBase implements ContainerDeriverInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a new LayoutBuilderLocalTaskDeriver.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, $base_plugin_id) {
+    return new static(
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDerivativeDefinitions($base_plugin_definition) {
+    foreach (array_keys($this->getEntityTypes()) as $entity_type_id) {
+      $this->derivatives["entity.$entity_type_id.layout"] = $base_plugin_definition + [
+        'route_name' => "entity.$entity_type_id.layout",
+        'weight' => 15,
+        'title' => $this->t('Layout'),
+        'base_route' => "entity.$entity_type_id.canonical",
+        'entity_type_id' => $entity_type_id,
+        'class' => LayoutBuilderLocalTask::class,
+      ];
+      $this->derivatives["entity.$entity_type_id.save_layout"] = $base_plugin_definition + [
+        'route_name' => "entity.$entity_type_id.save_layout",
+        'title' => $this->t('Save Layout'),
+        'parent_id' => "layout_builder_ui:entity.$entity_type_id.layout",
+        'entity_type_id' => $entity_type_id,
+        'class' => LayoutBuilderLocalTask::class,
+      ];
+      $this->derivatives["entity.$entity_type_id.cancel_layout"] = $base_plugin_definition + [
+        'route_name' => "entity.$entity_type_id.cancel_layout",
+        'title' => $this->t('Cancel Layout'),
+        'parent_id' => "layout_builder_ui:entity.$entity_type_id.layout",
+        'entity_type_id' => $entity_type_id,
+        'class' => LayoutBuilderLocalTask::class,
+        'weight' => 5,
+      ];
+    }
+
+    return $this->derivatives;
+  }
+
+  /**
+   * Returns an array of relevant entity types.
+   *
+   * @return \Drupal\Core\Entity\EntityTypeInterface[]
+   *   An array of entity types.
+   */
+  protected function getEntityTypes() {
+    return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) {
+      return $entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasLinkTemplate('canonical') && $entity_type->hasViewBuilderClass();
+    });
+  }
+
+}
diff --git a/core/modules/layout_builder/src/Plugin/Field/FieldFormatter/LayoutSectionFormatter.php b/core/modules/layout_builder/src/Plugin/Field/FieldFormatter/LayoutSectionFormatter.php
new file mode 100644
index 0000000..4638f0e
--- /dev/null
+++ b/core/modules/layout_builder/src/Plugin/Field/FieldFormatter/LayoutSectionFormatter.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace Drupal\layout_builder\Plugin\Field\FieldFormatter;
+
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Field\FormatterBase;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\layout_builder\LayoutSectionBuilder;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Plugin implementation of the 'layout_section' formatter.
+ *
+ * @FieldFormatter(
+ *   id = "layout_section",
+ *   label = @Translation("Layout Section"),
+ *   field_types = {
+ *     "layout_section"
+ *   }
+ * )
+ */
+class LayoutSectionFormatter extends FormatterBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The layout section builder.
+   *
+   * @var \Drupal\layout_builder\LayoutSectionBuilder
+   */
+  protected $builder;
+
+  /**
+   * Constructs a LayoutSectionFormatter object.
+   *
+   * @param \Drupal\layout_builder\LayoutSectionBuilder $builder
+   *   The layout section builder.
+   * @param string $plugin_id
+   *   The plugin ID for the formatter.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The definition of the field to which the formatter is associated.
+   * @param array $settings
+   *   The formatter settings.
+   * @param string $label
+   *   The formatter label display setting.
+   * @param string $view_mode
+   *   The view mode.
+   * @param array $third_party_settings
+   *   Any third party settings.
+   */
+  public function __construct(LayoutSectionBuilder $builder, $plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings) {
+    $this->builder = $builder;
+    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $container->get('layout_builder.builder'),
+      $plugin_id,
+      $plugin_definition,
+      $configuration['field_definition'],
+      $configuration['settings'],
+      $configuration['label'],
+      $configuration['view_mode'],
+      $configuration['third_party_settings']
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function viewElements(FieldItemListInterface $items, $langcode) {
+    $elements = [];
+
+    /** @var \Drupal\layout_builder\LayoutSectionItemInterface[] $items */
+    foreach ($items as $delta => $item) {
+      $elements[$delta] = $this->builder->buildSection($item->layout, $item->section);
+    }
+
+    return $elements;
+  }
+
+}
diff --git a/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php b/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php
new file mode 100644
index 0000000..2ca2248
--- /dev/null
+++ b/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace Drupal\layout_builder\Plugin\Field\FieldType;
+
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldItemBase;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TypedData\DataDefinition;
+use Drupal\Core\TypedData\MapDataDefinition;
+use Drupal\layout_builder\LayoutSectionItemInterface;
+
+/**
+ * Plugin implementation of the 'layout_section' field type.
+ *
+ * @FieldType(
+ *   id = "layout_section",
+ *   label = @Translation("Layout Section"),
+ *   description = @Translation("Layout Section"),
+ *   default_formatter = "layout_section",
+ *   no_ui = TRUE,
+ *   cardinality = \Drupal\Core\Field\FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
+ * )
+ */
+class LayoutSectionItem extends FieldItemBase implements LayoutSectionItemInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
+    // Prevent early t() calls by using the TranslatableMarkup.
+    $properties['layout'] = DataDefinition::create('string')
+      ->setLabel(new TranslatableMarkup('Layout'))
+      ->setSetting('case_sensitive', FALSE)
+      ->setRequired(TRUE);
+    $properties[static::mainPropertyName()] = MapDataDefinition::create('map')
+      ->setLabel(new TranslatableMarkup('Layout Section'))
+      ->setRequired(FALSE);
+
+    return $properties;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function mainPropertyName() {
+    return 'section';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function schema(FieldStorageDefinitionInterface $field_definition) {
+    $schema = [
+      'columns' => [
+        'layout' => [
+          'type' => 'varchar',
+          'length' => '255',
+          'binary' => FALSE,
+        ],
+        static::mainPropertyName() => [
+          'type' => 'blob',
+          'size' => 'normal',
+          'serialize' => TRUE,
+        ],
+      ],
+    ];
+
+    return $schema;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
+    $values['layout'] = 'layout_onecol';
+    $values[static::mainPropertyName()] = [];
+    return $values;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isEmpty() {
+    return empty($this->layout);
+  }
+
+}
diff --git a/core/modules/layout_builder/src/Plugin/Menu/LayoutBuilderLocalTask.php b/core/modules/layout_builder/src/Plugin/Menu/LayoutBuilderLocalTask.php
new file mode 100644
index 0000000..4f80e38
--- /dev/null
+++ b/core/modules/layout_builder/src/Plugin/Menu/LayoutBuilderLocalTask.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Drupal\layout_builder\Plugin\Menu;
+
+use Drupal\Core\Menu\LocalTaskDefault;
+use Drupal\Core\Routing\RouteMatchInterface;
+
+/**
+ * Provides route parameters needed to link to layout related tabs.
+ */
+class LayoutBuilderLocalTask extends LocalTaskDefault {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRouteParameters(RouteMatchInterface $route_match) {
+    $parameters = parent::getRouteParameters($route_match);
+
+    $parameters['layout_section_entity'] = $route_match->getParameter('layout_section_entity');
+    return $parameters;
+  }
+
+}
diff --git a/core/modules/layout_builder/src/Routing/LayoutBuilderRouteEnhancer.php b/core/modules/layout_builder/src/Routing/LayoutBuilderRouteEnhancer.php
new file mode 100644
index 0000000..d19024f
--- /dev/null
+++ b/core/modules/layout_builder/src/Routing/LayoutBuilderRouteEnhancer.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Drupal\layout_builder\Routing;
+
+use Drupal\Core\Routing\Enhancer\RouteEnhancerInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Enhances routes to ensure the entity is available with a generic name.
+ */
+class LayoutBuilderRouteEnhancer implements RouteEnhancerInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applies(Route $route) {
+    return $route->hasOption('_layout_builder');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function enhance(array $defaults, Request $request) {
+    // Copy the entity by reference so that any changes are reflected.
+    $defaults['layout_section_entity'] = &$defaults[$defaults['entity_type_id']];
+    return $defaults;
+  }
+
+}
diff --git a/core/modules/layout_builder/src/Routing/LayoutBuilderRoutes.php b/core/modules/layout_builder/src/Routing/LayoutBuilderRoutes.php
new file mode 100644
index 0000000..0566132
--- /dev/null
+++ b/core/modules/layout_builder/src/Routing/LayoutBuilderRoutes.php
@@ -0,0 +1,141 @@
+<?php
+
+namespace Drupal\layout_builder\Routing;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Routing\RouteSubscriberBase;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * Provides routes for the Layout Builder UI.
+ */
+class LayoutBuilderRoutes extends RouteSubscriberBase {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a new LayoutBuilderRoutes.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * Generates layout builder routes.
+   *
+   * @return \Symfony\Component\Routing\Route[]
+   *   An array of route objects.
+   */
+  public function getRoutes() {
+    $routes = [];
+
+    foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) {
+      $template = $entity_type->getLinkTemplate('canonical');
+      $route = (new Route("$template/layout"))
+        ->setDefaults([
+          '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout',
+          '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title',
+          'layout_section_entity' => NULL,
+          'entity_type_id' => $entity_type_id,
+        ])
+        ->addRequirements([
+          $entity_type_id => '\d+',
+          '_has_layout_selection' => 'true',
+        ])
+        ->addOptions([
+          '_layout_builder' => TRUE,
+          'parameters' => [
+            $entity_type_id => [
+              'type' => "entity:$entity_type_id",
+            ],
+          ],
+        ]);
+      $routes["entity.$entity_type_id.layout"] = $route;
+
+      $route = (new Route("$template/layout/save"))
+        ->setDefaults([
+          '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout',
+          'layout_section_entity' => NULL,
+          'entity_type_id' => $entity_type_id,
+        ])
+        ->addRequirements([
+          $entity_type_id => '\d+',
+          '_has_layout_selection' => 'true',
+        ])
+        ->addOptions([
+          '_layout_builder' => TRUE,
+          'parameters' => [
+            $entity_type_id => [
+              'type' => "entity:$entity_type_id",
+            ],
+          ],
+        ]);
+      $routes["entity.$entity_type_id.save_layout"] = $route;
+
+      $route = (new Route("$template/layout/cancel"))
+        ->setDefaults([
+          '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout',
+          'layout_section_entity' => NULL,
+          'entity_type_id' => $entity_type_id,
+        ])
+        ->addRequirements([
+          $entity_type_id => '\d+',
+          '_has_layout_selection' => 'true',
+        ])
+        ->addOptions([
+          '_layout_builder' => TRUE,
+          'parameters' => [
+            $entity_type_id => [
+              'type' => "entity:$entity_type_id",
+            ],
+          ],
+        ]);
+      $routes["entity.$entity_type_id.cancel_layout"] = $route;
+    }
+    return $routes;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function alterRoutes(RouteCollection $collection) {
+    $templates = ['canonical', 'edit_form', 'delete_form'];
+    foreach ($this->getEntityTypes() as $entity_type) {
+      foreach ($templates as $template) {
+        // Mark this as a Layout Builder route so that links like local tasks
+        // will be enhanced.
+        if ($route = $collection->get('entity.' . $entity_type->id() . '.' . $template)) {
+          $route->setOption('_layout_builder', TRUE);
+          $route->addDefaults([
+            'layout_section_entity' => NULL,
+            'entity_type_id' => $entity_type->id(),
+          ]);
+        }
+      }
+    }
+  }
+
+  /**
+   * Returns an array of relevant entity types.
+   *
+   * @return \Drupal\Core\Entity\EntityTypeInterface[]
+   *   An array of entity types.
+   */
+  protected function getEntityTypes() {
+    return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) {
+      return $entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasLinkTemplate('canonical') && $entity_type->hasViewBuilderClass();
+    });
+  }
+
+}
diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php
new file mode 100644
index 0000000..0d63b17
--- /dev/null
+++ b/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php
@@ -0,0 +1,333 @@
+<?php
+
+namespace Drupal\Tests\layout_builder\Functional;
+
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
+use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests the rendering of a layout section field.
+ *
+ * @group layout_builder
+ */
+class LayoutSectionTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['layout_builder', 'node', 'block_test'];
+
+  /**
+   * The name of the layout section field.
+   *
+   * @var string
+   */
+  protected $fieldName = 'layout_builder__layout';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->createContentType([
+      'type' => 'bundle_with_section_field',
+    ]);
+    $this->createContentType([
+      'type' => 'bundle_without_section_field',
+    ]);
+
+    layout_builder_add_layout_section_field('node', 'bundle_with_section_field');
+    $display = EntityViewDisplay::load('node.bundle_with_section_field.default');
+    $display->setThirdPartySetting('layout_builder', 'allow_custom', TRUE);
+    $display->save();
+
+    $this->drupalLogin($this->drupalCreateUser([
+      'configure any layout',
+    ], 'foobar'));
+  }
+
+  /**
+   * Provides test data for ::testLayoutSectionFormatter().
+   */
+  public function providerTestLayoutSectionFormatter() {
+    $data = [];
+    $data['block_with_context'] = [
+      [
+        [
+          'layout' => 'layout_onecol',
+          'section' => [
+            'content' => [
+              'baz' => [
+                'id' => 'test_context_aware',
+                'context_mapping' => [
+                  'user' => '@user.current_user_context:current_user',
+                ],
+              ],
+            ],
+          ],
+        ],
+      ],
+      [
+        '.layout--onecol',
+        '#test_context_aware--username',
+      ],
+      [
+        'foobar',
+        'User context found',
+      ],
+      'user',
+      'user:2',
+      'UNCACHEABLE',
+    ];
+    $data['single_section_single_block'] = [
+      [
+        [
+          'layout' => 'layout_onecol',
+          'section' => [
+            'content' => [
+              'baz' => [
+                'id' => 'system_powered_by_block',
+              ],
+            ],
+          ],
+        ],
+      ],
+      '.layout--onecol',
+      'Powered by',
+      '',
+      '',
+      'MISS',
+    ];
+    $data['multiple_sections'] = [
+      [
+        [
+          'layout' => 'layout_onecol',
+          'section' => [
+            'content' => [
+              'baz' => [
+                'id' => 'system_powered_by_block',
+              ],
+            ],
+          ],
+        ],
+        [
+          'layout' => 'layout_twocol',
+          'section' => [
+            'first' => [
+              'foo' => [
+                'id' => 'test_block_instantiation',
+                'display_message' => 'foo text',
+              ],
+            ],
+            'second' => [
+              'bar' => [
+                'id' => 'test_block_instantiation',
+                'display_message' => 'bar text',
+              ],
+            ],
+          ],
+        ],
+      ],
+      [
+        '.layout--onecol',
+        '.layout--twocol',
+      ],
+      [
+        'Powered by',
+        'foo text',
+        'bar text',
+      ],
+      'user.permissions',
+      '',
+      'MISS',
+    ];
+    return $data;
+  }
+
+  /**
+   * Tests layout_section formatter output.
+   *
+   * @dataProvider providerTestLayoutSectionFormatter
+   */
+  public function testLayoutSectionFormatter($layout_data, $expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, $expected_dynamic_cache) {
+    $this->createSectionNode($layout_data);
+
+    $this->drupalGet('node/1');
+    $this->assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, $expected_dynamic_cache);
+
+    $this->drupalGet('node/1/layout');
+    $this->assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, 'UNCACHEABLE');
+  }
+
+  /**
+   * Tests the access checking of the section formatter.
+   */
+  public function testLayoutSectionFormatterAccess() {
+    $this->createSectionNode([
+      [
+        'layout' => 'layout_onecol',
+        'section' => [
+          'content' => [
+            'baz' => [
+              'id' => 'test_access',
+            ],
+          ],
+        ],
+      ],
+    ]);
+
+    // Restrict access to the block.
+    $this->container->get('state')->set('test_block_access', FALSE);
+
+    $this->drupalGet('node/1');
+    $this->assertLayoutSection('.layout--onecol', NULL, '', '', 'UNCACHEABLE');
+    // Ensure the block was not rendered.
+    $this->assertSession()->pageTextNotContains('Hello test world');
+
+    // Grant access to the block, and ensure it was rendered.
+    $this->container->get('state')->set('test_block_access', TRUE);
+    $this->drupalGet('node/1');
+    $this->assertLayoutSection('.layout--onecol', 'Hello test world', '', '', 'UNCACHEABLE');
+  }
+
+  /**
+   * Tests the multilingual support of the section formatter.
+   */
+  public function testMultilingualLayoutSectionFormatter() {
+    $this->container->get('module_installer')->install(['content_translation']);
+    $this->rebuildContainer();
+
+    ConfigurableLanguage::createFromLangcode('es')->save();
+    $this->container->get('content_translation.manager')->setEnabled('node', 'bundle_with_section_field', TRUE);
+
+    $entity = $this->createSectionNode([
+      [
+        'layout' => 'layout_onecol',
+        'section' => [
+          'content' => [
+            'baz' => [
+              'id' => 'system_powered_by_block',
+            ],
+          ],
+        ],
+      ],
+    ]);
+    $entity->addTranslation('es', [
+      'title' => 'Translated node title',
+      $this->fieldName => [
+        [
+          'layout' => 'layout_twocol',
+          'section' => [
+            'first' => [
+              'foo' => [
+                'id' => 'test_block_instantiation',
+                'display_message' => 'foo text',
+              ],
+            ],
+            'second' => [
+              'bar' => [
+                'id' => 'test_block_instantiation',
+                'display_message' => 'bar text',
+              ],
+            ],
+          ],
+        ],
+      ],
+    ]);
+    $entity->save();
+
+    $this->drupalGet('node/1');
+    $this->assertLayoutSection('.layout--onecol', 'Powered by');
+    $this->drupalGet('es/node/1');
+    $this->assertLayoutSection('.layout--twocol', ['foo text', 'bar text']);
+  }
+
+  /**
+   * Ensures that the entity title is displayed.
+   */
+  public function testLayoutPageTitle() {
+    $this->drupalPlaceBlock('page_title_block');
+    $this->createSectionNode([]);
+
+    $this->drupalGet('node/1/layout');
+    $this->assertSession()->titleEquals('Edit layout for The node title | Drupal');
+    $this->assertEquals('Edit layout for The node title', $this->cssSelect('h1.page-title')[0]->getText());
+  }
+
+  /**
+   * Tests that no Layout link shows without a section field.
+   */
+  public function testLayoutUrlNoSectionField() {
+    $this->createNode([
+      'type' => 'bundle_without_section_field',
+      'title' => 'The node title',
+      'body' => [
+        [
+          'value' => 'The node body',
+        ],
+      ],
+    ]);
+    $this->drupalGet('node/1/layout');
+    $this->assertSession()->statusCodeEquals(403);
+  }
+
+  /**
+   * Asserts the output of a layout section.
+   *
+   * @param string|array $expected_selector
+   *   A selector or list of CSS selectors to find.
+   * @param string|array $expected_content
+   *   A string or list of strings to find.
+   * @param string $expected_cache_contexts
+   *   A string of cache contexts to be found in the header.
+   * @param string $expected_cache_tags
+   *   A string of cache tags to be found in the header.
+   * @param string $expected_dynamic_cache
+   *   The expected dynamic cache header. Either 'HIT', 'MISS' or 'UNCACHEABLE'.
+   */
+  protected function assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts = '', $expected_cache_tags = '', $expected_dynamic_cache = 'MISS') {
+    $assert_session = $this->assertSession();
+    // Find the given selector.
+    foreach ((array) $expected_selector as $selector) {
+      $element = $this->cssSelect($selector);
+      $this->assertNotEmpty($element);
+    }
+
+    // Find the given content.
+    foreach ((array) $expected_content as $content) {
+      $assert_session->pageTextContains($content);
+    }
+    if ($expected_cache_contexts) {
+      $assert_session->responseHeaderContains('X-Drupal-Cache-Contexts', $expected_cache_contexts);
+    }
+    if ($expected_cache_tags) {
+      $assert_session->responseHeaderContains('X-Drupal-Cache-Tags', $expected_cache_tags);
+    }
+    $assert_session->responseHeaderEquals('X-Drupal-Dynamic-Cache', $expected_dynamic_cache);
+  }
+
+  /**
+   * Creates a node with a section field.
+   *
+   * @param array $section_values
+   *   An array of values for a section field.
+   *
+   * @return \Drupal\node\NodeInterface
+   *   The node object.
+   */
+  protected function createSectionNode(array $section_values) {
+    return $this->createNode([
+      'type' => 'bundle_with_section_field',
+      'title' => 'The node title',
+      'body' => [
+        [
+          'value' => 'The node body',
+        ],
+      ],
+      $this->fieldName => $section_values,
+    ]);
+  }
+
+}
diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderTest.php
new file mode 100644
index 0000000..7da0494
--- /dev/null
+++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderTest.php
@@ -0,0 +1,255 @@
+<?php
+
+namespace Drupal\Tests\layout_builder\FunctionalJavascript;
+
+use Drupal\block_content\Entity\BlockContent;
+use Drupal\block_content\Entity\BlockContentType;
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
+
+/**
+ * Tests the Layout Builder UI.
+ *
+ * @group layout_builder
+ */
+class LayoutBuilderTest extends JavascriptTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'layout_builder',
+    'node',
+    'block_content',
+    'field_ui',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->drupalPlaceBlock('local_tasks_block');
+
+    $bundle = BlockContentType::create([
+      'id' => 'basic',
+      'label' => 'Basic',
+    ]);
+    $bundle->save();
+    block_content_add_body_field($bundle->id());
+    BlockContent::create([
+      'info' => 'My custom block',
+      'type' => 'basic',
+      'body' => [
+        [
+          'value' => 'This is the block content',
+          'format' => filter_default_format(),
+        ],
+      ],
+    ])->save();
+
+    $this->createContentType(['type' => 'bundle_with_section_field']);
+    $this->createNode([
+      'type' => 'bundle_with_section_field',
+      'title' => 'The node title',
+      'body' => [
+        [
+          'value' => 'The node body',
+        ],
+      ],
+    ]);
+
+    $this->drupalLogin($this->drupalCreateUser([
+      'access contextual links',
+      'configure any layout',
+      'administer node display',
+    ], 'foobar'));
+  }
+
+  /**
+   * Tests the Layout Builder UI.
+   *
+   * @todo:
+   *   Add tests for revision support.
+   */
+  public function test() {
+    $assert_session = $this->assertSession();
+    $page = $this->getSession()->getPage();
+
+    // Enable layout support.
+    $this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display');
+    $page->checkField('layout[allow_custom]');
+    $page->pressButton('Save');
+
+    // Ensure the block is not displayed initially.
+    $this->drupalGet('node/1');
+    $assert_session->pageTextNotContains('Powered by Drupal');
+
+    // Enter the layout editing mode.
+    $this->clickLink('Layout');
+    $assert_session->linkExists('Add Section');
+    $assert_session->linkNotExists('Add Block');
+
+    // Add a new section.
+    $this->clickLink('Add Section');
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->elementExists('css', '#drupal-off-canvas');
+
+    $this->clickLink('One column');
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->elementNotExists('css', '#drupal-off-canvas');
+
+    $assert_session->linkExists('Add Section');
+    $assert_session->linkExists('Add Block');
+
+    // Add a new block.
+    $this->clickLink('Add Block');
+    $assert_session->assertWaitOnAjaxRequest();
+
+    $assert_session->elementExists('css', '#drupal-off-canvas');
+
+    $this->clickLink('Powered by Drupal');
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->elementExists('css', '#drupal-off-canvas');
+
+    $page->fillField('settings[label]', 'This is the label');
+    $page->checkField('settings[label_display]');
+
+    // Save the new block, and ensure it is displayed on the page.
+    $page->pressButton('Add Block');
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->elementNotExists('css', '#drupal-off-canvas');
+
+    $assert_session->addressEquals('node/1/layout');
+    $assert_session->pageTextContains('Powered by Drupal');
+    $assert_session->pageTextContains('This is the label');
+
+    // Until the layout is saved, the new block is not visible on the node page.
+    $this->drupalGet('node/1');
+    $assert_session->pageTextNotContains('Powered by Drupal');
+
+    // When returning to the layout edit mode, the new block is visible.
+    $this->drupalGet('node/1/layout');
+    $assert_session->pageTextContains('Powered by Drupal');
+
+    // Save the layout, and the new block is visible.
+    $this->clickLink('Save Layout');
+    $assert_session->addressEquals('node/1');
+    $assert_session->pageTextContains('Powered by Drupal');
+    $assert_session->pageTextContains('This is the label');
+    $assert_session->elementExists('css', '.layout');
+
+    // Drag one block from one region to another.
+    $this->drupalGet('node/1/layout');
+    $this->clickLink('Add Section');
+    $assert_session->assertWaitOnAjaxRequest();
+
+    $this->clickLink('Two column');
+    $assert_session->assertWaitOnAjaxRequest();
+
+    $assert_session->elementNotExists('css', '.layout__region--second .block-system-powered-by-block');
+    $assert_session->elementTextNotContains('css', '.layout__region--second', 'Powered by Drupal');
+    // Drag the block from one layout to another.
+    $page->find('css', '.layout__region--content .block-system-powered-by-block')->dragTo($page->find('css', '.layout__region--second'));
+    $assert_session->assertWaitOnAjaxRequest();
+    // Ensure the drag succeeded.
+    $assert_session->elementExists('css', '.layout__region--second .block-system-powered-by-block');
+    $assert_session->elementTextContains('css', '.layout__region--second', 'Powered by Drupal');
+    // Ensure the drag persisted after reload.
+    $this->drupalGet('node/1/layout');
+    $assert_session->elementExists('css', '.layout__region--second .block-system-powered-by-block');
+    $assert_session->elementTextContains('css', '.layout__region--second', 'Powered by Drupal');
+    // Ensure the drag persisted after save.
+    $this->clickLink('Save Layout');
+    $assert_session->elementExists('css', '.layout__region--second .block-system-powered-by-block');
+    $assert_session->elementTextContains('css', '.layout__region--second', 'Powered by Drupal');
+
+    // Configure a block.
+    $this->drupalGet('node/1/layout');
+    $assert_session->assertWaitOnAjaxRequest();
+    $this->toggleContextualTriggerVisibility('.block-system-powered-by-block');
+    $assert_session->assertWaitOnAjaxRequest();
+    $page->find('css', '.block-system-powered-by-block .contextual .trigger')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+
+    $this->clickLink('Configure');
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->elementExists('css', '#drupal-off-canvas');
+
+    $page->fillField('settings[label]', 'This is the new label');
+    $page->pressButton('Update');
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->elementNotExists('css', '#drupal-off-canvas');
+
+    $assert_session->addressEquals('node/1/layout');
+    $assert_session->pageTextContains('Powered by Drupal');
+    $assert_session->pageTextContains('This is the new label');
+    $assert_session->pageTextNotContains('This is the label');
+
+    // Remove a block.
+    $this->drupalGet('node/1/layout');
+
+    $this->toggleContextualTriggerVisibility('.block-system-powered-by-block');
+    $page->find('css', '.block-system-powered-by-block .contextual .trigger')->click();
+    $this->clickLink('Remove block');
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->elementExists('css', '#drupal-off-canvas');
+
+    $page->pressButton('Remove');
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->elementNotExists('css', '#drupal-off-canvas');
+
+    $assert_session->pageTextNotContains('Powered by Drupal');
+    $assert_session->linkExists('Add Block');
+    $assert_session->addressEquals('node/1/layout');
+
+    $this->clickLink('Save Layout');
+    $assert_session->elementExists('css', '.layout');
+
+    // Test deriver-based blocks.
+    $this->drupalGet('node/1/layout');
+    $this->clickLink('Add Block');
+    $assert_session->assertWaitOnAjaxRequest();
+
+    $this->clickLink('My custom block');
+    $assert_session->assertWaitOnAjaxRequest();
+
+    $page->pressButton('Add Block');
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->pageTextContains('This is the block content');
+
+    // Remove both sections.
+    $this->clickLink('Remove section');
+    $assert_session->assertWaitOnAjaxRequest();
+
+    $page->pressButton('Remove');
+    $assert_session->assertWaitOnAjaxRequest();
+
+    $this->clickLink('Remove section');
+    $assert_session->assertWaitOnAjaxRequest();
+
+    $page->pressButton('Remove');
+    $assert_session->assertWaitOnAjaxRequest();
+
+    $assert_session->pageTextNotContains('This is the block content');
+    $assert_session->linkNotExists('Add Block');
+    $this->clickLink('Save Layout');
+    $assert_session->elementNotExists('css', '.layout');
+  }
+
+  /**
+   * Toggles the visibility of a contextual trigger.
+   *
+   * @todo Remove this function when related trait added in
+   *   https://www.drupal.org/node/2821724.
+   *
+   * @param string $selector
+   *   The selector for the element that contains the contextual link.
+   */
+  protected function toggleContextualTriggerVisibility($selector) {
+    // Hovering over the element itself with should be enough, but does not
+    // work. Manually remove the visually-hidden class.
+    $this->getSession()->executeScript("jQuery('{$selector} .contextual .trigger').toggleClass('visually-hidden');");
+  }
+
+}
diff --git a/core/modules/layout_builder/tests/src/Unit/LayoutSectionBuilderTest.php b/core/modules/layout_builder/tests/src/Unit/LayoutSectionBuilderTest.php
new file mode 100644
index 0000000..3908e6d
--- /dev/null
+++ b/core/modules/layout_builder/tests/src/Unit/LayoutSectionBuilderTest.php
@@ -0,0 +1,260 @@
+<?php
+
+namespace Drupal\Tests\layout_builder\Unit;
+
+use Drupal\Component\Plugin\Exception\PluginException;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Block\BlockManagerInterface;
+use Drupal\Core\Block\BlockPluginInterface;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Layout\LayoutInterface;
+use Drupal\Core\Layout\LayoutPluginManagerInterface;
+use Drupal\Core\Plugin\Context\ContextHandlerInterface;
+use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
+use Drupal\Core\Plugin\ContextAwarePluginInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\layout_builder\LayoutSectionBuilder;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\layout_builder\LayoutSectionBuilder
+ * @group layout_builder
+ */
+class LayoutSectionBuilderTest extends UnitTestCase {
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $account;
+
+  /**
+   * The layout plugin manager.
+   *
+   * @var \Drupal\Core\Layout\LayoutPluginManagerInterface
+   */
+  protected $layoutPluginManager;
+
+  /**
+   * The block plugin manager.
+   *
+   * @var \Drupal\Core\Block\BlockManagerInterface
+   */
+  protected $blockManager;
+
+  /**
+   * The plugin context handler.
+   *
+   * @var \Drupal\Core\Plugin\Context\ContextHandlerInterface
+   */
+  protected $contextHandler;
+
+  /**
+   * The context manager service.
+   *
+   * @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface
+   */
+  protected $contextRepository;
+
+  /**
+   * The object under test.
+   *
+   * @var \Drupal\layout_builder\LayoutSectionBuilder
+   */
+  protected $layoutSectionBuilder;
+
+  /**
+   * The layout plugin.
+   *
+   * @var \Drupal\Core\Layout\LayoutInterface
+   */
+  protected $layout;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->account = $this->prophesize(AccountInterface::class);
+    $this->layoutPluginManager = $this->prophesize(LayoutPluginManagerInterface::class);
+    $this->blockManager = $this->prophesize(BlockManagerInterface::class);
+    $this->contextHandler = $this->prophesize(ContextHandlerInterface::class);
+    $this->contextRepository = $this->prophesize(ContextRepositoryInterface::class);
+    $this->layoutSectionBuilder = new LayoutSectionBuilder($this->account->reveal(), $this->layoutPluginManager->reveal(), $this->blockManager->reveal(), $this->contextHandler->reveal(), $this->contextRepository->reveal());
+
+    $this->layout = $this->prophesize(LayoutInterface::class);
+    $this->layoutPluginManager->createInstance('layout_onecol')->willReturn($this->layout->reveal());
+  }
+
+  /**
+   * @covers ::buildSection
+   */
+  public function testBuildSection() {
+    $block_content = ['#markup' => 'The block content.'];
+    $render_array = [
+      '#theme' => 'block',
+      '#weight' => 0,
+      '#configuration' => [],
+      '#plugin_id' => 'block_plugin_id',
+      '#base_plugin_id' => 'block_plugin_id',
+      '#derivative_plugin_id' => NULL,
+      'content' => $block_content,
+    ];
+    $this->layout->build(['content' => ['some_uuid' => $render_array]])->willReturnArgument(0);
+
+    $block = $this->prophesize(BlockPluginInterface::class);
+    $this->blockManager->createInstance('block_plugin_id', ['id' => 'block_plugin_id'])->willReturn($block->reveal());
+
+    $access_result = AccessResult::allowed();
+    $block->access($this->account->reveal(), TRUE)->willReturn($access_result);
+    $block->build()->willReturn($block_content);
+    $block->getCacheContexts()->willReturn([]);
+    $block->getCacheTags()->willReturn([]);
+    $block->getCacheMaxAge()->willReturn(Cache::PERMANENT);
+    $block->getPluginId()->willReturn('block_plugin_id');
+    $block->getBaseId()->willReturn('block_plugin_id');
+    $block->getDerivativeId()->willReturn(NULL);
+    $block->getConfiguration()->willReturn([]);
+
+    $section = [
+      'content' => [
+        'some_uuid' => [
+          'id' => 'block_plugin_id',
+        ],
+      ],
+    ];
+    $expected = [
+      '#cache' => [
+        'contexts' => [],
+        'tags' => [],
+        'max-age' => -1,
+      ],
+      'content' => [
+        'some_uuid' => $render_array,
+      ],
+    ];
+    $result = $this->layoutSectionBuilder->buildSection('layout_onecol', $section);
+    $this->assertEquals($expected, $result);
+  }
+
+  /**
+   * @covers ::buildSection
+   */
+  public function testBuildSectionAccessDenied() {
+    $this->layout->build([])->willReturn([]);
+
+    $block = $this->prophesize(BlockPluginInterface::class);
+    $this->blockManager->createInstance('block_plugin_id', ['id' => 'block_plugin_id'])->willReturn($block->reveal());
+
+    $access_result = AccessResult::forbidden();
+    $block->access($this->account->reveal(), TRUE)->willReturn($access_result);
+    $block->build()->shouldNotBeCalled();
+
+    $section = [
+      'content' => [
+        'some_uuid' => [
+          'id' => 'block_plugin_id',
+        ],
+      ],
+    ];
+    $expected = [
+      '#cache' => [
+        'contexts' => [],
+        'tags' => [],
+        'max-age' => -1,
+      ],
+    ];
+    $result = $this->layoutSectionBuilder->buildSection('layout_onecol', $section);
+    $this->assertEquals($expected, $result);
+  }
+
+  /**
+   * @covers ::buildSection
+   */
+  public function testBuildSectionEmpty() {
+    $this->layout->build([])->willReturn([]);
+
+    $section = [];
+    $expected = [
+      '#cache' => [
+        'contexts' => [],
+        'tags' => [],
+        'max-age' => -1,
+      ],
+    ];
+    $result = $this->layoutSectionBuilder->buildSection('layout_onecol', $section);
+    $this->assertEquals($expected, $result);
+  }
+
+  /**
+   * @covers ::buildSection
+   * @covers ::getBlock
+   */
+  public function testContextAwareBlock() {
+    $render_array = [
+      '#theme' => 'block',
+      '#weight' => 0,
+      '#configuration' => [],
+      '#plugin_id' => 'block_plugin_id',
+      '#base_plugin_id' => 'block_plugin_id',
+      '#derivative_plugin_id' => NULL,
+      'content' => [],
+    ];
+    $this->layout->build(['content' => ['some_uuid' => $render_array]])->willReturnArgument(0);
+
+    $block = $this->prophesize(BlockPluginInterface::class)->willImplement(ContextAwarePluginInterface::class);
+    $this->blockManager->createInstance('block_plugin_id', ['id' => 'block_plugin_id'])->willReturn($block->reveal());
+
+    $access_result = AccessResult::allowed();
+    $block->access($this->account->reveal(), TRUE)->willReturn($access_result);
+    $block->build()->willReturn([]);
+    $block->getCacheContexts()->willReturn([]);
+    $block->getCacheTags()->willReturn([]);
+    $block->getCacheMaxAge()->willReturn(Cache::PERMANENT);
+    $block->getContextMapping()->willReturn([]);
+    $block->getPluginId()->willReturn('block_plugin_id');
+    $block->getBaseId()->willReturn('block_plugin_id');
+    $block->getDerivativeId()->willReturn(NULL);
+    $block->getConfiguration()->willReturn([]);
+
+    $this->contextRepository->getRuntimeContexts([])->willReturn([]);
+    $this->contextHandler->applyContextMapping($block->reveal(), [])->shouldBeCalled();
+
+    $section = [
+      'content' => [
+        'some_uuid' => [
+          'id' => 'block_plugin_id',
+        ],
+      ],
+    ];
+    $expected = [
+      '#cache' => [
+        'contexts' => [],
+        'tags' => [],
+        'max-age' => -1,
+      ],
+      'content' => [
+        'some_uuid' => $render_array,
+      ],
+    ];
+    $result = $this->layoutSectionBuilder->buildSection('layout_onecol', $section);
+    $this->assertEquals($expected, $result);
+  }
+
+  /**
+   * @covers ::buildSection
+   * @covers ::getBlock
+   */
+  public function testBuildSectionMissingPluginId() {
+    $section = [
+      'content' => [
+        'some_uuid' => [],
+      ],
+    ];
+    $this->setExpectedException(PluginException::class, 'No plugin ID specified for block with "some_uuid" UUID');
+    $this->layoutSectionBuilder->buildSection('layout_onecol', $section);
+  }
+
+}
diff --git a/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreRepositoryTest.php b/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreRepositoryTest.php
new file mode 100644
index 0000000..919b02c
--- /dev/null
+++ b/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreRepositoryTest.php
@@ -0,0 +1,110 @@
+<?php
+
+namespace Drupal\Tests\layout_builder\Unit;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\RevisionableInterface;
+use Drupal\Core\Language\Language;
+use Drupal\layout_builder\LayoutTempstoreRepository;
+use Drupal\Tests\UnitTestCase;
+use Drupal\user\SharedTempStore;
+use Drupal\user\SharedTempStoreFactory;
+
+/**
+ * @coversDefaultClass \Drupal\layout_builder\LayoutTempstoreRepository
+ * @group layout_builder
+ */
+class LayoutTempstoreRepositoryTest extends UnitTestCase {
+
+  /**
+   * @covers ::getFromId
+   * @covers ::get
+   * @covers ::generateTempstoreId
+   */
+  public function testGetFromIdEmptyTempstore() {
+    $tempstore = $this->prophesize(SharedTempStore::class);
+    $tempstore->get('the_entity_id.en')->shouldBeCalled();
+
+    $tempstore_factory = $this->prophesize(SharedTempStoreFactory::class);
+    $tempstore_factory->get('the_entity_type_id.layout_builder__layout')->willReturn($tempstore->reveal());
+
+    $entity = $this->prophesize(EntityInterface::class);
+    $entity->getEntityTypeId()->willReturn('the_entity_type_id');
+    $entity->id()->willReturn('the_entity_id');
+    $entity->language()->willReturn(new Language(['id' => 'en']));
+
+    $entity_storage = $this->prophesize(EntityStorageInterface::class);
+    $entity_storage->loadRevision('the_entity_id')->willReturn($entity->reveal());
+
+    $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class);
+    $entity_type_manager->getStorage('the_entity_type_id')->willReturn($entity_storage->reveal());
+
+    $repository = new LayoutTempstoreRepository($tempstore_factory->reveal(), $entity_type_manager->reveal());
+
+    $result = $repository->getFromId('the_entity_type_id', 'the_entity_id');
+    $this->assertSame($entity->reveal(), $result);
+  }
+
+  /**
+   * @covers ::getFromId
+   * @covers ::get
+   * @covers ::generateTempstoreId
+   */
+  public function testGetFromIdLoadedTempstore() {
+    $tempstore_entity = $this->prophesize(EntityInterface::class);
+    $tempstore = $this->prophesize(SharedTempStore::class);
+    $tempstore->get('the_entity_id.en')->willReturn(['entity' => $tempstore_entity->reveal()]);
+    $tempstore_factory = $this->prophesize(SharedTempStoreFactory::class);
+    $tempstore_factory->get('the_entity_type_id.layout_builder__layout')->willReturn($tempstore->reveal());
+
+    $entity = $this->prophesize(EntityInterface::class);
+    $entity->getEntityTypeId()->willReturn('the_entity_type_id');
+    $entity->id()->willReturn('the_entity_id');
+    $entity->language()->willReturn(new Language(['id' => 'en']));
+
+    $entity_storage = $this->prophesize(EntityStorageInterface::class);
+    $entity_storage->loadRevision('the_entity_id')->willReturn($entity->reveal());
+
+    $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class);
+    $entity_type_manager->getStorage('the_entity_type_id')->willReturn($entity_storage->reveal());
+
+    $repository = new LayoutTempstoreRepository($tempstore_factory->reveal(), $entity_type_manager->reveal());
+
+    $result = $repository->getFromId('the_entity_type_id', 'the_entity_id');
+    $this->assertSame($tempstore_entity->reveal(), $result);
+    $this->assertNotSame($entity->reveal(), $result);
+  }
+
+  /**
+   * @covers ::getFromId
+   * @covers ::get
+   * @covers ::generateTempstoreId
+   */
+  public function testGetFromIdRevisionable() {
+    $tempstore = $this->prophesize(SharedTempStore::class);
+    $tempstore->get('the_entity_id.en.the_revision_id')->shouldBeCalled();
+
+    $tempstore_factory = $this->prophesize(SharedTempStoreFactory::class);
+    $tempstore_factory->get('the_entity_type_id.layout_builder__layout')->willReturn($tempstore->reveal());
+
+    $entity = $this->prophesize(EntityInterface::class)->willImplement(RevisionableInterface::class);
+    $entity->getEntityTypeId()->willReturn('the_entity_type_id');
+    $entity->id()->willReturn('the_entity_id');
+    $entity->language()->willReturn(new Language(['id' => 'en']));
+    $entity->getRevisionId()->willReturn('the_revision_id');
+
+    $entity_storage = $this->prophesize(EntityStorageInterface::class);
+    $entity_storage->loadRevision('the_entity_id')->willReturn($entity->reveal());
+
+    $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class);
+    $entity_type_manager->getStorage('the_entity_type_id')->willReturn($entity_storage->reveal());
+
+    $repository = new LayoutTempstoreRepository($tempstore_factory->reveal(), $entity_type_manager->reveal());
+
+    $result = $repository->getFromId('the_entity_type_id', 'the_entity_id');
+    $this->assertSame($entity->reveal(), $result);
+  }
+
+}
