diff --git a/core/composer.json b/core/composer.json
index 48e66198804679502702aeaecdf84408e2676fbd..31bc51a0fa84d9acb958b84a220911377098cb59 100644
--- a/core/composer.json
+++ b/core/composer.json
@@ -112,6 +112,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/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 0000000000000000000000000000000000000000..b870007e33588738af1959f0fca25e93debf0ad6
--- /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 0000000000000000000000000000000000000000..11d8c5508f739650187e02617ccf8af9250b54d5
--- /dev/null
+++ b/core/modules/layout_builder/css/layout-builder.css
@@ -0,0 +1,51 @@
+.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-builder--layout__region {
+  outline: 2px dashed #2f91da;
+  padding: 1.5em 0;
+}
+
+.layout-section .layout-builder--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 .layout-selection li {
+  display: block;
+  padding-bottom: 1em;
+}
+
+#drupal-off-canvas .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 0000000000000000000000000000000000000000..391e462cc677542be9d0258cee1fced3abd9558c
--- /dev/null
+++ b/core/modules/layout_builder/js/layout-builder.es6.js
@@ -0,0 +1,37 @@
+(($, { ajax, behaviors }) => {
+  behaviors.layoutBuilder = {
+    attach(context) {
+      $(context).find('.layout-builder--layout__region').sortable({
+        items: '> .draggable',
+        connectWith: '.layout-builder--layout__region',
+
+        /**
+         * Updates the layout with the new position of the block.
+         *
+         * @param {jQuery.Event} event
+         *   The jQuery Event object.
+         * @param {Object} ui
+         *   An object containing information about the item being sorted.
+         */
+        update(event, ui) {
+          // Only process if the item was moved from one region to another.
+          if (ui.sender) {
+            ajax({
+              url: [
+                ui.item.closest('[data-layout-update-url]').data('layout-update-url'),
+                ui.sender.closest('[data-layout-delta]').data('layout-delta'),
+                ui.item.closest('[data-layout-delta]').data('layout-delta'),
+                ui.sender.data('region'),
+                $(this).data('region'),
+                ui.item.data('layout-block-uuid'),
+                ui.item.prev('[data-layout-block-uuid]').data('layout-block-uuid'),
+              ]
+              .filter(element => element !== undefined)
+              .join('/'),
+            }).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 0000000000000000000000000000000000000000..d4dadc89344ca9c9e125db695444ca65a7314df1
--- /dev/null
+++ b/core/modules/layout_builder/js/layout-builder.js
@@ -0,0 +1,30 @@
+/**
+* DO NOT EDIT THIS FILE.
+* See the following change record for more information,
+* https://www.drupal.org/node/2815083
+* @preserve
+**/
+
+(function ($, _ref) {
+  var ajax = _ref.ajax,
+      behaviors = _ref.behaviors;
+
+  behaviors.layoutBuilder = {
+    attach: function attach(context) {
+      $(context).find('.layout-builder--layout__region').sortable({
+        items: '> .draggable',
+        connectWith: '.layout-builder--layout__region',
+
+        update: function update(event, ui) {
+          if (ui.sender) {
+            ajax({
+              url: [ui.item.closest('[data-layout-update-url]').data('layout-update-url'), ui.sender.closest('[data-layout-delta]').data('layout-delta'), ui.item.closest('[data-layout-delta]').data('layout-delta'), ui.sender.data('region'), $(this).data('region'), ui.item.data('layout-block-uuid'), ui.item.prev('[data-layout-block-uuid]').data('layout-block-uuid')].filter(function (element) {
+                return element !== undefined;
+              }).join('/')
+            }).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 0000000000000000000000000000000000000000..e9859114643d4d4e361dacfc53f75ab35cb983da
--- /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
+  - contextual
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 0000000000000000000000000000000000000000..84727756365b2ee349ece0872bd059cb6fc7b1f3
--- /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
+    - core/drupal.dialog.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 0000000000000000000000000000000000000000..bcf2a9cf06938805d5f60574af1376cda30698a3
--- /dev/null
+++ b/core/modules/layout_builder/layout_builder.links.contextual.yml
@@ -0,0 +1,19 @@
+layout_builder_block_update:
+  title: 'Configure'
+  route_name: 'layout_builder.update_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 0000000000000000000000000000000000000000..b003d7737c83a2ed4513d269d32a9a76fbf3207c
--- /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 0000000000000000000000000000000000000000..2192168cb0cdfd93a99efdf1b329c3a3cb94fe9c
--- /dev/null
+++ b/core/modules/layout_builder/layout_builder.module
@@ -0,0 +1,200 @@
+<?php
+
+/**
+ * @file
+ * Provides hook implementations for Layout Builder.
+ */
+
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
+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.') . '</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_entity_type_alter().
+ */
+function layout_builder_entity_type_alter(array &$entity_types) {
+  /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
+  foreach ($entity_types as $entity_type) {
+    if ($entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasLinkTemplate('canonical') && $entity_type->hasViewBuilderClass()) {
+      $entity_type->setLinkTemplate('layout-builder', $entity_type->getLinkTemplate('canonical') . '/layout');
+    }
+  }
+}
+
+/**
+ * Removes the Layout Builder field both visually and from the #fields handling.
+ *
+ * This prevents any interaction with this field. It is rendered directly
+ * in layout_builder_entity_view_display_alter().
+ *
+ * @internal
+ */
+function _layout_builder_hide_layout_field(array &$form) {
+  unset($form['fields']['layout_builder__layout']);
+  $key = array_search('layout_builder__layout', $form['#fields']);
+  if ($key !== FALSE) {
+    unset($form['#fields'][$key]);
+  }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter() for \Drupal\field_ui\Form\EntityFormDisplayEditForm.
+ */
+function layout_builder_form_entity_form_display_edit_form_alter(&$form, FormStateInterface $form_state) {
+  _layout_builder_hide_layout_field($form);
+}
+
+/**
+ * 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());
+
+  _layout_builder_hide_layout_field($form);
+
+  // @todo Expand to work for all view modes in
+  //   https://www.drupal.org/node/2907413.
+  if (!in_array($display->getMode(), ['full', 'default'], TRUE)) {
+    return;
+  }
+
+  $form['layout'] = [
+    '#type' => 'details',
+    '#open' => TRUE,
+    '#title' => t('Layout options'),
+    '#tree' => TRUE,
+  ];
+  // @todo Unchecking this box is a destructive action, this should be made
+  //   clear to the user in https://www.drupal.org/node/2914484.
+  $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),
+  ];
+
+  $form['#entity_builders'][] = 'layout_builder_form_entity_view_display_edit_entity_builder';
+}
+
+/**
+ * Entity builder 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_entity_builder($entity_type_id, EntityViewDisplayInterface $display, &$form, FormStateInterface &$form_state) {
+  $new_value = (bool) $form_state->getValue(['layout', 'allow_custom'], FALSE);
+  $display->setThirdPartySetting('layout_builder', 'allow_custom', $new_value);
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_presave().
+ */
+function layout_builder_entity_view_display_presave(EntityViewDisplayInterface $display) {
+  $original_value = isset($display->original) ? $display->original->getThirdPartySetting('layout_builder', 'allow_custom', FALSE) : FALSE;
+  $new_value = $display->getThirdPartySetting('layout_builder', 'allow_custom', FALSE);
+  if ($original_value !== $new_value) {
+    $entity_type_id = $display->getTargetEntityTypeId();
+    $bundle = $display->getTargetBundle();
+
+    if ($new_value) {
+      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) {
+  if ($display->getThirdPartySetting('layout_builder', 'allow_custom', FALSE)) {
+    // Force the layout to render with no label.
+    $display->setComponent('layout_builder__layout', [
+      'label' => 'hidden',
+      'region' => '__layout_builder',
+    ]);
+  }
+  else {
+    $display->removeComponent('layout_builder__layout');
+  }
+}
+
+/**
+ * Implements hook_entity_view_alter().
+ */
+function layout_builder_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
+  if ($display->getThirdPartySetting('layout_builder', 'allow_custom', FALSE) && !$entity->layout_builder__layout->isEmpty()) {
+    // If field layout is active, that is all that needs to be removed.
+    if (\Drupal::moduleHandler()->moduleExists('field_layout') && isset($build['_field_layout'])) {
+      unset($build['_field_layout']);
+      return;
+    }
+
+    /** @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 ($name !== 'layout_builder__layout' && isset($field_definitions[$name]) && $field_definitions[$name]->isDisplayConfigurable('view')) {
+        unset($build[$name]);
+      }
+    }
+  }
+}
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 0000000000000000000000000000000000000000..00aac639244f9d6b2f219c0f3bcc8f98d1374e43
--- /dev/null
+++ b/core/modules/layout_builder/layout_builder.permissions.yml
@@ -0,0 +1,5 @@
+# @todo Expand permissions to be more granular in
+#   https://www.drupal.org/node/2914486.
+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 0000000000000000000000000000000000000000..8fe952afbead7ea9c65d4095331d3a7d9eb887d2
--- /dev/null
+++ b/core/modules/layout_builder/layout_builder.routing.yml
@@ -0,0 +1,129 @@
+layout_builder.choose_section:
+  path: '/layout_builder/choose/section/{entity_type_id}/{entity}/{delta}'
+  defaults:
+   _controller: '\Drupal\layout_builder\Controller\ChooseSectionController::build'
+  requirements:
+    _permission: 'configure any layout'
+  options:
+    _admin_route: TRUE
+    parameters:
+      entity:
+        type: entity:{entity_type_id}
+        layout_builder_tempstore: TRUE
+
+layout_builder.add_section:
+  path: '/layout_builder/add/section/{entity_type_id}/{entity}/{delta}/{plugin_id}'
+  defaults:
+    _controller: '\Drupal\layout_builder\Controller\AddSectionController::build'
+  requirements:
+    _permission: 'configure any layout'
+  options:
+    _admin_route: TRUE
+    parameters:
+      entity:
+        type: entity:{entity_type_id}
+        layout_builder_tempstore: TRUE
+
+layout_builder.configure_section:
+  path: '/layout_builder/configure/section/{entity_type_id}/{entity}/{delta}/{plugin_id}'
+  defaults:
+    _title: 'Configure section'
+    _form: '\Drupal\layout_builder\Form\ConfigureSectionForm'
+    # Adding a new section requires a plugin_id, while configuring an existing
+    # section does not.
+    plugin_id: null
+  requirements:
+    _permission: 'configure any layout'
+  options:
+    _admin_route: TRUE
+    parameters:
+      entity:
+        type: entity:{entity_type_id}
+        layout_builder_tempstore: TRUE
+
+layout_builder.remove_section:
+  path: '/layout_builder/remove/section/{entity_type_id}/{entity}/{delta}'
+  defaults:
+    _form: '\Drupal\layout_builder\Form\RemoveSectionForm'
+  requirements:
+    _permission: 'configure any layout'
+  options:
+    _admin_route: TRUE
+    parameters:
+      entity:
+        type: entity:{entity_type_id}
+        layout_builder_tempstore: TRUE
+
+layout_builder.choose_block:
+  path: '/layout_builder/choose/block/{entity_type_id}/{entity}/{delta}/{region}'
+  defaults:
+    _controller: '\Drupal\layout_builder\Controller\ChooseBlockController::build'
+  requirements:
+    _permission: 'configure any layout'
+  options:
+    _admin_route: TRUE
+    parameters:
+      entity:
+        type: entity:{entity_type_id}
+        layout_builder_tempstore: TRUE
+
+layout_builder.add_block:
+  path: '/layout_builder/add/block/{entity_type_id}/{entity}/{delta}/{region}/{plugin_id}'
+  defaults:
+    _form: '\Drupal\layout_builder\Form\AddBlockForm'
+  requirements:
+    _permission: 'configure any layout'
+  options:
+    _admin_route: TRUE
+    parameters:
+      entity:
+        type: entity:{entity_type_id}
+        layout_builder_tempstore: TRUE
+
+layout_builder.update_block:
+  path: '/layout_builder/update/block/{entity_type_id}/{entity}/{delta}/{region}/{uuid}'
+  defaults:
+    _form: '\Drupal\layout_builder\Form\UpdateBlockForm'
+  requirements:
+    _permission: 'configure any layout'
+  options:
+    _admin_route: TRUE
+    parameters:
+      entity:
+        type: entity:{entity_type_id}
+        layout_builder_tempstore: TRUE
+
+layout_builder.remove_block:
+  path: '/layout_builder/remove/block/{entity_type_id}/{entity}/{delta}/{region}/{uuid}'
+  defaults:
+    _form: '\Drupal\layout_builder\Form\RemoveBlockForm'
+  requirements:
+    _permission: 'configure any layout'
+  options:
+    _admin_route: TRUE
+    parameters:
+      entity:
+        type: entity:{entity_type_id}
+        layout_builder_tempstore: TRUE
+
+layout_builder.move_block:
+  path: '/layout_builder/move/block/{entity_type_id}/{entity}/{delta_from}/{delta_to}/{region_from}/{region_to}/{block_uuid}/{preceding_block_uuid}'
+  defaults:
+    _controller: '\Drupal\layout_builder\Controller\MoveBlockController::build'
+    delta_from: null
+    delta_to: null
+    region_from: null
+    region_to: null
+    block_uuid: null
+    preceding_block_uuid: null
+  requirements:
+    _permission: 'configure any layout'
+  options:
+    _admin_route: TRUE
+    parameters:
+      entity:
+        type: entity:{entity_type_id}
+        layout_builder_tempstore: 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 0000000000000000000000000000000000000000..518d9ee9421f272b364f0decf155b969b0c90116
--- /dev/null
+++ b/core/modules/layout_builder/layout_builder.services.yml
@@ -0,0 +1,30 @@
+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_section }
+  layout_builder.routes:
+    class: Drupal\layout_builder\Routing\LayoutBuilderRoutes
+    arguments: ['@entity_type.manager', '@entity_field.manager']
+  layout_builder.route_enhancer:
+    class: Drupal\layout_builder\Routing\LayoutBuilderRouteEnhancer
+    arguments: ['@entity_type.manager']
+    tags:
+      - { name: route_enhancer }
+  layout_builder.param_converter:
+    class: Drupal\layout_builder\Routing\LayoutTempstoreParamConverter
+    arguments: ['@entity.manager', '@layout_builder.tempstore_repository']
+    tags:
+      - { name: paramconverter, priority: 10 }
+  cache_context.layout_builder_is_active:
+    class: Drupal\layout_builder\Cache\LayoutBuilderIsActiveCacheContext
+    arguments: ['@current_route_match']
+    tags:
+      - { name: cache.context}
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 0000000000000000000000000000000000000000..e13a1492386a08af1d3424ed640299cd6634cecc
--- /dev/null
+++ b/core/modules/layout_builder/src/Access/LayoutSectionAccessCheck.php
@@ -0,0 +1,68 @@
+<?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.
+ *
+ * @internal
+ */
+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) {
+    // Attempt to retrieve the generic 'entity' parameter, otherwise look up the
+    // specific entity via the entity type ID.
+    $entity = $route_match->getParameter('entity') ?: $route_match->getParameter($route_match->getParameter('entity_type_id'));
+
+    // 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::allowedIfHasPermission($account, 'configure any layout');
+    }
+
+    return $access->addCacheableDependency($entity);
+  }
+
+}
diff --git a/core/modules/layout_builder/src/Cache/LayoutBuilderIsActiveCacheContext.php b/core/modules/layout_builder/src/Cache/LayoutBuilderIsActiveCacheContext.php
new file mode 100644
index 0000000000000000000000000000000000000000..c632f4b33a0d439afbefe092428de54facf09bef
--- /dev/null
+++ b/core/modules/layout_builder/src/Cache/LayoutBuilderIsActiveCacheContext.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace Drupal\layout_builder\Cache;
+
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Cache\Context\CalculatedCacheContextInterface;
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
+use Drupal\Core\Routing\RouteMatchInterface;
+
+/**
+ * Determines whether Layout Builder is active for a given entity type or not.
+ *
+ * Cache context ID: 'layout_builder_is_active:%entity_type_id', e.g.
+ * 'layout_builder_is_active:node' (to vary by whether the Node entity type has
+ * Layout Builder enabled).
+ */
+class LayoutBuilderIsActiveCacheContext implements CalculatedCacheContextInterface {
+
+  /**
+   * The current route match.
+   *
+   * @var \Drupal\Core\Routing\RouteMatchInterface
+   */
+  protected $routeMatch;
+
+  /**
+   * LayoutBuilderCacheContext constructor.
+   *
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The current route match.
+   */
+  public function __construct(RouteMatchInterface $route_match) {
+    $this->routeMatch = $route_match;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getLabel() {
+    return t('Layout Builder');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getContext($entity_type_id = NULL) {
+    if (!$entity_type_id) {
+      throw new \LogicException('Missing entity type ID');
+    }
+
+    $display = $this->getDisplay($entity_type_id);
+    return ($display && $display->getThirdPartySetting('layout_builder', 'allow_custom', FALSE)) ? '1' : '0';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheableMetadata($entity_type_id = NULL) {
+    if (!$entity_type_id) {
+      throw new \LogicException('Missing entity type ID');
+    }
+
+    $cacheable_metadata = new CacheableMetadata();
+    if ($display = $this->getDisplay($entity_type_id)) {
+      $cacheable_metadata->addCacheableDependency($display);
+    }
+    return $cacheable_metadata;
+  }
+
+  /**
+   * Returns the entity view display for a given entity type and view mode.
+   *
+   * @param string $entity_type_id
+   *   The entity type ID.
+   * @param string $view_mode
+   *   (optional) The view mode that should be used to render the entity.
+   *
+   * @return \Drupal\Core\Entity\Display\EntityViewDisplayInterface|null
+   *   The entity view display, if it exists.
+   */
+  protected function getDisplay($entity_type_id, $view_mode = 'full') {
+    if ($entity = $this->routeMatch->getParameter($entity_type_id)) {
+      return EntityViewDisplay::collectRenderDisplay($entity, $view_mode);
+    }
+  }
+
+}
diff --git a/core/modules/layout_builder/src/Controller/AddSectionController.php b/core/modules/layout_builder/src/Controller/AddSectionController.php
new file mode 100644
index 0000000000000000000000000000000000000000..d6771082382bfae78569af177548887e8fa7d28c
--- /dev/null
+++ b/core/modules/layout_builder/src/Controller/AddSectionController.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Drupal\layout_builder\Controller;
+
+use Drupal\Core\DependencyInjection\ClassResolverInterface;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+
+/**
+ * Defines a controller to add a new section.
+ *
+ * @internal
+ */
+class AddSectionController implements ContainerInjectionInterface {
+
+  use AjaxHelperTrait;
+  use LayoutRebuildTrait;
+
+  /**
+   * The layout tempstore repository.
+   *
+   * @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
+   */
+  protected $layoutTempstoreRepository;
+
+  /**
+   * AddSectionController constructor.
+   *
+   * @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
+   *   The layout tempstore repository.
+   * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
+   *   The class resolver.
+   */
+  public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, ClassResolverInterface $class_resolver) {
+    $this->layoutTempstoreRepository = $layout_tempstore_repository;
+    $this->classResolver = $class_resolver;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('layout_builder.tempstore_repository'),
+      $container->get('class_resolver')
+    );
+  }
+
+  /**
+   * Add the layout to the entity field in a tempstore.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity.
+   * @param int $delta
+   *   The delta of the section to splice.
+   * @param string $plugin_id
+   *   The plugin ID of the layout to add.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response
+   *   The controller response.
+   */
+  public function build(EntityInterface $entity, $delta, $plugin_id) {
+    /** @var \Drupal\layout_builder\Field\LayoutSectionItemListInterface $field_list */
+    $field_list = $entity->layout_builder__layout;
+    $field_list->addItem($delta, [
+      'layout' => $plugin_id,
+      'layout_settings' => [],
+      'section' => [],
+    ]);
+
+    $this->layoutTempstoreRepository->set($entity);
+
+    if ($this->isAjax()) {
+      return $this->rebuildAndClose($entity);
+    }
+    else {
+      $url = $entity->toUrl('layout-builder');
+      return new RedirectResponse($url->setAbsolute()->toString());
+    }
+  }
+
+}
diff --git a/core/modules/layout_builder/src/Controller/AjaxHelperTrait.php b/core/modules/layout_builder/src/Controller/AjaxHelperTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..072eccab352e43899101d59eac6f7dc1982f9382
--- /dev/null
+++ b/core/modules/layout_builder/src/Controller/AjaxHelperTrait.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Drupal\layout_builder\Controller;
+
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
+
+/**
+ * Provides a helper to determine if the current request is via AJAX.
+ *
+ * @internal
+ *
+ * @todo Move to \Drupal\Core in https://www.drupal.org/node/2896535.
+ */
+trait AjaxHelperTrait {
+
+  /**
+   * Determines if the current request is via AJAX.
+   *
+   * @return bool
+   *   TRUE if the current request is via AJAX, FALSE otherwise.
+   */
+  protected function isAjax() {
+    return in_array($this->getRequestWrapperFormat(), [
+      'drupal_ajax',
+      'drupal_dialog',
+      'drupal_dialog.off_canvas',
+      'drupal_modal',
+    ]);
+  }
+
+  /**
+   * Gets the wrapper format of the current request.
+   *
+   * @string
+   *   The wrapper format.
+   */
+  protected function getRequestWrapperFormat() {
+    return \Drupal::request()->get(MainContentViewSubscriber::WRAPPER_FORMAT);
+  }
+
+}
diff --git a/core/modules/layout_builder/src/Controller/ChooseBlockController.php b/core/modules/layout_builder/src/Controller/ChooseBlockController.php
new file mode 100644
index 0000000000000000000000000000000000000000..9eddb79fc34b8141c5c23f387b43a7065cc4e6d9
--- /dev/null
+++ b/core/modules/layout_builder/src/Controller/ChooseBlockController.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace Drupal\layout_builder\Controller;
+
+use Drupal\Core\Block\BlockManagerInterface;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Url;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Defines a controller to choose a new block.
+ *
+ * @internal
+ */
+class ChooseBlockController implements ContainerInjectionInterface {
+
+  use AjaxHelperTrait;
+
+  /**
+   * The block manager.
+   *
+   * @var \Drupal\Core\Block\BlockManagerInterface
+   */
+  protected $blockManager;
+
+  /**
+   * ChooseBlockController constructor.
+   *
+   * @param \Drupal\Core\Block\BlockManagerInterface $block_manager
+   *   The block manager.
+   */
+  public function __construct(BlockManagerInterface $block_manager) {
+    $this->blockManager = $block_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.block')
+    );
+  }
+
+  /**
+   * Provides the UI for choosing a new block.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity.
+   * @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 build(EntityInterface $entity, $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'] = [
+        '#theme' => 'links',
+      ];
+      foreach ($blocks as $block_id => $block) {
+        $link = [
+          'title' => $block['admin_label'],
+          'url' => Url::fromRoute('layout_builder.add_block',
+            [
+              'entity_type_id' => $entity->getEntityTypeId(),
+              'entity' => $entity->id(),
+              'delta' => $delta,
+              'region' => $region,
+              'plugin_id' => $block_id,
+            ]
+          ),
+        ];
+        if ($this->isAjax()) {
+          $link['attributes']['class'][] = 'use-ajax';
+          $link['attributes']['data-dialog-type'][] = 'dialog';
+          $link['attributes']['data-dialog-renderer'][] = 'off_canvas';
+        }
+        $build[$category]['links']['#links'][] = $link;
+      }
+    }
+    return $build;
+  }
+
+}
diff --git a/core/modules/layout_builder/src/Controller/ChooseSectionController.php b/core/modules/layout_builder/src/Controller/ChooseSectionController.php
new file mode 100644
index 0000000000000000000000000000000000000000..0414d2abf1d538d3e1068e7d8c99ad85e8d23042
--- /dev/null
+++ b/core/modules/layout_builder/src/Controller/ChooseSectionController.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace Drupal\layout_builder\Controller;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Layout\LayoutPluginManagerInterface;
+use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Url;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Defines a controller to choose a new section.
+ *
+ * @internal
+ */
+class ChooseSectionController implements ContainerInjectionInterface {
+
+  use AjaxHelperTrait;
+  use StringTranslationTrait;
+
+  /**
+   * The layout manager.
+   *
+   * @var \Drupal\Core\Layout\LayoutPluginManagerInterface
+   */
+  protected $layoutManager;
+
+  /**
+   * ChooseSectionController constructor.
+   *
+   * @param \Drupal\Core\Layout\LayoutPluginManagerInterface $layout_manager
+   *   The layout manager.
+   */
+  public function __construct(LayoutPluginManagerInterface $layout_manager) {
+    $this->layoutManager = $layout_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.core.layout')
+    );
+  }
+
+  /**
+   * Choose a layout plugin to add as a section.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity.
+   * @param int $delta
+   *   The delta of the section to splice.
+   *
+   * @return array
+   *   The render array.
+   */
+  public function build(EntityInterface $entity, $delta) {
+    $output['#title'] = $this->t('Choose a layout');
+
+    $items = [];
+    foreach ($this->layoutManager->getDefinitions() as $plugin_id => $definition) {
+      $layout = $this->layoutManager->createInstance($plugin_id);
+      $item = [
+        '#type' => 'link',
+        '#title' => [
+          $definition->getIcon(60, 80, 1, 3),
+          [
+            '#type' => 'container',
+            '#children' => $definition->getLabel(),
+          ],
+        ],
+        '#url' => Url::fromRoute(
+          $layout instanceof PluginFormInterface ? 'layout_builder.configure_section' : 'layout_builder.add_section',
+          [
+            'entity_type_id' => $entity->getEntityTypeId(),
+            'entity' => $entity->id(),
+            'delta' => $delta,
+            'plugin_id' => $plugin_id,
+          ]
+        ),
+      ];
+      if ($this->isAjax()) {
+        $item['#attributes']['class'][] = 'use-ajax';
+        $item['#attributes']['data-dialog-type'][] = 'dialog';
+        $item['#attributes']['data-dialog-renderer'][] = 'off_canvas';
+      }
+      $items[] = $item;
+    }
+    $output['layouts'] = [
+      '#theme' => 'item_list',
+      '#items' => $items,
+      '#attributes' => [
+        'class' => [
+          'layout-selection',
+        ],
+      ],
+    ];
+
+    return $output;
+  }
+
+}
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 0000000000000000000000000000000000000000..a4163a40a82151106eeca5b3abcf1fe08e79c44e
--- /dev/null
+++ b/core/modules/layout_builder/src/Controller/LayoutBuilderController.php
@@ -0,0 +1,319 @@
+<?php
+
+namespace Drupal\layout_builder\Controller;
+
+use Drupal\Core\Block\BlockManagerInterface;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Layout\LayoutPluginManagerInterface;
+use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Url;
+use Drupal\layout_builder\LayoutSectionBuilder;
+use Drupal\layout_builder\Field\LayoutSectionItemInterface;
+use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+
+/**
+ * Defines a controller to provide the Layout Builder admin UI.
+ *
+ * @internal
+ */
+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;
+
+  /**
+   * LayoutBuilderController 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\EntityInterface $entity
+   *   The entity.
+   *
+   * @return string
+   *   The title for the layout page.
+   */
+  public function title(EntityInterface $entity) {
+    return $this->t('Edit layout for %label', ['%label' => $entity->label()]);
+  }
+
+  /**
+   * Renders the Layout UI.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity.
+   * @param bool $is_rebuilding
+   *   (optional) Indicates if the layout is rebuilding, defaults to FALSE.
+   *
+   * @return array
+   *   A render array.
+   */
+  public function layout(EntityInterface $entity, $is_rebuilding = FALSE) {
+    $entity_id = $entity->id();
+    $entity_type_id = $entity->getEntityTypeId();
+
+    /** @var \Drupal\layout_builder\Field\LayoutSectionItemListInterface $field_list */
+    $field_list = $entity->layout_builder__layout;
+
+    // For a new layout override, begin with a single section of one column.
+    if (!$is_rebuilding && $field_list->isEmpty()) {
+      $field_list->addItem(0, ['layout' => 'layout_onecol']);
+      $this->layoutTempstoreRepository->set($entity);
+    }
+
+    $output = [];
+    $count = 0;
+    foreach ($field_list as $item) {
+      $output[] = $this->buildAddSectionLink($entity_type_id, $entity_id, $count);
+      $output[] = $this->buildAdministrativeSection($item, $entity, $count);
+      $count++;
+    }
+    $output[] = $this->buildAddSectionLink($entity_type_id, $entity_id, $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) {
+    return [
+      'link' => [
+        '#type' => 'link',
+        '#title' => $this->t('Add Section'),
+        '#url' => Url::fromRoute('layout_builder.choose_section',
+          [
+            'entity_type_id' => $entity_type_id,
+            'entity' => $entity_id,
+            'delta' => $delta,
+          ],
+          [
+            'attributes' => [
+              'class' => ['use-ajax'],
+              'data-dialog-type' => 'dialog',
+              'data-dialog-renderer' => 'off_canvas',
+            ],
+          ]
+        ),
+      ],
+      '#type' => 'container',
+      '#attributes' => [
+        'class' => ['add-section'],
+      ],
+    ];
+  }
+
+  /**
+   * Builds the render array for the layout section while editing.
+   *
+   * @param \Drupal\layout_builder\Field\LayoutSectionItemInterface $item
+   *   The layout section item.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity.
+   * @param int $delta
+   *   The delta of the section.
+   *
+   * @return array
+   *   The render array for a given section.
+   */
+  protected function buildAdministrativeSection(LayoutSectionItemInterface $item, EntityInterface $entity, $delta) {
+    $entity_type_id = $entity->getEntityTypeId();
+    $entity_id = $entity->id();
+
+    $layout = $this->layoutManager->createInstance($item->layout, $item->layout_settings);
+    $build = $this->builder->buildSectionFromLayout($layout, $item->section);
+    $layout_definition = $layout->getPluginDefinition();
+
+    foreach ($layout_definition->getRegions() as $region => $info) {
+      if (!empty($build[$region])) {
+        foreach ($build[$region] as $uuid => $block) {
+          $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' => $entity_id,
+                'delta' => $delta,
+                'region' => $region,
+                'uuid' => $uuid,
+              ],
+            ],
+          ];
+        }
+      }
+
+      $build[$region]['layout_builder_add_block']['link'] = [
+        '#type' => 'link',
+        '#title' => $this->t('Add Block'),
+        '#url' => Url::fromRoute('layout_builder.choose_block',
+          [
+            'entity_type_id' => $entity_type_id,
+            'entity' => $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']['#type'] = 'container';
+      $build[$region]['layout_builder_add_block']['#attributes'] = ['class' => ['add-block']];
+      $build[$region]['layout_builder_add_block']['#weight'] = -1000;
+      $build[$region]['#attributes']['data-region'] = $region;
+      $build[$region]['#attributes']['class'][] = 'layout-builder--layout__region';
+    }
+
+    $build['#attributes']['data-layout-update-url'] = Url::fromRoute('layout_builder.move_block', [
+      'entity_type_id' => $entity_type_id,
+      'entity' => $entity_id,
+    ])->toString();
+    $build['#attributes']['data-layout-delta'] = $delta;
+    $build['#attributes']['class'][] = 'layout-builder--layout';
+
+    return [
+      '#type' => 'container',
+      '#attributes' => [
+        'class' => ['layout-section'],
+      ],
+      'configure' => [
+        '#type' => 'link',
+        '#title' => $this->t('Configure section'),
+        '#access' => $layout instanceof PluginFormInterface,
+        '#url' => Url::fromRoute('layout_builder.configure_section', [
+          'entity_type_id' => $entity_type_id,
+          'entity' => $entity_id,
+          'delta' => $delta,
+        ]),
+        '#attributes' => [
+          'class' => ['use-ajax', 'configure-section'],
+          'data-dialog-type' => 'dialog',
+          'data-dialog-renderer' => 'off_canvas',
+        ],
+      ],
+      'remove' => [
+        '#type' => 'link',
+        '#title' => $this->t('Remove section'),
+        '#url' => Url::fromRoute('layout_builder.remove_section', [
+          'entity_type_id' => $entity_type_id,
+          'entity' => $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\EntityInterface $entity
+   *   The entity.
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse
+   *   A redirect response.
+   */
+  public function saveLayout(EntityInterface $entity) {
+    $entity->save();
+    $this->layoutTempstoreRepository->delete($entity);
+    return new RedirectResponse($entity->toUrl()->setAbsolute()->toString());
+  }
+
+  /**
+   * Cancels the layout.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity.
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse
+   *   A redirect response.
+   */
+  public function cancelLayout(EntityInterface $entity) {
+    $this->layoutTempstoreRepository->delete($entity);
+    return new RedirectResponse($entity->toUrl()->setAbsolute()->toString());
+  }
+
+}
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 0000000000000000000000000000000000000000..53fd4e8b0ac379bf0383df82a8f8588ed39597ea
--- /dev/null
+++ b/core/modules/layout_builder/src/Controller/LayoutRebuildTrait.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Drupal\layout_builder\Controller;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\CloseDialogCommand;
+use Drupal\Core\Ajax\ReplaceCommand;
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Provides AJAX responses to rebuild the Layout Builder.
+ *
+ * @internal
+ */
+trait LayoutRebuildTrait {
+
+  /**
+   * The class resolver.
+   *
+   * @var \Drupal\Core\DependencyInjection\ClassResolverInterface
+   */
+  protected $classResolver;
+
+  /**
+   * Rebuilds the layout.
+   *
+   * @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(EntityInterface $entity) {
+    $response = $this->rebuildLayout($entity);
+    $response->addCommand(new CloseDialogCommand('#drupal-off-canvas'));
+    return $response;
+  }
+
+  /**
+   * Rebuilds the layout.
+   *
+   * @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(EntityInterface $entity) {
+    $response = new AjaxResponse();
+    $layout_controller = $this->classResolver->getInstanceFromDefinition(LayoutBuilderController::class);
+    $layout = $layout_controller->layout($entity, TRUE);
+    $response->addCommand(new ReplaceCommand('#layout-builder', $layout));
+    return $response;
+  }
+
+}
diff --git a/core/modules/layout_builder/src/Controller/MoveBlockController.php b/core/modules/layout_builder/src/Controller/MoveBlockController.php
new file mode 100644
index 0000000000000000000000000000000000000000..d648416d9e418d0e4c94740197937ca60cf8302d
--- /dev/null
+++ b/core/modules/layout_builder/src/Controller/MoveBlockController.php
@@ -0,0 +1,102 @@
+<?php
+
+namespace Drupal\layout_builder\Controller;
+
+use Drupal\Core\DependencyInjection\ClassResolverInterface;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Defines a controller to move a block.
+ *
+ * @internal
+ */
+class MoveBlockController implements ContainerInjectionInterface {
+
+  use LayoutRebuildTrait;
+
+  /**
+   * The layout tempstore repository.
+   *
+   * @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
+   */
+  protected $layoutTempstoreRepository;
+
+  /**
+   * LayoutController constructor.
+   *
+   * @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
+   *   The layout tempstore repository.
+   * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
+   *   The class resolver.
+   */
+  public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, ClassResolverInterface $class_resolver) {
+    $this->layoutTempstoreRepository = $layout_tempstore_repository;
+    $this->classResolver = $class_resolver;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('layout_builder.tempstore_repository'),
+      $container->get('class_resolver')
+    );
+  }
+
+  /**
+   * Moves a block to another region.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity.
+   * @param int $delta_from
+   *   The delta of the original section.
+   * @param int $delta_to
+   *   The delta of the destination section.
+   * @param string $region_from
+   *   The original region for this block.
+   * @param string $region_to
+   *   The new region for this block.
+   * @param string $block_uuid
+   *   The UUID for this block.
+   * @param string|null $preceding_block_uuid
+   *   (optional) If provided, the UUID of the block to insert this block after.
+   *
+   * @return \Drupal\Core\Ajax\AjaxResponse
+   *   An AJAX response.
+   */
+  public function build(EntityInterface $entity, $delta_from, $delta_to, $region_from, $region_to, $block_uuid, $preceding_block_uuid = NULL) {
+    /** @var \Drupal\layout_builder\Field\LayoutSectionItemInterface $field */
+    $field = $entity->layout_builder__layout->get($delta_from);
+    $section = $field->getSection();
+
+    $block = $section->getBlock($region_from, $block_uuid);
+    $section->removeBlock($region_from, $block_uuid);
+
+    // If the block is moving from one section to another, update the original
+    // section and load the new one.
+    if ($delta_from !== $delta_to) {
+      $field->updateFromSection($section);
+      $field = $entity->layout_builder__layout->get($delta_to);
+      $section = $field->getSection();
+    }
+
+    // If a preceding block was specified, insert after that. Otherwise add the
+    // block to the front.
+    if (isset($preceding_block_uuid)) {
+      $section->insertBlock($region_to, $block_uuid, $block, $preceding_block_uuid);
+    }
+    else {
+      $section->addBlock($region_to, $block_uuid, $block);
+    }
+
+    $field->updateFromSection($section);
+
+    $this->layoutTempstoreRepository->set($entity);
+    return $this->rebuildLayout($entity);
+  }
+
+}
diff --git a/core/modules/layout_builder/src/Field/LayoutSectionItemInterface.php b/core/modules/layout_builder/src/Field/LayoutSectionItemInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..786b6b4b19acd4f932d7d64c922a46ba99b37613
--- /dev/null
+++ b/core/modules/layout_builder/src/Field/LayoutSectionItemInterface.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Drupal\layout_builder\Field;
+
+use Drupal\Core\Field\FieldItemInterface;
+use Drupal\layout_builder\Section;
+
+/**
+ * Defines an interface for the layout section field item.
+ *
+ * @internal
+ *   Layout Builder is currently experimental and should only be leveraged by
+ *   experimental modules and development releases of contributed modules.
+ *   See https://www.drupal.org/core/experimental for more information.
+ *
+ * @property string layout
+ * @property array[] layout_settings
+ * @property array[] section
+ */
+interface LayoutSectionItemInterface extends FieldItemInterface {
+
+  /**
+   * Gets a domain object for the layout section.
+   *
+   * @return \Drupal\layout_builder\Section
+   *   The layout section.
+   */
+  public function getSection();
+
+  /**
+   * Updates the stored value based on the domain object.
+   *
+   * @param \Drupal\layout_builder\Section $section
+   *   The layout section.
+   *
+   * @return $this
+   */
+  public function updateFromSection(Section $section);
+
+}
diff --git a/core/modules/layout_builder/src/Field/LayoutSectionItemList.php b/core/modules/layout_builder/src/Field/LayoutSectionItemList.php
new file mode 100644
index 0000000000000000000000000000000000000000..933ed6eb974e866bc32cdde910e0409e6e75d5e2
--- /dev/null
+++ b/core/modules/layout_builder/src/Field/LayoutSectionItemList.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Drupal\layout_builder\Field;
+
+use Drupal\Core\Field\FieldItemList;
+
+/**
+ * Defines a item list class for layout section fields.
+ *
+ * @internal
+ *
+ * @see \Drupal\layout_builder\Plugin\Field\FieldType\LayoutSectionItem
+ */
+class LayoutSectionItemList extends FieldItemList implements LayoutSectionItemListInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addItem($index, $value) {
+    if ($this->get($index)) {
+      $start = array_slice($this->list, 0, $index);
+      $end = array_slice($this->list, $index);
+      $item = $this->createItem($index, $value);
+      $this->list = array_merge($start, [$item], $end);
+    }
+    else {
+      $item = $this->appendItem($value);
+    }
+    return $item;
+  }
+
+}
diff --git a/core/modules/layout_builder/src/Field/LayoutSectionItemListInterface.php b/core/modules/layout_builder/src/Field/LayoutSectionItemListInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..81839d17747b6975efe3912847cab60bcafafaf3
--- /dev/null
+++ b/core/modules/layout_builder/src/Field/LayoutSectionItemListInterface.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Drupal\layout_builder\Field;
+
+use Drupal\Core\Field\FieldItemListInterface;
+
+/**
+ * Defines a item list class for layout section fields.
+ *
+ * @internal
+ *   Layout Builder is currently experimental and should only be leveraged by
+ *   experimental modules and development releases of contributed modules.
+ *   See https://www.drupal.org/core/experimental for more information.
+ *
+ * @see \Drupal\layout_builder\Plugin\Field\FieldType\LayoutSectionItem
+ */
+interface LayoutSectionItemListInterface extends FieldItemListInterface {
+
+  /**
+   * {@inheritdoc}
+   *
+   * @return \Drupal\layout_builder\Field\LayoutSectionItemInterface|null
+   *   The layout section item, if it exists.
+   */
+  public function get($index);
+
+  /**
+   * Adds a new item to the list.
+   *
+   * If an item exists at the given index, the item at that position and others
+   * after it are shifted backward.
+   *
+   * @param int $index
+   *   The position of the item in the list.
+   * @param mixed $value
+   *   The value of the item to be stored at the specified position.
+   *
+   * @return \Drupal\Core\TypedData\TypedDataInterface
+   *   The item that was appended.
+   *
+   * @todo Move to \Drupal\Core\TypedData\ListInterface directly in
+   *   https://www.drupal.org/node/2907417.
+   */
+  public function addItem($index, $value);
+
+}
diff --git a/core/modules/layout_builder/src/Form/AddBlockForm.php b/core/modules/layout_builder/src/Form/AddBlockForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..1757606002c2b2a85209cb6fa65f24bf3e908453
--- /dev/null
+++ b/core/modules/layout_builder/src/Form/AddBlockForm.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Drupal\layout_builder\Form;
+
+use Drupal\layout_builder\Section;
+
+/**
+ * Provides a form to add a block.
+ *
+ * @internal
+ */
+class AddBlockForm extends ConfigureBlockFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'layout_builder_add_block';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function submitLabel() {
+    return $this->t('Add Block');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function submitBlock(Section $section, $region, $uuid, array $configuration) {
+    $section->addBlock($region, $uuid, $configuration);
+  }
+
+}
diff --git a/core/modules/layout_builder/src/Form/AjaxFormHelperTrait.php b/core/modules/layout_builder/src/Form/AjaxFormHelperTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..a5a387020f9d6c383295ca037a4da8380f1d05e3
--- /dev/null
+++ b/core/modules/layout_builder/src/Form/AjaxFormHelperTrait.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Drupal\layout_builder\Form;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\ReplaceCommand;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\layout_builder\Controller\AjaxHelperTrait;
+
+/**
+ * Provides a helper to for submitting an AJAX form.
+ *
+ * @internal
+ *
+ * @todo Move to \Drupal\Core in https://www.drupal.org/node/2896535.
+ */
+trait AjaxFormHelperTrait {
+
+  use AjaxHelperTrait;
+
+  /**
+   * 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 represents a
+   *   successful submission.
+   */
+  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 {
+      $response = $this->successfulAjaxSubmit($form, $form_state);
+    }
+    return $response;
+  }
+
+  /**
+   * Allows the form to respond to a successful AJAX submission.
+   *
+   * @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.
+   */
+  abstract protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state);
+
+}
diff --git a/core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php b/core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..7356ef4ccdf18a0abc3670e0aad42ea1c0545694
--- /dev/null
+++ b/core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php
@@ -0,0 +1,282 @@
+<?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\EntityInterface;
+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 Drupal\layout_builder\Section;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a base form for configuring a block.
+ *
+ * @internal
+ */
+abstract class ConfigureBlockFormBase extends FormBase {
+
+  use AjaxFormHelperTrait;
+  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 block manager.
+   *
+   * @var \Drupal\Core\Block\BlockManagerInterface
+   */
+  protected $blockManager;
+
+  /**
+   * The UUID generator.
+   *
+   * @var \Drupal\Component\Uuid\UuidInterface
+   */
+  protected $uuid;
+
+  /**
+   * The plugin form manager.
+   *
+   * @var \Drupal\Core\Plugin\PluginFormFactoryInterface
+   */
+  protected $pluginFormFactory;
+
+  /**
+   * The field delta.
+   *
+   * @var int
+   */
+  protected $delta;
+
+  /**
+   * The current region.
+   *
+   * @var string
+   */
+  protected $region;
+
+  /**
+   * The entity.
+   *
+   * @var \Drupal\Core\Entity\EntityInterface
+   */
+  protected $entity;
+
+  /**
+   * Constructs a new block form.
+   *
+   * @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\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, BlockManagerInterface $block_manager, UuidInterface $uuid, ClassResolverInterface $class_resolver, PluginFormFactoryInterface $plugin_form_manager) {
+    $this->layoutTempstoreRepository = $layout_tempstore_repository;
+    $this->contextRepository = $context_repository;
+    $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('plugin.manager.block'),
+      $container->get('uuid'),
+      $container->get('class_resolver'),
+      $container->get('plugin_form.factory')
+    );
+  }
+
+  /**
+   * 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);
+  }
+
+  /**
+   * Builds the form for the block.
+   *
+   * @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.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity being configured.
+   * @param int $delta
+   *   The delta of the section.
+   * @param string $region
+   *   The region of the block.
+   * @param string|null $plugin_id
+   *   The plugin ID of the block to add.
+   * @param array $configuration
+   *   (optional) The array of configuration for the block.
+   *
+   * @return array
+   *   The form array.
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, EntityInterface $entity = NULL, $delta = NULL, $region = NULL, $plugin_id = NULL, array $configuration = []) {
+    $this->entity = $entity;
+    $this->delta = $delta;
+    $this->region = $region;
+    $this->block = $this->prepareBlock($plugin_id, $configuration);
+
+    $form_state->setTemporaryValue('gathered_contexts', $this->contextRepository->getAvailableContexts());
+
+    // @todo Remove once https://www.drupal.org/node/2268787 is resolved.
+    $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' => $this->submitLabel(),
+      '#button_type' => 'primary',
+    ];
+    if ($this->isAjax()) {
+      $form['actions']['submit']['#ajax']['callback'] = '::ajaxSubmit';
+    }
+
+    return $form;
+  }
+
+  /**
+   * Returns the label for the submit button.
+   *
+   * @return string
+   *   Submit label.
+   */
+  abstract protected function submitLabel();
+
+  /**
+   * Handles the submission of a block.
+   *
+   * @param \Drupal\layout_builder\Section $section
+   *   The layout section.
+   * @param string $region
+   *   The region name.
+   * @param string $uuid
+   *   The UUID of the block.
+   * @param array $configuration
+   *   The block configuration.
+   */
+  abstract protected function submitBlock(Section $section, $region, $uuid, array $configuration);
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    $subform_state = SubformState::createForSubform($form['settings'], $form, $form_state);
+    $this->getPluginForm($this->block)->validateConfigurationForm($form['settings'], $subform_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    // Call the plugin submit handler.
+    $subform_state = SubformState::createForSubform($form['settings'], $form, $form_state);
+    $this->getPluginForm($this->block)->submitConfigurationForm($form, $subform_state);
+
+    // If this block is context-aware, set the context mapping.
+    if ($this->block instanceof ContextAwarePluginInterface) {
+      $this->block->setContextMapping($subform_state->getValue('context_mapping', []));
+    }
+
+    $configuration = $this->block->getConfiguration();
+
+    /** @var \Drupal\layout_builder\Field\LayoutSectionItemInterface $field */
+    $field = $this->entity->layout_builder__layout->get($this->delta);
+    $section = $field->getSection();
+    $this->submitBlock($section, $this->region, $configuration['uuid'], ['block' => $configuration]);
+    $field->updateFromSection($section);
+
+    $this->layoutTempstoreRepository->set($this->entity);
+    $form_state->setRedirectUrl($this->entity->toUrl('layout-builder'));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state) {
+    return $this->rebuildAndClose($this->entity);
+  }
+
+  /**
+   * Retrieves the plugin form for a given block.
+   *
+   * @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/ConfigureSectionForm.php b/core/modules/layout_builder/src/Form/ConfigureSectionForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..17913237d51ddfe6b0beddc63a15ec0f8950e445
--- /dev/null
+++ b/core/modules/layout_builder/src/Form/ConfigureSectionForm.php
@@ -0,0 +1,216 @@
+<?php
+
+namespace Drupal\layout_builder\Form;
+
+use Drupal\Core\DependencyInjection\ClassResolverInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Form\SubformState;
+use Drupal\Core\Layout\LayoutInterface;
+use Drupal\Core\Layout\LayoutPluginManagerInterface;
+use Drupal\Core\Plugin\PluginFormFactoryInterface;
+use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\Core\Plugin\PluginWithFormsInterface;
+use Drupal\layout_builder\Controller\LayoutRebuildTrait;
+use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a form for configuring a layout section.
+ *
+ * @internal
+ */
+class ConfigureSectionForm extends FormBase {
+
+  use AjaxFormHelperTrait;
+  use LayoutRebuildTrait;
+
+  /**
+   * The layout tempstore repository.
+   *
+   * @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
+   */
+  protected $layoutTempstoreRepository;
+
+  /**
+   * The plugin being configured.
+   *
+   * @var \Drupal\Core\Layout\LayoutInterface|\Drupal\Core\Plugin\PluginFormInterface
+   */
+  protected $layout;
+
+  /**
+   * The layout manager.
+   *
+   * @var \Drupal\Core\Layout\LayoutPluginManagerInterface
+   */
+  protected $layoutManager;
+
+  /**
+   * The plugin form manager.
+   *
+   * @var \Drupal\Core\Plugin\PluginFormFactoryInterface
+   */
+  protected $pluginFormFactory;
+
+  /**
+   * The entity.
+   *
+   * @var \Drupal\Core\Entity\EntityInterface
+   */
+  protected $entity;
+
+  /**
+   * The field delta.
+   *
+   * @var int
+   */
+  protected $delta;
+
+  /**
+   * Indicates whether the section is being added or updated.
+   *
+   * @var bool
+   */
+  protected $isUpdate;
+
+  /**
+   * Constructs a new ConfigureSectionForm.
+   *
+   * @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
+   *   The layout tempstore repository.
+   * @param \Drupal\Core\Layout\LayoutPluginManagerInterface $layout_manager
+   *   The layout manager.
+   * @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, LayoutPluginManagerInterface $layout_manager, ClassResolverInterface $class_resolver, PluginFormFactoryInterface $plugin_form_manager) {
+    $this->layoutTempstoreRepository = $layout_tempstore_repository;
+    $this->layoutManager = $layout_manager;
+    $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('plugin.manager.core.layout'),
+      $container->get('class_resolver'),
+      $container->get('plugin_form.factory')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'layout_builder_configure_section';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, EntityInterface $entity = NULL, $delta = NULL, $plugin_id = NULL) {
+    $this->entity = $entity;
+    $this->delta = $delta;
+    $this->isUpdate = is_null($plugin_id);
+
+    $configuration = [];
+    if ($this->isUpdate) {
+      /** @var \Drupal\layout_builder\Field\LayoutSectionItemInterface $field */
+      $field = $this->entity->layout_builder__layout->get($this->delta);
+      $plugin_id = $field->layout;
+      $configuration = $field->layout_settings;
+    }
+    $this->layout = $this->layoutManager->createInstance($plugin_id, $configuration);
+
+    $form['#tree'] = TRUE;
+    $form['layout_settings'] = [];
+    $subform_state = SubformState::createForSubform($form['layout_settings'], $form, $form_state);
+    $form['layout_settings'] = $this->getPluginForm($this->layout)->buildConfigurationForm($form['layout_settings'], $subform_state);
+
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->isUpdate ? $this->t('Update') : $this->t('Add section'),
+      '#button_type' => 'primary',
+    ];
+    if ($this->isAjax()) {
+      $form['actions']['submit']['#ajax']['callback'] = '::ajaxSubmit';
+    }
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    $subform_state = SubformState::createForSubform($form['layout_settings'], $form, $form_state);
+    $this->getPluginForm($this->layout)->validateConfigurationForm($form['layout_settings'], $subform_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    // Call the plugin submit handler.
+    $subform_state = SubformState::createForSubform($form['layout_settings'], $form, $form_state);
+    $this->getPluginForm($this->layout)->submitConfigurationForm($form['layout_settings'], $subform_state);
+
+    $plugin_id = $this->layout->getPluginId();
+    $configuration = $this->layout->getConfiguration();
+
+    /** @var \Drupal\layout_builder\Field\LayoutSectionItemListInterface $field_list */
+    $field_list = $this->entity->layout_builder__layout;
+    if ($this->isUpdate) {
+      $field = $field_list->get($this->delta);
+      $field->layout = $plugin_id;
+      $field->layout_settings = $configuration;
+    }
+    else {
+      $field_list->addItem($this->delta, [
+        'layout' => $plugin_id,
+        'layout_settings' => $configuration,
+        'section' => [],
+      ]);
+    }
+
+    $this->layoutTempstoreRepository->set($this->entity);
+    $form_state->setRedirectUrl($this->entity->toUrl('layout-builder'));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state) {
+    return $this->rebuildAndClose($this->entity);
+  }
+
+  /**
+   * Retrieves the plugin form for a given layout.
+   *
+   * @param \Drupal\Core\Layout\LayoutInterface $layout
+   *   The layout plugin.
+   *
+   * @return \Drupal\Core\Plugin\PluginFormInterface
+   *   The plugin form for the layout.
+   */
+  protected function getPluginForm(LayoutInterface $layout) {
+    if ($layout instanceof PluginWithFormsInterface) {
+      return $this->pluginFormFactory->createInstance($layout, 'configure');
+    }
+
+    if ($layout instanceof PluginFormInterface) {
+      return $layout;
+    }
+
+    throw new \InvalidArgumentException(sprintf('The "%s" layout does not provide a configuration form', $layout->getPluginId()));
+  }
+
+}
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 0000000000000000000000000000000000000000..8bee062d4e45bd3d94860469c07797b4c91f30e3
--- /dev/null
+++ b/core/modules/layout_builder/src/Form/LayoutRebuildConfirmFormBase.php
@@ -0,0 +1,119 @@
+<?php
+
+namespace Drupal\layout_builder\Form;
+
+use Drupal\Core\DependencyInjection\ClassResolverInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\ConfirmFormBase;
+use Drupal\Core\Form\FormStateInterface;
+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.
+ *
+ * @internal
+ */
+abstract class LayoutRebuildConfirmFormBase extends ConfirmFormBase {
+
+  use AjaxFormHelperTrait;
+  use LayoutRebuildTrait;
+
+  /**
+   * The layout tempstore repository.
+   *
+   * @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
+   */
+  protected $layoutTempstoreRepository;
+
+  /**
+   * The entity.
+   *
+   * @var \Drupal\Core\Entity\EntityInterface
+   */
+  protected $entity;
+
+  /**
+   * The field delta.
+   *
+   * @var int
+   */
+  protected $delta;
+
+  /**
+   * Constructs a new RemoveSectionForm.
+   *
+   * @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
+   *   The layout tempstore repository.
+   * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
+   *   The class resolver.
+   */
+  public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, ClassResolverInterface $class_resolver) {
+    $this->layoutTempstoreRepository = $layout_tempstore_repository;
+    $this->classResolver = $class_resolver;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('layout_builder.tempstore_repository'),
+      $container->get('class_resolver')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelUrl() {
+    return $this->entity->toUrl('layout-builder', ['query' => ['layout_is_rebuilding' => TRUE]]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, EntityInterface $entity = NULL, $delta = NULL) {
+    $this->entity = $entity;
+    $this->delta = $delta;
+
+    $form = parent::buildForm($form, $form_state);
+
+    if ($this->isAjax()) {
+      $form['actions']['submit']['#ajax']['callback'] = '::ajaxSubmit';
+      $form['actions']['cancel']['#attributes']['class'][] = 'dialog-cancel';
+    }
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $this->handleEntity($this->entity, $form_state);
+
+    $this->layoutTempstoreRepository->set($this->entity);
+
+    $form_state->setRedirectUrl($this->getCancelUrl());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state) {
+    return $this->rebuildAndClose($this->entity);
+  }
+
+  /**
+   * 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);
+
+}
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 0000000000000000000000000000000000000000..139186af66541b656648f3ae3a356fce7e3ca5b1
--- /dev/null
+++ b/core/modules/layout_builder/src/Form/RemoveBlockForm.php
@@ -0,0 +1,70 @@
+<?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.
+ *
+ * @internal
+ */
+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, EntityInterface $entity = NULL, $delta = NULL, $region = NULL, $uuid = NULL) {
+    $this->region = $region;
+    $this->uuid = $uuid;
+    return parent::buildForm($form, $form_state, $entity, $delta);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function handleEntity(EntityInterface $entity, FormStateInterface $form_state) {
+    /** @var \Drupal\layout_builder\Field\LayoutSectionItemInterface $field */
+    $field = $entity->layout_builder__layout->get($this->delta);
+    $section = $field->getSection();
+    $section->removeBlock($this->region, $this->uuid);
+    $field->updateFromSection($section);
+  }
+
+}
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 0000000000000000000000000000000000000000..e44edd694736706a33d550f2ddc1ccacc5042ab9
--- /dev/null
+++ b/core/modules/layout_builder/src/Form/RemoveSectionForm.php
@@ -0,0 +1,43 @@
+<?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.
+ *
+ * @internal
+ */
+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/Form/UpdateBlockForm.php b/core/modules/layout_builder/src/Form/UpdateBlockForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..2f2aa600e44c7e08d5ed5cd5cf293cce3e372369
--- /dev/null
+++ b/core/modules/layout_builder/src/Form/UpdateBlockForm.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Drupal\layout_builder\Form;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\layout_builder\Section;
+
+/**
+ * Provides a form to update a block.
+ *
+ * @internal
+ */
+class UpdateBlockForm extends ConfigureBlockFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'layout_builder_update_block';
+  }
+
+  /**
+   * Builds the block form.
+   *
+   * @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.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity being configured.
+   * @param int $delta
+   *   The delta of the section.
+   * @param string $region
+   *   The region of the block.
+   * @param string $uuid
+   *   The UUID of the block being updated.
+   *
+   * @return array
+   *   The form array.
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, EntityInterface $entity = NULL, $delta = NULL, $region = NULL, $uuid = NULL) {
+    /** @var \Drupal\layout_builder\Field\LayoutSectionItemInterface $field */
+    $field = $entity->layout_builder__layout->get($delta);
+    $block = $field->getSection()->getBlock($region, $uuid);
+    if (empty($block['block']['id'])) {
+      throw new \InvalidArgumentException('Invalid UUID specified');
+    }
+
+    return parent::buildForm($form, $form_state, $entity, $delta, $region, $block['block']['id'], $block['block']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function submitLabel() {
+    return $this->t('Update');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function submitBlock(Section $section, $region, $uuid, array $configuration) {
+    $section->updateBlock($region, $uuid, $configuration);
+  }
+
+}
diff --git a/core/modules/layout_builder/src/LayoutSectionBuilder.php b/core/modules/layout_builder/src/LayoutSectionBuilder.php
new file mode 100644
index 0000000000000000000000000000000000000000..1682974f1134533f61792cf627a14d964beac892
--- /dev/null
+++ b/core/modules/layout_builder/src/LayoutSectionBuilder.php
@@ -0,0 +1,201 @@
+<?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\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\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Builds the UI for layout sections.
+ *
+ * @internal
+ */
+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 \Drupal\Core\Layout\LayoutInterface $layout
+   *   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 buildSectionFromLayout(LayoutInterface $layout, array $section) {
+    $cacheability = CacheableMetadata::createFromRenderArray([]);
+
+    $regions = [];
+    $weight = 0;
+    foreach ($section as $region => $blocks) {
+      if (!is_array($blocks)) {
+        throw new \InvalidArgumentException(sprintf('The "%s" region in the "%s" layout has invalid configuration', $region, $layout->getPluginId()));
+      }
+
+      foreach ($blocks as $uuid => $configuration) {
+        if (!is_array($configuration) || !isset($configuration['block'])) {
+          throw new \InvalidArgumentException(sprintf('The block with UUID of "%s" has invalid configuration', $uuid));
+        }
+
+        if ($block_output = $this->buildBlock($uuid, $configuration['block'], $cacheability)) {
+          $block_output['#weight'] = $weight++;
+          $regions[$region][$uuid] = $block_output;
+        }
+      }
+    }
+
+    $result = $layout->build($regions);
+    $cacheability->applyTo($result);
+    return $result;
+  }
+
+  /**
+   * Builds the render array for the layout section.
+   *
+   * @param string $layout_id
+   *   The ID of the layout.
+   * @param array $layout_settings
+   *   The configuration for 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 $layout_settings, array $section) {
+    $layout = $this->layoutPluginManager->createInstance($layout_id, $layout_settings);
+    return $this->buildSectionFromLayout($layout, $section);
+  }
+
+  /**
+   * Builds the render array for a given block.
+   *
+   * @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'.
+   * @param \Drupal\Core\Cache\CacheableMetadata $cacheability
+   *   The cacheability metadata.
+   *
+   * @return array|null
+   *   The render array representing this block, if accessible. NULL otherwise.
+   */
+  protected function buildBlock($uuid, array $configuration, CacheableMetadata $cacheability) {
+    $block = $this->getBlock($uuid, $configuration);
+
+    $access = $block->access($this->account, TRUE);
+    $cacheability->addCacheableDependency($access);
+
+    $block_output = NULL;
+    if ($access->isAllowed()) {
+      $block_output = [
+        '#theme' => 'block',
+        '#configuration' => $block->getConfiguration(),
+        '#plugin_id' => $block->getPluginId(),
+        '#base_plugin_id' => $block->getBaseId(),
+        '#derivative_plugin_id' => $block->getDerivativeId(),
+        'content' => $block->build(),
+      ];
+      $cacheability->addCacheableDependency($block);
+    }
+    return $block_output;
+  }
+
+  /**
+   * 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/LayoutTempstoreRepository.php b/core/modules/layout_builder/src/LayoutTempstoreRepository.php
new file mode 100644
index 0000000000000000000000000000000000000000..87baa1331c05c5724f55501f19f5c25752f2b288
--- /dev/null
+++ b/core/modules/layout_builder/src/LayoutTempstoreRepository.php
@@ -0,0 +1,118 @@
+<?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.
+ *
+ * @internal
+ */
+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) {
+    $id = $this->generateTempstoreId($entity);
+    $tempstore = $this->getTempstore($entity)->get($id);
+    if (!empty($tempstore['entity'])) {
+      $entity_type_id = $entity->getEntityTypeId();
+      $entity = $tempstore['entity'];
+
+      if (!($entity instanceof EntityInterface)) {
+        throw new \UnexpectedValueException(sprintf('The entry with entity type "%s" and ID "%s" is not a valid entity', $entity_type_id, $id));
+      }
+    }
+    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) {
+    $id = $this->generateTempstoreId($entity);
+    $this->getTempstore($entity)->set($id, ['entity' => $entity]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function delete(EntityInterface $entity) {
+    if ($this->get($entity)) {
+      $id = $this->generateTempstoreId($entity);
+      $this->getTempstore($entity)->delete($id);
+    }
+  }
+
+  /**
+   * Generates an ID for putting an entity in tempstore.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity being stored.
+   *
+   * @return string
+   *   The tempstore ID.
+   */
+  protected function generateTempstoreId(EntityInterface $entity) {
+    $id = "{$entity->id()}.{$entity->language()->getId()}";
+    if ($entity instanceof RevisionableInterface) {
+      $id .= '.' . $entity->getRevisionId();
+    }
+    return $id;
+  }
+
+  /**
+   * Gets the shared tempstore.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity being stored.
+   *
+   * @return \Drupal\user\SharedTempStore
+   *   The tempstore.
+   */
+  protected function getTempstore(EntityInterface $entity) {
+    $collection = $entity->getEntityTypeId() . '.layout_builder__layout';
+    return $this->tempStoreFactory->get($collection);
+  }
+
+}
diff --git a/core/modules/layout_builder/src/LayoutTempstoreRepositoryInterface.php b/core/modules/layout_builder/src/LayoutTempstoreRepositoryInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..ffce1c3008ad62e8dbb9c005653050cc7696eac3
--- /dev/null
+++ b/core/modules/layout_builder/src/LayoutTempstoreRepositoryInterface.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Drupal\layout_builder;
+
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Provides an interface for loading layouts from tempstore.
+ *
+ * @internal
+ *   Layout Builder is currently experimental and should only be leveraged by
+ *   experimental modules and development releases of contributed modules.
+ *   See https://www.drupal.org/core/experimental for more information.
+ */
+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.
+   *
+   * @throw \UnexpectedValueException
+   *   Thrown if a value exists, but is not an entity.
+   */
+  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.
+   *
+   * @throw \UnexpectedValueException
+   *   Thrown if a value exists, but is not an entity.
+   */
+  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 0000000000000000000000000000000000000000..7054dd360532901b48c099355d379b970104195a
--- /dev/null
+++ b/core/modules/layout_builder/src/Plugin/Derivative/LayoutBuilderLocalTaskDeriver.php
@@ -0,0 +1,96 @@
+<?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\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.
+ *
+ * @internal
+ */
+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_builder"] = $base_plugin_definition + [
+        'route_name' => "entity.$entity_type_id.layout_builder",
+        'weight' => 15,
+        'title' => $this->t('Layout'),
+        'base_route' => "entity.$entity_type_id.canonical",
+        'entity_type_id' => $entity_type_id,
+        'class' => LayoutBuilderLocalTask::class,
+        'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
+      ];
+      $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_builder",
+        'entity_type_id' => $entity_type_id,
+        'class' => LayoutBuilderLocalTask::class,
+        'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
+      ];
+      $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_builder",
+        'entity_type_id' => $entity_type_id,
+        'class' => LayoutBuilderLocalTask::class,
+        'weight' => 5,
+        'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
+      ];
+    }
+
+    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->hasLinkTemplate('layout-builder');
+    });
+  }
+
+}
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 0000000000000000000000000000000000000000..4951d01c4b9bca5ad8330adad9bbb9a59f638a99
--- /dev/null
+++ b/core/modules/layout_builder/src/Plugin/Field/FieldFormatter/LayoutSectionFormatter.php
@@ -0,0 +1,89 @@
+<?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.
+ *
+ * @internal
+ *
+ * @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\Field\LayoutSectionItemInterface[] $items */
+    foreach ($items as $delta => $item) {
+      $elements[$delta] = $this->builder->buildSection($item->layout, $item->layout_settings, $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 0000000000000000000000000000000000000000..fc1c63413fa1ff7a327312abd6b18ac75dbe2246
--- /dev/null
+++ b/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php
@@ -0,0 +1,132 @@
+<?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\Field\LayoutSectionItemInterface;
+use Drupal\layout_builder\Section;
+
+/**
+ * Plugin implementation of the 'layout_section' field type.
+ *
+ * @internal
+ *
+ * @FieldType(
+ *   id = "layout_section",
+ *   label = @Translation("Layout Section"),
+ *   description = @Translation("Layout Section"),
+ *   default_formatter = "layout_section",
+ *   list_class = "\Drupal\layout_builder\Field\LayoutSectionItemList",
+ *   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['layout_settings'] = MapDataDefinition::create('map')
+      ->setLabel(new TranslatableMarkup('Layout Settings'))
+      ->setRequired(FALSE);
+    $properties['section'] = MapDataDefinition::create('map')
+      ->setLabel(new TranslatableMarkup('Layout Section'))
+      ->setRequired(FALSE);
+
+    return $properties;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __get($name) {
+    // @todo \Drupal\Core\Field\FieldItemBase::__get() does not return default
+    //   values for uninstantiated properties. This will forcibly instantiate
+    //   all properties with the side-effect of a performance hit, resolve
+    //   properly in https://www.drupal.org/node/2413471.
+    $this->getProperties();
+
+    return parent::__get($name);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function mainPropertyName() {
+    return 'section';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function schema(FieldStorageDefinitionInterface $field_definition) {
+    $schema = [
+      'columns' => [
+        'layout' => [
+          'type' => 'varchar',
+          'length' => '255',
+          'binary' => FALSE,
+        ],
+        'layout_settings' => [
+          'type' => 'blob',
+          'size' => 'normal',
+          // @todo Address in https://www.drupal.org/node/2914503.
+          'serialize' => TRUE,
+        ],
+        'section' => [
+          'type' => 'blob',
+          'size' => 'normal',
+          // @todo Address in https://www.drupal.org/node/2914503.
+          'serialize' => TRUE,
+        ],
+      ],
+    ];
+
+    return $schema;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
+    $values['layout'] = 'layout_onecol';
+    $values['layout_settings'] = [];
+    // @todo Expand this in https://www.drupal.org/node/2912331.
+    $values['section'] = [];
+    return $values;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isEmpty() {
+    return empty($this->layout);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSection() {
+    return new Section($this->section);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateFromSection(Section $section) {
+    $this->section = $section->getValue();
+    return $this;
+  }
+
+}
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 0000000000000000000000000000000000000000..0098e353135f7916d6c8747bdc01b551fd86b6d8
--- /dev/null
+++ b/core/modules/layout_builder/src/Plugin/Menu/LayoutBuilderLocalTask.php
@@ -0,0 +1,26 @@
+<?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.
+ *
+ * @internal
+ */
+class LayoutBuilderLocalTask extends LocalTaskDefault {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRouteParameters(RouteMatchInterface $route_match) {
+    $parameters = parent::getRouteParameters($route_match);
+
+    // @todo Revisit this code once https://www.drupal.org/node/2912363 is in.
+    $parameters['entity'] = $route_match->getParameter('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 0000000000000000000000000000000000000000..66dbbbadc982524a5a093076e6f32711e42e3fd5
--- /dev/null
+++ b/core/modules/layout_builder/src/Routing/LayoutBuilderRouteEnhancer.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\layout_builder\Routing;
+
+use Drupal\Core\Routing\EnhancerInterface;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Enhances routes to ensure the entity is available with a generic name.
+ *
+ * @internal
+ */
+class LayoutBuilderRouteEnhancer implements EnhancerInterface {
+
+  /**
+   * Returns whether the enhancer runs on the current route.
+   *
+   * @param \Symfony\Component\Routing\Route $route
+   *   The current route.
+   *
+   * @return bool
+   *   TRUE if this enhancer applies to this route.
+   */
+  protected function applies(Route $route) {
+    return $route->getOption('_layout_builder') && $route->getDefault('entity_type_id');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function enhance(array $defaults, Request $request) {
+    $route = $defaults[RouteObjectInterface::ROUTE_OBJECT];
+    if (!$this->applies($route)) {
+      return $defaults;
+    }
+
+    $defaults['is_rebuilding'] = (bool) $request->query->get('layout_is_rebuilding', FALSE);
+
+    if (!isset($defaults[$defaults['entity_type_id']])) {
+      throw new \RuntimeException(sprintf('Failed to find the "%s" entity in route named %s', $defaults['entity_type_id'], $defaults[RouteObjectInterface::ROUTE_NAME]));
+    }
+
+    // Copy the entity by reference so that any changes are reflected.
+    $defaults['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 0000000000000000000000000000000000000000..b8221dfd79b801bf3abf6ccfaf9cea03dd1aa478
--- /dev/null
+++ b/core/modules/layout_builder/src/Routing/LayoutBuilderRoutes.php
@@ -0,0 +1,157 @@
+<?php
+
+namespace Drupal\layout_builder\Routing;
+
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Provides routes for the Layout Builder UI.
+ *
+ * @internal
+ */
+class LayoutBuilderRoutes {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The entity field manager.
+   *
+   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
+   */
+  protected $entityFieldManager;
+
+  /**
+   * Constructs a new LayoutBuilderRoutes.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
+   *   The entity field manager.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+    $this->entityFieldManager = $entity_field_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) {
+      $integer_id = $this->hasIntegerId($entity_type);
+
+      $template = $entity_type->getLinkTemplate('layout-builder');
+      $route = (new Route($template))
+        ->setDefaults([
+          '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout',
+          '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title',
+          'entity' => NULL,
+          'entity_type_id' => $entity_type_id,
+          'is_rebuilding' => FALSE,
+        ])
+        ->addRequirements([
+          '_has_layout_section' => 'true',
+        ])
+        ->addOptions([
+          '_layout_builder' => TRUE,
+          'parameters' => [
+            $entity_type_id => [
+              'type' => 'entity:{entity_type_id}',
+              'layout_builder_tempstore' => TRUE,
+            ],
+          ],
+        ]);
+      if ($integer_id) {
+        $route->setRequirement($entity_type_id, '\d+');
+      }
+      $routes["entity.$entity_type_id.layout_builder"] = $route;
+
+      $route = (new Route("$template/save"))
+        ->setDefaults([
+          '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout',
+          'entity' => NULL,
+          'entity_type_id' => $entity_type_id,
+        ])
+        ->addRequirements([
+          '_has_layout_section' => 'true',
+        ])
+        ->addOptions([
+          '_layout_builder' => TRUE,
+          'parameters' => [
+            $entity_type_id => [
+              'type' => 'entity:{entity_type_id}',
+              'layout_builder_tempstore' => TRUE,
+            ],
+          ],
+        ]);
+      if ($integer_id) {
+        $route->setRequirement($entity_type_id, '\d+');
+      }
+      $routes["entity.$entity_type_id.save_layout"] = $route;
+
+      $route = (new Route("$template/cancel"))
+        ->setDefaults([
+          '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout',
+          'entity' => NULL,
+          'entity_type_id' => $entity_type_id,
+        ])
+        ->addRequirements([
+          '_has_layout_section' => 'true',
+        ])
+        ->addOptions([
+          '_layout_builder' => TRUE,
+          'parameters' => [
+            $entity_type_id => [
+              'type' => 'entity:{entity_type_id}',
+              'layout_builder_tempstore' => TRUE,
+            ],
+          ],
+        ]);
+      if ($integer_id) {
+        $route->setRequirement($entity_type_id, '\d+');
+      }
+      $routes["entity.$entity_type_id.cancel_layout"] = $route;
+    }
+    return $routes;
+  }
+
+  /**
+   * Determines if this entity type's ID is stored as an integer.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   An entity type.
+   *
+   * @return bool
+   *   TRUE if this entity type's ID key is always an integer, FALSE otherwise.
+   */
+  protected function hasIntegerId(EntityTypeInterface $entity_type) {
+    $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type->id());
+    return $field_storage_definitions[$entity_type->getKey('id')]->getType() === 'integer';
+  }
+
+  /**
+   * 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->hasLinkTemplate('layout-builder');
+    });
+  }
+
+}
diff --git a/core/modules/layout_builder/src/Routing/LayoutTempstoreParamConverter.php b/core/modules/layout_builder/src/Routing/LayoutTempstoreParamConverter.php
new file mode 100644
index 0000000000000000000000000000000000000000..a1d5fc9140b0b2710ecb81681dda3856c1fe192b
--- /dev/null
+++ b/core/modules/layout_builder/src/Routing/LayoutTempstoreParamConverter.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\layout_builder\Routing;
+
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\ParamConverter\EntityConverter;
+use Drupal\Core\ParamConverter\ParamConverterInterface;
+use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Loads the entity from the layout tempstore.
+ *
+ * @internal
+ */
+class LayoutTempstoreParamConverter extends EntityConverter implements ParamConverterInterface {
+
+  /**
+   * The layout tempstore repository.
+   *
+   * @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
+   */
+  protected $layoutTempstoreRepository;
+
+  /**
+   * Constructs a new LayoutTempstoreParamConverter.
+   *
+   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+   *   The entity manager.
+   * @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
+   *   The layout tempstore repository.
+   */
+  public function __construct(EntityManagerInterface $entity_manager, LayoutTempstoreRepositoryInterface $layout_tempstore_repository) {
+    parent::__construct($entity_manager);
+    $this->layoutTempstoreRepository = $layout_tempstore_repository;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function convert($value, $definition, $name, array $defaults) {
+    if ($entity = parent::convert($value, $definition, $name, $defaults)) {
+      return $this->layoutTempstoreRepository->get($entity);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applies($definition, $name, Route $route) {
+    return !empty($definition['layout_builder_tempstore']);
+  }
+
+}
diff --git a/core/modules/layout_builder/src/Section.php b/core/modules/layout_builder/src/Section.php
new file mode 100644
index 0000000000000000000000000000000000000000..f5e19003b58a80304b287edb8e65c8fcc3cba099
--- /dev/null
+++ b/core/modules/layout_builder/src/Section.php
@@ -0,0 +1,162 @@
+<?php
+
+namespace Drupal\layout_builder;
+
+/**
+ * Provides a domain object for layout sections.
+ *
+ * A section is a multi-dimensional array, keyed first by region machine name,
+ * then by block UUID, containing block configuration values.
+ */
+class Section {
+
+  /**
+   * The section data.
+   *
+   * @var array
+   */
+  protected $section;
+
+  /**
+   * Constructs a new Section.
+   *
+   * @param array $section
+   *   The section data.
+   */
+  public function __construct(array $section) {
+    $this->section = $section;
+  }
+
+  /**
+   * Returns the value of the section.
+   *
+   * @return array
+   *   The section data.
+   */
+  public function getValue() {
+    return $this->section;
+  }
+
+  /**
+   * Gets the configuration of a given block from a region.
+   *
+   * @param string $region
+   *   The region name.
+   * @param string $uuid
+   *   The UUID of the block to retrieve.
+   *
+   * @return array
+   *   The block configuration.
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown when the expected region or UUID do not exist.
+   */
+  public function getBlock($region, $uuid) {
+    if (!isset($this->section[$region])) {
+      throw new \InvalidArgumentException('Invalid region');
+    }
+
+    if (!isset($this->section[$region][$uuid])) {
+      throw new \InvalidArgumentException('Invalid UUID');
+    }
+
+    return $this->section[$region][$uuid];
+  }
+
+  /**
+   * Updates the configuration of a given block from a region.
+   *
+   * @param string $region
+   *   The region name.
+   * @param string $uuid
+   *   The UUID of the block to retrieve.
+   * @param array $configuration
+   *   The block configuration.
+   *
+   * @return $this
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown when the expected region or UUID do not exist.
+   */
+  public function updateBlock($region, $uuid, array $configuration) {
+    if (!isset($this->section[$region])) {
+      throw new \InvalidArgumentException('Invalid region');
+    }
+
+    if (!isset($this->section[$region][$uuid])) {
+      throw new \InvalidArgumentException('Invalid UUID');
+    }
+
+    $this->section[$region][$uuid] = $configuration;
+
+    return $this;
+  }
+
+  /**
+   * Removes a given block from a region.
+   *
+   * @param string $region
+   *   The region name.
+   * @param string $uuid
+   *   The UUID of the block to remove.
+   *
+   * @return $this
+   */
+  public function removeBlock($region, $uuid) {
+    unset($this->section[$region][$uuid]);
+    $this->section = array_filter($this->section);
+    return $this;
+  }
+
+  /**
+   * Adds a block to the front of a region.
+   *
+   * @param string $region
+   *   The region name.
+   * @param string $uuid
+   *   The UUID of the block to add.
+   * @param array $configuration
+   *   The block configuration.
+   *
+   * @return $this
+   */
+  public function addBlock($region, $uuid, array $configuration) {
+    $this->section += [$region => []];
+    $this->section[$region] = array_merge([$uuid => $configuration], $this->section[$region]);
+    return $this;
+  }
+
+  /**
+   * Inserts a block after a specified existing block in a region.
+   *
+   * @param string $region
+   *   The region name.
+   * @param string $uuid
+   *   The UUID of the block to insert.
+   * @param array $configuration
+   *   The block configuration.
+   * @param string $preceding_uuid
+   *   The UUID of the existing block to insert after.
+   *
+   * @return $this
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown when the expected region does not exist.
+   */
+  public function insertBlock($region, $uuid, array $configuration, $preceding_uuid) {
+    if (!isset($this->section[$region])) {
+      throw new \InvalidArgumentException('Invalid region');
+    }
+
+    $slice_id = array_search($preceding_uuid, array_keys($this->section[$region]));
+    if ($slice_id === FALSE) {
+      throw new \InvalidArgumentException('Invalid preceding UUID');
+    }
+
+    $before = array_slice($this->section[$region], 0, $slice_id + 1);
+    $after = array_slice($this->section[$region], $slice_id + 1);
+    $this->section[$region] = array_merge($before, [$uuid => $configuration], $after);
+    return $this;
+  }
+
+}
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 0000000000000000000000000000000000000000..61e481c7c7c8d94be39a51309f002e4942cd70a0
--- /dev/null
+++ b/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php
@@ -0,0 +1,352 @@
+<?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' => [
+                'block' => [
+                  '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' => [
+                'block' => [
+                  'id' => 'system_powered_by_block',
+                ],
+              ],
+            ],
+          ],
+        ],
+      ],
+      '.layout--onecol',
+      'Powered by',
+      '',
+      '',
+      'MISS',
+    ];
+    $data['multiple_sections'] = [
+      [
+        [
+          'layout' => 'layout_onecol',
+          'section' => [
+            'content' => [
+              'baz' => [
+                'block' => [
+                  'id' => 'system_powered_by_block',
+                ],
+              ],
+            ],
+          ],
+        ],
+        [
+          'layout' => 'layout_twocol',
+          'section' => [
+            'first' => [
+              'foo' => [
+                'block' => [
+                  'id' => 'test_block_instantiation',
+                  'display_message' => 'foo text',
+                ],
+              ],
+            ],
+            'second' => [
+              'bar' => [
+                'block' => [
+                  '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) {
+    $node = $this->createSectionNode($layout_data);
+
+    $this->drupalGet($node->toUrl('canonical'));
+    $this->assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, $expected_dynamic_cache);
+
+    $this->drupalGet($node->toUrl('layout-builder'));
+    $this->assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, 'UNCACHEABLE');
+  }
+
+  /**
+   * Tests the access checking of the section formatter.
+   */
+  public function testLayoutSectionFormatterAccess() {
+    $node = $this->createSectionNode([
+      [
+        'layout' => 'layout_onecol',
+        'section' => [
+          'content' => [
+            'baz' => [
+              'block' => [
+                'id' => 'test_access',
+              ],
+            ],
+          ],
+        ],
+      ],
+    ]);
+
+    // Restrict access to the block.
+    $this->container->get('state')->set('test_block_access', FALSE);
+
+    $this->drupalGet($node->toUrl('canonical'));
+    $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->toUrl('canonical'));
+    $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' => [
+              'block' => [
+                'id' => 'system_powered_by_block',
+              ],
+            ],
+          ],
+        ],
+      ],
+    ]);
+    $entity->addTranslation('es', [
+      'title' => 'Translated node title',
+      $this->fieldName => [
+        [
+          'layout' => 'layout_twocol',
+          'section' => [
+            'first' => [
+              'foo' => [
+                'block' => [
+                  'id' => 'test_block_instantiation',
+                  'display_message' => 'foo text',
+                ],
+              ],
+            ],
+            'second' => [
+              'bar' => [
+                'block' => [
+                  'id' => 'test_block_instantiation',
+                  'display_message' => 'bar text',
+                ],
+              ],
+            ],
+          ],
+        ],
+      ],
+    ]);
+    $entity->save();
+
+    $this->drupalGet($entity->toUrl('canonical'));
+    $this->assertLayoutSection('.layout--onecol', 'Powered by');
+    $this->drupalGet($entity->toUrl('canonical')->setOption('prefix', 'es/'));
+    $this->assertLayoutSection('.layout--twocol', ['foo text', 'bar text']);
+  }
+
+  /**
+   * Ensures that the entity title is displayed.
+   */
+  public function testLayoutPageTitle() {
+    $this->drupalPlaceBlock('page_title_block');
+    $node = $this->createSectionNode([]);
+
+    $this->drupalGet($node->toUrl('layout-builder'));
+    $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() {
+    $node = $this->createNode([
+      'type' => 'bundle_without_section_field',
+      'title' => 'The node title',
+      'body' => [
+        [
+          'value' => 'The node body',
+        ],
+      ],
+    ]);
+    $node->save();
+    $this->drupalGet($node->toUrl('layout-builder'));
+    $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/Kernel/LayoutBuilderFieldLayoutCompatibilityTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderFieldLayoutCompatibilityTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..c3a23c78710fb271ef18d1682720e0db39d89ad1
--- /dev/null
+++ b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderFieldLayoutCompatibilityTest.php
@@ -0,0 +1,157 @@
+<?php
+
+namespace Drupal\Tests\layout_builder\Kernel;
+
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\entity_test\Entity\EntityTestBaseFieldDisplay;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Ensures that Layout Builder and Field Layout are compatible with each other.
+ *
+ * @group layout_builder
+ */
+class LayoutBuilderFieldLayoutCompatibilityTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'layout_discovery',
+    'field_layout',
+    'user',
+    'field',
+    'entity_test',
+    'system',
+    'text',
+    'filter',
+  ];
+
+  /**
+   * The entity view display.
+   *
+   * @var \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface
+   */
+  protected $display;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installEntitySchema('entity_test_base_field_display');
+    $this->installEntitySchema('user');
+    $this->installSchema('system', ['sequences', 'key_value']);
+    $this->installConfig(['field', 'filter', 'user', 'system']);
+
+    \Drupal::service('theme_handler')->install(['classy']);
+    $this->config('system.theme')->set('default', 'classy')->save();
+
+    $field_storage = FieldStorageConfig::create([
+      'entity_type' => 'entity_test_base_field_display',
+      'field_name' => 'test_field_display_configurable',
+      'type' => 'boolean',
+    ]);
+    $field_storage->save();
+    FieldConfig::create([
+      'field_storage' => $field_storage,
+      'bundle' => 'entity_test_base_field_display',
+      'label' => 'FieldConfig with configurable display',
+    ])->save();
+
+    $this->display = EntityViewDisplay::create([
+      'targetEntityType' => 'entity_test_base_field_display',
+      'bundle' => 'entity_test_base_field_display',
+      'mode' => 'default',
+      'status' => TRUE,
+    ]);
+    $this->display
+      ->setComponent('test_field_display_configurable', ['region' => 'content'])
+      ->setLayoutId('layout_twocol')
+      ->save();
+  }
+
+  /**
+   * Tests the compatibility of Layout Builder and Field Layout.
+   */
+  public function testCompatibility() {
+    // Create an entity with fields that are configurable and non-configurable.
+    $entity_storage = $this->container->get('entity_type.manager')->getStorage('entity_test_base_field_display');
+    // @todo Remove langcode workarounds after resolving
+    //   https://www.drupal.org/node/2915034.
+    $entity = $entity_storage->createWithSampleValues('entity_test_base_field_display', [
+      'langcode' => 'en',
+      'langcode_default' => TRUE,
+    ]);
+    $entity->save();
+
+    // Ensure that the configurable field is shown in the correct region and
+    // that the non-configurable field is shown outside the layout.
+    $original_markup = $this->renderEntity($entity);
+    $this->assertNotEmpty($this->cssSelect('.layout__region--first .field--name-test-display-configurable'));
+    $this->assertNotEmpty($this->cssSelect('.layout__region--first .field--name-test-field-display-configurable'));
+    $this->assertNotEmpty($this->cssSelect('.field--name-test-display-non-configurable'));
+    $this->assertEmpty($this->cssSelect('.layout__region .field--name-test-display-non-configurable'));
+
+    // Install the Layout Builder, configure it for this entity display, and
+    // reload the entity.
+    $this->enableModules(['layout_builder']);
+    $this->display->setThirdPartySetting('layout_builder', 'allow_custom', TRUE)->save();
+    $entity = EntityTestBaseFieldDisplay::load($entity->id());
+
+    // Without using Layout Builder for an override, the result has not changed.
+    $new_markup = $this->renderEntity($entity);
+    $this->assertSame($original_markup, $new_markup);
+
+    // Add a layout override.
+    /** @var \Drupal\layout_builder\Field\LayoutSectionItemListInterface $field_list */
+    $field_list = $entity->layout_builder__layout;
+    $field_list->appendItem([
+      'layout' => 'layout_onecol',
+      'layout_settings' => [],
+      'section' => [],
+    ]);
+    $entity->save();
+
+    // The rendered entity has now changed. The non-configurable field is shown
+    // outside the layout, the configurable field is not shown at all, and the
+    // layout itself is rendered (but empty).
+    $new_markup = $this->renderEntity($entity);
+    $this->assertNotSame($original_markup, $new_markup);
+    $this->assertEmpty($this->cssSelect('.field--name-test-display-configurable'));
+    $this->assertEmpty($this->cssSelect('.field--name-test-field-display-configurable'));
+    $this->assertNotEmpty($this->cssSelect('.field--name-test-display-non-configurable'));
+    $this->assertNotEmpty($this->cssSelect('.layout--onecol'));
+
+    // Removing the layout restores the original rendering of the entity.
+    $field_list->removeItem(0);
+    $entity->save();
+    $new_markup = $this->renderEntity($entity);
+    $this->assertSame($original_markup, $new_markup);
+  }
+
+  /**
+   * Renders the provided entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity to render.
+   * @param string $view_mode
+   *   (optional) The view mode that should be used to render the entity.
+   * @param string $langcode
+   *   (optional) For which language the entity should be rendered, defaults to
+   *   the current content language.
+   *
+   * @return string
+   *   The rendered string output (typically HTML).
+   */
+  protected function renderEntity(EntityInterface $entity, $view_mode = 'full', $langcode = NULL) {
+    $view_builder = $this->container->get('entity_type.manager')->getViewBuilder($entity->getEntityTypeId());
+    $build = $view_builder->view($entity, $view_mode, $langcode);
+    return $this->render($build);
+  }
+
+}
diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..0e13471c155814143b703687a788165ceb967cef
--- /dev/null
+++ b/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemTest.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Drupal\Tests\layout_builder\Kernel;
+
+use Drupal\Core\Field\FieldItemInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\layout_builder\Field\LayoutSectionItemInterface;
+use Drupal\layout_builder\Field\LayoutSectionItemListInterface;
+use Drupal\Tests\field\Kernel\FieldKernelTestBase;
+
+/**
+ * Tests the field type for Layout Sections.
+ *
+ * @group layout_builder
+ */
+class LayoutSectionItemTest extends FieldKernelTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['layout_builder', 'layout_discovery'];
+
+  /**
+   * Tests using entity fields of the layout section field type.
+   */
+  public function testLayoutSectionItem() {
+    layout_builder_add_layout_section_field('entity_test', 'entity_test');
+
+    $entity = EntityTest::create();
+    /** @var \Drupal\layout_builder\Field\LayoutSectionItemListInterface $field_list */
+    $field_list = $entity->layout_builder__layout;
+
+    // Test sample item generation.
+    $field_list->generateSampleItems();
+    $this->entityValidateAndSave($entity);
+
+    $field = $field_list->get(0);
+    $this->assertInstanceOf(LayoutSectionItemInterface::class, $field);
+    $this->assertInstanceOf(FieldItemInterface::class, $field);
+    $this->assertSame('section', $field->mainPropertyName());
+    $this->assertSame('layout_onecol', $field->layout);
+    $this->assertSame([], $field->layout_settings);
+    $this->assertSame([], $field->section);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function testLayoutSectionItemList() {
+    layout_builder_add_layout_section_field('entity_test', 'entity_test');
+
+    $entity = EntityTest::create();
+    /** @var \Drupal\layout_builder\Field\LayoutSectionItemListInterface $field_list */
+    $field_list = $entity->layout_builder__layout;
+    $this->assertInstanceOf(LayoutSectionItemListInterface::class, $field_list);
+    $this->assertInstanceOf(FieldItemListInterface::class, $field_list);
+    $entity->save();
+
+    $field_list->appendItem(['layout' => 'layout_twocol']);
+    $field_list->appendItem(['layout' => 'layout_onecol']);
+    $field_list->appendItem(['layout' => 'layout_threecol_25_50_25']);
+    $this->assertSame([
+      ['layout' => 'layout_twocol'],
+      ['layout' => 'layout_onecol'],
+      ['layout' => 'layout_threecol_25_50_25'],
+    ], $field_list->getValue());
+
+    $field_list->addItem(1, ['layout' => 'layout_threecol_33_34_33']);
+    $this->assertSame([
+      ['layout' => 'layout_twocol'],
+      ['layout' => 'layout_threecol_33_34_33'],
+      ['layout' => 'layout_onecol'],
+      ['layout' => 'layout_threecol_25_50_25'],
+    ], $field_list->getValue());
+
+    $field_list->addItem($field_list->count(), ['layout' => 'layout_twocol_bricks']);
+    $this->assertSame([
+      ['layout' => 'layout_twocol'],
+      ['layout' => 'layout_threecol_33_34_33'],
+      ['layout' => 'layout_onecol'],
+      ['layout' => 'layout_threecol_25_50_25'],
+      ['layout' => 'layout_twocol_bricks'],
+    ], $field_list->getValue());
+  }
+
+}
diff --git a/core/modules/layout_builder/tests/src/Unit/LayoutBuilderRouteEnhancerTest.php b/core/modules/layout_builder/tests/src/Unit/LayoutBuilderRouteEnhancerTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..2904480d024e894e5fcf2eb5e0bd4de4f1a9d522
--- /dev/null
+++ b/core/modules/layout_builder/tests/src/Unit/LayoutBuilderRouteEnhancerTest.php
@@ -0,0 +1,146 @@
+<?php
+
+namespace Drupal\Tests\layout_builder\Unit;
+
+use Drupal\layout_builder\Routing\LayoutBuilderRouteEnhancer;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+
+/**
+ * @coversDefaultClass \Drupal\layout_builder\Routing\LayoutBuilderRouteEnhancer
+ * @group layout_builder
+ */
+class LayoutBuilderRouteEnhancerTest extends UnitTestCase {
+
+  /**
+   * @covers ::applies
+   * @dataProvider providerTestApplies
+   */
+  public function testApplies($defaults, $options, $expected) {
+    $route_enhancer = new LayoutBuilderRouteEnhancer();
+    $route = new Route('/some/path', $defaults, [], $options);
+
+    $reflection_method = new \ReflectionMethod($route_enhancer, 'applies');
+    $reflection_method->setAccessible(TRUE);
+    $result = $reflection_method->invoke($route_enhancer, $route);
+    $this->assertSame($expected, $result);
+  }
+
+  /**
+   * Provides test data for ::testApplies().
+   */
+  public function providerTestApplies() {
+    $data = [];
+    $data['layout_builder_true'] = [
+      ['entity_type_id' => 'the_entity_type'],
+      ['_layout_builder' => TRUE],
+      TRUE,
+    ];
+    $data['layout_builder_false'] = [
+      ['entity_type_id' => 'the_entity_type'],
+      ['_layout_builder' => FALSE],
+      FALSE,
+    ];
+    $data['layout_builder_null'] = [
+      ['entity_type_id' => 'the_entity_type'],
+      ['_layout_builder' => NULL],
+      FALSE,
+    ];
+    $data['entity_type_id_empty'] = [
+      ['entity_type_id' => ''],
+      ['_layout_builder' => TRUE],
+      FALSE,
+    ];
+    $data['no_entity_type_id'] = [
+      [],
+      ['_layout_builder' => TRUE],
+      FALSE,
+    ];
+    $data['no_layout_builder'] = [
+      ['entity_type_id' => 'the_entity_type'],
+      [],
+      FALSE,
+    ];
+    $data['empty'] = [
+      [],
+      [],
+      FALSE,
+    ];
+    return $data;
+  }
+
+  /**
+   * @covers ::enhance
+   */
+  public function testEnhanceValidDefaults() {
+    $route = new Route('/the/path', ['entity_type_id' => 'the_entity_type'], [], ['_layout_builder' => TRUE]);
+    $route_enhancer = new LayoutBuilderRouteEnhancer();
+    $object = new \stdClass();
+    $defaults = [
+      'entity_type_id' => 'the_entity_type',
+      'the_entity_type' => $object,
+      RouteObjectInterface::ROUTE_NAME => 'the_route_name',
+      RouteObjectInterface::ROUTE_OBJECT => $route,
+    ];
+    // Ensure that the 'entity' key now contains the value stored for a given
+    // entity type.
+    $expected = [
+      'entity_type_id' => 'the_entity_type',
+      'the_entity_type' => $object,
+      RouteObjectInterface::ROUTE_NAME => 'the_route_name',
+      RouteObjectInterface::ROUTE_OBJECT => $route,
+      'entity' => $object,
+      'is_rebuilding' => TRUE,
+    ];
+    $result = $route_enhancer->enhance($defaults, new Request(['layout_is_rebuilding' => TRUE]));
+    $this->assertEquals($expected, $result);
+
+    $expected['is_rebuilding'] = FALSE;
+    $result = $route_enhancer->enhance($defaults, new Request());
+    $this->assertEquals($expected, $result);
+    $this->assertSame($object, $result['entity']);
+
+    // Modifying the original value updates the 'entity' copy.
+    $result['the_entity_type'] = 'something else';
+    $this->assertSame('something else', $result['entity']);
+  }
+
+  /**
+   * @covers ::enhance
+   */
+  public function testEnhanceMissingEntity() {
+    $route_enhancer = new LayoutBuilderRouteEnhancer();
+    $route = new Route('/the/path', ['entity_type_id' => 'the_entity_type'], [], ['_layout_builder' => TRUE]);
+    $defaults = [
+      RouteObjectInterface::ROUTE_NAME => 'the_route',
+      RouteObjectInterface::ROUTE_OBJECT => $route,
+      'entity_type_id' => 'the_entity_type',
+    ];
+    $this->setExpectedException(\RuntimeException::class, 'Failed to find the "the_entity_type" entity in route named the_route');
+    $route_enhancer->enhance($defaults, new Request());
+  }
+
+  /**
+   * Provides test data for ::testEnhanceException().
+   */
+  public function providerTestEnhanceException() {
+    $data = [];
+    $data['missing_entity'] = [
+      [
+        RouteObjectInterface::ROUTE_NAME => 'the_route',
+        'entity_type_id' => 'the_entity_type',
+      ],
+      'Failed to find the "the_entity_type" entity in route named the_route',
+    ];
+    $data['missing_entity_type_id'] = [
+      [
+        RouteObjectInterface::ROUTE_NAME => 'the_route',
+      ],
+      'Failed to find an entity type ID in route named the_route',
+    ];
+    return $data;
+  }
+
+}
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 0000000000000000000000000000000000000000..3e5b2ffad148a47c1ff54354b0befdbb698d0d7b
--- /dev/null
+++ b/core/modules/layout_builder/tests/src/Unit/LayoutSectionBuilderTest.php
@@ -0,0 +1,301 @@
+<?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;
+use Prophecy\Argument;
+
+/**
+ * @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' => [
+          'block' => [
+            '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' => [
+          'block' => [
+            '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' => [
+          'block' => [
+            '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' => [
+          'block' => [],
+        ],
+      ],
+    ];
+    $this->setExpectedException(PluginException::class, 'No plugin ID specified for block with "some_uuid" UUID');
+    $this->layoutSectionBuilder->buildSection('layout_onecol', [], $section);
+  }
+
+  /**
+   * @covers ::buildSection
+   *
+   * @dataProvider providerTestBuildSectionMalformedData
+   */
+  public function testBuildSectionMalformedData($section, $message) {
+    $this->layout->build(Argument::type('array'))->willReturnArgument(0);
+    $this->layout->getPluginId()->willReturn('the_plugin_id');
+    $this->setExpectedException(\InvalidArgumentException::class, $message);
+    $this->layoutSectionBuilder->buildSection('layout_onecol', [], $section);
+  }
+
+  /**
+   * Provides test data for ::testBuildSectionMalformedData().
+   */
+  public function providerTestBuildSectionMalformedData() {
+    $data = [];
+    $data['invalid_region'] = [
+      ['content' => 'bar'],
+      'The "content" region in the "the_plugin_id" layout has invalid configuration',
+    ];
+    $data['invalid_configuration'] = [
+      ['content' => ['some_uuid' => 'bar']],
+      'The block with UUID of "some_uuid" has invalid configuration',
+    ];
+    $data['invalid_blocks'] = [
+      ['content' => ['some_uuid' => []]],
+      'The block with UUID of "some_uuid" has invalid configuration',
+    ];
+    return $data;
+  }
+
+}
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 0000000000000000000000000000000000000000..a652d4a56d5ce8fb0a72cf252b2fc888dcbb2ff0
--- /dev/null
+++ b/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreRepositoryTest.php
@@ -0,0 +1,133 @@
+<?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);
+  }
+
+  /**
+   * @covers ::get
+   */
+  public function testGetInvalidEntity() {
+    $tempstore = $this->prophesize(SharedTempStore::class);
+    $tempstore->get('the_entity_id.en')->willReturn(['entity' => 'this_is_not_an_entity']);
+
+    $tempstore_factory = $this->prophesize(SharedTempStoreFactory::class);
+    $tempstore_factory->get('the_entity_type_id.layout_builder__layout')->willReturn($tempstore->reveal());
+
+    $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class);
+
+    $repository = new LayoutTempstoreRepository($tempstore_factory->reveal(), $entity_type_manager->reveal());
+
+    $entity = $this->prophesize(EntityInterface::class);
+    $entity->language()->willReturn(new Language(['id' => 'en']));
+    $entity->getEntityTypeId()->willReturn('the_entity_type_id');
+    $entity->id()->willReturn('the_entity_id');
+
+    $this->setExpectedException(\UnexpectedValueException::class, 'The entry with entity type "the_entity_type_id" and ID "the_entity_id.en" is not a valid entity');
+    $repository->get($entity->reveal());
+  }
+
+}
diff --git a/core/modules/layout_builder/tests/src/Unit/SectionTest.php b/core/modules/layout_builder/tests/src/Unit/SectionTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..33a338706e27ec78aace18dc38969453ae03363e
--- /dev/null
+++ b/core/modules/layout_builder/tests/src/Unit/SectionTest.php
@@ -0,0 +1,265 @@
+<?php
+
+namespace Drupal\Tests\layout_builder\Unit;
+
+use Drupal\layout_builder\Section;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\layout_builder\Section
+ * @group layout_builder
+ */
+class SectionTest extends UnitTestCase {
+
+  /**
+   * The section object to test.
+   *
+   * @var \Drupal\layout_builder\Section
+   */
+  protected $section;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->section = new Section([
+      'empty-region' => [],
+      'some-region' => [
+        'existing-uuid' => [
+          'block' => [
+            'id' => 'existing-block-id',
+          ],
+        ],
+      ],
+      'ordered-region' => [
+        'first-uuid' => [
+          'block' => [
+            'id' => 'first-block-id',
+          ],
+        ],
+        'second-uuid' => [
+          'block' => [
+            'id' => 'second-block-id',
+          ],
+        ],
+      ],
+    ]);
+  }
+
+  /**
+   * @covers ::__construct
+   * @covers ::getValue
+   */
+  public function testGetValue() {
+    $expected = [
+      'empty-region' => [],
+      'some-region' => [
+        'existing-uuid' => [
+          'block' => [
+            'id' => 'existing-block-id',
+          ],
+        ],
+      ],
+      'ordered-region' => [
+        'first-uuid' => [
+          'block' => [
+            'id' => 'first-block-id',
+          ],
+        ],
+        'second-uuid' => [
+          'block' => [
+            'id' => 'second-block-id',
+          ],
+        ],
+      ],
+    ];
+    $result = $this->section->getValue();
+    $this->assertSame($expected, $result);
+  }
+
+  /**
+   * @covers ::getBlock
+   */
+  public function testGetBlockInvalidRegion() {
+    $this->setExpectedException(\InvalidArgumentException::class, 'Invalid region');
+    $this->section->getBlock('invalid-region', 'existing-uuid');
+  }
+
+  /**
+   * @covers ::getBlock
+   */
+  public function testGetBlockInvalidUuid() {
+    $this->setExpectedException(\InvalidArgumentException::class, 'Invalid UUID');
+    $this->section->getBlock('some-region', 'invalid-uuid');
+  }
+
+  /**
+   * @covers ::getBlock
+   */
+  public function testGetBlock() {
+    $expected = ['block' => ['id' => 'existing-block-id']];
+
+    $block = $this->section->getBlock('some-region', 'existing-uuid');
+    $this->assertSame($expected, $block);
+  }
+
+  /**
+   * @covers ::removeBlock
+   */
+  public function testRemoveBlock() {
+    $this->section->removeBlock('some-region', 'existing-uuid');
+    $expected = [
+      'ordered-region' => [
+        'first-uuid' => [
+          'block' => [
+            'id' => 'first-block-id',
+          ],
+        ],
+        'second-uuid' => [
+          'block' => [
+            'id' => 'second-block-id',
+          ],
+        ],
+      ],
+    ];
+    $this->assertSame($expected, $this->section->getValue());
+  }
+
+  /**
+   * @covers ::addBlock
+   */
+  public function testAddBlock() {
+    $this->section->addBlock('some-region', 'new-uuid', []);
+    $expected = [
+      'empty-region' => [],
+      'some-region' => [
+        'new-uuid' => [],
+        'existing-uuid' => [
+          'block' => [
+            'id' => 'existing-block-id',
+          ],
+        ],
+      ],
+      'ordered-region' => [
+        'first-uuid' => [
+          'block' => [
+            'id' => 'first-block-id',
+          ],
+        ],
+        'second-uuid' => [
+          'block' => [
+            'id' => 'second-block-id',
+          ],
+        ],
+      ],
+    ];
+    $this->assertSame($expected, $this->section->getValue());
+  }
+
+  /**
+   * @covers ::insertBlock
+   */
+  public function testInsertBlock() {
+    $this->section->insertBlock('ordered-region', 'new-uuid', [], 'first-uuid');
+    $expected = [
+      'empty-region' => [],
+      'some-region' => [
+        'existing-uuid' => [
+          'block' => [
+            'id' => 'existing-block-id',
+          ],
+        ],
+      ],
+      'ordered-region' => [
+        'first-uuid' => [
+          'block' => [
+            'id' => 'first-block-id',
+          ],
+        ],
+        'new-uuid' => [],
+        'second-uuid' => [
+          'block' => [
+            'id' => 'second-block-id',
+          ],
+        ],
+      ],
+    ];
+    $this->assertSame($expected, $this->section->getValue());
+  }
+
+  /**
+   * @covers ::insertBlock
+   */
+  public function testInsertBlockInvalidRegion() {
+    $this->setExpectedException(\InvalidArgumentException::class, 'Invalid region');
+    $this->section->insertBlock('invalid-region', 'new-uuid', [], 'first-uuid');
+  }
+
+  /**
+   * @covers ::insertBlock
+   */
+  public function testInsertBlockInvalidUuid() {
+    $this->setExpectedException(\InvalidArgumentException::class, 'Invalid preceding UUID');
+    $this->section->insertBlock('ordered-region', 'new-uuid', [], 'invalid-uuid');
+  }
+
+  /**
+   * @covers ::updateBlock
+   */
+  public function testUpdateBlock() {
+    $this->section->updateBlock('some-region', 'existing-uuid', [
+      'block' => [
+        'id' => 'existing-block-id',
+        'settings' => [
+          'foo' => 'bar',
+        ],
+      ],
+    ]);
+
+    $expected = [
+      'empty-region' => [],
+      'some-region' => [
+        'existing-uuid' => [
+          'block' => [
+            'id' => 'existing-block-id',
+            'settings' => [
+              'foo' => 'bar',
+            ],
+          ],
+        ],
+      ],
+      'ordered-region' => [
+        'first-uuid' => [
+          'block' => [
+            'id' => 'first-block-id',
+          ],
+        ],
+        'second-uuid' => [
+          'block' => [
+            'id' => 'second-block-id',
+          ],
+        ],
+      ],
+    ];
+    $this->assertSame($expected, $this->section->getValue());
+  }
+
+  /**
+   * @covers ::updateBlock
+   */
+  public function testUpdateBlockInvalidRegion() {
+    $this->setExpectedException(\InvalidArgumentException::class, 'Invalid region');
+    $this->section->updateBlock('invalid-region', 'new-uuid', []);
+  }
+
+  /**
+   * @covers ::updateBlock
+   */
+  public function testUpdateBlockInvalidUuid() {
+    $this->setExpectedException(\InvalidArgumentException::class, 'Invalid UUID');
+    $this->section->updateBlock('ordered-region', 'new-uuid', []);
+  }
+
+}