diff --git a/core/composer.json b/core/composer.json
index 53ab8fa..862b5a3 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",
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..b328a75
--- /dev/null
+++ b/core/modules/field_layout/config/schema/field_layout.schema.yml
@@ -0,0 +1,23 @@
+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:
+    layout:
+      type: string
+      label: 'Layout'
+    fields:
+      type: sequence
+      label: 'Fields'
+      sequence:
+        type: mapping
+        label: 'Field'
+        mapping:
+          region:
+            type: string
+            label: 'Region'
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..62f225a
--- /dev/null
+++ b/core/modules/field_layout/field_layout.info.yml
@@ -0,0 +1,6 @@
+name: 'Field Layout'
+type: module
+description: 'Adds layout capabilities to the Field UI.'
+package: Core (Experimental)
+version: VERSION
+core: 8.x
diff --git a/core/modules/field_layout/field_layout.install b/core/modules/field_layout/field_layout.install
new file mode 100644
index 0000000..c0fbe3f
--- /dev/null
+++ b/core/modules/field_layout/field_layout.install
@@ -0,0 +1,24 @@
+<?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;
+
+/**
+ * Implements hook_install().
+ */
+function field_layout_install() {
+  // Save each entity display in order to trigger field_layout_entity_presave().
+  $entities = array_merge(EntityFormDisplay::loadMultiple(), EntityViewDisplay::loadMultiple());
+  foreach ($entities as $entity) {
+    $entity->save();
+  }
+
+  // Invalidate the render cache since all content will now have a layout.
+  Cache::invalidateTags(['rendered']);
+}
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..59ed060
--- /dev/null
+++ b/core/modules/field_layout/field_layout.libraries.yml
@@ -0,0 +1,6 @@
+drupal.field_layout:
+  version: VERSION
+  js:
+    js/field_layout.js: {}
+  dependencies:
+    - field_ui/drupal.field_ui
diff --git a/core/modules/field_layout/field_layout.module b/core/modules/field_layout/field_layout.module
new file mode 100644
index 0000000..b62c89a
--- /dev/null
+++ b/core/modules/field_layout/field_layout.module
@@ -0,0 +1,126 @@
+<?php
+
+/**
+ * @file
+ * Provides hook implementations for Field Layout.
+ */
+
+use Drupal\Core\Entity\ContentEntityFormInterface;
+use Drupal\Core\Entity\Display\EntityDisplayInterface;
+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\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[] */
+  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_presave().
+ */
+function field_layout_entity_presave(EntityInterface $entity) {
+  // Whenever creating a new entity display, set the layout to default and
+  // initialize the field layout based on default configuration.
+  if ($entity instanceof EntityDisplayInterface && !$entity->getThirdPartySettings('field_layout')) {
+    $field_layout = [];
+    $field_layout += array_fill_keys(array_keys($entity->get('content')), ['region' => 'content']);
+    $field_layout += array_fill_keys(array_keys($entity->get('hidden')), ['region' => 'hidden']);
+    $entity->setThirdPartySetting('field_layout', 'fields', $field_layout);
+    $entity->setThirdPartySetting('field_layout', 'layout', 'default');
+  }
+}
+
+/**
+ * Implements hook_entity_view_alter().
+ */
+function field_layout_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
+  _field_layout_apply_layout($build, $display);
+}
+
+/**
+ * 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) {
+    _field_layout_apply_layout($form, $form_object->getFormDisplay($form_state));
+  }
+}
+
+/**
+ * Applies the layout to an entity build.
+ *
+ * @param array $build
+ *   A renderable array representing the entity content or form.
+ * @param \Drupal\Core\Entity\Display\EntityDisplayInterface $display
+ *   The entity display holding the display options configured for the
+ *   entity components.
+ */
+function _field_layout_apply_layout(array &$build, EntityDisplayInterface $display) {
+  if (!$layout_definition = \Drupal::service('field_layout.layout_repository')->getLayoutForDisplay($display)) {
+    return;
+  }
+
+  // Add the regions to the $build in the correct order.
+  foreach ($layout_definition['regions'] as $region => $region_info) {
+    $build['field_layout__' . $region]['#theme_wrappers'][] = 'field_layout_region';
+    $build['field_layout__' . $region]['#region'] = $region;
+  }
+
+  // Move the field from the top-level of $build into a region-specific section.
+  foreach ($display->getThirdPartySetting('field_layout', 'fields', []) as $name => $field) {
+    if (isset($build[$name]) && $field['region'] !== 'hidden') {
+      $build['field_layout__' . $field['region']][$name] = $build[$name];
+    }
+    unset($build[$name]);
+  }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function field_layout_theme($existing, $type, $theme, $path) {
+  return [
+    'field_layout_region' => [
+      'render element' => 'elements',
+    ],
+  ];
+}
+
+/**
+ * Prepares variables for field layout region templates.
+ *
+ * Default template: field-layout-region.html.twig.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - elements: An associative array containing properties of the region.
+ */
+function template_preprocess_field_layout_region(&$variables) {
+  // Create the $content variable that templates expect.
+  $variables['content'] = $variables['elements']['#children'];
+  $variables['region'] = $variables['elements']['#region'];
+}
diff --git a/core/modules/field_layout/field_layout.services.yml b/core/modules/field_layout/field_layout.services.yml
new file mode 100644
index 0000000..b4082ff
--- /dev/null
+++ b/core/modules/field_layout/field_layout.services.yml
@@ -0,0 +1,4 @@
+services:
+  field_layout.layout_repository:
+    class: Drupal\field_layout\LayoutRepository
+    arguments: ['@string_translation']
diff --git a/core/modules/field_layout/js/field_layout.js b/core/modules/field_layout/js/field_layout.js
new file mode 100644
index 0000000..9b8ab45
--- /dev/null
+++ b/core/modules/field_layout/js/field_layout.js
@@ -0,0 +1,76 @@
+/**
+ * @file
+ * Attaches the behaviors for the Field Layout module.
+ */
+(function ($, Drupal) {
+
+  "use strict";
+
+  /**
+   * Row handlers for the 'Manage display' screen.
+   */
+  Drupal.fieldUIDisplayOverview = Drupal.fieldUIDisplayOverview || {};
+
+  Drupal.fieldUIDisplayOverview.field_layout = function (row, data) {
+    this.row = row;
+    this.name = data.name;
+    this.region = data.region;
+    this.tableDrag = data.tableDrag;
+
+    // Attach change listener to the 'region' select.
+    this.$regionSelect = $(row).find('select.field-layout-region');
+    this.$regionSelect.on('change', Drupal.fieldUIOverview.onChange);
+
+    // Load the Field UI row handler.
+    this.fieldRowHandler = new Drupal.fieldUIDisplayOverview.field(row, data);
+
+    return this;
+  };
+
+  Drupal.fieldUIDisplayOverview.field_layout.prototype = {
+
+    /**
+     * Returns the region corresponding to the current form values of the row.
+     *
+     * @return {string}
+     *   Either 'hidden' or 'content'.
+     */
+    getRegion: function () {
+      return this.$regionSelect.val();
+    },
+
+    /**
+     * Reacts to a row being changed regions.
+     *
+     * This function is called when the row is moved to a different region, as
+     * a
+     * result of either :
+     * - a drag-and-drop action (the row's form elements then probably need to
+     * be updated accordingly)
+     * - user input in one of the form elements watched by the
+     *   {@link Drupal.fieldUIOverview.onChange} change listener.
+     *
+     * @param {string} region
+     *   The name of the new region for the row.
+     *
+     * @return {object}
+     *   A hash object indicating which rows should be Ajax-updated as a result
+     *   of the change, in the format expected by
+     *   {@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);
+
+      // Call the field UI row handler implementation.
+      // @todo Remove the massaging of region once
+      //   Drupal.fieldUIDisplayOverview.field.regionChange is fixed in
+      //   https://www.drupal.org/node/2796885.
+      return this.fieldRowHandler.regionChange(region === 'hidden' || 'content');
+    }
+  };
+
+})(jQuery, Drupal);
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..b769bee
--- /dev/null
+++ b/core/modules/field_layout/src/Form/FieldLayoutEntityDisplayFormTrait.php
@@ -0,0 +1,176 @@
+<?php
+
+namespace Drupal\field_layout\Form;
+
+use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Provides shared code for entity display forms.
+ */
+trait FieldLayoutEntityDisplayFormTrait {
+
+  /**
+   * The field layout repository.
+   *
+   * @var \Drupal\field_layout\LayoutRepositoryInterface
+   */
+  protected $fieldLayoutRepository;
+
+  /**
+   * Overrides \Drupal\field_ui\Form\EntityDisplayFormBase::getRegions().
+   */
+  public function getRegions() {
+    $regions = [];
+
+    $layout = $this->fieldLayoutRepository->getLayoutForDisplay($this->getEntity());
+    foreach ($layout['regions'] 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;
+  }
+
+  /**
+   * Adds the field layout section to the form.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return array
+   */
+  protected function addFieldLayout(array $form, FormStateInterface $form_state) {
+    $form['#entity_builders'][] = [static::class, 'updateFieldLayout'];
+
+    $form['#attached']['library'][] = 'field_layout/drupal.field_layout';
+
+    $form['fields']['#tabledrag'][] = [
+      'action' => 'match',
+      'relationship' => 'parent',
+      'group' => 'field-layout-region',
+      'subgroup' => 'field-layout-region',
+      'source' => 'field-name',
+    ];
+
+    $form['field_layouts'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Layout for @bundle in @view_mode', [
+        '@bundle' => str_replace('_', ' ', $this->getEntity()->getTargetBundle()),
+        '@view_mode' => str_replace('_', ' ', $this->getEntity()->getMode()),
+      ]),
+    ];
+    $layout_options = [];
+    foreach ($this->fieldLayoutRepository->getLayoutDefinitions() as $name => $layout) {
+      $layout_options[$name] = $layout['label'];
+    }
+    $form['field_layouts']['field_layout'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Select a layout'),
+      '#options' => $layout_options,
+      '#default_value' => $this->getEntity()->getThirdPartySetting('field_layout', 'layout', 'default'),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * Overrides \Drupal\field_ui\Form\EntityDisplayFormBase::getRowRegion().
+   */
+  public function getRowRegion($row) {
+    switch ($row['#row_type']) {
+      case 'field':
+      case 'extra_field':
+        return $row['region']['#value'] ?: 'hidden';
+    }
+  }
+
+  /**
+   * An #entity_builder callback to update the Field Layout settings.
+   *
+   * @param string $entity_type_id
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   * @param array $form
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   */
+  public static function updateFieldLayout($entity_type_id, EntityInterface $entity, &$form, FormStateInterface $form_state) {
+    if ($form_state->isSubmitted() && $entity instanceof ThirdPartySettingsInterface) {
+      $old_layout = $entity->getThirdPartySetting('field_layout', 'layout');
+      $new_layout = $form_state->getValue('field_layout');
+      $fields = [];
+      foreach ($form_state->getValue('fields') as $field_name => $values) {
+        // If the layout is changing, reset all fields.
+        // @todo Devise a mechanism for mapping old regions to new ones in
+        //   https://www.drupal.org/node/2796877.
+        $fields[$field_name]['region'] = $old_layout === $new_layout ? $values['region'] : 'hidden';
+      }
+      $entity->setThirdPartySetting('field_layout', 'fields', $fields);
+      $entity->setThirdPartySetting('field_layout', 'layout', $new_layout);
+    }
+  }
+
+  /**
+   * Adds a column with the Region select.
+   *
+   * @param array $row
+   *   A table row array.
+   * @param string $field_name
+   *   The field name.
+   * @param string $label
+   *   The field name.
+   *
+   * @return array
+   *   A table row array.
+   */
+  protected function addRegionSelect(array $row, $field_name, $label) {
+    $row['#js_settings']['rowHandler'] = 'field_layout';
+    $field_layout = $this->getEntity()->getThirdPartySetting('field_layout', 'fields', []);
+    $new_row = [
+      'region' => [
+        '#type' => 'select',
+        '#title' => $this->t('Region for @title', ['@title' => $label]),
+        '#title_display' => 'invisible',
+        '#options' => $this->getRegionOptions(),
+        '#empty_value' => 'hidden',
+        '#default_value' => isset($field_layout[$field_name]['region']) ? $field_layout[$field_name]['region'] : 'hidden',
+        '#attributes' => ['class' => ['field-layout-region']],
+      ],
+    ];
+
+    $position = array_search('parent_wrapper', array_keys($row)) + 1;
+    return array_merge(array_slice($row, 0, $position), $new_row, array_slice($row, $position));
+  }
+
+  /**
+   * Adjusts the table header for field layout.
+   *
+   * @param array $header
+   *   The table header.
+   *
+   * @return array
+   *   The table header.
+   */
+  protected function adjustTableHeader(array $header) {
+    array_splice($header, 3, 0, [$this->t('Region')]);
+    return $header;
+  }
+
+  /**
+   * Gets the form entity.
+   *
+   * @return \Drupal\Core\Entity\Display\EntityDisplayInterface
+   *   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..3579155
--- /dev/null
+++ b/core/modules/field_layout/src/Form/FieldLayoutEntityFormDisplayEditForm.php
@@ -0,0 +1,78 @@
+<?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\field_layout\LayoutRepositoryInterface;
+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\field_layout\LayoutRepositoryInterface $field_layout_repository
+   *   The field layout repository.
+   */
+  public function __construct(FieldTypePluginManagerInterface $field_type_manager, PluginManagerBase $plugin_manager, LayoutRepositoryInterface $field_layout_repository) {
+    parent::__construct($field_type_manager, $plugin_manager);
+    $this->fieldLayoutRepository = $field_layout_repository;
+  }
+
+  /**
+   * {@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('field_layout.layout_repository')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form = parent::form($form, $form_state);
+    return $this->addFieldLayout($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildFieldRow(FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state) {
+    $row = parent::buildFieldRow($field_definition, $form, $form_state);
+    return $this->addRegionSelect($row, $field_definition->getName(), $field_definition->getLabel());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildExtraFieldRow($field_id, $extra_field) {
+    $row = parent::buildExtraFieldRow($field_id, $extra_field);
+    return $this->addRegionSelect($row, $field_id, $extra_field['label']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getTableHeader() {
+    $header = parent::getTableHeader();
+    return $this->adjustTableHeader($header);
+  }
+
+}
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..c36fc1d
--- /dev/null
+++ b/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php
@@ -0,0 +1,78 @@
+<?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\field_layout\LayoutRepositoryInterface;
+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\field_layout\LayoutRepositoryInterface $field_layout_repository
+   *   The field layout repository.
+   */
+  public function __construct(FieldTypePluginManagerInterface $field_type_manager, PluginManagerBase $plugin_manager, LayoutRepositoryInterface $field_layout_repository) {
+    parent::__construct($field_type_manager, $plugin_manager);
+    $this->fieldLayoutRepository = $field_layout_repository;
+  }
+
+  /**
+   * {@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('field_layout.layout_repository')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form = parent::form($form, $form_state);
+    return $this->addFieldLayout($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildFieldRow(FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state) {
+    $row = parent::buildFieldRow($field_definition, $form, $form_state);
+    return $this->addRegionSelect($row, $field_definition->getName(), $field_definition->getLabel());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildExtraFieldRow($field_id, $extra_field) {
+    $row = parent::buildExtraFieldRow($field_id, $extra_field);
+    return $this->addRegionSelect($row, $field_id, $extra_field['label']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getTableHeader() {
+    $header = parent::getTableHeader();
+    return $this->adjustTableHeader($header);
+  }
+
+}
diff --git a/core/modules/field_layout/src/LayoutRepository.php b/core/modules/field_layout/src/LayoutRepository.php
new file mode 100644
index 0000000..83b3108
--- /dev/null
+++ b/core/modules/field_layout/src/LayoutRepository.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Drupal\field_layout;
+
+use Drupal\Core\Entity\Display\EntityDisplayInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+
+/**
+ * Provides a repository for field layouts.
+ */
+class LayoutRepository implements LayoutRepositoryInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * LayoutRepository constructor.
+   *
+   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
+   *   The string translation service.
+   */
+  public function __construct(TranslationInterface $string_translation) {
+    $this->stringTranslation = $string_translation;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLayoutDefinitions() {
+    // @todo Replace with layout_plugin in https://www.drupal.org/node/2296423.
+    $layouts = [
+      'default' => [
+        'label' => $this->t('Default'),
+        'regions' => [
+          'content' => [
+            'label' => $this->t('Content'),
+          ],
+        ],
+      ],
+      '1col_stacked' => [
+        'label' => $this->t('One column stacked'),
+        'regions' => [
+          'top' => [
+            'label' => $this->t('Top'),
+          ],
+          'bottom' => [
+            'label' => $this->t('Bottom'),
+          ],
+        ],
+      ],
+      '2col' => [
+        'label' => $this->t('Two column'),
+        'regions' => [
+          'left' => [
+            'label' => $this->t('Left'),
+          ],
+          'right' => [
+            'label' => $this->t('Right'),
+          ],
+        ],
+      ],
+    ];
+    return $layouts;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLayoutForDisplay(EntityDisplayInterface $display) {
+    $layout_name = $display->getThirdPartySetting('field_layout', 'layout', 'default');
+    $layout_definitions = $this->getLayoutDefinitions();
+    if (!$layout_name || !isset($layout_definitions[$layout_name])) {
+      return [];
+    }
+
+    return $layout_definitions[$layout_name];
+  }
+
+}
diff --git a/core/modules/field_layout/src/LayoutRepositoryInterface.php b/core/modules/field_layout/src/LayoutRepositoryInterface.php
new file mode 100644
index 0000000..f8287f3
--- /dev/null
+++ b/core/modules/field_layout/src/LayoutRepositoryInterface.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Drupal\field_layout;
+
+use Drupal\Core\Entity\Display\EntityDisplayInterface;
+
+/**
+ * Provides the interface for a repository of field layouts.
+ */
+interface LayoutRepositoryInterface {
+
+  /**
+   * Gets all field layout definitions.
+   *
+   * @return array[]
+   *   An array of layout definitions.
+   */
+  public function getLayoutDefinitions();
+
+  /**
+   * Gets the layout definition for the layout used by a display.
+   *
+   * @param \Drupal\Core\Entity\Display\EntityDisplayInterface $display
+   *   The entity display using a layout.
+   *
+   * @return mixed[]
+   *   The layout definition for the given display.
+   */
+  public function getLayoutForDisplay(EntityDisplayInterface $display);
+
+}
diff --git a/core/modules/field_layout/templates/field-layout-region.html.twig b/core/modules/field_layout/templates/field-layout-region.html.twig
new file mode 100644
index 0000000..6969b5f
--- /dev/null
+++ b/core/modules/field_layout/templates/field-layout-region.html.twig
@@ -0,0 +1,27 @@
+{#
+/**
+ * @file
+ * Default theme implementation to display a region.
+ *
+ * Available variables:
+ * - content: The content for this region, typically blocks.
+ * - attributes: HTML attributes for the region <div>.
+ * - region: The name of the region variable as defined in the theme's
+ *   .info.yml file.
+ *
+ * @see template_preprocess_region()
+ *
+ * @ingroup themeable
+ */
+#}
+{%
+set classes = [
+'field-layout-region',
+'field-layout-region--' ~ region|clean_class,
+]
+%}
+{% if content %}
+  <div{{ attributes.addClass(classes) }}>
+    {{ content }}
+  </div>
+{% endif %}
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/FunctionalJavascript/FieldLayoutTest.php b/core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php
new file mode 100644
index 0000000..ea007e0
--- /dev/null
+++ b/core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php
@@ -0,0 +1,249 @@
+<?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', 'node', 'field_layout_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->createContentType([
+      'type' => 'article',
+    ]);
+    $this->createNode([
+      'type' => 'article',
+      'title' => 'The node title',
+      'body' => [[
+        'value' => 'The node body',
+      ]],
+    ]);
+    $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',
+      'administer content types',
+      'administer nodes',
+      'administer node fields',
+      'administer node display',
+      'administer node form display',
+      '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 default layout is used.
+    $this->drupalGet('entity_test/manage/1/edit');
+    $this->assertFieldInRegion('field_test_text[0][value]', 'content');
+
+    // The default 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', '2col');
+    $this->submitForm([], 'Save');
+
+    $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->assertSession()->elementNotExists('css', '.field-layout-region');
+    $this->assertSession()->fieldNotExists('field_test_text[0][value]');
+
+    // 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 left region.
+    $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();
+    $field_test_text_row->hasClass('.drag-previous');
+    $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--left .field--name-field-test-text');
+    $this->assertFieldInRegion('field_test_text[0][value]', 'left');
+
+    // 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 default 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', '2col');
+    $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-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();
+    $field_test_text_row->hasClass('.drag-previous');
+    $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][type]', 'text_trimmed')->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-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-region--right .field--name-field-test-text');
+  }
+
+  /**
+   * Tests an entity type that has fields shown by default.
+   */
+  public function testNodeView() {
+    // By default, the default layout is used.
+    $this->drupalGet('node/1');
+    $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;
+  }
+
+  /**
+   * 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->assertSession()->fieldExists($field_selector, $region_element);
+  }
+
+}
diff --git a/core/modules/field_layout/tests/src/Unit/LayoutRepositoryTest.php b/core/modules/field_layout/tests/src/Unit/LayoutRepositoryTest.php
new file mode 100644
index 0000000..303ce6c
--- /dev/null
+++ b/core/modules/field_layout/tests/src/Unit/LayoutRepositoryTest.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Drupal\Tests\field_layout\Unit;
+
+use Drupal\Core\Entity\Display\EntityDisplayInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\field_layout\LayoutRepository;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\field_layout\LayoutRepository
+ * @group field_layout
+ */
+class LayoutRepositoryTest extends UnitTestCase {
+
+  /**
+   * @covers ::getLayoutDefinitions
+   */
+  public function testGetLayoutDefinitions() {
+    $layout_repository = new LayoutRepository($this->getStringTranslationStub());
+
+    $expected = ['default', '1col_stacked', '2col'];
+
+    $layout_definitions = $layout_repository->getLayoutDefinitions();
+    $this->assertEquals($expected, array_keys($layout_definitions));
+  }
+
+  /**
+   * @covers ::getLayoutForDisplay
+   */
+  public function testGetLayoutForDisplay() {
+    $layout_repository = new LayoutRepository($this->getStringTranslationStub());
+
+    $display = $this->prophesize(EntityDisplayInterface::class);
+    $display->getThirdPartySetting('field_layout', 'layout', 'default')->willReturn('2col');
+
+    $expected = ['left', 'right'];
+
+    $layout_definition = $layout_repository->getLayoutForDisplay($display->reveal());
+    $this->assertEquals($expected, array_keys($layout_definition['regions']));
+  }
+
+  /**
+   * @covers ::getLayoutForDisplay
+   */
+  public function testGetLayoutForDisplayEmpty() {
+    $layout_repository = new LayoutRepository($this->getStringTranslationStub());
+
+    $display = $this->prophesize(EntityDisplayInterface::class);
+    $display->getThirdPartySetting('field_layout', 'layout', 'default')->willReturn('default');
+
+    $expected = ['content'];
+
+    $layout_definition = $layout_repository->getLayoutForDisplay($display->reveal());
+    $this->assertEquals($expected, array_keys($layout_definition['regions']));
+  }
+
+}
