diff --git a/core/composer.json b/core/composer.json
index 503704c..45d2c80 100644
--- a/core/composer.json
+++ b/core/composer.json
@@ -138,6 +138,7 @@
         "drupal/taxonomy": "self.version",
         "drupal/telephone": "self.version",
         "drupal/text": "self.version",
+        "drupal/theme_layout": "self.version",
         "drupal/toolbar": "self.version",
         "drupal/tour": "self.version",
         "drupal/tracker": "self.version",
diff --git a/core/modules/theme_layout/config/schema/theme_layout.schema.yml b/core/modules/theme_layout/config/schema/theme_layout.schema.yml
new file mode 100644
index 0000000..388ba5d
--- /dev/null
+++ b/core/modules/theme_layout/config/schema/theme_layout.schema.yml
@@ -0,0 +1,10 @@
+theme_settings.third_party.theme_layout:
+  type: mapping
+  label: 'Per-theme layout settings'
+  mapping:
+    layout:
+      type: string
+      label: 'Layout ID'
+    settings:
+      type: layout_plugin.settings.[%parent.id]
+      label: 'Layout settings'
diff --git a/core/modules/theme_layout/tests/src/Functional/ThemeLayoutTest.php b/core/modules/theme_layout/tests/src/Functional/ThemeLayoutTest.php
new file mode 100644
index 0000000..29029f6
--- /dev/null
+++ b/core/modules/theme_layout/tests/src/Functional/ThemeLayoutTest.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace Drupal\Tests\theme_layout\Functional;
+
+use Drupal\Core\Url;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * @todo.
+ *
+ * @group theme_layout
+ */
+class ThemeLayoutTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['block', 'system'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->container->get('theme_installer')->install(['seven']);
+    $this->container->get('theme_handler')->setDefault('seven');
+
+    $this->drupalLogin($this->drupalCreateUser([
+      'access administration pages',
+      'administer blocks',
+      'administer themes',
+      'view the administration theme',
+    ]));
+    $this->placeBlock('system_branding_block', ['region' => 'header', 'id' => 'branding']);
+    $this->placeBlock('system_messages_block', ['region' => 'help', 'id' => 'messages']);
+    $this->placeBlock('system_main_block', ['region' => 'content', 'id' => 'main']);
+    $this->placeBlock('system_powered_by_block', ['region' => 'content', 'id' => 'powered_by']);
+  }
+
+  /**
+   */
+  public function test() {
+    $this->drupalGet(Url::fromRoute('block.admin_display_theme', ['theme' => 'seven']));
+    $this->assertRegionNames([
+      'branding' => 'header',
+      'messages' => 'help',
+      'main' => 'content',
+      'powered_by' => 'content',
+    ]);
+
+    $this->container->get('module_installer')->install(['theme_layout']);
+
+    $this->drupalGet(Url::fromRoute('system.theme_settings_theme', ['theme' => 'seven']));
+    $select = $this->assertSession()->selectExists('theme_layout');
+    $this->assertSame('seven', $select->getValue());
+
+    $this->drupalGet(Url::fromRoute('block.admin_display_theme', ['theme' => 'seven']));
+    $this->assertRegionNames([
+      'branding' => 'header',
+      'messages' => 'help',
+      'main' => 'content',
+      'powered_by' => 'content',
+    ]);
+  }
+
+  /**
+   * @todo.
+   */
+  protected function assertRegionNames(array $mappings) {
+    $this->assertSession()->pageTextContains('Header');
+    $this->assertSession()->pageTextContains('Pre-content');
+    $this->assertSession()->pageTextContains('Breadcrumb');
+    $this->assertSession()->pageTextContains('Highlighted');
+    $this->assertSession()->pageTextContains('Help');
+    $this->assertSession()->pageTextContains('Content');
+    foreach ($mappings as $block_id => $region) {
+      $this->assertSame($region, $this->assertSession()->selectExists('blocks[' . $block_id . '][region]')->getValue());
+    }
+  }
+
+}
diff --git a/core/modules/theme_layout/theme_layout.info.yml b/core/modules/theme_layout/theme_layout.info.yml
new file mode 100644
index 0000000..6151e9a
--- /dev/null
+++ b/core/modules/theme_layout/theme_layout.info.yml
@@ -0,0 +1,8 @@
+name: 'Theme Layout'
+type: module
+description: 'Adds layout capabilities to themes.'
+package: Core (Experimental)
+version: VERSION
+core: 8.x
+dependencies:
+  - layout_discovery
diff --git a/core/modules/theme_layout/theme_layout.install b/core/modules/theme_layout/theme_layout.install
new file mode 100644
index 0000000..9a81b2f
--- /dev/null
+++ b/core/modules/theme_layout/theme_layout.install
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @file
+ * Contains install and update functions for Theme Layout.
+ */
+
+/**
+ * Implements hook_install().
+ */
+function theme_layout_install() {
+  // Set the layout for known themes.
+  $themes = \Drupal::service('theme_handler')->listInfo();
+  _theme_layout_install_theme_settings(array_keys($themes));
+
+  // Refresh the theme info now that the layout has been changed.
+  \Drupal::service('theme_handler')->refreshInfo();
+}
+
+/**
+ * Implements hook_themes_installed().
+ */
+function theme_layout_themes_installed($theme_list) {
+  _theme_layout_install_theme_settings($theme_list);
+
+  // Refresh the theme info now that the layout has been changed.
+  \Drupal::service('theme_handler')->refreshInfo();
+}
+
+/**
+ * @todo.
+ */
+function _theme_layout_install_theme_settings($themes) {
+  $layout_definitions = \Drupal::service('plugin.manager.core.layout')->getDefinitions();
+  foreach (array_intersect($themes, array_keys($layout_definitions)) as $theme) {
+    \Drupal::configFactory()->getEditable($theme . '.settings')
+      ->set('third_party_settings.theme_layout.layout', $theme)
+      ->save(TRUE);
+  }
+}
diff --git a/core/modules/theme_layout/theme_layout.module b/core/modules/theme_layout/theme_layout.module
new file mode 100644
index 0000000..80231a5
--- /dev/null
+++ b/core/modules/theme_layout/theme_layout.module
@@ -0,0 +1,139 @@
+<?php
+
+/**
+ * @file
+ * Provides hook implementations for Theme Layout.
+ */
+
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element;
+use Drupal\Core\Routing\RouteMatchInterface;
+
+/**
+ * Implements hook_help().
+ */
+function theme_layout_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    case 'help.page.theme_layout':
+      $output = '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('The Theme Layout module allows you to assign regions for your theme layout.') . '</p>';
+      $output .= '<p>' . t('For more information, see the <a href=":theme-layout-documentation">online documentation for the Theme Layout module</a>.', [':theme-layout-documentation' => 'https://www.drupal.org/documentation/modules/@todo_once_module_name_is_decided_upon']) . '</p>';
+      return $output;
+  }
+}
+
+/**
+ * Implements hook_form_system_theme_settings_alter().
+ */
+function theme_layout_form_system_theme_settings_alter(&$form, FormStateInterface $form_state) {
+  $build_info = $form_state->getBuildInfo();
+  if (!isset($build_info['args'][0]) || !($theme = $build_info['args'][0])) {
+    return;
+  }
+
+  $form['theme_layouts'] = [
+    '#type' => 'details',
+    '#title' => t('Layout'),
+    '#open' => TRUE,
+  ];
+  $layout_options = \Drupal::service('plugin.manager.core.layout')->getLayoutOptions();
+  $form['theme_layouts']['theme_layout'] = [
+    '#type' => 'select',
+    '#title' => t('Select a layout'),
+    '#options' => $layout_options,
+    '#default_value' => \Drupal::config($theme . '.settings')->get('third_party_settings.theme_layout.layout') ?: 'default',
+  ];
+  // Color module removes values from $form_state, so run first.
+  array_unshift($form['#submit'], 'theme_layout_system_theme_settings_submit');
+}
+
+/**
+ * Form submission handler for the system theme settings form.
+ */
+function theme_layout_system_theme_settings_submit(array &$form, FormStateInterface $form_state) {
+  $theme = $form_state->getValue('theme');
+  $config = \Drupal::configFactory()->getEditable($theme . '.settings');
+  $old_layout = $config->get('third_party_settings.theme_layout.layout');
+  $new_layout = $form_state->getValue('theme_layout');
+  $config
+    ->set('third_party_settings.theme_layout.layout', $new_layout)
+    ->save();
+
+  // Refresh the theme info now that the layout has been changed.
+  \Drupal::service('theme_handler')->refreshInfo();
+
+  if ($new_layout !== $old_layout) {
+    // @todo Devise a mechanism for mapping old regions to new ones in
+    //   https://www.drupal.org/node/2796877.
+    $layout_definition = \Drupal::service('plugin.manager.core.layout')->getLayout($new_layout);
+    $new_region = isset($layout_definition['default_region']) ? $layout_definition['default_region'] : key($layout_definition['regions']);
+
+    /** @var \Drupal\block\BlockInterface[] $blocks */
+    $blocks = \Drupal::entityTypeManager()->getStorage('block')->loadByProperties(['theme' => $theme]);
+    foreach ($blocks as $block) {
+      $block->setRegion($new_region)->save();
+    }
+  }
+
+  // Remove the value to prevent theme_settings_convert_to_config() from
+  // directly saving it.
+  $form_state->unsetValue('theme_layout');
+}
+
+/**
+ * Implements hook_system_info_alter().
+ */
+function theme_layout_system_info_alter(array &$info, Extension $file, $type) {
+  if ($file->getType() === 'theme' && $layout_definition = \Drupal::service('plugin.manager.core.layout')->getDefinition(_theme_layout_get_layout_id($file->getName()), FALSE)) {
+    unset($info['regions'], $info['regions_hidden']);
+    foreach ($layout_definition->getRegions() as $region => $region_info) {
+      $info['regions'][$region] = (string) $region_info['label'];
+      if (isset($region_info['hidden'])) {
+        $info['regions_hidden'][] = $region;
+      }
+    }
+  }
+}
+
+/**
+ * Implements hook_element_info_alter().
+ */
+function theme_layout_element_info_alter(array &$info) {
+  if (isset($info['page'])) {
+    $info['page']['#pre_render'][] = 'theme_layout_page_pre_render';
+  }
+}
+
+/**
+ * #pre_render callback: Sets the #theme and #attached library for a layout.
+ */
+function theme_layout_page_pre_render($element) {
+  $theme = \Drupal::theme()->getActiveTheme()->getName();
+  if ($layout_id = _theme_layout_get_layout_id($theme)) {
+    $regions = [];
+    foreach (Element::children($element) as $name) {
+      // Move the item from the top-level of $build into a region-specific
+      // section.
+      // @todo Ideally the array structure would remain unchanged, see
+      //   https://www.drupal.org/node/2846393.
+      $regions[$name] = $element[$name];
+      unset($element[$name]);
+    }
+
+    $layout = \Drupal::service('plugin.manager.core.layout')->createInstance($layout_id);
+    $element['_theme_layout'] = $layout->build($regions);
+    unset($element['#type'], $element['#theme']);
+  }
+  return $element;
+}
+
+/**
+ * Gets the layout ID for a given theme.
+ *
+ * @return string
+ *   The layout ID for the given theme.
+ */
+function _theme_layout_get_layout_id($theme) {
+  return \Drupal::config($theme . '.settings')->get('third_party_settings.theme_layout.layout');
+}
diff --git a/core/themes/seven/seven.layouts.yml b/core/themes/seven/seven.layouts.yml
new file mode 100644
index 0000000..84b87da
--- /dev/null
+++ b/core/themes/seven/seven.layouts.yml
@@ -0,0 +1,28 @@
+seven:
+  label: 'Seven'
+  theme_hook: page
+  library: seven/global-styling
+  category: 'Theme-specific'
+  default_region: content
+  regions:
+    header:
+      label: Header
+    pre_content:
+      label: Pre-Content
+    breadcrumb:
+      label: Breadcrumb
+    highlighted:
+      label: Highlighted
+    help:
+      label: Help
+    content:
+      label: Content
+    page_top:
+      label: 'Page top'
+      hidden: true
+    page_bottom:
+      label: 'Page bottom'
+      hidden: true
+    sidebar_first:
+      label: 'Sidebar first'
+      hidden: true
