diff --git a/core/composer.json b/core/composer.json
index 53ab8fa..c52add0 100644
--- a/core/composer.json
+++ b/core/composer.json
@@ -94,6 +94,7 @@
         "drupal/editor": "self.version",
         "drupal/entity_reference": "self.version",
         "drupal/field": "self.version",
+        "drupal/field_layout": "self.version",
         "drupal/field_ui": "self.version",
         "drupal/file": "self.version",
         "drupal/filter": "self.version",
@@ -104,6 +105,7 @@
         "drupal/image": "self.version",
         "drupal/inline_form_errors": "self.version",
         "drupal/language": "self.version",
+        "drupal/layout_plugin": "self.version",
         "drupal/link": "self.version",
         "drupal/locale": "self.version",
         "drupal/minimal": "self.version",
diff --git a/core/config/schema/core.entity.schema.yml b/core/config/schema/core.entity.schema.yml
index bf0e12d..711ca0f 100644
--- a/core/config/schema/core.entity.schema.yml
+++ b/core/config/schema/core.entity.schema.yml
@@ -64,6 +64,9 @@ core.entity_view_display.*.*.*:
           weight:
             type: integer
             label: 'Weight'
+          region:
+            type: string
+            label: 'Region'
           label:
              type: string
              label: 'Label setting machine name'
@@ -115,6 +118,9 @@ core.entity_form_display.*.*.*:
           weight:
             type: integer
             label: 'Weight'
+          region:
+            type: string
+            label: 'Region'
           settings:
             type: field.widget.settings.[%parent.type]
             label: 'Settings'
diff --git a/core/lib/Drupal/Core/Entity/EntityDisplayBase.php b/core/lib/Drupal/Core/Entity/EntityDisplayBase.php
index 4de891a..eddcb5e 100644
--- a/core/lib/Drupal/Core/Entity/EntityDisplayBase.php
+++ b/core/lib/Drupal/Core/Entity/EntityDisplayBase.php
@@ -154,6 +154,7 @@ public function __construct(array $values, $entity_type) {
   protected function init() {
     // Only populate defaults for "official" view modes and form modes.
     if ($this->mode !== static::CUSTOM_MODE) {
+      $default_region = $this->getDefaultRegion();
       // Fill in defaults for extra fields.
       $context = $this->displayContext == 'view' ? 'display' : $this->displayContext;
       $extra_fields = \Drupal::entityManager()->getExtraFields($this->targetEntityType, $this->bundle);
@@ -170,6 +171,10 @@ protected function init() {
             $this->hidden[$name] = TRUE;
           }
         }
+        // Ensure extra fields have a 'region'.
+        if (isset($this->content[$name])) {
+          $this->content[$name] += ['region' => $default_region];
+        }
       }
 
       // Fill in defaults for fields.
@@ -178,10 +183,17 @@ protected function init() {
         if (!$definition->isDisplayConfigurable($this->displayContext) || (!isset($this->content[$name]) && !isset($this->hidden[$name]))) {
           $options = $definition->getDisplayOptions($this->displayContext);
 
-          if (!empty($options['type']) && $options['type'] == 'hidden') {
+          // @todo Remove handling of 'type' in https://www.drupal.org/node/2799641.
+          if (!isset($options['region']) && !empty($options['type']) && $options['type'] === 'hidden') {
+            $options['region'] = 'hidden';
+            @trigger_error("Specifying 'type' => 'hidden' is deprecated, use 'region' => 'hidden' instead.", E_USER_DEPRECATED);
+          }
+
+          if (!empty($options['region']) && $options['region'] === 'hidden') {
             $this->hidden[$name] = TRUE;
           }
           elseif ($options) {
+            $options += ['region' => $default_region];
             $this->content[$name] = $this->pluginManager->prepareConfiguration($definition->getType(), $options);
           }
           // Note: (base) fields that do not specify display options are not
@@ -239,12 +251,40 @@ public function id() {
    * {@inheritdoc}
    */
   public function preSave(EntityStorageInterface $storage, $update = TRUE) {
+    // Ensure that a region is set on each component.
+    foreach ($this->getComponents() as $name => $component) {
+      $this->handleHiddenType($name, $component);
+      // Ensure that a region is set.
+      if (isset($this->content[$name]) && !isset($component['region'])) {
+        // Directly set the component to bypass other changes in setComponent().
+        $this->content[$name]['region'] = $this->getDefaultRegion();
+      }
+    }
+
     ksort($this->content);
     ksort($this->hidden);
     parent::preSave($storage, $update);
   }
 
   /**
+   * Handles a component type of 'hidden'.
+   *
+   * @deprecated This method exists only for backwards compatibility.
+   *
+   * @todo Remove this in https://www.drupal.org/node/2799641.
+   *
+   * @param string $name
+   *   The name of the component.
+   * @param array $component
+   *   The component array.
+   */
+  protected function handleHiddenType($name, array $component) {
+    if (!isset($component['region']) && isset($component['type']) && $component['type'] === 'hidden') {
+      $this->removeComponent($name);
+    }
+  }
+
+  /**
    * {@inheritdoc}
    */
   public function calculateDependencies() {
@@ -505,6 +545,16 @@ protected function getPluginRemovedDependencies(array $plugin_dependencies, arra
   }
 
   /**
+   * Gets the default region.
+   *
+   * @return string
+   *   The default region for this display.
+   */
+  protected function getDefaultRegion() {
+    return 'content';
+  }
+
+  /**
    * {@inheritdoc}
    */
   public function __sleep() {
diff --git a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php
index 594f31d..e28d997 100644
--- a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php
+++ b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php
@@ -414,7 +414,7 @@ public function setDisplayOptions($display_context, array $options) {
   public function setDisplayConfigurable($display_context, $configurable) {
     // If no explicit display options have been specified, default to 'hidden'.
     if (empty($this->definition['display'][$display_context])) {
-      $this->definition['display'][$display_context]['options'] = array('type' => 'hidden');
+      $this->definition['display'][$display_context]['options'] = array('region' => 'hidden');
     }
     $this->definition['display'][$display_context]['configurable'] = $configurable;
     return $this;
diff --git a/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.default.yml b/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.default.yml
index e3c23cf..e0232cf 100644
--- a/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.default.yml
+++ b/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.default.yml
@@ -11,20 +11,26 @@ content:
   checked:
     type: timestamp_ago
     weight: 1
+    region: content
     settings: {  }
     third_party_settings: {  }
     label: inline
   description:
     weight: 3
+    region: content
   feed_icon:
     weight: 5
+    region: content
   image:
     weight: 2
+    region: content
   items:
     weight: 0
+    region: content
   link:
     type: uri_link
     weight: 4
+    region: content
     settings: {  }
     third_party_settings: {  }
     label: inline
diff --git a/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.summary.yml b/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.summary.yml
index 40425f2..5e5e468 100644
--- a/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.summary.yml
+++ b/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.summary.yml
@@ -12,8 +12,10 @@ mode: summary
 content:
   items:
     weight: 0
+    region: content
   more_link:
     weight: 1
+    region: content
 hidden:
   checked: true
   description: true
diff --git a/core/modules/aggregator/config/install/core.entity_view_display.aggregator_item.aggregator_item.summary.yml b/core/modules/aggregator/config/install/core.entity_view_display.aggregator_item.aggregator_item.summary.yml
index 837bee0..8e29395 100644
--- a/core/modules/aggregator/config/install/core.entity_view_display.aggregator_item.aggregator_item.summary.yml
+++ b/core/modules/aggregator/config/install/core.entity_view_display.aggregator_item.aggregator_item.summary.yml
@@ -12,6 +12,7 @@ mode: summary
 content:
   timestamp:
     weight: 0
+    region: content
 hidden:
   author: true
   description: true
diff --git a/core/modules/book/config/install/core.entity_form_display.node.book.default.yml b/core/modules/book/config/install/core.entity_form_display.node.book.default.yml
index 1ec4eb1..58aba45 100644
--- a/core/modules/book/config/install/core.entity_form_display.node.book.default.yml
+++ b/core/modules/book/config/install/core.entity_form_display.node.book.default.yml
@@ -14,6 +14,7 @@ content:
   body:
     type: text_textarea_with_summary
     weight: 26
+    region: content
     settings:
       rows: 9
       summary_rows: 3
@@ -22,6 +23,7 @@ content:
   created:
     type: datetime_timestamp
     weight: 10
+    region: content
     settings: {  }
     third_party_settings: {  }
   promote:
@@ -29,16 +31,19 @@ content:
     settings:
       display_label: true
     weight: 15
+    region: content
     third_party_settings: {  }
   sticky:
     type: boolean_checkbox
     settings:
       display_label: true
     weight: 16
+    region: content
     third_party_settings: {  }
   title:
     type: string_textfield
     weight: -5
+    region: content
     settings:
       size: 60
       placeholder: ''
@@ -46,6 +51,7 @@ content:
   uid:
     type: entity_reference_autocomplete
     weight: 5
+    region: content
     settings:
       match_operator: CONTAINS
       size: 60
diff --git a/core/modules/book/config/install/core.entity_view_display.node.book.default.yml b/core/modules/book/config/install/core.entity_view_display.node.book.default.yml
index 729516e..d6ef64d 100644
--- a/core/modules/book/config/install/core.entity_view_display.node.book.default.yml
+++ b/core/modules/book/config/install/core.entity_view_display.node.book.default.yml
@@ -16,8 +16,10 @@ content:
     label: hidden
     type: text_default
     weight: 100
+    region: content
     settings: {  }
     third_party_settings: {  }
   links:
     weight: 101
+    region: content
 hidden: {  }
diff --git a/core/modules/book/config/install/core.entity_view_display.node.book.teaser.yml b/core/modules/book/config/install/core.entity_view_display.node.book.teaser.yml
index fb22db6..77a62c3 100644
--- a/core/modules/book/config/install/core.entity_view_display.node.book.teaser.yml
+++ b/core/modules/book/config/install/core.entity_view_display.node.book.teaser.yml
@@ -17,9 +17,11 @@ content:
     label: hidden
     type: text_summary_or_trimmed
     weight: 100
+    region: content
     settings:
       trim_length: 600
     third_party_settings: {  }
   links:
     weight: 101
+    region: content
 hidden: {  }
diff --git a/core/modules/field/src/Entity/FieldConfig.php b/core/modules/field/src/Entity/FieldConfig.php
index d7ccc30..f5e8ab3 100644
--- a/core/modules/field/src/Entity/FieldConfig.php
+++ b/core/modules/field/src/Entity/FieldConfig.php
@@ -306,7 +306,7 @@ public function isDisplayConfigurable($context) {
    */
   public function getDisplayOptions($display_context) {
     // Hide configurable fields by default.
-    return array('type' => 'hidden');
+    return array('region' => 'hidden');
   }
 
   /**
diff --git a/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldFormatterSettingsTest.php b/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldFormatterSettingsTest.php
index 4c9cbf1..2ee3c77 100644
--- a/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldFormatterSettingsTest.php
+++ b/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldFormatterSettingsTest.php
@@ -32,6 +32,7 @@ public function testEntityDisplaySettings() {
       'type' => 'text_trimmed',
       'settings' => array('trim_length' => 600),
       'third_party_settings' => array(),
+      'region' => 'content',
     );
 
     // Can we load any entity display.
diff --git a/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldWidgetSettingsTest.php b/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldWidgetSettingsTest.php
index a16040f..e7bb636 100644
--- a/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldWidgetSettingsTest.php
+++ b/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldWidgetSettingsTest.php
@@ -33,6 +33,7 @@ public function testWidgetSettings() {
     $expected = array('weight' => 1, 'type' => 'text_textfield');
     $expected['settings'] = array('size' => 60, 'placeholder' => '');
     $expected['third_party_settings'] = array();
+    $expected['region'] = 'content';
     $this->assertIdentical($expected, $component, 'Text field settings are correct.');
 
     // Integer field.
diff --git a/core/modules/field_layout/config/schema/field_layout.schema.yml b/core/modules/field_layout/config/schema/field_layout.schema.yml
new file mode 100644
index 0000000..2dd8a83
--- /dev/null
+++ b/core/modules/field_layout/config/schema/field_layout.schema.yml
@@ -0,0 +1,16 @@
+core.entity_view_display.*.*.*.third_party.field_layout:
+  type: field_layout.third_party_settings
+
+core.entity_form_display.*.*.*.third_party.field_layout:
+  type: field_layout.third_party_settings
+
+field_layout.third_party_settings:
+  type: mapping
+  label: 'Per-view mode field layout settings'
+  mapping:
+    id:
+      type: string
+      label: 'Layout ID'
+    settings:
+      type: layout.settings.[%parent.id]
+      label: 'Layout settings'
diff --git a/core/modules/field_layout/field_layout.info.yml b/core/modules/field_layout/field_layout.info.yml
new file mode 100644
index 0000000..67f2ba0
--- /dev/null
+++ b/core/modules/field_layout/field_layout.info.yml
@@ -0,0 +1,8 @@
+name: 'Field Layout'
+type: module
+description: 'Adds layout capabilities to the Field UI.'
+package: Core (Experimental)
+version: VERSION
+core: 8.x
+dependencies:
+  - layout_plugin
diff --git a/core/modules/field_layout/field_layout.install b/core/modules/field_layout/field_layout.install
new file mode 100644
index 0000000..6f94674
--- /dev/null
+++ b/core/modules/field_layout/field_layout.install
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * @file
+ * Contains install and update functions for Field Layout.
+ */
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Entity\Entity\EntityFormDisplay;
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Implements hook_install().
+ */
+function field_layout_install() {
+  // Save each entity display in order to trigger ::preSave().
+  $entity_save = function (EntityInterface $entity) {
+    $entity->save();
+  };
+  array_map($entity_save, EntityViewDisplay::loadMultiple());
+  array_map($entity_save, EntityFormDisplay::loadMultiple());
+
+  // Invalidate the render cache since all content will now have a layout.
+  Cache::invalidateTags(['rendered']);
+}
diff --git a/core/modules/field_layout/field_layout.layouts.yml b/core/modules/field_layout/field_layout.layouts.yml
new file mode 100644
index 0000000..b8c6ef3
--- /dev/null
+++ b/core/modules/field_layout/field_layout.layouts.yml
@@ -0,0 +1,21 @@
+onecol:
+  label: 'One column'
+  path: layouts/onecol
+  template: field-layout--onecol
+  category: 'Columns: 1'
+  default_region: content
+  regions:
+    content:
+      label: Content
+twocol:
+  label: 'Two column'
+  path: layouts/twocol
+  template: field-layout--twocol
+  library: field_layout/drupal.field_layout.twocol
+  category: 'Columns: 2'
+  default_region: left
+  regions:
+    left:
+      label: Left
+    right:
+      label: Right
diff --git a/core/modules/field_layout/field_layout.libraries.yml b/core/modules/field_layout/field_layout.libraries.yml
new file mode 100644
index 0000000..d87df5e
--- /dev/null
+++ b/core/modules/field_layout/field_layout.libraries.yml
@@ -0,0 +1,5 @@
+drupal.field_layout.twocol:
+  version: VERSION
+  css:
+    layout:
+      layouts/twocol/twocol.layout.css: {}
diff --git a/core/modules/field_layout/field_layout.module b/core/modules/field_layout/field_layout.module
new file mode 100644
index 0000000..1a4806a
--- /dev/null
+++ b/core/modules/field_layout/field_layout.module
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * @file
+ * Provides hook implementations for Field Layout.
+ */
+
+use Drupal\Core\Entity\ContentEntityFormInterface;
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\field_layout\Display\EntityDisplayWithLayoutInterface;
+use Drupal\field_layout\Entity\FieldLayoutEntityFormDisplay;
+use Drupal\field_layout\Entity\FieldLayoutEntityViewDisplay;
+use Drupal\field_layout\FieldLayoutBuilder;
+use Drupal\field_layout\Form\FieldLayoutEntityFormDisplayEditForm;
+use Drupal\field_layout\Form\FieldLayoutEntityViewDisplayEditForm;
+
+/**
+ * Implements hook_help().
+ */
+function field_layout_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    case 'help.page.field_layout':
+      $output = '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('The Field Layout module allows you to arrange fields into regions for content forms and displays.') . '</p>';
+      $output .= '<p>' . t('For more information, see the <a href=":field-layout-documentation">online documentation for the Field Layout module</a>.', [':field-layout-documentation' => 'https://www.drupal.org/documentation/modules/@todo_once_module_name_is_decided_upon']) . '</p>';
+      return $output;
+  }
+}
+
+/**
+ * Implements hook_entity_type_alter().
+ */
+function field_layout_entity_type_alter(array &$entity_types) {
+  /** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */
+  $entity_types['entity_view_display']->setClass(FieldLayoutEntityViewDisplay::class);
+  $entity_types['entity_form_display']->setClass(FieldLayoutEntityFormDisplay::class);
+
+  // The form classes are only needed when Field UI is installed.
+  if (\Drupal::moduleHandler()->moduleExists('field_ui')) {
+    $entity_types['entity_view_display']->setFormClass('edit', FieldLayoutEntityViewDisplayEditForm::class);
+    $entity_types['entity_form_display']->setFormClass('edit', FieldLayoutEntityFormDisplayEditForm::class);
+  }
+}
+
+/**
+ * Implements hook_entity_view_alter().
+ */
+function field_layout_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
+  if ($display instanceof EntityDisplayWithLayoutInterface) {
+    \Drupal::classResolver()->getInstanceFromDefinition(FieldLayoutBuilder::class)
+      ->build($build, $display, 'view');
+  }
+}
+
+/**
+ * Implements hook_form_alter().
+ */
+function field_layout_form_alter(&$form, FormStateInterface $form_state, $form_id) {
+  $form_object = $form_state->getFormObject();
+  if ($form_object instanceof ContentEntityFormInterface && $display = $form_object->getFormDisplay($form_state)) {
+    if ($display instanceof EntityDisplayWithLayoutInterface) {
+      \Drupal::classResolver()->getInstanceFromDefinition(FieldLayoutBuilder::class)
+        ->build($form, $display, 'form');
+    }
+  }
+}
diff --git a/core/modules/field_layout/layouts/onecol/field-layout--onecol.html.twig b/core/modules/field_layout/layouts/onecol/field-layout--onecol.html.twig
new file mode 100644
index 0000000..cce4893
--- /dev/null
+++ b/core/modules/field_layout/layouts/onecol/field-layout--onecol.html.twig
@@ -0,0 +1,24 @@
+{#
+/**
+ * @file
+ * Default theme implementation to display a one column layout.
+ *
+ * Available variables:
+ * - content: The content for this layout.
+ * - attributes: HTML attributes for the layout <div>.
+ *
+ * @ingroup themeable
+ */
+#}
+{%
+set classes = [
+'field-layout--onecol',
+]
+%}
+{% if content %}
+<div{{ attributes.addClass(classes) }}>
+  <div class="field-layout-region field-layout-region--content">
+    {{ content }}
+  </div>
+</div>
+{% endif %}
diff --git a/core/modules/field_layout/layouts/twocol/field-layout--twocol.html.twig b/core/modules/field_layout/layouts/twocol/field-layout--twocol.html.twig
new file mode 100644
index 0000000..4dffc01
--- /dev/null
+++ b/core/modules/field_layout/layouts/twocol/field-layout--twocol.html.twig
@@ -0,0 +1,28 @@
+{#
+/**
+ * @file
+ * Default theme implementation to display a two column layout.
+ *
+ * Available variables:
+ * - content: The content for this layout.
+ * - attributes: HTML attributes for the layout <div>.
+ *
+ * @ingroup themeable
+ */
+#}
+{%
+set classes = [
+'field-layout--twocol',
+]
+%}
+{% if content %}
+  <div{{ attributes.addClass(classes) }}>
+    <div class="field-layout-region field-layout-region--left">
+      {{ content.left }}
+    </div>
+
+    <div class="field-layout-region field-layout-region--right">
+      {{ content.right }}
+    </div>
+  </div>
+{% endif %}
diff --git a/core/modules/field_layout/layouts/twocol/twocol.layout.css b/core/modules/field_layout/layouts/twocol/twocol.layout.css
new file mode 100644
index 0000000..8e2f623
--- /dev/null
+++ b/core/modules/field_layout/layouts/twocol/twocol.layout.css
@@ -0,0 +1,14 @@
+.field-layout--twocol {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-between;
+}
+.field-layout--twocol > .field-layout-region {
+  flex: 0 1 50%;
+  max-width: 50%;
+}
+
+.field-layout--twocol > .field-layout-region--left {
+  max-width: calc(50% - 10px);
+  margin-right: 10px;
+}
diff --git a/core/modules/field_layout/src/Display/EntityDisplayWithLayoutInterface.php b/core/modules/field_layout/src/Display/EntityDisplayWithLayoutInterface.php
new file mode 100644
index 0000000..15204ac
--- /dev/null
+++ b/core/modules/field_layout/src/Display/EntityDisplayWithLayoutInterface.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Drupal\field_layout\Display;
+
+use Drupal\Core\Entity\Display\EntityDisplayInterface;
+
+/**
+ * Provides a common interface for entity displays that have layout.
+ */
+interface EntityDisplayWithLayoutInterface extends EntityDisplayInterface {
+
+  /**
+   * Gets the default region.
+   *
+   * @return string
+   *   The default region for this display.
+   */
+  public function getDefaultRegion();
+
+  /**
+   * Gets the layout ID for this display.
+   *
+   * @return string
+   *   The layout ID.
+   */
+  public function getLayoutId();
+
+  /**
+   * Gets the layout settings for this display.
+   *
+   * @return mixed[]
+   *   The layout settings.
+   */
+  public function getLayoutSettings();
+
+  /**
+   * Sets the layout ID for this display.
+   *
+   * @param string|null $layout_id
+   *   Either a valid layout ID, or NULL to remove the layout setting.
+   * @param array $layout_settings
+   *   (optional) An array of settings for this layout.
+   *
+   * @return $this
+   */
+  public function setLayout($layout_id, array $layout_settings = []);
+
+  /**
+   * Gets the layout plugin for this display.
+   *
+   * @return \Drupal\layout_plugin\Plugin\Layout\LayoutInterface
+   *   The layout plugin.
+   */
+  public function getLayoutPlugin();
+
+}
diff --git a/core/modules/field_layout/src/Entity/FieldLayoutEntityDisplayTrait.php b/core/modules/field_layout/src/Entity/FieldLayoutEntityDisplayTrait.php
new file mode 100644
index 0000000..aec3b8e
--- /dev/null
+++ b/core/modules/field_layout/src/Entity/FieldLayoutEntityDisplayTrait.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace Drupal\field_layout\Entity;
+
+use Drupal\Core\Entity\EntityStorageInterface;
+
+/**
+ * Provides shared code for entity displays.
+ */
+trait FieldLayoutEntityDisplayTrait {
+
+  /**
+   * Gets a layout definition.
+   *
+   * @param string $layout_id
+   *   The layout ID.
+   *
+   * @return \Drupal\layout_plugin\Plugin\Layout\LayoutDefinitionInterface
+   *   The layout definition.
+   */
+  protected function getLayoutDefinition($layout_id) {
+    return \Drupal::service('plugin.manager.layout_plugin')->getDefinition($layout_id);
+  }
+
+  /**
+   * Implements \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface::getLayoutId().
+   */
+  public function getLayoutId() {
+    return $this->getThirdPartySetting('field_layout', 'id');
+  }
+
+  /**
+   * Implements \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface::getLayoutSettings().
+   */
+  public function getLayoutSettings() {
+    return $this->getThirdPartySetting('field_layout', 'settings', []);
+  }
+
+  /**
+   * Implements \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface::setLayout().
+   */
+  public function setLayout($layout_id, array $layout_settings = []) {
+    $this->setThirdPartySetting('field_layout', 'id', $layout_id);
+    $this->setThirdPartySetting('field_layout', 'settings', $layout_settings);
+    return $this;
+  }
+
+  /**
+   * Implements \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface::getLayoutPlugin().
+   */
+  public function getLayoutPlugin() {
+    return \Drupal::service('plugin.manager.layout_plugin')->createInstance($this->getLayoutId(), $this->getLayoutSettings());
+  }
+
+  /**
+   * Overrides \Drupal\Core\Entity\EntityDisplayBase::preSave().
+   */
+  public function preSave(EntityStorageInterface $storage) {
+    if (!$this->getLayoutId()) {
+      $this->setLayout('onecol');
+    }
+
+    parent::preSave($storage);
+  }
+
+}
diff --git a/core/modules/field_layout/src/Entity/FieldLayoutEntityFormDisplay.php b/core/modules/field_layout/src/Entity/FieldLayoutEntityFormDisplay.php
new file mode 100644
index 0000000..2f3111e
--- /dev/null
+++ b/core/modules/field_layout/src/Entity/FieldLayoutEntityFormDisplay.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Drupal\field_layout\Entity;
+
+use Drupal\Core\Entity\Entity\EntityFormDisplay;
+use Drupal\field_layout\Display\EntityDisplayWithLayoutInterface;
+
+/**
+ * Provides an entity form display entity that has layout.
+ */
+class FieldLayoutEntityFormDisplay extends EntityFormDisplay implements EntityDisplayWithLayoutInterface {
+
+  use FieldLayoutEntityDisplayTrait;
+
+  /**
+   * {@inheritdoc}
+   *
+   * This cannot be provided by the trait due to https://bugs.php.net/bug.php?id=71414
+   * which is fixed in PHP 7.0.6.
+   */
+  public function getDefaultRegion() {
+    return $this->getLayoutDefinition($this->getLayoutId() ?: 'onecol')->getDefaultRegion();
+  }
+
+}
diff --git a/core/modules/field_layout/src/Entity/FieldLayoutEntityViewDisplay.php b/core/modules/field_layout/src/Entity/FieldLayoutEntityViewDisplay.php
new file mode 100644
index 0000000..b527059
--- /dev/null
+++ b/core/modules/field_layout/src/Entity/FieldLayoutEntityViewDisplay.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Drupal\field_layout\Entity;
+
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
+use Drupal\field_layout\Display\EntityDisplayWithLayoutInterface;
+
+/**
+ * Provides an entity view display entity that has layout.
+ */
+class FieldLayoutEntityViewDisplay extends EntityViewDisplay implements EntityDisplayWithLayoutInterface {
+
+  use FieldLayoutEntityDisplayTrait;
+
+  /**
+   * {@inheritdoc}
+   *
+   * This cannot be provided by the trait due to https://bugs.php.net/bug.php?id=71414
+   * which is fixed in PHP 7.0.6.
+   */
+  public function getDefaultRegion() {
+    return $this->getLayoutDefinition($this->getLayoutId() ?: 'onecol')->getDefaultRegion();
+  }
+
+}
diff --git a/core/modules/field_layout/src/FieldLayoutBuilder.php b/core/modules/field_layout/src/FieldLayoutBuilder.php
new file mode 100644
index 0000000..47c6de2
--- /dev/null
+++ b/core/modules/field_layout/src/FieldLayoutBuilder.php
@@ -0,0 +1,123 @@
+<?php
+
+namespace Drupal\field_layout;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\field_layout\Display\EntityDisplayWithLayoutInterface;
+use Drupal\layout_plugin\LayoutPluginManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Builds a field layout.
+ */
+class FieldLayoutBuilder implements ContainerInjectionInterface {
+
+  /**
+   * The layout plugin manager.
+   *
+   * @var \Drupal\layout_plugin\LayoutPluginManagerInterface
+   */
+  protected $layoutPluginManager;
+
+  /**
+   * The entity field manager.
+   *
+   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
+   */
+  protected $entityFieldManager;
+
+  /**
+   * FieldLayoutBuilder constructor.
+   *
+   * @param \Drupal\layout_plugin\LayoutPluginManagerInterface $layout_plugin_manager
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
+   */
+  public function __construct(LayoutPluginManagerInterface $layout_plugin_manager, EntityFieldManagerInterface $entity_field_manager) {
+    $this->layoutPluginManager = $layout_plugin_manager;
+    $this->entityFieldManager = $entity_field_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.layout_plugin'),
+      $container->get('entity_field.manager')
+    );
+  }
+
+  /**
+   * Applies the layout to an entity build.
+   *
+   * @param array $build
+   *   A renderable array representing the entity content or form.
+   * @param \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface $display
+   *   The entity display holding the display options configured for the entity
+   *   components.
+   * @param string $display_context
+   *   The display context, either 'form' or 'view'. If in a 'form' context, an
+   *   alternate method will be used to render fields in their regions.
+   */
+  public function build(array &$build, EntityDisplayWithLayoutInterface $display, $display_context) {
+    $layout_definition = $this->layoutPluginManager->getDefinition($display->getLayoutId(), FALSE);
+    if ($layout_definition && $fields = $this->getFields($build, $display, $display_context)) {
+      // Add the regions to the $build in the correct order.
+      $fill = [];
+      if ($display_context === 'form') {
+        $fill['#process'][] = '\Drupal\Core\Render\Element\RenderElement::processGroup';
+        $fill['#pre_render'][] = '\Drupal\Core\Render\Element\RenderElement::preRenderGroup';
+      }
+      $regions = array_fill_keys($layout_definition->getRegionNames(), $fill);
+
+      foreach ($fields as $name => $field) {
+        // If this is a form, #group can be used to relocate the fields. This
+        // avoids breaking hook_form_alter() implementations by not actually
+        // moving the field in the form structure.
+        if ($display_context === 'form') {
+          $build[$name]['#group'] = $field['region'];
+        }
+        // Otherwise, move the field from the top-level of $build into a
+        // region-specific section.
+        else {
+          $regions[$field['region']][$name] = $build[$name];
+          unset($build[$name]);
+        }
+      }
+      $build['field_layout'] = $display->getLayoutPlugin()->build($regions);
+    }
+  }
+
+  /**
+   * Gets the fields that need to be processed.
+   *
+   * @param array $build
+   *   A renderable array representing the entity content or form.
+   * @param \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface $display
+   *   The entity display holding the display options configured for the entity
+   *   components.
+   * @param string $display_context
+   *   The display context, either 'form' or 'view'.
+   *
+   * @return array
+   *   An array of configurable fields present in the build.
+   */
+  protected function getFields(array $build, EntityDisplayWithLayoutInterface $display, $display_context) {
+    $components = $display->getComponents();
+
+    $field_definitions = $this->entityFieldManager->getFieldDefinitions($display->getTargetEntityTypeId(), $display->getTargetBundle());
+    $non_configurable_fields = array_filter($field_definitions, function (FieldDefinitionInterface $field_definition) use ($display_context) {
+      return !$field_definition->isDisplayConfigurable($display_context);
+    });
+    // Remove non-configurable fields.
+    $components = array_diff_key($components, $non_configurable_fields);
+
+    // Only include fields present in the build.
+    $components = array_intersect_key($components, $build);
+
+    return $components;
+  }
+
+}
diff --git a/core/modules/field_layout/src/Form/FieldLayoutEntityDisplayFormTrait.php b/core/modules/field_layout/src/Form/FieldLayoutEntityDisplayFormTrait.php
new file mode 100644
index 0000000..2b92d07
--- /dev/null
+++ b/core/modules/field_layout/src/Form/FieldLayoutEntityDisplayFormTrait.php
@@ -0,0 +1,194 @@
+<?php
+
+namespace Drupal\field_layout\Form;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Form\SubformState;
+use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\field_layout\Display\EntityDisplayWithLayoutInterface;
+
+/**
+ * Provides shared code for entity display forms.
+ */
+trait FieldLayoutEntityDisplayFormTrait {
+
+  /**
+   * The field layout plugin manager.
+   *
+   * @var \Drupal\layout_plugin\LayoutPluginManagerInterface
+   */
+  protected $layoutPluginManager;
+
+  /**
+   * Overrides \Drupal\field_ui\Form\EntityDisplayFormBase::getRegions().
+   */
+  public function getRegions() {
+    $regions = [];
+
+    $layout_definition = $this->layoutPluginManager->getDefinition($this->getEntity()->getLayoutId() ?: 'onecol');
+    foreach ($layout_definition->getRegions() as $name => $region) {
+      $regions[$name] = [
+        'title' => $region['label'],
+        'message' => $this->t('No field is displayed.')
+      ];
+    }
+
+    $regions['hidden'] = [
+      'title' => $this->t('Disabled', [], ['context' => 'Plural']),
+      'message' => $this->t('No field is hidden.')
+    ];
+
+    return $regions;
+  }
+
+  /**
+   * Overrides \Drupal\field_ui\Form\EntityDisplayFormBase::form().
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form = parent::form($form, $form_state);
+
+    $form['field_layouts'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Layout settings'),
+    ];
+
+    $layout_options = [];
+    foreach ($this->layoutPluginManager->getGroupedDefinitions() as $category => $layout_definitions) {
+      foreach ($layout_definitions as $name => $layout_definition) {
+        $layout_options[$category][$name] = $layout_definition->getLabel();
+      }
+    }
+
+    $layout_plugin = $this->getLayoutPlugin($this->getEntity(), $form_state);
+    $form_state->set('layout_plugin', $layout_plugin);
+
+    $form['field_layouts']['field_layout'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Select a layout'),
+      '#options' => $layout_options,
+      '#default_value' => $layout_plugin->getPluginId(),
+      '#ajax' => [
+        'callback' => '::settingsAjax',
+        'wrapper' => 'field-layout-settings-wrapper',
+        'trigger_as' => ['name' => 'field_layout_change'],
+      ],
+    ];
+    $form['field_layouts']['submit'] = [
+      '#type' => 'submit',
+      '#name' => 'field_layout_change',
+      '#value' => $this->t('Change layout'),
+      '#submit' => ['::settingsAjaxSubmit'],
+      '#attributes' => ['class' => ['js-hide']],
+      '#ajax' => [
+        'callback' => '::settingsAjax',
+        'wrapper' => 'field-layout-settings-wrapper',
+      ],
+    ];
+
+    $form['field_layouts']['settings_wrapper'] = [
+      '#type' => 'container',
+      '#id' => 'field-layout-settings-wrapper',
+      '#tree' => TRUE,
+    ];
+
+    if ($layout_plugin instanceof PluginFormInterface) {
+      $form['field_layouts']['settings_wrapper']['layout_settings'] = [];
+      $subform_state = SubformState::createForSubform($form['field_layouts']['settings_wrapper']['layout_settings'], $form, $form_state);
+      $form['field_layouts']['settings_wrapper']['layout_settings'] = $layout_plugin->buildConfigurationForm($form['field_layouts']['settings_wrapper']['layout_settings'], $subform_state);
+    }
+
+    return $form;
+  }
+
+  /**
+   * Gets the layout plugin for the currently selected field layout.
+   *
+   * @param \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface $entity
+   *   The current form entity.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return \Drupal\layout_plugin\Plugin\Layout\LayoutInterface
+   *   The layout plugin.
+   */
+  protected function getLayoutPlugin(EntityDisplayWithLayoutInterface $entity, FormStateInterface $form_state) {
+    $stored_layout_id = $entity->getLayoutId();
+    // If a new field layout was selected, use that. Otherwise try to use the
+    // stored layout. Finally, fall back to the one column layout.
+    $layout_id = $form_state->getValue('field_layout') ?: ($stored_layout_id ?: 'onecol');
+    // If the current layout is the stored layout, use the stored layout
+    // settings. Otherwise leave the settings empty.
+    $layout_settings = $layout_id === $stored_layout_id ? $entity->getLayoutSettings() : [];
+
+    return $this->layoutPluginManager->createInstance($layout_id, $layout_settings);
+  }
+
+  /**
+   * Ajax callback for the field layout settings form.
+   */
+  public static function settingsAjax($form, FormStateInterface $form_state) {
+    return $form['field_layouts']['settings_wrapper'];
+  }
+
+  /**
+   * Submit handler for the non-JS case.
+   */
+  public static function settingsAjaxSubmit($form, FormStateInterface $form_state) {
+    $form_state->setRebuild();
+  }
+
+  /**
+   * Overrides \Drupal\field_ui\Form\EntityDisplayFormBase::validateForm().
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    parent::validateForm($form, $form_state);
+
+    $layout_plugin = $form_state->get('layout_plugin');
+    if ($layout_plugin instanceof PluginFormInterface) {
+      $subform_state = SubformState::createForSubform($form['field_layouts']['settings_wrapper']['layout_settings'], $form, $form_state);
+      $layout_plugin->validateConfigurationForm($form['field_layouts']['settings_wrapper']['layout_settings'], $subform_state);
+    }
+  }
+
+  /**
+   * Overrides \Drupal\field_ui\Form\EntityDisplayFormBase::submitForm().
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    parent::submitForm($form, $form_state);
+
+    $entity = $this->getEntity();
+    $old_layout = $entity->getLayoutId();
+    $new_layout = $form_state->getValue('field_layout');
+
+    $layout_plugin = $form_state->get('layout_plugin');
+    if ($layout_plugin instanceof PluginFormInterface) {
+      $layout_plugin->submitConfigurationForm($form['field_layouts']['settings_wrapper']['layout_settings'], SubformState::createForSubform($form['field_layouts']['settings_wrapper']['layout_settings'], $form, $form_state));
+    }
+    $entity->setLayout($new_layout, $layout_plugin->getConfiguration());
+
+    // If the layout is changing, reset all fields.
+    if ($new_layout !== $old_layout) {
+      // @todo Devise a mechanism for mapping old regions to new ones in
+      //   https://www.drupal.org/node/2796877.
+      $new_region = $entity->getDefaultRegion();
+      foreach ($form_state->getValue('fields') as $field_name => $values) {
+        if (($component = $entity->getComponent($field_name)) && $new_region !== 'hidden') {
+          $component['region'] = $new_region;
+          $entity->setComponent($field_name, $component);
+        }
+        else {
+          $entity->removeComponent($field_name);
+        }
+      }
+    }
+  }
+
+  /**
+   * Gets the form entity.
+   *
+   * @return \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface
+   *   The current form entity.
+   */
+  abstract public function getEntity();
+
+}
diff --git a/core/modules/field_layout/src/Form/FieldLayoutEntityFormDisplayEditForm.php b/core/modules/field_layout/src/Form/FieldLayoutEntityFormDisplayEditForm.php
new file mode 100644
index 0000000..20719c3
--- /dev/null
+++ b/core/modules/field_layout/src/Form/FieldLayoutEntityFormDisplayEditForm.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Drupal\field_layout\Form;
+
+use Drupal\Component\Plugin\PluginManagerBase;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldTypePluginManagerInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\layout_plugin\LayoutPluginManagerInterface;
+use Drupal\field_ui\Form\EntityFormDisplayEditForm;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Edit form for the EntityFormDisplay entity type.
+ */
+class FieldLayoutEntityFormDisplayEditForm extends EntityFormDisplayEditForm {
+
+  use FieldLayoutEntityDisplayFormTrait;
+
+  /**
+   * FieldLayoutEntityFormDisplayEditForm constructor.
+   *
+   * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
+   *   The field type manager.
+   * @param \Drupal\Component\Plugin\PluginManagerBase $plugin_manager
+   *   The widget plugin manager.
+   * @param \Drupal\layout_plugin\LayoutPluginManagerInterface $field_layout_plugin_manager
+   *   The field layout plugin manager.
+   */
+  public function __construct(FieldTypePluginManagerInterface $field_type_manager, PluginManagerBase $plugin_manager, LayoutPluginManagerInterface $field_layout_plugin_manager) {
+    parent::__construct($field_type_manager, $plugin_manager);
+    $this->layoutPluginManager = $field_layout_plugin_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.field.field_type'),
+      $container->get('plugin.manager.field.widget'),
+      $container->get('plugin.manager.layout_plugin')
+    );
+  }
+
+}
diff --git a/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php b/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php
new file mode 100644
index 0000000..f3da650
--- /dev/null
+++ b/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Drupal\field_layout\Form;
+
+use Drupal\Component\Plugin\PluginManagerBase;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldTypePluginManagerInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\layout_plugin\LayoutPluginManagerInterface;
+use Drupal\field_ui\Form\EntityViewDisplayEditForm;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Edit form for the EntityViewDisplay entity type.
+ */
+class FieldLayoutEntityViewDisplayEditForm extends EntityViewDisplayEditForm {
+
+  use FieldLayoutEntityDisplayFormTrait;
+
+  /**
+   * FieldLayoutEntityViewDisplayEditForm constructor.
+   *
+   * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
+   *   The field type manager.
+   * @param \Drupal\Component\Plugin\PluginManagerBase $plugin_manager
+   *   The formatter plugin manager.
+   * @param \Drupal\layout_plugin\LayoutPluginManagerInterface $field_layout_plugin_manager
+   *   The field layout plugin manager.
+   */
+  public function __construct(FieldTypePluginManagerInterface $field_type_manager, PluginManagerBase $plugin_manager, LayoutPluginManagerInterface $field_layout_plugin_manager) {
+    parent::__construct($field_type_manager, $plugin_manager);
+    $this->layoutPluginManager = $field_layout_plugin_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.field.field_type'),
+      $container->get('plugin.manager.field.formatter'),
+      $container->get('plugin.manager.layout_plugin')
+    );
+  }
+
+}
diff --git a/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.info.yml b/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.info.yml
new file mode 100644
index 0000000..4d699e4
--- /dev/null
+++ b/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.info.yml
@@ -0,0 +1,8 @@
+name: 'Field Layout test'
+type: module
+description: 'Support module for Field Layout tests.'
+core: 8.x
+package: Testing
+version: VERSION
+dependencies:
+  - entity_test
diff --git a/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.routing.yml b/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.routing.yml
new file mode 100644
index 0000000..bcea288
--- /dev/null
+++ b/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.routing.yml
@@ -0,0 +1,7 @@
+entity.entity_test.test_view_mode:
+  path: '/entity_test/{entity_test}/test'
+  defaults:
+    _entity_view: 'entity_test.test'
+    _title: 'Test test view mode'
+  requirements:
+    _entity_access: 'entity_test.view'
diff --git a/core/modules/field_layout/tests/src/Functional/FieldLayoutTest.php b/core/modules/field_layout/tests/src/Functional/FieldLayoutTest.php
new file mode 100644
index 0000000..35b4bdd
--- /dev/null
+++ b/core/modules/field_layout/tests/src/Functional/FieldLayoutTest.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Drupal\Tests\field_layout\Functional;
+
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests using field layout for entity displays.
+ *
+ * @group field_layout
+ */
+class FieldLayoutTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['field_layout', 'field_ui', 'node'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->createContentType([
+      'type' => 'article',
+    ]);
+    $this->createNode([
+      'type' => 'article',
+      'title' => 'The node title',
+      'body' => [[
+        'value' => 'The node body',
+      ]],
+    ]);
+    $this->drupalLogin($this->drupalCreateUser([
+      'access administration pages',
+      'administer content types',
+      'administer nodes',
+      'administer node fields',
+      'administer node display',
+      'administer node form display',
+      'view the administration theme',
+    ]));
+  }
+
+  /**
+   * Tests an entity type that has fields shown by default.
+   */
+  public function testNodeView() {
+    // By default, the one column layout is used.
+    $this->drupalGet('node/1');
+    $this->assertSession()->elementExists('css', '.field-layout--onecol');
+    $this->assertSession()->elementExists('css', '.field-layout-region--content .field--name-body');
+
+    $this->drupalGet('admin/structure/types/manage/article/display');
+    $this->assertEquals(['Content', 'Disabled'], $this->getRegionTitles());
+    $this->assertSession()->optionExists('fields[body][region]', 'content');
+  }
+
+  /**
+   * Gets the region titles on the page.
+   *
+   * @return string[]
+   *   An array of region titles.
+   */
+  protected function getRegionTitles() {
+    $region_titles = [];
+    $region_title_elements = $this->getSession()->getPage()->findAll('css', '.region-title td');
+    /** @var \Behat\Mink\Element\NodeElement[] $region_title_elements */
+    foreach ($region_title_elements as $region_title_element) {
+      $region_titles[] = $region_title_element->getText();
+    }
+    return $region_titles;
+  }
+
+}
diff --git a/core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php b/core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php
new file mode 100644
index 0000000..3e91740
--- /dev/null
+++ b/core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php
@@ -0,0 +1,266 @@
+<?php
+
+namespace Drupal\Tests\field_layout\FunctionalJavascript;
+
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
+
+/**
+ * Tests using field layout for entity displays.
+ *
+ * @group field_layout
+ */
+class FieldLayoutTest extends JavascriptTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['field_layout', 'field_ui', 'field_layout_test', 'layout_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $entity = EntityTest::create([
+      'name' => 'The name for this entity',
+      'field_test_text' => [[
+        'value' => 'The field test text value',
+      ]],
+    ]);
+    $entity->save();
+    $this->drupalLogin($this->drupalCreateUser([
+      'access administration pages',
+      'view test entity',
+      'administer entity_test content',
+      'administer entity_test fields',
+      'administer entity_test display',
+      'administer entity_test form display',
+      'view the administration theme',
+    ]));
+  }
+
+  /**
+   * Tests that layouts are unique per-view mode.
+   */
+  public function testEntityViewModes() {
+    // By default, the field is not visible.
+    $this->drupalGet('entity_test/1/test');
+    $this->assertSession()->elementNotExists('css', '.field-layout-region--content .field--name-field-test-text');
+    $this->drupalGet('entity_test/1');
+    $this->assertSession()->elementNotExists('css', '.field-layout-region--content .field--name-field-test-text');
+
+    // Change the layout for the "test" view mode. See
+    // core.entity_view_mode.entity_test.test.yml.
+    $this->drupalGet('entity_test/structure/entity_test/display');
+    $this->click('#edit-modes');
+    $this->getSession()->getPage()->checkField('display_modes_custom[test]');
+    $this->submitForm([], 'Save');
+    $this->clickLink('configure them');
+    $this->getSession()->getPage()->pressButton('Show row weights');
+    $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'content');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->submitForm([], 'Save');
+
+    // Each view mode has a different layout.
+    $this->drupalGet('entity_test/1/test');
+    $this->assertSession()->elementExists('css', '.field-layout-region--content .field--name-field-test-text');
+    $this->drupalGet('entity_test/1');
+    $this->assertSession()->elementNotExists('css', '.field-layout-region--content .field--name-field-test-text');
+  }
+
+  /**
+   * Tests the use of field layout for entity form displays.
+   */
+  public function testEntityForm() {
+    // By default, the one column layout is used.
+    $this->drupalGet('entity_test/manage/1/edit');
+    $this->assertFieldInRegion('field_test_text[0][value]', 'content');
+
+    // The one column layout is in use.
+    $this->drupalGet('entity_test/structure/entity_test/form-display');
+    $this->assertEquals(['Content', 'Disabled'], $this->getRegionTitles());
+
+    // Switch the layout to two columns.
+    $this->click('#edit-field-layouts');
+    $this->getSession()->getPage()->selectFieldOption('field_layout', 'twocol');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->submitForm([], 'Save');
+
+    // The field is moved to the default region for the new layout.
+    $this->assertSession()->pageTextContains('Your settings have been saved.');
+    $this->assertEquals(['Left', 'Right', 'Disabled'], $this->getRegionTitles());
+
+    $this->drupalGet('entity_test/manage/1/edit');
+    // No fields are visible, and the regions don't display when empty.
+    $this->assertFieldInRegion('field_test_text[0][value]', 'left');
+    $this->assertSession()->elementExists('css', '.field-layout-region--left .field--name-field-test-text');
+
+    // After a refresh the new regions are still there.
+    $this->drupalGet('entity_test/structure/entity_test/form-display');
+    $this->assertEquals(['Left', 'Right', 'Disabled'], $this->getRegionTitles());
+
+    // Drag the field to the right region.
+    $field_test_text_row = $this->getSession()->getPage()->find('css', '#field-test-text');
+    $right_region_row = $this->getSession()->getPage()->find('css', '.region-right-message');
+    $field_test_text_row->find('css', '.handle')->dragTo($right_region_row);
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->submitForm([], 'Save');
+    $this->assertSession()->pageTextContains('Your settings have been saved.');
+
+    // The new layout is used.
+    $this->drupalGet('entity_test/manage/1/edit');
+    $this->assertSession()->elementExists('css', '.field-layout-region--right .field--name-field-test-text');
+    $this->assertFieldInRegion('field_test_text[0][value]', 'right');
+
+    // Move the field to the right region without tabledrag.
+    $this->drupalGet('entity_test/structure/entity_test/form-display');
+    $this->getSession()->getPage()->pressButton('Show row weights');
+    $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'right');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->submitForm([], 'Save');
+    $this->assertSession()->pageTextContains('Your settings have been saved.');
+
+    // The updated region is used.
+    $this->drupalGet('entity_test/manage/1/edit');
+    $this->assertFieldInRegion('field_test_text[0][value]', 'right');
+
+    // The layout is still in use without Field UI.
+    $this->container->get('module_installer')->uninstall(['field_ui']);
+    $this->drupalGet('entity_test/manage/1/edit');
+    $this->assertFieldInRegion('field_test_text[0][value]', 'right');
+  }
+
+  /**
+   * Tests the use of field layout for entity view displays.
+   */
+  public function testEntityView() {
+    // The one column layout is in use.
+    $this->drupalGet('entity_test/structure/entity_test/display');
+    $this->assertEquals(['Content', 'Disabled'], $this->getRegionTitles());
+
+    // Switch the layout to two columns.
+    $this->click('#edit-field-layouts');
+    $this->getSession()->getPage()->selectFieldOption('field_layout', 'twocol');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->submitForm([], 'Save');
+
+    $this->assertSession()->pageTextContains('Your settings have been saved.');
+    $this->assertEquals(['Left', 'Right', 'Disabled'], $this->getRegionTitles());
+
+    $this->drupalGet('entity_test/1');
+    // No fields are visible, and the regions don't display when empty.
+    $this->assertSession()->elementNotExists('css', '.field-layout--twocol');
+    $this->assertSession()->elementNotExists('css', '.field-layout-region');
+    $this->assertSession()->elementNotExists('css', '.field--name-field-test-text');
+
+    // After a refresh the new regions are still there.
+    $this->drupalGet('entity_test/structure/entity_test/display');
+    $this->assertEquals(['Left', 'Right', 'Disabled'], $this->getRegionTitles());
+
+    // Drag the field to the left region.
+    $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->isSelected());
+    $field_test_text_row = $this->getSession()->getPage()->find('css', '#field-test-text');
+    $left_region_row = $this->getSession()->getPage()->find('css', '.region-left-message');
+    $field_test_text_row->find('css', '.handle')->dragTo($left_region_row);
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertFalse($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->isSelected());
+    $this->submitForm([], 'Save');
+    $this->assertSession()->pageTextContains('Your settings have been saved.');
+
+    // The new layout is used.
+    $this->drupalGet('entity_test/1');
+    $this->assertSession()->elementExists('css', '.field-layout--twocol');
+    $this->assertSession()->elementExists('css', '.field-layout-region--left .field--name-field-test-text');
+
+    // Move the field to the right region without tabledrag.
+    $this->drupalGet('entity_test/structure/entity_test/display');
+    $this->getSession()->getPage()->pressButton('Show row weights');
+    $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'right');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->submitForm([], 'Save');
+    $this->assertSession()->pageTextContains('Your settings have been saved.');
+
+    // The updated region is used.
+    $this->drupalGet('entity_test/1');
+    $this->assertSession()->elementExists('css', '.field-layout-region--right .field--name-field-test-text');
+
+    // The layout is still in use without Field UI.
+    $this->container->get('module_installer')->uninstall(['field_ui']);
+    $this->drupalGet('entity_test/1');
+    $this->assertSession()->elementExists('css', '.field-layout--twocol');
+    $this->assertSession()->elementExists('css', '.field-layout-region--right .field--name-field-test-text');
+  }
+
+  /**
+   * Tests layout plugins with forms.
+   */
+  public function testLayoutForms() {
+    $this->drupalGet('entity_test/structure/entity_test/display');
+    // Switch to a field layout with settings.
+    $this->click('#edit-field-layouts');
+
+    // Test switching between layouts with and without forms.
+    $this->getSession()->getPage()->selectFieldOption('field_layout', 'layout_test_plugin');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertSession()->fieldExists('settings_wrapper[layout_settings][setting_1]');
+
+    $this->getSession()->getPage()->selectFieldOption('field_layout', 'layout_test_2col');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertSession()->fieldNotExists('settings_wrapper[layout_settings][setting_1]');
+
+    $this->getSession()->getPage()->selectFieldOption('field_layout', 'layout_test_plugin');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertSession()->fieldExists('settings_wrapper[layout_settings][setting_1]');
+
+    // Move the test field to the content region.
+    $this->getSession()->getPage()->pressButton('Show row weights');
+    $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'content');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->submitForm([], 'Save');
+
+    $this->drupalGet('entity_test/1');
+    $this->assertSession()->pageTextContains('Blah: Default');
+
+    // Update the field layout settings.
+    $this->drupalGet('entity_test/structure/entity_test/display');
+    $this->click('#edit-field-layouts');
+    $this->getSession()->getPage()->fillField('settings_wrapper[layout_settings][setting_1]', 'Test text');
+    $this->submitForm([], 'Save');
+
+    $this->drupalGet('entity_test/1');
+    $this->assertSession()->pageTextContains('Blah: Test text');
+  }
+
+  /**
+   * Gets the region titles on the page.
+   *
+   * @return string[]
+   *   An array of region titles.
+   */
+  protected function getRegionTitles() {
+    $region_titles = [];
+    $region_title_elements = $this->getSession()->getPage()->findAll('css', '.region-title td');
+    /** @var \Behat\Mink\Element\NodeElement[] $region_title_elements */
+    foreach ($region_title_elements as $region_title_element) {
+      $region_titles[] = $region_title_element->getText();
+    }
+    return $region_titles;
+  }
+
+  /**
+   * Asserts that a field exists in a given region.
+   *
+   * @param string $field_selector
+   *   The field selector, one of field id|name|label|value.
+   * @param string $region_name
+   *   The machine name of the region.
+   */
+  protected function assertFieldInRegion($field_selector, $region_name) {
+    $region_element = $this->getSession()->getPage()->find('css', ".field-layout-region--$region_name");
+    $this->assertNotNull($region_element);
+    $this->assertSession()->fieldExists($field_selector, $region_element);
+  }
+
+}
diff --git a/core/modules/field_layout/tests/src/Unit/FieldLayoutBuilderTest.php b/core/modules/field_layout/tests/src/Unit/FieldLayoutBuilderTest.php
new file mode 100644
index 0000000..bc48a14
--- /dev/null
+++ b/core/modules/field_layout/tests/src/Unit/FieldLayoutBuilderTest.php
@@ -0,0 +1,261 @@
+<?php
+
+namespace Drupal\Tests\field_layout\Unit;
+
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\field_layout\Display\EntityDisplayWithLayoutInterface;
+use Drupal\field_layout\FieldLayoutBuilder;
+use Drupal\layout_plugin\LayoutPluginManagerInterface;
+use Drupal\layout_plugin\Plugin\Layout\LayoutDefault;
+use Drupal\layout_plugin\Plugin\Layout\LayoutDefinition;
+use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
+
+/**
+ * @coversDefaultClass \Drupal\field_layout\FieldLayoutBuilder
+ * @group field_layout
+ */
+class FieldLayoutBuilderTest extends UnitTestCase {
+
+  /**
+   * @var \Drupal\layout_plugin\LayoutPluginManager|\Prophecy\Prophecy\ProphecyInterface
+   */
+  protected $layoutPluginManager;
+
+  /**
+   * @var \Drupal\Core\Entity\EntityFieldManagerInterface|\Prophecy\Prophecy\ProphecyInterface
+   */
+  protected $entityFieldManager;
+
+  /**
+   * @var \Drupal\field_layout\FieldLayoutBuilder
+   */
+  protected $fieldLayoutBuilder;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->layoutPluginManager = $this->prophesize(LayoutPluginManagerInterface::class);
+    $this->layoutPluginManager->getDefinition('unknown', FALSE)->willReturn(NULL);
+
+    $twocol_definition = new LayoutDefinition([
+      'library' => 'field_layout/drupal.field_layout.twocol',
+      'theme' => 'field_layout__twocol',
+      'regions' => [
+        'left' => [
+          'label' => 'Left',
+        ],
+        'right' => [
+          'label' => 'Right',
+        ],
+      ],
+    ]);
+    $layout_plugin = new LayoutDefault([], 'twocol', $twocol_definition);
+    $this->layoutPluginManager->createInstance('twocol', [])->willReturn($layout_plugin);
+    $this->layoutPluginManager->getDefinition('twocol', FALSE)->willReturn($twocol_definition);
+
+    $this->entityFieldManager = $this->prophesize(EntityFieldManagerInterface::class);
+
+    $this->fieldLayoutBuilder = new FieldLayoutBuilder($this->layoutPluginManager->reveal(), $this->entityFieldManager->reveal());
+  }
+
+  /**
+   * @covers ::build
+   * @covers ::getFields
+   */
+  public function testBuildView() {
+    $definitions = [];
+    $non_configurable_field_definition = $this->prophesize(FieldDefinitionInterface::class);
+    $non_configurable_field_definition->isDisplayConfigurable('view')->willReturn(FALSE);
+    $definitions['non_configurable_field'] = $non_configurable_field_definition->reveal();
+    $this->entityFieldManager->getFieldDefinitions('the_entity_type_id', 'the_entity_type_bundle')->willReturn($definitions);
+
+    $build = [
+      'test1' => [
+        '#markup' => 'Test1',
+      ],
+      'non_configurable_field' => [
+        '#markup' => 'Non-configurable',
+      ],
+    ];
+
+    $display = $this->prophesize(EntityDisplayWithLayoutInterface::class);
+    $display->getTargetEntityTypeId()->willReturn('the_entity_type_id');
+    $display->getTargetBundle()->willReturn('the_entity_type_bundle');
+    $display->getLayoutId()->willReturn('twocol');
+    $display->getLayoutSettings()->willReturn([]);
+    $display->getComponents()->willReturn([
+      'test1' => [
+        'region' => 'right',
+      ],
+      'non_configurable_field' => [
+        'region' => 'left',
+      ],
+    ]);
+
+    $display_context = 'view';
+
+    $expected = [
+      'non_configurable_field' => [
+        '#markup' => 'Non-configurable',
+      ],
+      'field_layout' => [
+        'left' => [],
+        'right' => [
+          'test1' => [
+            '#markup' => 'Test1',
+          ],
+        ],
+        '#settings' => [],
+        '#theme' => 'field_layout__twocol',
+        '#attached' => [
+          'library' => [
+            'field_layout/drupal.field_layout.twocol',
+          ],
+        ],
+      ],
+    ];
+    $this->fieldLayoutBuilder->build($build, $display->reveal(), $display_context);
+    $this->assertSame($expected, $build);
+  }
+
+  /**
+   * @covers ::build
+   * @covers ::getFields
+   */
+  public function testBuildForm() {
+    $definitions = [];
+    $non_configurable_field_definition = $this->prophesize(FieldDefinitionInterface::class);
+    $non_configurable_field_definition->isDisplayConfigurable('form')->willReturn(FALSE);
+    $definitions['non_configurable_field'] = $non_configurable_field_definition->reveal();
+    $this->entityFieldManager->getFieldDefinitions('the_entity_type_id', 'the_entity_type_bundle')->willReturn($definitions);
+
+    $build = [
+      'test1' => [
+        '#markup' => 'Test1',
+      ],
+      'non_configurable_field' => [
+        '#markup' => 'Non-configurable',
+      ],
+    ];
+
+    $display = $this->prophesize(EntityDisplayWithLayoutInterface::class);
+    $display->getTargetEntityTypeId()->willReturn('the_entity_type_id');
+    $display->getTargetBundle()->willReturn('the_entity_type_bundle');
+    $display->getLayoutId()->willReturn('twocol');
+    $display->getLayoutSettings()->willReturn([]);
+    $display->getComponents()->willReturn([
+      'test1' => [
+        'region' => 'right',
+      ],
+      'non_configurable_field' => [
+        'region' => 'left',
+      ],
+    ]);
+
+    $display_context = 'form';
+
+    $expected = [
+      'test1' => [
+        '#markup' => 'Test1',
+        '#group' => 'right',
+      ],
+      'non_configurable_field' => [
+        '#markup' => 'Non-configurable',
+      ],
+      'field_layout' => [
+        'left' => [
+          '#process' => ['\Drupal\Core\Render\Element\RenderElement::processGroup'],
+          '#pre_render' => ['\Drupal\Core\Render\Element\RenderElement::preRenderGroup'],
+        ],
+        'right' => [
+          '#process' => ['\Drupal\Core\Render\Element\RenderElement::processGroup'],
+          '#pre_render' => ['\Drupal\Core\Render\Element\RenderElement::preRenderGroup'],
+        ],
+        '#settings' => [],
+        '#theme' => 'field_layout__twocol',
+        '#attached' => [
+          'library' => [
+            'field_layout/drupal.field_layout.twocol',
+          ],
+        ],
+      ],
+    ];
+    $this->fieldLayoutBuilder->build($build, $display->reveal(), $display_context);
+    $this->assertSame($expected, $build);
+  }
+
+  /**
+   * @covers ::build
+   */
+  public function testBuildEmpty() {
+    $definitions = [];
+    $non_configurable_field_definition = $this->prophesize(FieldDefinitionInterface::class);
+    $non_configurable_field_definition->isDisplayConfigurable('form')->willReturn(FALSE);
+    $definitions['non_configurable_field'] = $non_configurable_field_definition->reveal();
+    $this->entityFieldManager->getFieldDefinitions('the_entity_type_id', 'the_entity_type_bundle')->willReturn($definitions);
+
+    $build = [
+      'non_configurable_field' => [
+        '#markup' => 'Non-configurable',
+      ],
+    ];
+
+    $display = $this->prophesize(EntityDisplayWithLayoutInterface::class);
+    $display->getTargetEntityTypeId()->willReturn('the_entity_type_id');
+    $display->getTargetBundle()->willReturn('the_entity_type_bundle');
+    $display->getLayoutId()->willReturn('twocol');
+    $display->getLayoutSettings()->willReturn([]);
+    $display->getComponents()->willReturn([
+      'test1' => [
+        'region' => 'right',
+      ],
+      'non_configurable_field' => [
+        'region' => 'left',
+      ],
+    ]);
+
+    $display_context = 'form';
+
+    $expected = [
+      'non_configurable_field' => [
+        '#markup' => 'Non-configurable',
+      ],
+    ];
+    $this->fieldLayoutBuilder->build($build, $display->reveal(), $display_context);
+    $this->assertSame($expected, $build);
+  }
+
+  /**
+   * @covers ::build
+   */
+  public function testBuildNoLayout() {
+    $this->entityFieldManager->getFieldDefinitions(Argument::any(), Argument::any())->shouldNotBeCalled();
+
+    $build = [
+      'test1' => [
+        '#markup' => 'Test1',
+      ],
+    ];
+
+    $display = $this->prophesize(EntityDisplayWithLayoutInterface::class);
+    $display->getLayoutId()->willReturn('unknown');
+    $display->getLayoutSettings()->willReturn([]);
+    $display->getComponents()->shouldNotBeCalled();
+
+    $display_context = 'form';
+
+    $expected = [
+      'test1' => [
+        '#markup' => 'Test1',
+      ],
+    ];
+    $this->fieldLayoutBuilder->build($build, $display->reveal(), $display_context);
+    $this->assertSame($expected, $build);
+  }
+
+}
diff --git a/core/modules/field_ui/field_ui.js b/core/modules/field_ui/field_ui.js
index a53553d..6933136 100644
--- a/core/modules/field_ui/field_ui.js
+++ b/core/modules/field_ui/field_ui.js
@@ -128,9 +128,13 @@
       var refreshRows = {};
       refreshRows[rowHandler.name] = $trigger.get(0);
 
-      // Handle region change.
+      // Handle region or type change.
       var region = rowHandler.getRegion();
-      if (region !== rowHandler.region) {
+      var typeRegion = rowHandler.getType();
+      if (region !== rowHandler.region || typeRegion !== rowHandler.region) {
+        if (region === rowHandler.region) {
+          region = typeRegion;
+        }
         // Remove parenting.
         $row.find('select.js-field-parent').val('');
         // Let the row handler deal with the region change.
@@ -270,6 +274,10 @@ else if ($this.is('.region-empty')) {
     this.$pluginSelect = $(row).find('select.field-plugin-type');
     this.$pluginSelect.on('change', Drupal.fieldUIOverview.onChange);
 
+    // Attach change listener to the 'region' select.
+    this.$regionSelect = $(row).find('select.field-region');
+    this.$regionSelect.on('change', Drupal.fieldUIOverview.onChange);
+
     return this;
   };
 
@@ -282,6 +290,16 @@ else if ($this.is('.region-empty')) {
      *   Either 'hidden' or 'content'.
      */
     getRegion: function () {
+      return this.$regionSelect.val();
+    },
+
+    /**
+     * Returns the region corresponding to the current form values of the row.
+     *
+     * @returns {string}
+     *   Either 'hidden' or 'content'.
+     */
+    getType: function () {
       return (this.$pluginSelect.val() === 'hidden') ? 'hidden' : 'content';
     },
 
@@ -305,14 +323,17 @@ else if ($this.is('.region-empty')) {
      *   {@link Drupal.fieldUIOverview.AJAXRefreshRows}.
      */
     regionChange: function (region) {
+      // Replace dashes with underscores.
+      region = region.replace(/-/g, '_');
+
+      // Set the region of the select list.
+      this.$regionSelect.val(region);
 
       // When triggered by a row drag, the 'format' select needs to be adjusted
       // to the new region.
       var currentValue = this.$pluginSelect.val();
       var value;
-      // @TODO Check if this couldn't just be like
-      // if (region !== 'hidden') {
-      if (region === 'content') {
+      if (region !== 'hidden') {
         if (currentValue === 'hidden') {
           // Restore the formatter back to the default formatter. Pseudo-fields
           // do not have default formatters, we just return to 'visible' for
diff --git a/core/modules/field_ui/src/Form/EntityDisplayFormBase.php b/core/modules/field_ui/src/Form/EntityDisplayFormBase.php
index 586f1ef..ce69fb7 100644
--- a/core/modules/field_ui/src/Form/EntityDisplayFormBase.php
+++ b/core/modules/field_ui/src/Form/EntityDisplayFormBase.php
@@ -4,6 +4,7 @@
 
 use Drupal\Component\Plugin\Factory\DefaultFactory;
 use Drupal\Component\Plugin\PluginManagerBase;
+use Drupal\Core\Entity\Display\EntityDisplayInterface;
 use Drupal\Core\Entity\EntityForm;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
@@ -172,6 +173,13 @@ public function form(array $form, FormStateInterface $form_state) {
           'subgroup' => 'field-parent',
           'source' => 'field-name',
         ),
+        array(
+          'action' => 'match',
+          'relationship' => 'parent',
+          'group' => 'field-region',
+          'subgroup' => 'field-region',
+          'source' => 'field-name',
+        ),
       ),
     );
 
@@ -309,6 +317,15 @@ protected function buildFieldRow(FieldDefinitionInterface $field_definition, arr
           '#attributes' => array('class' => array('field-name')),
         ),
       ),
+      'region' => array(
+        '#type' => 'select',
+        '#title' => $this->t('Region for @title', array('@title' => $label)),
+        '#title_display' => 'invisible',
+        '#options' => $this->getRegionOptions(),
+        '#empty_value' => 'hidden',
+        '#default_value' => isset($display_options['region']) ? $display_options['region'] : 'hidden',
+        '#attributes' => array('class' => array('field-region')),
+      ),
     );
 
     $field_row['plugin'] = array(
@@ -474,6 +491,15 @@ protected function buildExtraFieldRow($field_id, $extra_field) {
           '#attributes' => array('class' => array('field-name')),
         ),
       ),
+      'region' => array(
+        '#type' => 'select',
+        '#title' => $this->t('Region for @title', array('@title' => $extra_field['label'])),
+        '#title_display' => 'invisible',
+        '#options' => $this->getRegionOptions(),
+        '#empty_value' => 'hidden',
+        '#default_value' => $display_options ? $display_options['region'] : 'hidden',
+        '#attributes' => array('class' => array('field-region')),
+      ),
       'plugin' => array(
         'type' => array(
           '#type' => 'select',
@@ -550,7 +576,7 @@ protected function copyFormValuesToEntity(EntityInterface $entity, array $form,
     foreach ($form['#fields'] as $field_name) {
       $values = $form_values['fields'][$field_name];
 
-      if ($values['type'] == 'hidden') {
+      if ($values['region'] == 'hidden') {
         $entity->removeComponent($field_name);
       }
       else {
@@ -567,6 +593,7 @@ protected function copyFormValuesToEntity(EntityInterface $entity, array $form,
 
         $options['type'] = $values['type'];
         $options['weight'] = $values['weight'];
+        $options['region'] = $values['region'];
         // Only formatters have configurable label visibility.
         if (isset($values['label'])) {
           $options['label'] = $values['label'];
@@ -577,12 +604,13 @@ protected function copyFormValuesToEntity(EntityInterface $entity, array $form,
 
     // Collect data for 'extra' fields.
     foreach ($form['#extra'] as $name) {
-      if ($form_values['fields'][$name]['type'] == 'hidden') {
+      if ($form_values['fields'][$name]['region'] == 'hidden') {
         $entity->removeComponent($name);
       }
       else {
         $entity->setComponent($name, array(
           'weight' => $form_values['fields'][$name]['weight'],
+          'region' => $form_values['fields'][$name]['region'],
         ));
       }
     }
@@ -813,7 +841,7 @@ public function getRowRegion($row) {
     switch ($row['#row_type']) {
       case 'field':
       case 'extra_field':
-        return ($row['plugin']['type']['#value'] == 'hidden' ? 'hidden' : 'content');
+        return $row['region']['#value'] ?: 'hidden';
     }
   }
 
diff --git a/core/modules/field_ui/src/Form/EntityFormDisplayEditForm.php b/core/modules/field_ui/src/Form/EntityFormDisplayEditForm.php
index 741b98d..af8e2ed 100644
--- a/core/modules/field_ui/src/Form/EntityFormDisplayEditForm.php
+++ b/core/modules/field_ui/src/Form/EntityFormDisplayEditForm.php
@@ -94,6 +94,7 @@ protected function getTableHeader() {
       $this->t('Field'),
       $this->t('Weight'),
       $this->t('Parent'),
+      $this->t('Region'),
       array('data' => $this->t('Widget'), 'colspan' => 3),
     );
   }
diff --git a/core/modules/field_ui/src/Form/EntityViewDisplayEditForm.php b/core/modules/field_ui/src/Form/EntityViewDisplayEditForm.php
index f273325..174726f 100644
--- a/core/modules/field_ui/src/Form/EntityViewDisplayEditForm.php
+++ b/core/modules/field_ui/src/Form/EntityViewDisplayEditForm.php
@@ -127,6 +127,7 @@ protected function getTableHeader() {
       $this->t('Field'),
       $this->t('Weight'),
       $this->t('Parent'),
+      $this->t('Region'),
       $this->t('Label'),
       array('data' => $this->t('Format'), 'colspan' => 3),
     );
diff --git a/core/modules/field_ui/src/Tests/ManageDisplayTest.php b/core/modules/field_ui/src/Tests/ManageDisplayTest.php
index 4cd2901..05178ba 100644
--- a/core/modules/field_ui/src/Tests/ManageDisplayTest.php
+++ b/core/modules/field_ui/src/Tests/ManageDisplayTest.php
@@ -3,6 +3,7 @@
 namespace Drupal\field_ui\Tests;
 
 use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Entity\Entity\EntityFormDisplay;
 use Drupal\Core\Entity\Entity\EntityViewDisplay;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Language\LanguageInterface;
@@ -98,8 +99,28 @@ function testFormatterUI() {
     );
     $this->assertEqual($options, $expected_options, 'The expected formatter ordering is respected.');
 
+    // Ensure that fields can be hidden directly by changing the region.
+    $this->drupalGet($manage_display);
+    $this->assertFieldByName('fields[field_test][region]', 'content');
+    $edit = ['fields[field_test][region]' => 'hidden'];
+    $this->drupalPostForm($manage_display, $edit, t('Save'));
+    $this->assertFieldByName('fields[field_test][region]', 'hidden');
+    $display = EntityViewDisplay::load("node.{$this->type}.default");
+    $this->assertNull($display->getComponent('field_test'));
+
+    // Restore the field to the content region.
+    $edit = [
+      'fields[field_test][type]' => 'field_test_default',
+      'fields[field_test][region]' => 'content',
+    ];
+    $this->drupalPostForm($manage_display, $edit, t('Save'));
+
     // Change the formatter and check that the summary is updated.
-    $edit = array('fields[field_test][type]' => 'field_test_multiple', 'refresh_rows' => 'field_test');
+    $edit = array(
+      'fields[field_test][type]' => 'field_test_multiple',
+      'fields[field_test][region]' => 'content',
+      'refresh_rows' => 'field_test'
+    );
     $this->drupalPostAjaxForm(NULL, $edit, array('op' => t('Refresh')));
     $format = 'field_test_multiple';
     $default_settings = \Drupal::service('plugin.manager.field.formatter')->getDefaultSettings($format);
@@ -147,7 +168,10 @@ function testFormatterUI() {
     $this->assertFieldByName($fieldname, '');
 
     // Test the empty setting formatter.
-    $edit = array('fields[field_test][type]' => 'field_empty_setting');
+    $edit = array(
+      'fields[field_test][type]' => 'field_empty_setting',
+      'fields[field_test][region]' => 'content',
+      );
     $this->drupalPostForm(NULL, $edit, t('Save'));
     $this->assertNoText('Default empty setting now has a value.');
     $this->assertFieldById('edit-fields-field-test-settings-edit');
@@ -159,7 +183,11 @@ function testFormatterUI() {
 
     // Test the settings form behavior. An edit button should be present since
     // there are third party settings to configure.
-    $edit = array('fields[field_test][type]' => 'field_no_settings', 'refresh_rows' => 'field_test');
+    $edit = array(
+      'fields[field_test][type]' => 'field_no_settings',
+      'fields[field_test][region]' => 'content',
+      'refresh_rows' => 'field_test',
+    );
     $this->drupalPostAjaxForm(NULL, $edit, array('op' => t('Refresh')));
     $this->assertFieldByName('field_test_settings_edit');
 
@@ -230,7 +258,11 @@ public function testWidgetUI() {
     $this->assertEqual($options, $expected_options, 'The expected widget ordering is respected.');
 
     // Change the widget and check that the summary is updated.
-    $edit = array('fields[field_test][type]' => 'test_field_widget_multiple', 'refresh_rows' => 'field_test');
+    $edit = array(
+      'fields[field_test][type]' => 'test_field_widget_multiple',
+      'fields[field_test][region]' => 'content',
+      'refresh_rows' => 'field_test',
+    );
     $this->drupalPostAjaxForm(NULL, $edit, array('op' => t('Refresh')));
     $widget_type = 'test_field_widget_multiple';
     $default_settings = \Drupal::service('plugin.manager.field.widget')->getDefaultSettings($widget_type);
@@ -284,6 +316,14 @@ public function testWidgetUI() {
     // Checks if the select elements contain the specified options.
     $this->assertFieldSelectOptions('fields[field_test][type]', array('test_field_widget', 'test_field_widget_multiple', 'hidden'));
     $this->assertFieldSelectOptions('fields[field_onewidgetfield][type]', array('test_field_widget', 'hidden'));
+
+    // Ensure that fields can be hidden directly by changing the region.
+    $this->assertFieldByName('fields[field_test][region]', 'content');
+    $edit = ['fields[field_test][region]' => 'hidden'];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertFieldByName('fields[field_test][region]', 'hidden');
+    $display = EntityFormDisplay::load("node.{$this->type}.default");
+    $this->assertNull($display->getComponent('field_test'));
   }
 
   /**
@@ -321,6 +361,7 @@ function testViewModeCustom() {
     // accordingly in 'rss' mode.
     $edit = array(
       'fields[field_test][type]' => 'field_test_with_prepare_view',
+      'fields[field_test][region]' => 'content',
     );
     $this->drupalPostForm('admin/structure/types/manage/' . $this->type . '/display', $edit, t('Save'));
     $this->assertNodeViewText($node, 'rss', $output['field_test_with_prepare_view'], "The field is displayed as expected in view modes that use 'default' settings.");
@@ -335,7 +376,7 @@ function testViewModeCustom() {
     // Set the field to 'hidden' in the view mode, check that the field is
     // hidden.
     $edit = array(
-      'fields[field_test][type]' => 'hidden',
+      'fields[field_test][region]' => 'hidden',
     );
     $this->drupalPostForm('admin/structure/types/manage/' . $this->type . '/display/rss', $edit, t('Save'));
     $this->assertNodeViewNoText($node, 'rss', $value, "The field is hidden in 'rss' mode.");
diff --git a/core/modules/field_ui/tests/src/FunctionalJavascript/EntityDisplayTest.php b/core/modules/field_ui/tests/src/FunctionalJavascript/EntityDisplayTest.php
new file mode 100644
index 0000000..7cde00c
--- /dev/null
+++ b/core/modules/field_ui/tests/src/FunctionalJavascript/EntityDisplayTest.php
@@ -0,0 +1,95 @@
+<?php
+
+namespace Drupal\Tests\field_ui\FunctionalJavascript;
+
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
+
+/**
+ * Tests the UI for entity displays.
+ *
+ * @group field_ui
+ */
+class EntityDisplayTest extends JavascriptTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['field_ui', 'entity_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $entity = EntityTest::create([
+      'name' => 'The name for this entity',
+      'field_test_text' => [[
+        'value' => 'The field test text value',
+      ]],
+    ]);
+    $entity->save();
+    $this->drupalLogin($this->drupalCreateUser([
+      'access administration pages',
+      'view test entity',
+      'administer entity_test content',
+      'administer entity_test fields',
+      'administer entity_test display',
+      'administer entity_test form display',
+      'view the administration theme',
+    ]));
+  }
+
+  /**
+   * Tests the use of regions for entity form displays.
+   */
+  public function testEntityForm() {
+    $this->drupalGet('entity_test/manage/1/edit');
+    $this->assertSession()->fieldExists('field_test_text[0][value]');
+
+    $this->drupalGet('entity_test/structure/entity_test/form-display');
+    $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
+    $this->assertFalse($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->isSelected());
+
+    $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'hidden');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
+    $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->isSelected());
+
+    $this->submitForm([], 'Save');
+    $this->assertSession()->pageTextContains('Your settings have been saved.');
+    $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
+    $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->isSelected());
+
+    $this->drupalGet('entity_test/manage/1/edit');
+    $this->assertSession()->fieldNotExists('field_test_text[0][value]');
+  }
+
+  /**
+   * Tests the use of regions for entity view displays.
+   */
+  public function testEntityView() {
+    $this->drupalGet('entity_test/1');
+    $this->assertSession()->elementNotExists('css', '.field--name-field-test-text');
+
+    $this->drupalGet('entity_test/structure/entity_test/display');
+    $this->assertSession()->elementExists('css', '.region-content-message.region-empty');
+    $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
+    $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->isSelected());
+
+    $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'content');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
+    $this->assertFalse($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->isSelected());
+
+    $this->submitForm([], 'Save');
+    $this->assertSession()->pageTextContains('Your settings have been saved.');
+    $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
+    $this->assertFalse($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->isSelected());
+
+    $this->drupalGet('entity_test/1');
+    $this->assertSession()->elementExists('css', '.field--name-field-test-text');
+  }
+
+}
diff --git a/core/modules/field_ui/tests/src/Kernel/EntityDisplayTest.php b/core/modules/field_ui/tests/src/Kernel/EntityDisplayTest.php
index be188db..d060163 100644
--- a/core/modules/field_ui/tests/src/Kernel/EntityDisplayTest.php
+++ b/core/modules/field_ui/tests/src/Kernel/EntityDisplayTest.php
@@ -69,6 +69,7 @@ public function testEntityDisplayCRUD() {
     $display->save();
     $display = EntityViewDisplay::load($display->id());
     foreach (array('component_1', 'component_2', 'component_3') as $name) {
+      $expected[$name]['region'] = 'content';
       $this->assertEqual($display->getComponent($name), $expected[$name]);
     }
 
@@ -86,6 +87,7 @@ public function testEntityDisplayCRUD() {
         'link_to_entity' => FALSE,
       ),
       'third_party_settings' => array(),
+      'region' => 'content'
     );
     $this->assertEqual($display->getComponents(), $expected);
 
@@ -148,7 +150,7 @@ public function testEntityGetDisplay() {
     $display = entity_get_display('entity_test', 'entity_test', 'default');
     $this->assertFalse($display->isNew());
     $this->assertEqual($display->id(), 'entity_test.entity_test.default');
-    $this->assertEqual($display->getComponent('component_1'), array( 'weight' => 10, 'settings' => array(), 'third_party_settings' => array()));
+    $this->assertEqual($display->getComponent('component_1'), array( 'weight' => 10, 'settings' => array(), 'third_party_settings' => array(), 'region' => 'content'));
   }
 
   /**
@@ -164,7 +166,36 @@ public function testExtraFieldComponent() {
 
     // Check that the default visibility taken into account for extra fields
     // unknown in the display.
-    $this->assertEqual($display->getComponent('display_extra_field'), array('weight' => 5));
+    $this->assertEqual($display->getComponent('display_extra_field'), array('weight' => 5, 'region' => 'content'));
+    $this->assertNull($display->getComponent('display_extra_field_hidden'));
+
+    // Check that setting explicit options overrides the defaults.
+    $display->removeComponent('display_extra_field');
+    $display->setComponent('display_extra_field_hidden', array('weight' => 10));
+    $this->assertNull($display->getComponent('display_extra_field'));
+    $this->assertEqual($display->getComponent('display_extra_field_hidden'), array('weight' => 10, 'settings' => array(), 'third_party_settings' => array()));
+  }
+
+  /**
+   * Tests the behavior of an extra field component with initial invalid values.
+   */
+  public function testExtraFieldComponentInitialInvalidConfig() {
+    entity_test_create_bundle('bundle_with_extra_fields');
+    $display = EntityViewDisplay::create(array(
+      'targetEntityType' => 'entity_test',
+      'bundle' => 'bundle_with_extra_fields',
+      'mode' => 'default',
+      // Add the extra field to the initial config, without a 'type'.
+      'content' => [
+        'display_extra_field' => [
+          'weight' => 5,
+        ],
+      ],
+    ));
+
+    // Check that the default visibility taken into account for extra fields
+    // unknown in the display that were included in the initial config.
+    $this->assertEqual($display->getComponent('display_extra_field'), array('weight' => 5, 'region' => 'content'));
     $this->assertNull($display->getComponent('display_extra_field_hidden'));
 
     // Check that setting explicit options overrides the defaults.
@@ -258,6 +289,7 @@ public function testBaseFieldComponent() {
         'settings' => $formatter_settings,
         'third_party_settings' => array(),
         'weight' => 10,
+        'region' => 'content',
       ),
       'test_display_non_configurable' => array(
         'label' => 'above',
@@ -265,6 +297,7 @@ public function testBaseFieldComponent() {
         'settings' => $formatter_settings,
         'third_party_settings' => array(),
         'weight' => 11,
+        'region' => 'content',
       ),
     );
     foreach ($expected as $field_name => $options) {
diff --git a/core/modules/field_ui/tests/src/Kernel/EntityFormDisplayTest.php b/core/modules/field_ui/tests/src/Kernel/EntityFormDisplayTest.php
index e38db7c..6f770d6 100644
--- a/core/modules/field_ui/tests/src/Kernel/EntityFormDisplayTest.php
+++ b/core/modules/field_ui/tests/src/Kernel/EntityFormDisplayTest.php
@@ -43,7 +43,7 @@ public function testEntityGetFromDisplay() {
     $form_display = entity_get_form_display('entity_test', 'entity_test', 'default');
     $this->assertFalse($form_display->isNew());
     $this->assertEqual($form_display->id(), 'entity_test.entity_test.default');
-    $this->assertEqual($form_display->getComponent('component_1'), array('weight' => 10, 'settings' => array(), 'third_party_settings' => array()));
+    $this->assertEqual($form_display->getComponent('component_1'), array('weight' => 10, 'settings' => array(), 'third_party_settings' => array(), 'region' => 'content'));
   }
 
   /**
@@ -134,12 +134,14 @@ public function testBaseFieldComponent() {
         'settings' => $formatter_settings,
         'third_party_settings' => array(),
         'weight' => 10,
+        'region' => 'content',
       ),
       'test_display_non_configurable' => array(
         'type' => 'text_textfield',
         'settings' => $formatter_settings,
         'third_party_settings' => array(),
         'weight' => 11,
+        'region' => 'content',
       ),
     );
     foreach ($expected as $field_name => $options) {
diff --git a/core/modules/file/src/Tests/FileFieldDisplayTest.php b/core/modules/file/src/Tests/FileFieldDisplayTest.php
index 142751a..b25b514 100644
--- a/core/modules/file/src/Tests/FileFieldDisplayTest.php
+++ b/core/modules/file/src/Tests/FileFieldDisplayTest.php
@@ -36,9 +36,17 @@ function testNodeDisplay() {
     // case.
     $file_formatters = array('file_table', 'file_url_plain', 'hidden', 'file_default');
     foreach ($file_formatters as $formatter) {
-      $edit = array(
-        "fields[$field_name][type]" => $formatter,
-      );
+      if ($formatter === 'hidden') {
+        $edit = [
+          "fields[$field_name][region]" => 'hidden',
+        ];
+      }
+      else {
+        $edit = [
+          "fields[$field_name][type]" => $formatter,
+          "fields[$field_name][region]" => 'content',
+        ];
+      }
       $this->drupalPostForm("admin/structure/types/manage/$type_name/display", $edit, t('Save'));
       $this->drupalGet('node/' . $node->id());
       $this->assertNoText($field_name, format_string('Field label is hidden when no file attached for formatter %formatter', array('%formatter' => $formatter)));
diff --git a/core/modules/file/src/Tests/FileFieldRSSContentTest.php b/core/modules/file/src/Tests/FileFieldRSSContentTest.php
index 0422514..4e18e71 100644
--- a/core/modules/file/src/Tests/FileFieldRSSContentTest.php
+++ b/core/modules/file/src/Tests/FileFieldRSSContentTest.php
@@ -37,7 +37,10 @@ function testFileFieldRSSContent() {
 
     // Change the format to 'RSS enclosure'.
     $this->drupalGet("admin/structure/types/manage/$type_name/display/rss");
-    $edit = array("fields[$field_name][type]" => 'file_rss_enclosure');
+    $edit = array(
+      "fields[$field_name][type]" => 'file_rss_enclosure',
+      "fields[$field_name][region]" => 'content',
+    );
     $this->drupalPostForm(NULL, $edit, t('Save'));
 
     // Create a new node with a file field set. Promote to frontpage
diff --git a/core/modules/forum/config/optional/core.entity_form_display.comment.comment_forum.default.yml b/core/modules/forum/config/optional/core.entity_form_display.comment.comment_forum.default.yml
index a09c30b..4738bbb 100644
--- a/core/modules/forum/config/optional/core.entity_form_display.comment.comment_forum.default.yml
+++ b/core/modules/forum/config/optional/core.entity_form_display.comment.comment_forum.default.yml
@@ -13,9 +13,11 @@ mode: default
 content:
   author:
     weight: -2
+    region: content
   comment_body:
     type: text_textarea
     weight: 11
+    region: content
     settings:
       rows: 5
       placeholder: ''
@@ -23,6 +25,7 @@ content:
   subject:
     type: string_textfield
     weight: 10
+    region: content
     settings:
       size: 60
       placeholder: ''
diff --git a/core/modules/forum/config/optional/core.entity_form_display.node.forum.default.yml b/core/modules/forum/config/optional/core.entity_form_display.node.forum.default.yml
index c66ba23..6773d32 100644
--- a/core/modules/forum/config/optional/core.entity_form_display.node.forum.default.yml
+++ b/core/modules/forum/config/optional/core.entity_form_display.node.forum.default.yml
@@ -17,6 +17,7 @@ content:
   body:
     type: text_textarea_with_summary
     weight: 27
+    region: content
     settings:
       rows: 9
       summary_rows: 3
@@ -25,11 +26,13 @@ content:
   comment_forum:
     type: comment_default
     weight: 20
+    region: content
     settings: {  }
     third_party_settings: {  }
   created:
     type: datetime_timestamp
     weight: 10
+    region: content
     settings: {  }
     third_party_settings: {  }
   promote:
@@ -37,21 +40,25 @@ content:
     settings:
       display_label: true
     weight: 15
+    region: content
     third_party_settings: {  }
   sticky:
     type: boolean_checkbox
     settings:
       display_label: true
     weight: 16
+    region: content
     third_party_settings: {  }
   taxonomy_forums:
     type: options_select
     weight: 26
+    region: content
     settings: {  }
     third_party_settings: {  }
   title:
     type: string_textfield
     weight: -5
+    region: content
     settings:
       size: 60
       placeholder: ''
@@ -59,6 +66,7 @@ content:
   uid:
     type: entity_reference_autocomplete
     weight: 5
+    region: content
     settings:
       match_operator: CONTAINS
       size: 60
diff --git a/core/modules/forum/config/optional/core.entity_form_display.taxonomy_term.forums.default.yml b/core/modules/forum/config/optional/core.entity_form_display.taxonomy_term.forums.default.yml
index b18c869..50df98a 100644
--- a/core/modules/forum/config/optional/core.entity_form_display.taxonomy_term.forums.default.yml
+++ b/core/modules/forum/config/optional/core.entity_form_display.taxonomy_term.forums.default.yml
@@ -14,11 +14,13 @@ content:
   description:
     type: text_textfield
     weight: 0
+    region: content
     settings: {  }
     third_party_settings: {  }
   name:
     type: string_textfield
     weight: -5
+    region: content
     settings:
       size: 60
       placeholder: ''
diff --git a/core/modules/forum/config/optional/core.entity_view_display.comment.comment_forum.default.yml b/core/modules/forum/config/optional/core.entity_view_display.comment.comment_forum.default.yml
index f4f0112..befeba8 100644
--- a/core/modules/forum/config/optional/core.entity_view_display.comment.comment_forum.default.yml
+++ b/core/modules/forum/config/optional/core.entity_view_display.comment.comment_forum.default.yml
@@ -15,8 +15,10 @@ content:
     label: hidden
     type: text_default
     weight: 0
+    region: content
     settings: {  }
     third_party_settings: {  }
   links:
     weight: 100
+    region: content
 hidden: {  }
diff --git a/core/modules/forum/config/optional/core.entity_view_display.node.forum.default.yml b/core/modules/forum/config/optional/core.entity_view_display.node.forum.default.yml
index b157c83..f3e8c5c 100644
--- a/core/modules/forum/config/optional/core.entity_view_display.node.forum.default.yml
+++ b/core/modules/forum/config/optional/core.entity_view_display.node.forum.default.yml
@@ -20,21 +20,25 @@ content:
     label: hidden
     type: text_default
     weight: 0
+    region: content
     settings: {  }
     third_party_settings: {  }
   comment_forum:
     label: hidden
     type: comment_default
     weight: 20
+    region: content
     settings:
       view_mode: default
       pager_id: 0
     third_party_settings: {  }
   links:
     weight: 100
+    region: content
   taxonomy_forums:
     type: entity_reference_label
     weight: -1
+    region: content
     label: above
     settings:
       link: true
diff --git a/core/modules/forum/config/optional/core.entity_view_display.node.forum.teaser.yml b/core/modules/forum/config/optional/core.entity_view_display.node.forum.teaser.yml
index 4405e71..7b174f4 100644
--- a/core/modules/forum/config/optional/core.entity_view_display.node.forum.teaser.yml
+++ b/core/modules/forum/config/optional/core.entity_view_display.node.forum.teaser.yml
@@ -19,14 +19,17 @@ content:
     label: hidden
     type: text_summary_or_trimmed
     weight: 100
+    region: content
     settings:
       trim_length: 600
     third_party_settings: {  }
   links:
     weight: 101
+    region: content
   taxonomy_forums:
     type: entity_reference_label
     weight: 10
+    region: content
     label: above
     settings:
       link: true
diff --git a/core/modules/forum/config/optional/core.entity_view_display.taxonomy_term.forums.default.yml b/core/modules/forum/config/optional/core.entity_view_display.taxonomy_term.forums.default.yml
index d1242d9..b326039 100644
--- a/core/modules/forum/config/optional/core.entity_view_display.taxonomy_term.forums.default.yml
+++ b/core/modules/forum/config/optional/core.entity_view_display.taxonomy_term.forums.default.yml
@@ -14,6 +14,7 @@ content:
   description:
     type: text_default
     weight: 0
+    region: content
     settings: {  }
     third_party_settings: {  }
     label: above
diff --git a/core/modules/layout_plugin/config/schema/layout_plugin.schema.yml b/core/modules/layout_plugin/config/schema/layout_plugin.schema.yml
new file mode 100644
index 0000000..0dcda5e
--- /dev/null
+++ b/core/modules/layout_plugin/config/schema/layout_plugin.schema.yml
@@ -0,0 +1,6 @@
+layout.settings:
+  type: mapping
+  label: 'Layout settings'
+
+layout.settings.*:
+  type: layout.settings
diff --git a/core/modules/layout_plugin/layout_plugin.info.yml b/core/modules/layout_plugin/layout_plugin.info.yml
new file mode 100644
index 0000000..7058dca
--- /dev/null
+++ b/core/modules/layout_plugin/layout_plugin.info.yml
@@ -0,0 +1,6 @@
+name: 'Layout Plugin'
+type: module
+description: 'Provides a way for modules or themes to register layouts.'
+package: Core (Experimental)
+version: VERSION
+core: 8.x
diff --git a/core/modules/layout_plugin/layout_plugin.module b/core/modules/layout_plugin/layout_plugin.module
new file mode 100644
index 0000000..f35c1dd
--- /dev/null
+++ b/core/modules/layout_plugin/layout_plugin.module
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * @file
+ * Provides hook implementations for Layout Plugin.
+ */
+
+use Drupal\Core\Routing\RouteMatchInterface;
+
+/**
+ * Implements hook_help().
+ */
+function layout_plugin_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    case 'help.page.layout_plugin':
+      $output = '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('Layout Plugin allows modules or themes to register layouts, and for other modules to list the available layouts and render them.') . '</p>';
+      $output .= '<p>' . t('For more information, see the <a href=":layout-plugin-documentation">online documentation for the Layout Plugin module</a>.', [':layout-plugin-documentation' => 'https://www.drupal.org/node/2619128']) . '</p>';
+      return $output;
+  }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function layout_plugin_theme($existing, $type, $theme, $path) {
+  return \Drupal::service('plugin.manager.layout_plugin')->getThemeImplementations();
+}
+
+/**
+ * Implements hook_theme_registry_alter().
+ */
+function layout_plugin_theme_registry_alter(&$theme_registry) {
+  \Drupal::service('plugin.manager.layout_plugin')->alterThemeImplementations($theme_registry);
+}
+
+/**
+ * Prepares variables for layout templates.
+ *
+ * @param array &$variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #settings, #layout.
+ */
+function _layout_plugin_preprocess_layout(&$variables) {
+  $variables['settings'] = isset($variables['content']['#settings']) ? $variables['content']['#settings'] : [];
+}
diff --git a/core/modules/layout_plugin/layout_plugin.services.yml b/core/modules/layout_plugin/layout_plugin.services.yml
new file mode 100644
index 0000000..74225de
--- /dev/null
+++ b/core/modules/layout_plugin/layout_plugin.services.yml
@@ -0,0 +1,4 @@
+services:
+  plugin.manager.layout_plugin:
+    class: Drupal\layout_plugin\LayoutPluginManager
+    arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@theme_handler']
diff --git a/core/modules/layout_plugin/src/Annotation/Layout.php b/core/modules/layout_plugin/src/Annotation/Layout.php
new file mode 100644
index 0000000..13595a2
--- /dev/null
+++ b/core/modules/layout_plugin/src/Annotation/Layout.php
@@ -0,0 +1,129 @@
+<?php
+
+namespace Drupal\layout_plugin\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+use Drupal\layout_plugin\Plugin\Layout\LayoutDefault;
+use Drupal\layout_plugin\Plugin\Layout\LayoutDefinition;
+
+/**
+ * Defines a Layout annotation object.
+ *
+ * Layouts are used to define a list of regions and then output render arrays
+ * in each of the regions, usually using a template.
+ *
+ * Plugin namespace: Plugin\Layout
+ *
+ * @see \Drupal\layout_plugin\Plugin\Layout\LayoutInterface
+ * @see \Drupal\layout_plugin\Plugin\Layout\LayoutBase
+ * @see \Drupal\layout_plugin\LayoutPluginManager
+ * @see plugin_api
+ *
+ * @Annotation
+ */
+class Layout extends Plugin {
+
+  /**
+   * The plugin ID.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The human-readable name.
+   *
+   * @var string
+   *
+   * @ingroup plugin_translatable
+   */
+  public $label;
+
+  /**
+   * The human-readable category.
+   *
+   * @var string
+   *
+   * @ingroup plugin_translatable
+   */
+  public $category;
+
+  /**
+   * The template file to render this layout (relative to the 'path' given).
+   *
+   * If specified, then the layout_plugin module will register the template with
+   * hook_theme() and the module or theme registering this layout does not need
+   * to do it.
+   *
+   * @var string optional
+   *
+   * @see hook_theme()
+   */
+  public $template;
+
+  /**
+   * The theme hook used to render this layout.
+   *
+   * If specified, it's assumed that the module or theme registering this layout
+   * will also register the theme hook with hook_theme() itself. This is
+   * mutually exclusive with 'template' - you can't specify both.
+   *
+   * @var string optional
+   *
+   * @see hook_theme()
+   */
+  public $theme;
+
+  /**
+   * Path (relative to the module or theme) to resources like icon or template.
+   *
+   * @var string optional
+   */
+  public $path;
+
+  /**
+   * The asset library.
+   *
+   * @var string optional
+   */
+  public $library;
+
+  /**
+   * An associative array of regions in this layout.
+   *
+   * The key of the array is the machine name of the region, and the value is
+   * an associative array with the following keys:
+   * - label: (string) The human-readable name of the region.
+   *
+   * Any remaining keys may have special meaning for the given layout plugin,
+   * but are undefined here.
+   *
+   * @var array
+   */
+  public $regions = [];
+
+  /**
+   * The default region.
+   *
+   * @var string
+   */
+  public $default_region;
+
+  /**
+   * The layout plugin class.
+   *
+   * This default value is used for plugins defined in layouts.yml that do not
+   * specify a class themselves.
+   *
+   * @var string
+   */
+  public $class = LayoutDefault::class;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function get() {
+    return new LayoutDefinition($this->definition);
+  }
+
+}
diff --git a/core/modules/layout_plugin/src/LayoutPluginManager.php b/core/modules/layout_plugin/src/LayoutPluginManager.php
new file mode 100644
index 0000000..df2b7e1
--- /dev/null
+++ b/core/modules/layout_plugin/src/LayoutPluginManager.php
@@ -0,0 +1,218 @@
+<?php
+
+namespace Drupal\layout_plugin;
+
+use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Extension\ThemeHandlerInterface;
+use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery;
+use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
+use Drupal\Core\Plugin\Discovery\YamlDiscoveryDecorator;
+use Drupal\layout_plugin\Annotation\Layout;
+use Drupal\layout_plugin\Plugin\Layout\LayoutDefinitionInterface;
+use Drupal\layout_plugin\Plugin\Layout\LayoutInterface;
+
+/**
+ * Provides a plugin manager for layouts.
+ */
+class LayoutPluginManager extends DefaultPluginManager implements LayoutPluginManagerInterface {
+
+  /**
+   * The theme handler.
+   *
+   * @var \Drupal\Core\Extension\ThemeHandlerInterface
+   */
+  protected $themeHandler;
+
+  /**
+   * LayoutPluginManager constructor.
+   *
+   * @param \Traversable $namespaces
+   *   An object that implements \Traversable which contains the root paths
+   *   keyed by the corresponding namespace to look for plugin implementations.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   Cache backend instance to use.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler to invoke the alter hook with.
+   * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
+   *   The theme handle to invoke the alter hook with.
+   */
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler) {
+    parent::__construct('Plugin/Layout', $namespaces, $module_handler, LayoutInterface::class, Layout::class);
+    $this->themeHandler = $theme_handler;
+
+    $this->setCacheBackend($cache_backend, 'layout');
+    $this->alterInfo('layout');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function providerExists($provider) {
+    return $this->moduleHandler->moduleExists($provider) || $this->themeHandler->themeExists($provider);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDiscovery() {
+    if (!$this->discovery) {
+      $discovery = new AnnotatedClassDiscovery($this->subdir, $this->namespaces, $this->pluginDefinitionAnnotationName, $this->additionalAnnotationNamespaces);
+      $discovery = new YamlDiscoveryDecorator($discovery, 'layouts', $this->moduleHandler->getModuleDirectories() + $this->themeHandler->getThemeDirectories());
+      $discovery = new ObjectDefinitionDiscoveryDecorator($discovery, $this->pluginDefinitionAnnotationName);
+      $discovery = new ContainerDerivativeDiscoveryDecorator($discovery);
+      $this->discovery = $discovery;
+    }
+    return $this->discovery;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processDefinition(&$definition, $plugin_id) {
+    parent::processDefinition($definition, $plugin_id);
+
+    if (!$definition instanceof LayoutDefinitionInterface) {
+      throw new InvalidPluginDefinitionException($plugin_id, sprintf('The "%s" layout definition must implement interface %s', $plugin_id, LayoutDefinitionInterface::class));
+    }
+
+    // Add the module or theme path to the 'path'.
+    $provider = $definition->getProvider();
+    if ($this->moduleHandler->moduleExists($provider)) {
+      $base_path = $this->moduleHandler->getModule($provider)->getPath();
+    }
+    elseif ($this->themeHandler->themeExists($provider)) {
+      $base_path = $this->themeHandler->getTheme($provider)->getPath();
+    }
+    else {
+      $base_path = '';
+    }
+
+    $path = $definition->getPath();
+    $path = !empty($path) ? $base_path . '/' . $path : $base_path;
+    $definition->setPath($path);
+
+    // If 'template' is set, then we'll derive 'template_path' and 'theme'.
+    $template = $definition->getTemplate();
+    if (!empty($template)) {
+      $template_parts = explode('/', $template);
+
+      $template = array_pop($template_parts);
+      $template_path = $path;
+      if (count($template_parts) > 0) {
+        $template_path .= '/' . implode('/', $template_parts);
+      }
+      $definition->setTemplate($template);
+      $definition->setTheme(strtr($template, '-', '_'));
+      $definition->setTemplatePath($template_path);
+    }
+
+    if (!$definition->getDefaultRegion()) {
+      $definition->setDefaultRegion(key($definition->getRegions()));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getThemeImplementations() {
+    $hooks = [];
+    /** @var \Drupal\layout_plugin\Plugin\Layout\LayoutDefinitionInterface[] $definitions */
+    $definitions = $this->getDefinitions();
+    foreach ($definitions as $definition) {
+      if ($definition->hasThemeImplementation()) {
+        $hooks[$definition->getTheme()] = [
+          'render element' => 'content',
+          'template' => $definition->getTemplate(),
+          'path' => $definition->getTemplatePath(),
+        ];
+      }
+    }
+    return $hooks;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function alterThemeImplementations(array &$theme_registry) {
+    /** @var \Drupal\layout_plugin\Plugin\Layout\LayoutDefinitionInterface[] $definitions */
+    $definitions = $this->getDefinitions();
+
+    // Find all the theme hooks which are for automatically registered templates
+    // (we ignore manually set theme hooks because we don't know how they were
+    // registered).
+    $layout_theme_hooks = [];
+    foreach ($definitions as $definition) {
+      if ($definition->hasThemeImplementation() && isset($theme_registry[$definition->getTheme()])) {
+        $layout_theme_hooks[] = $definition->getTheme();
+      }
+    }
+
+    // Go through the theme registry looking for our theme hooks and any
+    // suggestions based on them.
+    foreach ($theme_registry as $theme_hook => &$info) {
+      if (in_array($theme_hook, $layout_theme_hooks) || (!empty($info['base hook']) && in_array($info['base hook'], $layout_theme_hooks))) {
+        // If 'template_preprocess' is included, we want to put our preprocess
+        // after to not mess up the expectation that 'template_process' always
+        // runs first.
+        if (($index = array_search('template_preprocess', $info['preprocess functions'])) !== FALSE) {
+          $index++;
+        }
+        else {
+          // Otherwise, put our preprocess function first.
+          $index = 0;
+        }
+
+        array_splice($info['preprocess functions'], $index, 0, '_layout_plugin_preprocess_layout');
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCategories() {
+    // Fetch all categories from definitions and remove duplicates.
+    $categories = array_unique(array_values(array_map(function (LayoutDefinitionInterface $definition) {
+      return $definition->getCategory();
+    }, $this->getDefinitions())));
+    natcasesort($categories);
+    return $categories;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @return \Drupal\layout_plugin\Plugin\Layout\LayoutDefinitionInterface[]
+   */
+  public function getSortedDefinitions(array $definitions = NULL, $label_key = 'label') {
+    // Sort the plugins first by category, then by label.
+    $definitions = isset($definitions) ? $definitions : $this->getDefinitions();
+    // Suppress errors because PHPUnit will indirectly modify the contents,
+    // triggering https://bugs.php.net/bug.php?id=50688.
+    @uasort($definitions, function (LayoutDefinitionInterface $a, LayoutDefinitionInterface $b) {
+      if ($a->getCategory() != $b->getCategory()) {
+        return strnatcasecmp($a->getCategory(), $b->getCategory());
+      }
+      return strnatcasecmp($a->getLabel(), $b->getLabel());
+    });
+    return $definitions;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @return \Drupal\layout_plugin\Plugin\Layout\LayoutDefinitionInterface[][]
+   */
+  public function getGroupedDefinitions(array $definitions = NULL, $label_key = 'label') {
+    $definitions = $this->getSortedDefinitions(isset($definitions) ? $definitions : $this->getDefinitions(), $label_key);
+    $grouped_definitions = [];
+    foreach ($definitions as $id => $definition) {
+      $grouped_definitions[(string) $definition->getCategory()][$id] = $definition;
+    }
+    return $grouped_definitions;
+  }
+
+}
diff --git a/core/modules/layout_plugin/src/LayoutPluginManagerInterface.php b/core/modules/layout_plugin/src/LayoutPluginManagerInterface.php
new file mode 100644
index 0000000..a433747
--- /dev/null
+++ b/core/modules/layout_plugin/src/LayoutPluginManagerInterface.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Drupal\layout_plugin;
+
+use Drupal\Component\Plugin\CategorizingPluginManagerInterface;
+
+/**
+ * Provides the interface for a plugin manager of layouts.
+ */
+interface LayoutPluginManagerInterface extends CategorizingPluginManagerInterface {
+
+  /**
+   * Gets theme implementations for layouts.
+   *
+   * @return array
+   *   An associative array of the same format as returned by hook_theme().
+   *
+   * @see hook_theme()
+   */
+  public function getThemeImplementations();
+
+  /**
+   * Modifies the theme implementations for the layouts that were registered.
+   *
+   * @param array &$theme_registry
+   *   An associative array of the same format as passed to
+   *   hook_theme_registry_alter().
+   *
+   * @see hook_theme_registry_alter()
+   */
+  public function alterThemeImplementations(array &$theme_registry);
+
+  /**
+   * {@inheritdoc}
+   *
+   * @return \Drupal\layout_plugin\Plugin\Layout\LayoutInterface
+   */
+  public function createInstance($plugin_id, array $configuration = []);
+
+  /**
+   * {@inheritdoc}
+   *
+   * @return \Drupal\layout_plugin\Plugin\Layout\LayoutDefinitionInterface|null
+   */
+  public function getDefinition($plugin_id, $exception_on_invalid = TRUE);
+
+  /**
+   * {@inheritdoc}
+   *
+   * @return \Drupal\layout_plugin\Plugin\Layout\LayoutDefinitionInterface[]
+   */
+  public function getDefinitions();
+
+  /**
+   * {@inheritdoc}
+   *
+   * @return \Drupal\layout_plugin\Plugin\Layout\LayoutDefinitionInterface[]
+   */
+  public function getSortedDefinitions(array $definitions = NULL);
+
+  /**
+   * {@inheritdoc}
+   *
+   * @return \Drupal\layout_plugin\Plugin\Layout\LayoutDefinitionInterface[][]
+   */
+  public function getGroupedDefinitions(array $definitions = NULL);
+
+}
diff --git a/core/modules/layout_plugin/src/ObjectDefinitionDiscoveryDecorator.php b/core/modules/layout_plugin/src/ObjectDefinitionDiscoveryDecorator.php
new file mode 100644
index 0000000..013f753
--- /dev/null
+++ b/core/modules/layout_plugin/src/ObjectDefinitionDiscoveryDecorator.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace Drupal\layout_plugin;
+
+use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
+use Drupal\Component\Plugin\Discovery\DiscoveryTrait;
+
+/**
+ * Ensures that all array-based definitions are converted to objects.
+ */
+class ObjectDefinitionDiscoveryDecorator implements DiscoveryInterface {
+
+  use DiscoveryTrait;
+
+  /**
+   * The decorated plugin discovery.
+   *
+   * @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface
+   */
+  protected $decorated;
+
+  /**
+   * The name of the annotation that contains the plugin definition.
+   *
+   * The class corresponding to this name must implement
+   * \Drupal\Component\Annotation\AnnotationInterface.
+   *
+   * @var string|null
+   */
+  protected $pluginDefinitionAnnotationName;
+
+  /**
+   * ObjectDefinitionDiscoveryDecorator constructor.
+   *
+   * @param \Drupal\Component\Plugin\Discovery\DiscoveryInterface $decorated
+   *   The discovery object that is being decorated.
+   * @param string $plugin_definition_annotation_name
+   *   The name of the annotation that contains the plugin definition.
+   */
+  public function __construct(DiscoveryInterface $decorated, $plugin_definition_annotation_name) {
+    $this->decorated = $decorated;
+    $this->pluginDefinitionAnnotationName = $plugin_definition_annotation_name;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefinitions() {
+    $definitions = $this->decorated->getDefinitions();
+    foreach ($definitions as $id => $definition) {
+      if (is_array($definition)) {
+        $definitions[$id] = (new $this->pluginDefinitionAnnotationName($definition))->get();
+      }
+    }
+    return $definitions;
+  }
+
+  /**
+   * Passes through all unknown calls onto the decorated object.
+   *
+   * @param string $method
+   *   The method to call on the decorated plugin discovery.
+   * @param array $args
+   *   The arguments to send to the method.
+   *
+   * @return mixed
+   *   The method result.
+   */
+  public function __call($method, $args) {
+    return call_user_func_array([$this->decorated, $method], $args);
+  }
+
+}
diff --git a/core/modules/layout_plugin/src/Plugin/Layout/LayoutBase.php b/core/modules/layout_plugin/src/Plugin/Layout/LayoutBase.php
new file mode 100644
index 0000000..6549108
--- /dev/null
+++ b/core/modules/layout_plugin/src/Plugin/Layout/LayoutBase.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Drupal\layout_plugin\Plugin\Layout;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Plugin\PluginBase;
+
+/**
+ * Provides a base class for Layout plugins.
+ */
+abstract class LayoutBase extends PluginBase implements LayoutInterface {
+
+  /**
+   * The layout definition.
+   *
+   * @var \Drupal\layout_plugin\Plugin\Layout\LayoutDefinitionInterface
+   */
+  protected $pluginDefinition;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->setConfiguration($configuration);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build(array $regions) {
+    $build = array_intersect_key($regions, $this->pluginDefinition->getRegions());
+    $build['#settings'] = $this->getConfiguration();
+    $build['#theme'] = $this->pluginDefinition->getTheme();
+    if ($library = $this->pluginDefinition->getLibrary()) {
+      $build['#attached']['library'][] = $library;
+    }
+    return $build;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfiguration() {
+    return $this->configuration;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setConfiguration(array $configuration) {
+    $this->configuration = NestedArray::mergeDeep($this->defaultConfiguration(), $configuration);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function calculateDependencies() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @return \Drupal\layout_plugin\Plugin\Layout\LayoutDefinitionInterface
+   */
+  public function getPluginDefinition() {
+    return parent::getPluginDefinition();
+  }
+
+}
diff --git a/core/modules/layout_plugin/src/Plugin/Layout/LayoutDefault.php b/core/modules/layout_plugin/src/Plugin/Layout/LayoutDefault.php
new file mode 100644
index 0000000..462b68d
--- /dev/null
+++ b/core/modules/layout_plugin/src/Plugin/Layout/LayoutDefault.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Drupal\layout_plugin\Plugin\Layout;
+
+/**
+ * Provides a default class for Layout plugins.
+ */
+class LayoutDefault extends LayoutBase {
+
+}
diff --git a/core/modules/layout_plugin/src/Plugin/Layout/LayoutDefinition.php b/core/modules/layout_plugin/src/Plugin/Layout/LayoutDefinition.php
new file mode 100644
index 0000000..01e8575
--- /dev/null
+++ b/core/modules/layout_plugin/src/Plugin/Layout/LayoutDefinition.php
@@ -0,0 +1,315 @@
+<?php
+
+namespace Drupal\layout_plugin\Plugin\Layout;
+
+/**
+ * Provides an implementation of a layout definition and its metadata.
+ */
+class LayoutDefinition implements LayoutDefinitionInterface {
+
+  /**
+   * The plugin ID.
+   *
+   * @var string
+   */
+  protected $id;
+
+  /**
+   * The name of the provider of this layout definition.
+   *
+   * This property is public to allow
+   * \Drupal\Core\Plugin\DefaultPluginManager::findDefinitions() to access it.
+   *
+   * @var string
+   */
+  public $provider;
+
+  /**
+   * The human-readable name.
+   *
+   * @var string
+   */
+  protected $label;
+
+  /**
+   * The human-readable category.
+   *
+   * @var string
+   */
+  protected $category;
+
+  /**
+   * The template file to render this layout (relative to the 'path' given).
+   *
+   * @var string|null
+   */
+  protected $template;
+
+  /**
+   * The path to the template.
+   *
+   * @var string
+   */
+  protected $templatePath;
+
+  /**
+   * The theme hook used to render this layout.
+   *
+   * @var string|null
+   */
+  protected $theme;
+
+  /**
+   * Path (relative to the module or theme) to resources like icon or template.
+   *
+   * @var string
+   */
+  protected $path;
+
+  /**
+   * The asset library.
+   *
+   * @var string|null
+   */
+  protected $library;
+
+  /**
+   * An associative array of regions in this layout.
+   *
+   * The key of the array is the machine name of the region, and the value is
+   * an associative array with the following keys:
+   * - label: (string) The human-readable name of the region.
+   *
+   * Any remaining keys may have special meaning for the given layout plugin,
+   * but are undefined here.
+   *
+   * @var array
+   */
+  protected $regions = [];
+
+  /**
+   * The default region.
+   *
+   * @var string
+   */
+  protected $default_region;
+
+  /**
+   * The name of the layout class.
+   *
+   * @var string
+   */
+  protected $class;
+
+  /**
+   * The name of the original layout class.
+   *
+   * This is only set if the class name is changed.
+   *
+   * @var string
+   */
+  protected $originalClass;
+
+  /**
+   * LayoutDefinition constructor.
+   *
+   * @param array $definition
+   *   An array of values from the annotation.
+   */
+  public function __construct(array $definition) {
+    foreach ($definition as $property => $value) {
+      $this->{$property} = $value;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function id() {
+    return $this->id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getClass() {
+    return $this->class;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOriginalClass() {
+    return $this->originalClass ?: $this->class;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setClass($class) {
+    if (!$this->originalClass && $this->class) {
+      // If the original class is currently not set, set it to the current
+      // class, assume that is the original class name.
+      $this->originalClass = $this->class;
+    }
+    $this->class = $class;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLabel() {
+    return $this->label;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setLabel($label) {
+    $this->label = $label;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCategory() {
+    return $this->category;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setCategory($category) {
+    $this->category = $category;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTemplate() {
+    return $this->template;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setTemplate($template) {
+    $this->template = $template;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTemplatePath() {
+    return $this->templatePath;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setTemplatePath($template_path) {
+    $this->templatePath = $template_path;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTheme() {
+    return $this->theme;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasThemeImplementation() {
+    return $this->getTemplate() && $this->getTheme();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setTheme($theme) {
+    $this->theme = $theme;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPath() {
+    return $this->path;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setPath($path) {
+    $this->path = $path;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLibrary() {
+    return $this->library;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setLibrary($library) {
+    $this->library = $library;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRegions() {
+    return $this->regions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setRegions(array $regions) {
+    $this->regions = $regions;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRegionNames() {
+    return array_keys($this->getRegions());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultRegion() {
+    return $this->default_region;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setDefaultRegion($default_region) {
+    $this->default_region = $default_region;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getProvider() {
+    return $this->provider;
+  }
+
+}
diff --git a/core/modules/layout_plugin/src/Plugin/Layout/LayoutDefinitionInterface.php b/core/modules/layout_plugin/src/Plugin/Layout/LayoutDefinitionInterface.php
new file mode 100644
index 0000000..64b0364
--- /dev/null
+++ b/core/modules/layout_plugin/src/Plugin/Layout/LayoutDefinitionInterface.php
@@ -0,0 +1,223 @@
+<?php
+
+namespace Drupal\layout_plugin\Plugin\Layout;
+
+use Drupal\Component\Plugin\Definition\PluginDefinitionInterface;
+
+/**
+ * Provides an interface for a layout definition and its metadata.
+ */
+interface LayoutDefinitionInterface extends PluginDefinitionInterface {
+
+  /**
+   * Gets the unique identifier of the layout definition.
+   *
+   * @return string
+   *   The unique identifier of the layout definition.
+   */
+  public function id();
+
+  /**
+   * Gets the name of the original layout class.
+   *
+   * In case the class name was changed with setClass(), this will return
+   * the initial value.
+   *
+   * @return string
+   *   The name of the original layout class.
+   */
+  public function getOriginalClass();
+
+  /**
+   * Gets the human-readable name of the layout definition.
+   *
+   * @return string|\Drupal\Core\StringTranslation\TranslatableMarkup
+   *   The human-readable name of the layout definition.
+   */
+  public function getLabel();
+
+  /**
+   * Sets the human-readable name of the layout definition.
+   *
+   * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $label
+   *   The human-readable name of the layout definition.
+   *
+   * @return $this
+   */
+  public function setLabel($label);
+
+  /**
+   * Gets the human-readable category of the layout definition.
+   *
+   * @return string|\Drupal\Core\StringTranslation\TranslatableMarkup
+   *   The human-readable category of the layout definition.
+   */
+  public function getCategory();
+
+  /**
+   * Sets the human-readable category of the layout definition.
+   *
+   * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $category
+   *   The human-readable category of the layout definition.
+   *
+   * @return $this
+   */
+  public function setCategory($category);
+
+  /**
+   * Gets the theme hook.
+   *
+   * @return string|null
+   *   The theme hook, if it exists.
+   */
+  public function getTheme();
+
+  /**
+   * Sets the theme hook.
+   *
+   * @param string $theme
+   *   The theme hook.
+   *
+   * @return $this
+   */
+  public function setTheme($theme);
+
+  /**
+   * Determines if this layout definition has a theme implementation.
+   *
+   * @return bool
+   *   TRUE if this layout definition specifies a theme implementation, FALSE
+   *   otherwise.
+   */
+  public function hasThemeImplementation();
+
+  /**
+   * Gets the template name.
+   *
+   * @return string|null
+   *   The template name, if it exists.
+   */
+  public function getTemplate();
+
+  /**
+   * Sets the template name.
+   *
+   * @param string|null $template
+   *   The template name.
+   *
+   * @return $this
+   */
+  public function setTemplate($template);
+
+  /**
+   * Gets the template path.
+   *
+   * @return string
+   *   The template path.
+   */
+  public function getTemplatePath();
+
+  /**
+   * Sets the template path.
+   *
+   * @param string $template_path
+   *   The template path.
+   *
+   * @return $this
+   */
+  public function setTemplatePath($template_path);
+
+  /**
+   * Gets the base path for this layout definition.
+   *
+   * @return string
+   *   The base path.
+   */
+  public function getPath();
+
+  /**
+   * Sets the base path for this layout definition.
+   *
+   * @param string $path
+   *   The base path.
+   *
+   * @return $this
+   */
+  public function setPath($path);
+
+  /**
+   * Gets the asset library for this layout definition.
+   *
+   * @return string|null
+   *   The asset library, if it exists.
+   */
+  public function getLibrary();
+
+  /**
+   * Sets the asset library for this layout definition.
+   *
+   * @param string|null $library
+   *   The asset library.
+   *
+   * @return $this
+   */
+  public function setLibrary($library);
+
+  /**
+   * Gets the regions for this layout definition.
+   *
+   * @return array[]
+   *    The layout regions. The keys of the array are the machine names of the
+   *    regions, and the values are an associative array with the following
+   *    keys:
+   *     - label: (string) The human-readable name of the region.
+   *    Any remaining keys may have special meaning for the given layout plugin,
+   *    but are undefined here.
+   */
+  public function getRegions();
+
+  /**
+   * Sets the regions for this layout definition.
+   *
+   * @param array[] $regions
+   *   An array of regions, see ::getRegions() for the format.
+   *
+   * @return $this
+   */
+  public function setRegions(array $regions);
+
+  /**
+   * Gets the machine-readable region names.
+   *
+   * @return string[]
+   *   An array of machine-readable region names.
+   */
+  public function getRegionNames();
+
+  /**
+   * Gets the default region.
+   *
+   * @return string
+   *   The machine-readable name of the default region.
+   */
+  public function getDefaultRegion();
+
+  /**
+   * Sets the default region.
+   *
+   * @param string $default_region
+   *   The machine-readable name of the default region.
+   *
+   * @return $this
+   */
+  public function setDefaultRegion($default_region);
+
+  /**
+   * Gets the name of the provider of this layout definition.
+   *
+   * @return string
+   *   The name of the provider of this layout definition.
+   */
+  public function getProvider();
+
+}
diff --git a/core/modules/layout_plugin/src/Plugin/Layout/LayoutInterface.php b/core/modules/layout_plugin/src/Plugin/Layout/LayoutInterface.php
new file mode 100644
index 0000000..b87c7d3
--- /dev/null
+++ b/core/modules/layout_plugin/src/Plugin/Layout/LayoutInterface.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\layout_plugin\Plugin\Layout;
+
+use Drupal\Component\Plugin\DerivativeInspectionInterface;
+use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\Component\Plugin\ConfigurablePluginInterface;
+
+/**
+ * Provides an interface for static Layout plugins.
+ */
+interface LayoutInterface extends PluginInspectionInterface, DerivativeInspectionInterface, ConfigurablePluginInterface {
+
+  /**
+   * Build a render array for layout with regions.
+   *
+   * @param array $regions
+   *   An associative array keyed by region name, containing render arrays
+   *   representing the content that should be placed in each region.
+   *
+   * @return array
+   *   Render array for the layout with regions.
+   */
+  public function build(array $regions);
+
+}
diff --git a/core/modules/layout_plugin/tests/modules/layout_test/config/schema/layout_test.schema.yml b/core/modules/layout_plugin/tests/modules/layout_test/config/schema/layout_test.schema.yml
new file mode 100644
index 0000000..32be5f8
--- /dev/null
+++ b/core/modules/layout_plugin/tests/modules/layout_test/config/schema/layout_test.schema.yml
@@ -0,0 +1,7 @@
+layout.settings.layout_test_plugin:
+  type: layout.settings
+  label: 'Layout test plugin settings'
+  mapping:
+    setting_1:
+      type: string
+      label: 'Setting 1'
diff --git a/core/modules/layout_plugin/tests/modules/layout_test/css/layout-test-1col.css b/core/modules/layout_plugin/tests/modules/layout_test/css/layout-test-1col.css
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/core/modules/layout_plugin/tests/modules/layout_test/css/layout-test-1col.css
@@ -0,0 +1 @@
+
diff --git a/core/modules/layout_plugin/tests/modules/layout_test/css/layout-test-2col.css b/core/modules/layout_plugin/tests/modules/layout_test/css/layout-test-2col.css
new file mode 100644
index 0000000..d5c05b9
--- /dev/null
+++ b/core/modules/layout_plugin/tests/modules/layout_test/css/layout-test-2col.css
@@ -0,0 +1,16 @@
+
+.layout-example-2col .region-left {
+  float: left;
+  width: 50%;
+}
+* html .layout-example-2col .region-left {
+  width: 49.9%;
+}
+
+.layout-example-2col .region-right {
+  float: left;
+  width: 50%;
+}
+* html .layout-example-2col .region-right {
+  width: 49.9%;
+}
diff --git a/core/modules/layout_plugin/tests/modules/layout_test/layout_test.info.yml b/core/modules/layout_plugin/tests/modules/layout_test/layout_test.info.yml
new file mode 100644
index 0000000..34a9a1d
--- /dev/null
+++ b/core/modules/layout_plugin/tests/modules/layout_test/layout_test.info.yml
@@ -0,0 +1,8 @@
+name: 'Layout test'
+type: module
+description: 'Support module for testing layouts.'
+package: Testing
+version: VERSION
+core: 8.x
+dependencies:
+  - layout_plugin
diff --git a/core/modules/layout_plugin/tests/modules/layout_test/layout_test.layouts.yml b/core/modules/layout_plugin/tests/modules/layout_test/layout_test.layouts.yml
new file mode 100644
index 0000000..8372de2
--- /dev/null
+++ b/core/modules/layout_plugin/tests/modules/layout_test/layout_test.layouts.yml
@@ -0,0 +1,21 @@
+layout_test_1col:
+  label: 1 column layout
+  category: Layout test
+  template: templates/layout-test-1col
+  library: layout_test/layout_test_1col
+  regions:
+    top:
+      label: Top region
+    bottom:
+      label: Bottom region
+
+layout_test_2col:
+  label: 2 column layout
+  category: Layout test
+  template: templates/layout-test-2col
+  library: layout_test/layout_test_2col
+  regions:
+    left:
+      label: Left region
+    right:
+      label: Right region
diff --git a/core/modules/layout_plugin/tests/modules/layout_test/layout_test.libraries.yml b/core/modules/layout_plugin/tests/modules/layout_test/layout_test.libraries.yml
new file mode 100644
index 0000000..f2bfb5a
--- /dev/null
+++ b/core/modules/layout_plugin/tests/modules/layout_test/layout_test.libraries.yml
@@ -0,0 +1,11 @@
+layout_test_1col:
+  version: 1.x
+  css:
+    theme:
+      css/layout-test-1col.css: {}
+
+layout_test_2col:
+  version: 1.x
+  css:
+    theme:
+      css/layout-test-2col.css: {}
diff --git a/core/modules/layout_plugin/tests/modules/layout_test/src/Plugin/Layout/LayoutTestPlugin.php b/core/modules/layout_plugin/tests/modules/layout_test/src/Plugin/Layout/LayoutTestPlugin.php
new file mode 100644
index 0000000..9ef5397
--- /dev/null
+++ b/core/modules/layout_plugin/tests/modules/layout_test/src/Plugin/Layout/LayoutTestPlugin.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Drupal\layout_test\Plugin\Layout;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\layout_plugin\Plugin\Layout\LayoutBase;
+
+/**
+ * The plugin that handles the default layout template.
+ *
+ * @Layout(
+ *   id = "layout_test_plugin",
+ *   label = @Translation("Layout plugin (with settings)"),
+ *   category = @Translation("Layout test"),
+ *   description = @Translation("Test layout"),
+ *   template = "templates/layout-test-plugin",
+ *   regions = {
+ *     "main" = {
+ *       "label" = @Translation("Main Region")
+ *     }
+ *   }
+ * )
+ */
+class LayoutTestPlugin extends LayoutBase implements PluginFormInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [
+      'setting_1' => 'Default',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $form['setting_1'] = [
+      '#type' => 'textfield',
+      '#title' => 'Blah',
+      '#default_value' => $this->configuration['setting_1'],
+    ];
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    $this->configuration['setting_1'] = $form_state->getValue('setting_1');
+  }
+
+}
diff --git a/core/modules/layout_plugin/tests/modules/layout_test/templates/layout-test-1col.html.twig b/core/modules/layout_plugin/tests/modules/layout_test/templates/layout-test-1col.html.twig
new file mode 100644
index 0000000..e7a7eb5
--- /dev/null
+++ b/core/modules/layout_plugin/tests/modules/layout_test/templates/layout-test-1col.html.twig
@@ -0,0 +1,14 @@
+{#
+/**
+ * @file
+ * Template for an example 1 column layout.
+ */
+#}
+<div class="layout-example-1col clearfix">
+  <div class="region-top">
+    {{ content.top }}
+  </div>
+  <div class="region-bottom">
+    {{ content.bottom }}
+  </div>
+</div>
diff --git a/core/modules/layout_plugin/tests/modules/layout_test/templates/layout-test-2col.html.twig b/core/modules/layout_plugin/tests/modules/layout_test/templates/layout-test-2col.html.twig
new file mode 100644
index 0000000..11433ee
--- /dev/null
+++ b/core/modules/layout_plugin/tests/modules/layout_test/templates/layout-test-2col.html.twig
@@ -0,0 +1,14 @@
+{#
+/**
+ * @file
+ * Template for an example 2 column layout.
+ */
+#}
+<div class="layout-example-2col clearfix">
+  <div class="region-left">
+    {{ content.left }}
+  </div>
+  <div class="region-right">
+    {{ content.right }}
+  </div>
+</div>
diff --git a/core/modules/layout_plugin/tests/modules/layout_test/templates/layout-test-plugin.html.twig b/core/modules/layout_plugin/tests/modules/layout_test/templates/layout-test-plugin.html.twig
new file mode 100644
index 0000000..e49942c
--- /dev/null
+++ b/core/modules/layout_plugin/tests/modules/layout_test/templates/layout-test-plugin.html.twig
@@ -0,0 +1,15 @@
+{#
+/**
+ * @file
+ * Template for layout_test_plugin layout.
+ */
+#}
+<div class="layout-test-plugin clearfix">
+  <div>
+    <span class="setting-1-label">Blah: </span>
+    {{ settings.setting_1 }}
+  </div>
+  <div class="region-main">
+    {{ content.main }}
+  </div>
+</div>
diff --git a/core/modules/layout_plugin/tests/src/Kernel/LayoutTest.php b/core/modules/layout_plugin/tests/src/Kernel/LayoutTest.php
new file mode 100644
index 0000000..4c5a2a0
--- /dev/null
+++ b/core/modules/layout_plugin/tests/src/Kernel/LayoutTest.php
@@ -0,0 +1,127 @@
+<?php
+
+namespace Drupal\Tests\layout_plugin\Kernel;
+
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests Layout functionality.
+ *
+ * @group layout_plugin
+ */
+class LayoutTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['system', 'layout_plugin', 'layout_test'];
+
+  /**
+   * The layout plugin manager.
+   *
+   * @var \Drupal\layout_plugin\LayoutPluginManagerInterface
+   */
+  protected $layoutPluginManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->layoutPluginManager = $this->container->get('plugin.manager.layout_plugin');
+  }
+
+  /**
+   * Test rendering a layout.
+   *
+   * @dataProvider renderLayoutData
+   */
+  public function testRenderLayout($layout_id, $config, $regions, $html) {
+    /** @var \Drupal\layout_plugin\Plugin\Layout\LayoutInterface $layout */
+    $layout = $this->layoutPluginManager->createInstance($layout_id, $config);
+    $built = $layout->build($regions);
+    $this->render($built);
+    $this->assertRaw($html);
+  }
+
+  /**
+   * Data provider for testRenderLayout().
+   */
+  public function renderLayoutData() {
+    $data['layout_test_1col'] = [
+      'layout_test_1col',
+      [],
+      [
+        'top' => [
+          '#markup' => 'This is the top',
+        ],
+        'bottom' => [
+          '#markup' => 'This is the bottom',
+        ],
+      ],
+    ];
+
+    $data['layout_test_2col'] = [
+      'layout_test_2col',
+      [],
+      [
+        'left' => [
+          '#markup' => 'This is the left',
+        ],
+        'right' => [
+          '#markup' => 'This is the right',
+        ],
+      ],
+    ];
+
+    $data['layout_test_plugin'] = [
+      'layout_test_plugin',
+      [
+        'setting_1' => 'Config value',
+      ],
+      [
+        'main' => [
+          '#markup' => 'Main region',
+        ],
+      ],
+    ];
+
+    $data['layout_test_1col'][] = <<<'EOD'
+<div class="layout-example-1col clearfix">
+  <div class="region-top">
+    This is the top
+  </div>
+  <div class="region-bottom">
+    This is the bottom
+  </div>
+</div>
+EOD;
+
+    $data['layout_test_2col'][] = <<<'EOD'
+<div class="layout-example-2col clearfix">
+  <div class="region-left">
+    This is the left
+  </div>
+  <div class="region-right">
+    This is the right
+  </div>
+</div>
+EOD;
+
+    $data['layout_test_plugin'][] = <<<'EOD'
+<div class="layout-test-plugin clearfix">
+  <div>
+    <span class="setting-1-label">Blah: </span>
+    Config value
+  </div>
+  <div class="region-main">
+    Main region
+  </div>
+</div>
+EOD;
+
+    return $data;
+  }
+
+}
diff --git a/core/modules/layout_plugin/tests/src/Unit/LayoutPluginManagerTest.php b/core/modules/layout_plugin/tests/src/Unit/LayoutPluginManagerTest.php
new file mode 100644
index 0000000..55d3ac2
--- /dev/null
+++ b/core/modules/layout_plugin/tests/src/Unit/LayoutPluginManagerTest.php
@@ -0,0 +1,406 @@
+<?php
+
+namespace Drupal\Tests\layout_plugin\Unit;
+
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Extension\ThemeHandlerInterface;
+use Drupal\layout_plugin\LayoutPluginManager;
+use Drupal\layout_plugin\Plugin\Layout\LayoutDefault;
+use Drupal\layout_plugin\Plugin\Layout\LayoutDefinition;
+use Drupal\layout_plugin\Plugin\Layout\LayoutDefinitionInterface;
+use Drupal\Tests\UnitTestCase;
+use org\bovigo\vfs\vfsStream;
+use Prophecy\Argument;
+
+/**
+ * @coversDefaultClass \Drupal\layout_plugin\LayoutPluginManager
+ * @group layout_plugin
+ */
+class LayoutPluginManagerTest extends UnitTestCase {
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * The theme handler.
+   *
+   * @var \Drupal\Core\Extension\ThemeHandlerInterface
+   */
+  protected $themeHandler;
+
+  /**
+   * Cache backend instance.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface
+   */
+  protected $cacheBackend;
+
+  /**
+   * The layout plugin manager.
+   *
+   * @var \Drupal\layout_plugin\LayoutPluginManagerInterface
+   */
+  protected $layoutPluginManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->setUpFilesystem();
+
+    $container = new ContainerBuilder();
+    $container->set('string_translation', $this->getStringTranslationStub());
+    \Drupal::setContainer($container);
+
+    $this->moduleHandler = $this->prophesize(ModuleHandlerInterface::class);
+
+    $this->moduleHandler->moduleExists('module_a')->willReturn(TRUE);
+    $this->moduleHandler->moduleExists('theme_a')->willReturn(FALSE);
+    $this->moduleHandler->moduleExists('core')->willReturn(FALSE);
+    $this->moduleHandler->moduleExists('invalid_provider')->willReturn(FALSE);
+
+    $module_a = new Extension('/', 'module', vfsStream::url('root/modules/module_a/module_a.layouts.yml'));
+    $this->moduleHandler->getModule('module_a')->willReturn($module_a);
+    $this->moduleHandler->getModuleDirectories()->willReturn(['module_a' => vfsStream::url('root/modules/module_a')]);
+    $this->moduleHandler->alter('layout', Argument::type('array'))->shouldBeCalled();
+
+    $this->themeHandler = $this->prophesize(ThemeHandlerInterface::class);
+
+    $this->themeHandler->themeExists('theme_a')->willReturn(TRUE);
+    $this->themeHandler->themeExists('core')->willReturn(FALSE);
+    $this->themeHandler->themeExists('invalid_provider')->willReturn(FALSE);
+
+    $theme_a = new Extension('/', 'theme', vfsStream::url('root/themes/theme_a/theme_a.layouts.yml'));
+    $this->themeHandler->getTheme('theme_a')->willReturn($theme_a);
+    $this->themeHandler->getThemeDirectories()->willReturn(['theme_a' => vfsStream::url('root/themes/theme_a')]);
+
+    $this->cacheBackend = $this->prophesize(CacheBackendInterface::class);
+
+    $namespaces = new \ArrayObject(['Drupal\Core' => vfsStream::url('root/core/lib/Drupal/Core')]);
+    $this->layoutPluginManager = new LayoutPluginManager($namespaces, $this->cacheBackend->reveal(), $this->moduleHandler->reveal(), $this->themeHandler->reveal(), $this->getStringTranslationStub());
+  }
+
+  /**
+   * @covers ::getDefinitions
+   * @covers ::providerExists
+   */
+  public function testGetDefinitions() {
+    $expected = [
+      'module_a_provided_layout',
+      'theme_a_provided_layout',
+      'plugin_provided_layout',
+    ];
+
+    $layout_definitions = $this->layoutPluginManager->getDefinitions();
+    $this->assertEquals($expected, array_keys($layout_definitions));
+    $this->assertContainsOnlyInstancesOf(LayoutDefinitionInterface::class, $layout_definitions);
+  }
+
+  /**
+   * @covers ::getDefinition
+   * @covers ::processDefinition
+   */
+  public function testGetDefinition() {
+    $theme_a_path = vfsStream::url('root/themes/theme_a');
+    $layout_definition = $this->layoutPluginManager->getDefinition('theme_a_provided_layout');
+    $this->assertSame('theme_a_provided_layout', $layout_definition->id());
+    $this->assertSame('2 column layout', $layout_definition->getLabel());
+    $this->assertSame('Columns: 2', $layout_definition->getCategory());
+    $this->assertSame('twocol', $layout_definition->getTemplate());
+    $this->assertSame("$theme_a_path/templates", $layout_definition->getPath());
+    $this->assertSame('theme_a/twocol', $layout_definition->getLibrary());
+    $this->assertSame('twocol', $layout_definition->getTheme());
+    $this->assertSame("$theme_a_path/templates", $layout_definition->getTemplatePath());
+    $this->assertSame('theme_a', $layout_definition->getProvider());
+    $this->assertSame('right', $layout_definition->getDefaultRegion());
+    $this->assertSame(LayoutDefault::class, $layout_definition->getClass());
+    $expected_regions = [
+      'left' => [
+        'label' => 'Left region',
+      ],
+      'right' => [
+        'label' => 'Right region',
+      ],
+    ];
+    $this->assertSame($expected_regions, $layout_definition->getRegions());
+
+    $module_a_path = vfsStream::url('root/modules/module_a');
+    $layout_definition = $this->layoutPluginManager->getDefinition('module_a_provided_layout');
+    $this->assertSame('module_a_provided_layout', $layout_definition->id());
+    $this->assertSame('1 column layout', $layout_definition->getLabel());
+    $this->assertSame('Columns: 1', $layout_definition->getCategory());
+    $this->assertSame(NULL, $layout_definition->getTemplate());
+    $this->assertSame("$module_a_path/layouts", $layout_definition->getPath());
+    $this->assertSame('module_a/onecol', $layout_definition->getLibrary());
+    $this->assertSame('onecol', $layout_definition->getTheme());
+    $this->assertSame(NULL, $layout_definition->getTemplatePath());
+    $this->assertSame('module_a', $layout_definition->getProvider());
+    $this->assertSame('top', $layout_definition->getDefaultRegion());
+    $this->assertSame(LayoutDefault::class, $layout_definition->getClass());
+    $expected_regions = [
+      'top' => [
+        'label' => 'Top region',
+      ],
+      'bottom' => [
+        'label' => 'Bottom region',
+      ],
+    ];
+    $this->assertSame($expected_regions, $layout_definition->getRegions());
+
+    $core_path = '/core/lib/Drupal/Core';
+    $layout_definition = $this->layoutPluginManager->getDefinition('plugin_provided_layout');
+    $this->assertSame('plugin_provided_layout', $layout_definition->id());
+    $this->assertEquals('Layout plugin', $layout_definition->getLabel());
+    $this->assertEquals('Columns: 1', $layout_definition->getCategory());
+    $this->assertSame('plugin-provided-layout', $layout_definition->getTemplate());
+    $this->assertSame($core_path, $layout_definition->getPath());
+    $this->assertSame(NULL, $layout_definition->getLibrary());
+    $this->assertSame('plugin_provided_layout', $layout_definition->getTheme());
+    $this->assertSame("$core_path/templates", $layout_definition->getTemplatePath());
+    $this->assertSame('core', $layout_definition->getProvider());
+    $this->assertSame('main', $layout_definition->getDefaultRegion());
+    $this->assertSame('Drupal\Core\Plugin\Layout\TestLayout', $layout_definition->getClass());
+    $expected_regions = [
+      'main' => [
+        'label' => 'Main Region',
+      ],
+    ];
+    $this->assertEquals($expected_regions, $layout_definition->getRegions());
+  }
+
+  /**
+   * @covers ::processDefinition
+   */
+  public function testProcessDefinition() {
+    $this->moduleHandler->alter('layout', Argument::type('array'))->shouldNotBeCalled();
+    $this->setExpectedException(InvalidPluginDefinitionException::class, 'The "module_a_derived_layout:array_based" layout definition must implement interface');
+    $module_a_provided_layout = <<<'EOS'
+module_a_derived_layout:
+  deriver: \Drupal\Tests\layout_plugin\Unit\LayoutDeriver
+  array_based: true
+EOS;
+    vfsStream::create([
+      'modules' => [
+        'module_a' => [
+          'module_a.layouts.yml' => $module_a_provided_layout,
+        ],
+      ],
+    ]);
+    $this->layoutPluginManager->getDefinitions();
+  }
+
+  /**
+   * @covers ::getThemeImplementations
+   */
+  public function testGetThemeImplementations() {
+    $core_path = '/core/lib/Drupal/Core';
+    $theme_a_path = vfsStream::url('root/themes/theme_a');
+    $expected = [
+      'twocol' => [
+        'render element' => 'content',
+        'template' => 'twocol',
+        'path' => "$theme_a_path/templates",
+      ],
+      'plugin_provided_layout' => [
+        'render element' => 'content',
+        'template' => 'plugin-provided-layout',
+        'path' => "$core_path/templates",
+      ],
+    ];
+    $theme_implementations = $this->layoutPluginManager->getThemeImplementations();
+    $this->assertSame($expected, $theme_implementations);
+    return $theme_implementations;
+  }
+
+  /**
+   * @covers ::alterThemeImplementations
+   * @depends testGetThemeImplementations
+   */
+  public function testAlterThemeImplementations(array $theme_implementations) {
+    $expected = $theme_implementations;
+
+    $theme_implementations['twocol']['preprocess functions'][] = 'other_preprocess_function';
+    $theme_implementations['plugin_provided_layout']['preprocess functions'][] = 'template_preprocess';
+    $theme_implementations['plugin_provided_layout']['preprocess functions'][] = 'other_preprocess_function';
+
+    $expected['twocol']['preprocess functions'][] = '_layout_plugin_preprocess_layout';
+    $expected['twocol']['preprocess functions'][] = 'other_preprocess_function';
+    $expected['plugin_provided_layout']['preprocess functions'][] = 'template_preprocess';
+    $expected['plugin_provided_layout']['preprocess functions'][] = '_layout_plugin_preprocess_layout';
+    $expected['plugin_provided_layout']['preprocess functions'][] = 'other_preprocess_function';
+
+    $this->layoutPluginManager->alterThemeImplementations($theme_implementations);
+    $this->assertEquals($expected, $theme_implementations);
+  }
+
+  /**
+   * @covers ::getCategories
+   */
+  public function testGetCategories() {
+    $expected = [
+      'Columns: 1',
+      'Columns: 2',
+    ];
+    $categories = $this->layoutPluginManager->getCategories();
+    $this->assertEquals($expected, $categories);
+  }
+
+  /**
+   * @covers ::getSortedDefinitions
+   */
+  public function testGetSortedDefinitions() {
+    $expected = [
+      'module_a_provided_layout',
+      'plugin_provided_layout',
+      'theme_a_provided_layout',
+    ];
+
+    $layout_definitions = $this->layoutPluginManager->getSortedDefinitions();
+    $this->assertEquals($expected, array_keys($layout_definitions));
+    $this->assertContainsOnlyInstancesOf(LayoutDefinitionInterface::class, $layout_definitions);
+  }
+
+  /**
+   * @covers ::getGroupedDefinitions
+   */
+  public function testGetGroupedDefinitions() {
+    $category_expected = [
+      'Columns: 1' => [
+        'module_a_provided_layout',
+        'plugin_provided_layout',
+      ],
+      'Columns: 2' => [
+        'theme_a_provided_layout',
+      ],
+    ];
+
+    $definitions = $this->layoutPluginManager->getGroupedDefinitions();
+    $this->assertEquals(array_keys($category_expected), array_keys($definitions));
+    foreach ($category_expected as $category => $expected) {
+      $this->assertArrayHasKey($category, $definitions);
+      $this->assertEquals($expected, array_keys($definitions[$category]));
+      $this->assertContainsOnlyInstancesOf(LayoutDefinitionInterface::class, $definitions[$category]);
+    }
+  }
+
+  /**
+   * Sets up the filesystem with YAML files and annotated plugins.
+   */
+  protected function setUpFilesystem() {
+    $module_a_provided_layout = <<<'EOS'
+module_a_provided_layout:
+  label: 1 column layout
+  category: 'Columns: 1'
+  theme: onecol
+  path: layouts
+  library: module_a/onecol
+  regions:
+    top:
+      label: Top region
+    bottom:
+      label: Bottom region
+module_a_derived_layout:
+  deriver: \Drupal\Tests\layout_plugin\Unit\LayoutDeriver
+  invalid_provider: true
+EOS;
+    $theme_a_provided_layout = <<<'EOS'
+theme_a_provided_layout:
+  label: 2 column layout
+  category: 'Columns: 2'
+  template: twocol
+  path: templates
+  library: theme_a/twocol
+  default_region: right
+  regions:
+    left:
+      label: Left region
+    right:
+      label: Right region
+EOS;
+    $plugin_provided_layout = <<<'EOS'
+<?php
+namespace Drupal\Core\Plugin\Layout;
+use Drupal\layout_plugin\Plugin\Layout\LayoutBase;
+/**
+ * @Layout(
+ *   id = "plugin_provided_layout",
+ *   label = @Translation("Layout plugin"),
+ *   category = @Translation("Columns: 1"),
+ *   description = @Translation("Test layout"),
+ *   path = "core/lib/Drupal/Core",
+ *   template = "templates/plugin-provided-layout",
+ *   regions = {
+ *     "main" = {
+ *       "label" = @Translation("Main Region")
+ *     }
+ *   }
+ * )
+ */
+class TestLayout extends LayoutBase {}
+EOS;
+    vfsStream::setup('root');
+    vfsStream::create([
+      'modules' => [
+        'module_a' => [
+          'module_a.layouts.yml' => $module_a_provided_layout,
+        ],
+      ],
+    ]);
+    vfsStream::create([
+      'themes' => [
+        'theme_a' => [
+          'theme_a.layouts.yml' => $theme_a_provided_layout,
+        ],
+      ],
+    ]);
+    vfsStream::create([
+      'core' => [
+        'lib' => [
+          'Drupal' => [
+            'Core' => [
+              'Plugin' => [
+                'Layout' => [
+                  'TestLayout.php' => $plugin_provided_layout,
+                ],
+              ],
+            ],
+          ],
+        ],
+      ],
+    ]);
+  }
+
+}
+/**
+ * Provides a dynamic layout deriver for the test.
+ */
+class LayoutDeriver extends DeriverBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDerivativeDefinitions($base_plugin_definition) {
+    if (!empty($base_plugin_definition->array_based)) {
+      $this->derivatives['array_based'] = [];
+    }
+    if (!empty($base_plugin_definition->invalid_provider)) {
+      $this->derivatives['invalid_provider'] = new LayoutDefinition([
+        'id' => 'invalid_provider',
+        'provider' => 'invalid_provider',
+      ]);
+    }
+    return $this->derivatives;
+  }
+
+}
diff --git a/core/modules/node/src/Tests/NodeTypeInitialLanguageTest.php b/core/modules/node/src/Tests/NodeTypeInitialLanguageTest.php
index 73bfdb3..9f9c9c2 100644
--- a/core/modules/node/src/Tests/NodeTypeInitialLanguageTest.php
+++ b/core/modules/node/src/Tests/NodeTypeInitialLanguageTest.php
@@ -109,6 +109,7 @@ function testLanguageFieldVisibility() {
     // Configures Language field formatter and check if it is saved.
     $edit = array(
       'fields[langcode][type]' => 'language',
+      'fields[langcode][region]' => 'content',
     );
     $this->drupalPostForm('admin/structure/types/manage/article/display', $edit, t('Save'));
     $this->drupalGet('admin/structure/types/manage/article/display');
diff --git a/core/modules/options/src/Tests/OptionsFieldUITest.php b/core/modules/options/src/Tests/OptionsFieldUITest.php
index d2c11f0..ee6cf2e 100644
--- a/core/modules/options/src/Tests/OptionsFieldUITest.php
+++ b/core/modules/options/src/Tests/OptionsFieldUITest.php
@@ -335,6 +335,7 @@ function testNodeDisplay() {
     foreach ($file_formatters as $formatter) {
       $edit = array(
         "fields[$this->fieldName][type]" => $formatter,
+        "fields[$this->fieldName][region]" => 'content',
       );
       $this->drupalPostForm('admin/structure/types/manage/' . $this->typeName . '/display', $edit, t('Save'));
       $this->drupalGet('node/' . $node->id());
diff --git a/core/modules/options/tests/options_config_install_test/config/install/core.entity_form_display.node.options_install_test.default.yml b/core/modules/options/tests/options_config_install_test/config/install/core.entity_form_display.node.options_install_test.default.yml
index 19a2ef7..ff5f0ec 100644
--- a/core/modules/options/tests/options_config_install_test/config/install/core.entity_form_display.node.options_install_test.default.yml
+++ b/core/modules/options/tests/options_config_install_test/config/install/core.entity_form_display.node.options_install_test.default.yml
@@ -14,6 +14,7 @@ content:
   title:
     type: string_textfield
     weight: -5
+    region: content
     settings:
       size: 60
       placeholder: ''
@@ -21,6 +22,7 @@ content:
   uid:
     type: entity_reference_autocomplete
     weight: 5
+    region: content
     settings:
       match_operator: CONTAINS
       size: 60
@@ -29,6 +31,7 @@ content:
   created:
     type: datetime_timestamp
     weight: 10
+    region: content
     settings: {  }
     third_party_settings: {  }
   promote:
@@ -36,16 +39,19 @@ content:
     settings:
       display_label: true
     weight: 15
+    region: content
     third_party_settings: {  }
   sticky:
     type: boolean_checkbox
     settings:
       display_label: true
     weight: 16
+    region: content
     third_party_settings: {  }
   body:
     type: text_textarea_with_summary
     weight: 26
+    region: content
     settings:
       rows: 9
       summary_rows: 3
diff --git a/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.default.yml b/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.default.yml
index c107b10..aaea1cb 100644
--- a/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.default.yml
+++ b/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.default.yml
@@ -14,10 +14,12 @@ mode: default
 content:
   links:
     weight: 100
+    region: content
   body:
     label: hidden
     type: text_default
     weight: 101
+    region: content
     settings: {  }
     third_party_settings: {  }
 hidden:
diff --git a/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.teaser.yml b/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.teaser.yml
index 3b472a7..6e79af9 100644
--- a/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.teaser.yml
+++ b/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.teaser.yml
@@ -15,10 +15,12 @@ mode: teaser
 content:
   links:
     weight: 100
+    region: content
   body:
     label: hidden
     type: text_summary_or_trimmed
     weight: 101
+    region: content
     settings:
       trim_length: 600
     third_party_settings: {  }
diff --git a/core/modules/responsive_image/src/Tests/ResponsiveImageFieldUiTest.php b/core/modules/responsive_image/src/Tests/ResponsiveImageFieldUiTest.php
index 8b1030f..fe3167f 100644
--- a/core/modules/responsive_image/src/Tests/ResponsiveImageFieldUiTest.php
+++ b/core/modules/responsive_image/src/Tests/ResponsiveImageFieldUiTest.php
@@ -52,7 +52,11 @@ function testResponsiveImageFormatterUI() {
     $this->drupalGet($manage_display);
 
     // Change the formatter and check that the summary is updated.
-    $edit = array('fields[field_image][type]' => 'responsive_image', 'refresh_rows' => 'field_image');
+    $edit = array(
+      'fields[field_image][type]' => 'responsive_image',
+      'fields[field_image][region]' => 'content',
+      'refresh_rows' => 'field_image',
+      );
     $this->drupalPostAjaxForm(NULL, $edit, array('op' => t('Refresh')));
     $this->assertText("Select a responsive image style.", 'The expected summary is displayed.');
 
diff --git a/core/modules/system/src/Tests/System/DateTimeTest.php b/core/modules/system/src/Tests/System/DateTimeTest.php
index 9ca050f..8e30366 100644
--- a/core/modules/system/src/Tests/System/DateTimeTest.php
+++ b/core/modules/system/src/Tests/System/DateTimeTest.php
@@ -197,6 +197,7 @@ function testEnteringDateTimeViaSelectors() {
     $this->drupalGet('admin/structure/types/manage/page_with_date/form-display');
     $edit = array(
       'fields[field_dt][type]' => 'datetime_datelist',
+      'fields[field_dt][region]' => 'content',
     );
     $this->drupalPostForm('admin/structure/types/manage/page_with_date/form-display', $edit, t('Save'));
     $this->drupalLogout();
diff --git a/core/modules/system/src/Tests/Update/UpdateEntityDisplayTest.php b/core/modules/system/src/Tests/Update/UpdateEntityDisplayTest.php
new file mode 100644
index 0000000..0a2cbf4
--- /dev/null
+++ b/core/modules/system/src/Tests/Update/UpdateEntityDisplayTest.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Drupal\system\Tests\Update;
+
+use Drupal\Core\Entity\Entity\EntityFormDisplay;
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
+
+/**
+ * Tests system_post_update_add_region_to_entity_displays().
+ *
+ * @group Update
+ */
+class UpdateEntityDisplayTest extends UpdatePathTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setDatabaseDumpFiles() {
+    $this->databaseDumpFiles = [
+      __DIR__ . '/../../../tests/fixtures/update/drupal-8.bare.standard.php.gz',
+    ];
+  }
+
+  /**
+   * Tests that entity displays are updated with regions for their fields.
+   */
+  public function testUpdate() {
+    // No region key appears pre-update.
+    $entity_form_display = EntityFormDisplay::load('node.article.default');
+    $options = $entity_form_display->getComponent('body');
+    $this->assertFalse(array_key_exists('region', $options));
+
+    $entity_view_display = EntityViewDisplay::load('node.article.default');
+    $options = $entity_view_display->getComponent('body');
+    $this->assertFalse(array_key_exists('region', $options));
+
+    $this->runUpdates();
+
+    // The region key has been populated with 'content'.
+    $entity_form_display = EntityFormDisplay::load('node.article.default');
+    $options = $entity_form_display->getComponent('body');
+    $this->assertIdentical('content', $options['region']);
+
+    $entity_view_display = EntityViewDisplay::load('node.article.default');
+    $options = $entity_view_display->getComponent('body');
+    $this->assertIdentical('content', $options['region']);
+  }
+
+}
diff --git a/core/modules/system/system.post_update.php b/core/modules/system/system.post_update.php
index b75625c..59a49a4 100644
--- a/core/modules/system/system.post_update.php
+++ b/core/modules/system/system.post_update.php
@@ -5,6 +5,10 @@
  * Post update functions for System.
  */
 
+use Drupal\Core\Entity\Display\EntityDisplayInterface;
+use Drupal\Core\Entity\Entity\EntityFormDisplay;
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
+
 /**
  * @addtogroup updates-8.0.0-beta
  * @{
@@ -41,3 +45,15 @@ function system_post_update_recalculate_configuration_entity_dependencies(&$sand
 /**
  * @} End of "addtogroup updates-8.0.0-beta".
  */
+
+/**
+ * Update entity displays to contain the region for each field.
+ */
+function system_post_update_add_region_to_entity_displays() {
+  $entity_save = function (EntityDisplayInterface $entity) {
+    // preSave() will fill in the correct region based on the 'type'.
+    $entity->save();
+  };
+  array_map($entity_save, EntityViewDisplay::loadMultiple());
+  array_map($entity_save, EntityFormDisplay::loadMultiple());
+}
diff --git a/core/modules/taxonomy/src/Tests/RssTest.php b/core/modules/taxonomy/src/Tests/RssTest.php
index effd4dd..f26cb07 100644
--- a/core/modules/taxonomy/src/Tests/RssTest.php
+++ b/core/modules/taxonomy/src/Tests/RssTest.php
@@ -81,6 +81,7 @@ function testTaxonomyRss() {
     $this->drupalGet("admin/structure/types/manage/article/display/rss");
     $edit = array(
       "fields[taxonomy_" . $this->vocabulary->id() . "][type]" => 'entity_reference_rss_category',
+      "fields[taxonomy_" . $this->vocabulary->id() . "][region]" => 'content',
     );
     $this->drupalPostForm(NULL, $edit, t('Save'));
 
diff --git a/core/profiles/standard/config/install/core.entity_form_display.block_content.basic.default.yml b/core/profiles/standard/config/install/core.entity_form_display.block_content.basic.default.yml
index ee0c138..7ccb5b0 100644
--- a/core/profiles/standard/config/install/core.entity_form_display.block_content.basic.default.yml
+++ b/core/profiles/standard/config/install/core.entity_form_display.block_content.basic.default.yml
@@ -14,6 +14,7 @@ content:
   body:
     type: text_textarea_with_summary
     weight: -4
+    region: content
     settings:
       rows: 9
       summary_rows: 3
@@ -22,6 +23,7 @@ content:
   info:
     type: string_textfield
     weight: -5
+    region: content
     settings:
       size: 60
       placeholder: ''
diff --git a/core/profiles/standard/config/install/core.entity_form_display.comment.comment.default.yml b/core/profiles/standard/config/install/core.entity_form_display.comment.comment.default.yml
index fa5d834..1010be2 100644
--- a/core/profiles/standard/config/install/core.entity_form_display.comment.comment.default.yml
+++ b/core/profiles/standard/config/install/core.entity_form_display.comment.comment.default.yml
@@ -13,9 +13,11 @@ mode: default
 content:
   author:
     weight: -2
+    region: content
   comment_body:
     type: text_textarea
     weight: 11
+    region: content
     settings:
       rows: 5
       placeholder: ''
@@ -23,6 +25,7 @@ content:
   subject:
     type: string_textfield
     weight: 10
+    region: content
     settings:
       size: 60
       placeholder: ''
diff --git a/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml b/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml
index 79156b2..c94e36e 100644
--- a/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml
+++ b/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml
@@ -21,6 +21,7 @@ content:
   body:
     type: text_textarea_with_summary
     weight: 1
+    region: content
     settings:
       rows: 9
       summary_rows: 3
@@ -29,16 +30,19 @@ content:
   comment:
     type: comment_default
     weight: 20
+    region: content
     settings: {  }
     third_party_settings: {  }
   created:
     type: datetime_timestamp
     weight: 10
+    region: content
     settings: {  }
     third_party_settings: {  }
   field_image:
     type: image_image
     weight: 4
+    region: content
     settings:
       progress_indicator: throbber
       preview_image_style: thumbnail
@@ -46,11 +50,13 @@ content:
   field_tags:
     type: entity_reference_autocomplete_tags
     weight: 3
+    region: content
     settings: {  }
     third_party_settings: {  }
   path:
     type: path
     weight: 30
+    region: content
     settings: {  }
     third_party_settings: {  }
   promote:
@@ -58,16 +64,19 @@ content:
     settings:
       display_label: true
     weight: 15
+    region: content
     third_party_settings: {  }
   sticky:
     type: boolean_checkbox
     settings:
       display_label: true
     weight: 16
+    region: content
     third_party_settings: {  }
   title:
     type: string_textfield
     weight: 0
+    region: content
     settings:
       size: 60
       placeholder: ''
@@ -75,6 +84,7 @@ content:
   uid:
     type: entity_reference_autocomplete
     weight: 5
+    region: content
     settings:
       match_operator: CONTAINS
       size: 60
diff --git a/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml b/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml
index 1fef06d..0b7ffd1 100644
--- a/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml
+++ b/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml
@@ -15,6 +15,7 @@ content:
   body:
     type: text_textarea_with_summary
     weight: 31
+    region: content
     settings:
       rows: 9
       summary_rows: 3
@@ -23,11 +24,13 @@ content:
   created:
     type: datetime_timestamp
     weight: 10
+    region: content
     settings: {  }
     third_party_settings: {  }
   path:
     type: path
     weight: 30
+    region: content
     settings: {  }
     third_party_settings: {  }
   promote:
@@ -35,16 +38,19 @@ content:
     settings:
       display_label: true
     weight: 15
+    region: content
     third_party_settings: {  }
   sticky:
     type: boolean_checkbox
     settings:
       display_label: true
     weight: 16
+    region: content
     third_party_settings: {  }
   title:
     type: string_textfield
     weight: -5
+    region: content
     settings:
       size: 60
       placeholder: ''
@@ -52,6 +58,7 @@ content:
   uid:
     type: entity_reference_autocomplete
     weight: 5
+    region: content
     settings:
       match_operator: CONTAINS
       size: 60
diff --git a/core/profiles/standard/config/install/core.entity_form_display.user.user.default.yml b/core/profiles/standard/config/install/core.entity_form_display.user.user.default.yml
index 466b6e0..6832229 100644
--- a/core/profiles/standard/config/install/core.entity_form_display.user.user.default.yml
+++ b/core/profiles/standard/config/install/core.entity_form_display.user.user.default.yml
@@ -14,12 +14,16 @@ mode: default
 content:
   account:
     weight: -10
+    region: content
   contact:
     weight: 5
+    region: content
   language:
     weight: 0
+    region: content
   timezone:
     weight: 6
+    region: content
   user_picture:
     type: image_image
     settings:
@@ -27,4 +31,5 @@ content:
       preview_image_style: thumbnail
     third_party_settings: {  }
     weight: -1
+    region: content
 hidden: {  }
diff --git a/core/profiles/standard/config/install/core.entity_view_display.block_content.basic.default.yml b/core/profiles/standard/config/install/core.entity_view_display.block_content.basic.default.yml
index bd52f77..e494882 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.block_content.basic.default.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.block_content.basic.default.yml
@@ -15,6 +15,7 @@ content:
     label: hidden
     type: text_default
     weight: 0
+    region: content
     settings: {  }
     third_party_settings: {  }
 hidden: {  }
diff --git a/core/profiles/standard/config/install/core.entity_view_display.comment.comment.default.yml b/core/profiles/standard/config/install/core.entity_view_display.comment.comment.default.yml
index 1ed49ce..6ae213d 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.comment.comment.default.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.comment.comment.default.yml
@@ -15,8 +15,10 @@ content:
     label: hidden
     type: text_default
     weight: 0
+    region: content
     settings: {  }
     third_party_settings: {  }
   links:
     weight: 100
+    region: content
 hidden: {  }
diff --git a/core/profiles/standard/config/install/core.entity_view_display.node.article.default.yml b/core/profiles/standard/config/install/core.entity_view_display.node.article.default.yml
index 98a2de8..5c43252 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.node.article.default.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.node.article.default.yml
@@ -22,12 +22,14 @@ content:
   body:
     type: text_default
     weight: 0
+    region: content
     settings: {  }
     third_party_settings: {  }
     label: hidden
   comment:
     type: comment_default
     weight: 110
+    region: content
     label: above
     settings:
       view_mode: default
@@ -36,6 +38,7 @@ content:
   field_image:
     type: image
     weight: -1
+    region: content
     settings:
       image_style: large
       image_link: ''
@@ -44,12 +47,14 @@ content:
   field_tags:
     type: entity_reference_label
     weight: 10
+    region: content
     label: above
     settings:
       link: true
     third_party_settings: {  }
   links:
     weight: 100
+    region: content
 hidden:
   field_image: true
   field_tags: true
diff --git a/core/profiles/standard/config/install/core.entity_view_display.node.article.rss.yml b/core/profiles/standard/config/install/core.entity_view_display.node.article.rss.yml
index 75a14a3..84660b6 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.node.article.rss.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.node.article.rss.yml
@@ -17,6 +17,7 @@ mode: rss
 content:
   links:
     weight: 100
+    region: content
 hidden:
   body: true
   comment: true
diff --git a/core/profiles/standard/config/install/core.entity_view_display.node.article.teaser.yml b/core/profiles/standard/config/install/core.entity_view_display.node.article.teaser.yml
index 43ee079..7b96908 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.node.article.teaser.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.node.article.teaser.yml
@@ -21,6 +21,7 @@ content:
   body:
     type: text_summary_or_trimmed
     weight: 0
+    region: content
     settings:
       trim_length: 600
     third_party_settings: {  }
@@ -28,6 +29,7 @@ content:
   field_image:
     type: image
     weight: -1
+    region: content
     settings:
       image_style: medium
       image_link: content
@@ -36,12 +38,14 @@ content:
   field_tags:
     type: entity_reference_label
     weight: 10
+    region: content
     settings:
       link: true
     third_party_settings: {  }
     label: above
   links:
     weight: 100
+    region: content
 hidden:
   comment: true
   field_image: true
diff --git a/core/profiles/standard/config/install/core.entity_view_display.node.page.default.yml b/core/profiles/standard/config/install/core.entity_view_display.node.page.default.yml
index dcb2d3e..8afd942 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.node.page.default.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.node.page.default.yml
@@ -16,8 +16,10 @@ content:
     label: hidden
     type: text_default
     weight: 100
+    region: content
     settings: {  }
     third_party_settings: {  }
   links:
     weight: 101
+    region: content
 hidden: {  }
diff --git a/core/profiles/standard/config/install/core.entity_view_display.node.page.teaser.yml b/core/profiles/standard/config/install/core.entity_view_display.node.page.teaser.yml
index f235a10..bc7a68c 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.node.page.teaser.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.node.page.teaser.yml
@@ -17,9 +17,11 @@ content:
     label: hidden
     type: text_summary_or_trimmed
     weight: 100
+    region: content
     settings:
       trim_length: 600
     third_party_settings: {  }
   links:
     weight: 101
+    region: content
 hidden: {  }
diff --git a/core/profiles/standard/config/install/core.entity_view_display.user.user.compact.yml b/core/profiles/standard/config/install/core.entity_view_display.user.user.compact.yml
index 4c13792..2ff13ad 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.user.user.compact.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.user.user.compact.yml
@@ -16,6 +16,7 @@ content:
   user_picture:
     type: image
     weight: 0
+    region: content
     settings:
       image_style: thumbnail
       image_link: content
diff --git a/core/profiles/standard/config/install/core.entity_view_display.user.user.default.yml b/core/profiles/standard/config/install/core.entity_view_display.user.user.default.yml
index 9e4621d..ef1fdd7 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.user.user.default.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.user.user.default.yml
@@ -14,9 +14,11 @@ mode: default
 content:
   member_for:
     weight: 5
+    region: content
   user_picture:
     type: image
     weight: 0
+    region: content
     settings:
       image_style: thumbnail
       image_link: content
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayBaseTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayBaseTest.php
new file mode 100644
index 0000000..137201a
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayBaseTest.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Entity;
+
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Entity\EntityDisplayBase
+ *
+ * @group Entity
+ */
+class EntityDisplayBaseTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['entity_test'];
+
+  /**
+   * @covers ::preSave
+   */
+  public function testPreSave() {
+    $entity_display = EntityViewDisplay::create([
+      'targetEntityType' => 'entity_test',
+      'bundle' => 'entity_test',
+      'mode' => 'default',
+      'status' => TRUE,
+      'content' => [
+        'foo' => ['type' => 'visible'],
+        'bar' => ['type' => 'hidden'],
+        'name' => ['type' => 'hidden', 'region' => 'content'],
+      ],
+    ]);
+
+    // Ensure that no region is set on the component.
+    $this->assertArrayNotHasKey('region', $entity_display->getComponent('foo'));
+    $this->assertArrayNotHasKey('region', $entity_display->getComponent('bar'));
+
+    // Ensure that a region is set on the component after saving.
+    $entity_display->save();
+
+    // The component with a visible type has been assigned a region.
+    $component = $entity_display->getComponent('foo');
+    $this->assertArrayHasKey('region', $component);
+    $this->assertSame('content', $component['region']);
+
+    // The component with a hidden type has been removed.
+    $this->assertNull($entity_display->getComponent('bar'));
+
+    // The component with a valid region and hidden type is unchanged.
+    $component = $entity_display->getComponent('name');
+    $this->assertArrayHasKey('region', $component);
+    $this->assertSame('content', $component['region']);
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayFormBaseTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayFormBaseTest.php
new file mode 100644
index 0000000..86efc77
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayFormBaseTest.php
@@ -0,0 +1,283 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Entity;
+
+use Drupal\Core\Entity\Display\EntityDisplayInterface;
+use Drupal\Core\Form\FormState;
+use Drupal\field_ui\Form\EntityViewDisplayEditForm;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * @coversDefaultClass \Drupal\field_ui\Form\EntityDisplayFormBase
+ *
+ * @group Entity
+ */
+class EntityDisplayFormBaseTest extends KernelTestBase {
+
+  /**
+   * @covers ::copyFormValuesToEntity
+   */
+  public function testCopyFormValuesToEntity() {
+    $field_values = [];
+    $entity = $this->prophesize(EntityDisplayInterface::class);
+    $entity->getPluginCollections()->willReturn([]);
+
+    // A field with no initial values, with mismatched submitted values, type is
+    // hidden.
+    $entity->getComponent('new_field_mismatch_type_hidden')->willReturn([]);
+    $field_values['new_field_mismatch_type_hidden'] = [
+      'weight' => 0,
+      'type' => 'hidden',
+      'region' => 'content',
+    ];
+    $entity
+      ->setComponent('new_field_mismatch_type_hidden', [
+        'weight' => 0,
+        'type' => 'hidden',
+        'region' => 'content',
+      ])
+      ->will(function($args) {
+        // On subsequent calls, getComponent() will return the newly set values.
+        $this->getComponent($args[0])->willReturn($args[1]);
+      })
+      ->shouldBeCalled();
+
+    // A field with no initial values, with mismatched submitted values, type is
+    // visible.
+    $entity->getComponent('new_field_mismatch_type_visible')->willReturn([]);
+    $field_values['new_field_mismatch_type_visible'] = [
+      'weight' => 0,
+      'type' => 'textfield',
+      'region' => 'hidden',
+    ];
+    $entity->removeComponent('new_field_mismatch_type_visible')
+      ->will(function ($args) {
+        // On subsequent calls, getComponent() will return an empty array.
+        $this->getComponent($args[0])->willReturn([]);
+      })
+      ->shouldBeCalled();
+
+    // An initially hidden field, with identical submitted values.
+    $entity->getComponent('field_hidden_no_changes')
+      ->willReturn([
+        'weight' => 0,
+        'type' => 'hidden',
+        'region' => 'hidden',
+      ]);
+    $field_values['field_hidden_no_changes'] = [
+      'weight' => 0,
+      'type' => 'hidden',
+      'region' => 'hidden',
+    ];
+    $entity->removeComponent('field_hidden_no_changes')
+      ->will(function ($args) {
+        // On subsequent calls, getComponent() will return an empty array.
+        $this->getComponent($args[0])->willReturn([]);
+      })
+      ->shouldBeCalled();
+
+    // An initially visible field, with identical submitted values.
+    $entity->getComponent('field_visible_no_changes')
+      ->willReturn([
+        'weight' => 0,
+        'type' => 'textfield',
+        'region' => 'content',
+      ]);
+    $field_values['field_visible_no_changes'] = [
+      'weight' => 0,
+      'type' => 'textfield',
+      'region' => 'content',
+    ];
+    $entity
+      ->setComponent('field_visible_no_changes', [
+        'weight' => 0,
+        'type' => 'textfield',
+        'region' => 'content',
+      ])
+      ->shouldBeCalled();
+
+    // An initially hidden field, with a submitted type change.
+    $entity->getComponent('field_start_hidden_change_type')
+      ->willReturn([
+        'weight' => 0,
+        'type' => 'hidden',
+        'region' => 'hidden',
+      ]);
+    $field_values['field_start_hidden_change_type'] = [
+      'weight' => 0,
+      'type' => 'textfield',
+      'region' => 'hidden',
+    ];
+    $entity->removeComponent('field_start_hidden_change_type')
+      ->will(function ($args) {
+        // On subsequent calls, getComponent() will return an empty array.
+        $this->getComponent($args[0])->willReturn([]);
+      })
+      ->shouldBeCalled();
+
+    // An initially hidden field, with a submitted region change.
+    $entity->getComponent('field_start_hidden_change_region')
+      ->willReturn([
+        'weight' => 0,
+        'type' => 'hidden',
+        'region' => 'hidden',
+      ]);
+    $field_values['field_start_hidden_change_region'] = [
+      'weight' => 0,
+      'type' => 'hidden',
+      'region' => 'content',
+    ];
+    $entity
+      ->setComponent('field_start_hidden_change_region', [
+        'weight' => 0,
+        'type' => 'hidden',
+        'region' => 'content',
+      ])
+      ->will(function($args) {
+        // On subsequent calls, getComponent() will return the newly set values.
+        $this->getComponent($args[0])->willReturn($args[1]);
+      })
+      ->shouldBeCalled();
+
+    // An initially hidden field, with a submitted region and type change.
+    $entity->getComponent('field_start_hidden_change_both')
+      ->willReturn([
+        'weight' => 0,
+        'type' => 'hidden',
+        'region' => 'hidden',
+      ]);
+    $field_values['field_start_hidden_change_both'] = [
+      'weight' => 0,
+      'type' => 'textfield',
+      'region' => 'content',
+    ];
+    $entity
+      ->setComponent('field_start_hidden_change_both', [
+        'weight' => 0,
+        'type' => 'textfield',
+        'region' => 'content',
+      ])
+      ->will(function($args) {
+        // On subsequent calls, getComponent() will return the newly set values.
+        $this->getComponent($args[0])->willReturn($args[1]);
+      })
+      ->shouldBeCalled();
+
+    // An initially visible field, with a submitted type change.
+    $entity->getComponent('field_start_visible_change_type')
+      ->willReturn([
+        'weight' => 0,
+        'type' => 'textfield',
+        'region' => 'content',
+      ]);
+    $field_values['field_start_visible_change_type'] = [
+      'weight' => 0,
+      'type' => 'hidden',
+      'region' => 'content',
+    ];
+    $entity
+      ->setComponent('field_start_visible_change_type', [
+        'weight' => 0,
+        'type' => 'hidden',
+        'region' => 'content',
+      ])
+      ->will(function($args) {
+        // On subsequent calls, getComponent() will return the newly set values.
+        $this->getComponent($args[0])->willReturn($args[1]);
+      })
+      ->shouldBeCalled();
+
+    // An initially visible field, with a submitted region change.
+    $entity->getComponent('field_start_visible_change_region')
+      ->willReturn([
+        'weight' => 0,
+        'type' => 'textfield',
+        'region' => 'content',
+      ]);
+    $field_values['field_start_visible_change_region'] = [
+      'weight' => 0,
+      'type' => 'textfield',
+      'region' => 'hidden',
+    ];
+    $entity->removeComponent('field_start_visible_change_region')
+      ->will(function ($args) {
+        // On subsequent calls, getComponent() will return an empty array.
+        $this->getComponent($args[0])->willReturn([]);
+      })
+      ->shouldBeCalled();
+
+    // An initially visible field, with a submitted region and type change.
+    $entity->getComponent('field_start_visible_change_both')
+      ->willReturn([
+        'weight' => 0,
+        'type' => 'textfield',
+        'region' => 'content',
+      ]);
+    $field_values['field_start_visible_change_both'] = [
+      'weight' => 0,
+      'type' => 'hidden',
+      'region' => 'hidden',
+    ];
+    $entity->removeComponent('field_start_visible_change_both')
+      ->will(function ($args) {
+        // On subsequent calls, getComponent() will return an empty array.
+        $this->getComponent($args[0])->willReturn([]);
+      })
+      ->shouldBeCalled();
+
+    // A field that is flagged for plugin settings update on the second build.
+    $entity->getComponent('field_plugin_settings_update')
+      ->willReturn([
+        'weight' => 0,
+        'type' => 'textfield',
+        'region' => 'content',
+      ]);
+    $field_values['field_plugin_settings_update'] = [
+      'weight' => 0,
+      'type' => 'textfield',
+      'region' => 'content',
+      'settings_edit_form' => [
+        'third_party_settings' => [
+          'foo' => 'bar',
+        ],
+      ],
+    ];
+    $entity
+      ->setComponent('field_plugin_settings_update', [
+        'weight' => 0,
+        'type' => 'textfield',
+        'region' => 'content',
+      ])
+      ->will(function ($args) {
+        // On subsequent calls, getComponent() will return the newly set values.
+        $this->getComponent($args[0])->willReturn($args[1]);
+        $args[1] += [
+          'settings' => [],
+          'third_party_settings' => [
+            'foo' => 'bar',
+          ],
+        ];
+        $this->setComponent($args[0], $args[1])->shouldBeCalled();
+      })
+      ->shouldBeCalled();
+
+    $form_object = new EntityViewDisplayEditForm($this->container->get('plugin.manager.field.field_type'), $this->container->get('plugin.manager.field.formatter'));
+    $form_object->setEntity($entity->reveal());
+
+    $form = [
+      '#fields' => array_keys($field_values),
+      '#extra' => [],
+    ];
+    $form_state = new FormState();
+    $form_state->setValues(['fields' => $field_values]);
+
+    $form_object->buildEntity($form, $form_state);
+
+    // Flag one field for updating plugin settings.
+    $form_state->set('plugin_settings_update', 'field_plugin_settings_update');
+    // During form submission, buildEntity() will be called twice. Simulate that
+    // here to prove copyFormValuesToEntity() is idempotent.
+    $form_object->buildEntity($form, $form_state);
+  }
+
+}
