diff --git a/core/config/schema/core.layout.schema.yml b/core/config/schema/core.layout.schema.yml
new file mode 100644
index 0000000..604c5b8
--- /dev/null
+++ b/core/config/schema/core.layout.schema.yml
@@ -0,0 +1,6 @@
+layout.settings:
+  type: mapping
+  mapping: { }
+
+layout.settings.*:
+  type: layout.settings
diff --git a/core/core.services.yml b/core/core.services.yml
index 6310c21..7d5f1e9 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -586,6 +586,9 @@ services:
   plugin.manager.block:
     class: Drupal\Core\Block\BlockManager
     parent: default_plugin_manager
+  plugin.manager.layout:
+    class: Drupal\Core\Layout\Plugin\LayoutPluginManager
+    arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@theme_handler']
   plugin.manager.field.field_type:
     class: Drupal\Core\Field\FieldTypePluginManager
     arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@typed_data_manager']
diff --git a/core/lib/Drupal/Core/Layout/Annotation/Layout.php b/core/lib/Drupal/Core/Layout/Annotation/Layout.php
new file mode 100644
index 0000000..2493935
--- /dev/null
+++ b/core/lib/Drupal/Core/Layout/Annotation/Layout.php
@@ -0,0 +1,136 @@
+<?php
+
+namespace Drupal\Core\Layout\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * 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\Core\Layout\Plugin\LayoutInterface
+ * @see \Drupal\Core\Layout\Plugin\LayoutBase
+ * @see \Drupal\Core\Layout\Plugin\LayoutPluginManager
+ * @see plugin_api
+ *
+ * @Annotation
+ */
+class Layout extends Plugin {
+
+  /**
+   * The plugin ID.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The layout type.
+   *
+   * Available options:
+   *  - full: Layout for the whole page.
+   *  - page: Layout for the main page response.
+   *  - partial: A partial layout that is typically used for sub-regions.
+   *
+   * @var string
+   */
+  public $type = 'page';
+
+  /**
+   * The human-readable name.
+   *
+   * @var string
+   *
+   * @ingroup plugin_translatable
+   */
+  public $label;
+
+  /**
+   * An optional description for advanced layouts.
+   *
+   * Sometimes layouts are so complex that the name is insufficient to describe
+   * a layout such that a visually impaired administrator could layout a page
+   * for a non-visually impaired audience. If specified, it will provide a
+   * description that is used for accessibility purposes.
+   *
+   * @var string
+   *
+   * @ingroup plugin_translatable
+   */
+  public $description;
+
+  /**
+   * The human-readable category.
+   *
+   * @var string
+   *
+   * @ingroup plugin_translatable
+   */
+  public $category;
+
+  /**
+   * 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;
+
+  /**
+   * The template file to render this layout (relative to the 'path' given).
+   *
+   * If specified, then the template will be registered with hook_theme()
+   * automatically and the module or theme registering this layout does not need
+   * to do it. This is mutually exclusive with 'theme' - you can't specify both.
+   *
+   * @var string optional
+   *
+   * @see hook_theme()
+   */
+  public $template;
+
+  /**
+   * 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;
+
+  /**
+   * The path to the preview image (relative to the 'path' given).
+   *
+   * @var string optional
+   */
+  public $icon;
+
+  /**
+   * 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 = array();
+
+}
diff --git a/core/lib/Drupal/Core/Layout/Plugin/LayoutBase.php b/core/lib/Drupal/Core/Layout/Plugin/LayoutBase.php
new file mode 100644
index 0000000..18c6dda
--- /dev/null
+++ b/core/lib/Drupal/Core/Layout/Plugin/LayoutBase.php
@@ -0,0 +1,179 @@
+<?php
+
+namespace Drupal\Core\Layout\Plugin;
+
+use Drupal\Component\Plugin\ConfigurablePluginInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\PluginBase;
+use Drupal\Core\Plugin\PluginFormInterface;
+
+/**
+ * Provides a base class for Layout plugins.
+ */
+abstract class LayoutBase extends PluginBase implements LayoutInterface, ConfigurablePluginInterface, PluginFormInterface {
+
+  /**
+   * The layout configuration.
+   *
+   * @var array
+   */
+  protected $configuration = [];
+
+  /**
+   * Gets the human-readable name.
+   *
+   * @return string|NULL
+   *   The human-readable name.
+   */
+  public function getLabel() {
+    return $this->pluginDefinition['label'];
+  }
+
+  /**
+   * Gets the optional description for advanced layouts.
+   *
+   * @return string|NULL
+   *   The layout description.
+   */
+  public function getDescription() {
+    return isset($this->pluginDefinition['description']) ? $this->pluginDefinition['description'] : NULL;
+  }
+
+  /**
+   * Gets the human-readable category.
+   *
+   * @return string
+   *   The human-readable category.
+   */
+  public function getCategory() {
+    return $this->pluginDefinition['category'];
+  }
+
+  /**
+   * Gets human-readable list of regions keyed by machine name.
+   *
+   * @return string[]
+   *   An array of human-readable region names keyed by machine name.
+   */
+  public function getRegionNames() {
+    return $this->pluginDefinition['region_names'];
+  }
+
+  /**
+   * Gets information on regions keyed by machine name.
+   *
+   * @return array
+   *   An array of information on regions keyed by machine name.
+   */
+  public function getRegionDefinitions() {
+    return $this->pluginDefinition['regions'];
+  }
+
+  /**
+   * Gets the path to resources like icon or template.
+   *
+   * @return string|NULL
+   *   The path relative to the Drupal root.
+   */
+  public function getBasePath() {
+    return isset($this->pluginDefinition['path']) ? $this->pluginDefinition['path'] : NULL;
+  }
+
+  /**
+   * Gets the path to the preview image.
+   *
+   * This can optionally be used in the user interface to show the layout of
+   * regions visually.
+   *
+   * @return string|NULL
+   *   The path to preview image file.
+   */
+  public function getIconFilename() {
+    return isset($this->pluginDefinition['icon']) ? $this->pluginDefinition['icon'] : NULL;
+  }
+
+  /**
+   * Get the asset library.
+   *
+   * @return string|NULL
+   *   The asset library.
+   */
+  public function getLibrary() {
+    return isset($this->pluginDefinition['library']) ? $this->pluginDefinition['library'] : NULL;
+  }
+
+  /**
+   * Gets the theme hook used to render this layout.
+   *
+   * @return string|NULL
+   *   Theme hook.
+   */
+  public function getThemeHook() {
+    return isset($this->pluginDefinition['theme']) ? $this->pluginDefinition['theme'] : NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build(array $regions) {
+    $build['#content'] = $regions;
+    $build['#layout'] = $this->getPluginDefinition();
+    $build['#settings'] = $this->getConfiguration();
+    if ($theme = $this->getThemeHook()) {
+      $build['#theme'] = $theme;
+    }
+    if ($library = $this->getLibrary()) {
+      $build['#attached']['library'][] = $library;
+    }
+    return $build;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    $this->configuration = $form_state->getValues();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfiguration() {
+    return array_merge($this->defaultConfiguration(), $this->configuration);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setConfiguration(array $configuration) {
+    $this->configuration = $configuration;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function calculateDependencies() {
+    return isset($this->configuration['dependencies']) ? $this->configuration['dependencies'] : [];
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Layout/Plugin/LayoutDefault.php b/core/lib/Drupal/Core/Layout/Plugin/LayoutDefault.php
new file mode 100644
index 0000000..355885b
--- /dev/null
+++ b/core/lib/Drupal/Core/Layout/Plugin/LayoutDefault.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Drupal\Core\Layout\Plugin;
+
+/**
+ * Provides a default class for Layout plugins.
+ */
+class LayoutDefault extends LayoutBase {
+
+}
diff --git a/core/lib/Drupal/Core/Layout/Plugin/LayoutInterface.php b/core/lib/Drupal/Core/Layout/Plugin/LayoutInterface.php
new file mode 100644
index 0000000..6128255
--- /dev/null
+++ b/core/lib/Drupal/Core/Layout/Plugin/LayoutInterface.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Drupal\Core\Layout\Plugin;
+
+use Drupal\Component\Plugin\DerivativeInspectionInterface;
+use Drupal\Component\Plugin\PluginInspectionInterface;
+
+/**
+ * Provides an interface for static Layout plugins.
+ */
+interface LayoutInterface extends PluginInspectionInterface, DerivativeInspectionInterface {
+
+  /**
+   * 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/lib/Drupal/Core/Layout/Plugin/LayoutPluginManager.php b/core/lib/Drupal/Core/Layout/Plugin/LayoutPluginManager.php
new file mode 100644
index 0000000..fe437f2
--- /dev/null
+++ b/core/lib/Drupal/Core/Layout/Plugin/LayoutPluginManager.php
@@ -0,0 +1,172 @@
+<?php
+
+namespace Drupal\Core\Layout\Plugin;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Extension\ThemeHandlerInterface;
+use Drupal\Core\Plugin\CategorizingPluginManagerTrait;
+use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\Core\Plugin\Discovery\YamlDiscoveryDecorator;
+
+/**
+ * Plugin type manager for all layouts.
+ */
+class LayoutPluginManager extends DefaultPluginManager implements LayoutPluginManagerInterface {
+
+  use CategorizingPluginManagerTrait;
+
+  /**
+   * The theme handler.
+   *
+   * @var \Drupal\Core\Extension\ThemeHandlerInterface
+   */
+  protected $themeHandler;
+
+  /**
+   * Constructs a LayoutPluginManager object.
+   *
+   * @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) {
+    $plugin_interface = 'Drupal\Core\Layout\Plugin\LayoutInterface';
+    $plugin_definition_annotation_name = 'Drupal\Core\Layout\Annotation\Layout';
+    parent::__construct("Plugin/Layout", $namespaces, $module_handler, $plugin_interface, $plugin_definition_annotation_name);
+    $discovery = $this->getDiscovery();
+    $this->discovery = new YamlDiscoveryDecorator($discovery, 'layouts', $module_handler->getModuleDirectories() + $theme_handler->getThemeDirectories());
+    $this->themeHandler = $theme_handler;
+
+    $this->defaults += array(
+      'type' => 'page',
+      // Used for plugins defined in layouts.yml that do not specify a class
+      // themselves.
+      'class' => 'Drupal\Core\Layout\Plugin\LayoutDefault',
+    );
+
+    $this->setCacheBackend($cache_backend, 'layout');
+    $this->alterInfo('layout');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function providerExists($provider) {
+    return $this->moduleHandler->moduleExists($provider) || $this->themeHandler->themeExists($provider);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processDefinition(&$definition, $plugin_id) {
+    parent::processDefinition($definition, $plugin_id);
+
+    // Add the module or theme path to the 'path'.
+    if ($this->moduleHandler->moduleExists($definition['provider'])) {
+      $definition['provider_type'] = 'module';
+      $base_path = $this->moduleHandler->getModule($definition['provider'])->getPath();
+    }
+    elseif ($this->themeHandler->themeExists($definition['provider'])) {
+      $definition['provider_type'] = 'theme';
+      $base_path = $this->themeHandler->getTheme($definition['provider'])->getPath();
+    }
+    else {
+      $base_path = '';
+    }
+    $definition['path'] = !empty($definition['path']) ? $base_path . '/' . $definition['path'] : $base_path;
+
+    // Add a dependency on the provider of the library.
+    if (!empty($definition['library'])) {
+      list ($library_provider, ) = explode('/', $definition['library']);
+      if ($this->moduleHandler->moduleExists($library_provider)) {
+        $definition['dependencies'] = ['module' => [$library_provider]];
+      }
+      elseif ($this->themeHandler->themeExists($library_provider)) {
+        $definition['dependencies'] = ['theme' => [$library_provider]];
+      }
+    }
+
+    // Add the path to the icon filename.
+    if (!empty($definition['icon'])) {
+      $definition['icon'] = $definition['path'] . '/' . $definition['icon'];
+    }
+
+    // If 'template' is set, then we'll derive 'template_path' and 'theme'.
+    if (!empty($definition['template'])) {
+      $template_parts = explode('/', $definition['template']);
+
+      $definition['template'] = array_pop($template_parts);
+      $definition['theme'] = strtr($definition['template'], '-', '_');
+      $definition['template_path'] = $definition['path'];
+      if (count($template_parts) > 0) {
+        $definition['template_path'] .= '/' . implode('/', $template_parts);
+      }
+    }
+
+    // Generate the 'region_names' key from the 'regions' key.
+    $definition['region_names'] = array();
+    if (!empty($definition['regions']) && is_array($definition['regions'])) {
+      foreach ($definition['regions'] as $region_id => $region_definition) {
+        $definition['region_names'][$region_id] = $region_definition['label'];
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLayoutOptions(array $params = []) {
+    $group_by_category = !empty($params['group_by_category']);
+    $plugins = $group_by_category ? $this->getGroupedDefinitions() : ['default' => $this->getDefinitions()];
+    $categories = $group_by_category ? $this->getCategories() : ['default'];
+
+    // Go through each category, sort it, and get just the labels out.
+    $options = array();
+    foreach ($categories as $category) {
+      // Convert from a translation to a real string.
+      $category = (string) $category;
+
+      // Sort the category.
+      $plugins[$category] = $this->getSortedDefinitions($plugins[$category]);
+
+      // Put only the label in the options array.
+      foreach ($plugins[$category] as $id => $plugin) {
+        $options[$category][$id] = $plugin['label'];
+      }
+    }
+
+    return $group_by_category ? $options : $options['default'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getThemeImplementations() {
+    $plugins = $this->getDefinitions();
+
+    $theme_registry = [];
+    foreach ($plugins as $id => $definition) {
+      if (!empty($definition['template']) && !empty($definition['theme'])) {
+        $theme_registry[$definition['theme']] = [
+          'template' => $definition['template'],
+          'path' => $definition['template_path'],
+          'variables' => array(
+            'content' => [],
+            'settings' => [],
+            'layout' => [],
+          ),
+        ];
+      }
+    }
+
+    return $theme_registry;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Layout/Plugin/LayoutPluginManagerInterface.php b/core/lib/Drupal/Core/Layout/Plugin/LayoutPluginManagerInterface.php
new file mode 100644
index 0000000..7ee292a
--- /dev/null
+++ b/core/lib/Drupal/Core/Layout/Plugin/LayoutPluginManagerInterface.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Drupal\Core\Layout\Plugin;
+
+use Drupal\Component\Plugin\CategorizingPluginManagerInterface;
+
+/**
+ * Provides an interface for the discovery and instantiation of layout plugins.
+ */
+interface LayoutPluginManagerInterface extends CategorizingPluginManagerInterface {
+
+  /**
+   * Get all available layouts as an options array.
+   *
+   * If group_by_category option/parameter passed group the options by
+   * category.
+   *
+   * @param array $params
+   *   (optional) An associative array with the following keys:
+   *   - group_by_category: (bool) If set to TRUE, return an array of arrays
+   *   grouped by the category name; otherwise, return a single-level
+   *   associative array.
+   *
+   * @return array
+   *   Layout options, as array.
+   */
+  public function getLayoutOptions(array $params = []);
+
+  /**
+   * Get theme implementations for layouts that give only a template.
+   *
+   * @return array
+   *   An associative array of the same format as returned by hook_theme().
+   *
+   * @see hook_theme()
+   */
+  public function getThemeImplementations();
+
+}
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 2f627be..5743b6b 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -224,7 +224,9 @@ function system_theme() {
       ),
       'template' => 'entity-add-list',
     ),
-  ));
+  ),
+  // Theme functions automatically registered for layouts.
+  \Drupal::service('plugin.manager.layout')->getThemeImplementations());
 }
 
 /**
diff --git a/core/modules/system/tests/modules/layout_test/config/schema/layout_test.schema.yml b/core/modules/system/tests/modules/layout_test/config/schema/layout_test.schema.yml
new file mode 100644
index 0000000..32be5f8
--- /dev/null
+++ b/core/modules/system/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/system/tests/modules/layout_test/css/layout-test-1col.css b/core/modules/system/tests/modules/layout_test/css/layout-test-1col.css
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/core/modules/system/tests/modules/layout_test/css/layout-test-1col.css
@@ -0,0 +1 @@
+
diff --git a/core/modules/system/tests/modules/layout_test/css/layout-test-2col.css b/core/modules/system/tests/modules/layout_test/css/layout-test-2col.css
new file mode 100644
index 0000000..d5c05b9
--- /dev/null
+++ b/core/modules/system/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/system/tests/modules/layout_test/layout_test.info.yml b/core/modules/system/tests/modules/layout_test/layout_test.info.yml
new file mode 100644
index 0000000..bfed2a5
--- /dev/null
+++ b/core/modules/system/tests/modules/layout_test/layout_test.info.yml
@@ -0,0 +1,6 @@
+name: 'Layout test'
+type: module
+description: 'Support module for testing layouts.'
+package: Testing
+version: VERSION
+core: 8.x
diff --git a/core/modules/system/tests/modules/layout_test/layout_test.layouts.yml b/core/modules/system/tests/modules/layout_test/layout_test.layouts.yml
new file mode 100644
index 0000000..8372de2
--- /dev/null
+++ b/core/modules/system/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/system/tests/modules/layout_test/layout_test.libraries.yml b/core/modules/system/tests/modules/layout_test/layout_test.libraries.yml
new file mode 100644
index 0000000..f2bfb5a
--- /dev/null
+++ b/core/modules/system/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/system/tests/modules/layout_test/src/Plugin/Layout/LayoutTestPlugin.php b/core/modules/system/tests/modules/layout_test/src/Plugin/Layout/LayoutTestPlugin.php
new file mode 100644
index 0000000..0cb0242
--- /dev/null
+++ b/core/modules/system/tests/modules/layout_test/src/Plugin/Layout/LayoutTestPlugin.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Drupal\layout_test\Plugin\Layout;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Layout\Plugin\LayoutBase;
+
+/**
+ * The plugin that handles the default layout template.
+ *
+ * @ingroup layout_template_plugins
+ *
+ * @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 {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return parent::defaultConfiguration() + [
+      'setting_1' => 'Default',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $configuration = $this->getConfiguration();
+    $form['setting_1'] = [
+      '#type' => 'textfield',
+      '#title' => 'Blah',
+      '#default_value' => $configuration['setting_1'],
+    ];
+    return $form;
+  }
+
+  /**
+   * @inheritDoc
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    parent::submitConfigurationForm($form, $form_state);
+
+    $this->configuration['setting_1'] = $form_state->getValue('setting_1');
+  }
+
+}
diff --git a/core/modules/system/tests/modules/layout_test/templates/layout-test-1col.html.twig b/core/modules/system/tests/modules/layout_test/templates/layout-test-1col.html.twig
new file mode 100644
index 0000000..e7a7eb5
--- /dev/null
+++ b/core/modules/system/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/system/tests/modules/layout_test/templates/layout-test-2col.html.twig b/core/modules/system/tests/modules/layout_test/templates/layout-test-2col.html.twig
new file mode 100644
index 0000000..11433ee
--- /dev/null
+++ b/core/modules/system/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/system/tests/modules/layout_test/templates/layout-test-plugin.html.twig b/core/modules/system/tests/modules/layout_test/templates/layout-test-plugin.html.twig
new file mode 100644
index 0000000..e49942c
--- /dev/null
+++ b/core/modules/system/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/tests/Drupal/KernelTests/Core/Layout/LayoutTest.php b/core/tests/Drupal/KernelTests/Core/Layout/LayoutTest.php
new file mode 100644
index 0000000..fc8ab15
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Layout/LayoutTest.php
@@ -0,0 +1,141 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Layout;
+
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests Layout functionality.
+ *
+ * @group Layout
+ */
+class LayoutTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['system', 'layout_test'];
+
+  /**
+   * The layout plugin manager.
+   *
+   * @var \Drupal\Core\Layout\Plugin\LayoutPluginManagerInterface
+   */
+  protected $layoutManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->layoutManager = $this->container->get('plugin.manager.layout');
+  }
+
+  /**
+   * Test listing the available layouts.
+   */
+  public function testLayoutDefinitions() {
+    $expected_layouts = [
+      'layout_test_1col',
+      'layout_test_2col',
+      'layout_test_plugin',
+    ];
+    $this->assertEquals($expected_layouts, array_keys($this->layoutManager->getDefinitions()));
+  }
+
+  /**
+   * Test rendering a layout.
+   *
+   * @dataProvider renderLayoutData
+   */
+  public function testRenderLayout($layout_id, $config, $regions, $html) {
+    /** @var \Drupal\Core\Layout\Plugin\LayoutInterface $layout */
+    $layout = $this->layoutManager->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',
+          ],
+        ],
+      ],
+
+      'layout_test_2col' => [
+        'layout_test_2col',
+        [],
+        [
+          'left' => [
+            '#markup' => 'This is the left',
+          ],
+          'right' => [
+            '#markup' => 'This is the right',
+          ],
+        ],
+      ],
+
+      '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/tests/Drupal/Tests/Core/Layout/LayoutPluginManagerTest.php b/core/tests/Drupal/Tests/Core/Layout/LayoutPluginManagerTest.php
new file mode 100644
index 0000000..2dea742
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Layout/LayoutPluginManagerTest.php
@@ -0,0 +1,182 @@
+<?php
+
+namespace Drupal\Tests\Core\Layout;
+
+use Drupal\Core\Layout\Plugin\LayoutPluginManager;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests the LayoutPluginManager.
+ *
+ * @coversDefaultClass \Drupal\Core\Layout\Plugin\LayoutPluginManager
+ *
+ * @group Layout
+ */
+class LayoutPluginManagerTest extends UnitTestCase {
+
+  /**
+   * Test processDefinition.
+   *
+   * @covers ::processDefinition
+   */
+  public function testProcessDefinition() {
+    $namespaces = new \ArrayObject();
+    $namespaces['Drupal\layout_plugin_test'] = $this->root . '/modules/layout_plugin_test/src';
+
+    $cache_backend = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
+
+    $module_handler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
+    $module_handler->method('getModuleDirectories')->willReturn(array());
+    $module_handler->method('moduleExists')->willReturn(TRUE);
+    $extension = $this->getMockBuilder('Drupal\Core\Extension\Extension')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $extension->method('getPath')->willReturn('modules/layout_plugin_test');
+    $module_handler->method('getModule')->willReturn($extension);
+
+    $theme_handler = $this->getMock('Drupal\Core\Extension\ThemeHandlerInterface');
+    $theme_handler->method('getThemeDirectories')->willReturn(array());
+
+    $plugin_manager = new LayoutPluginManager($namespaces, $cache_backend, $module_handler, $theme_handler);
+
+    // A simple definition with only the required keys.
+    $definition = [
+      'label' => 'Simple layout',
+      'category' => 'Test layouts',
+      'theme' => 'simple_layout',
+      'provider' => 'layout_plugin_test',
+      'regions' => [
+        'first' => ['label' => 'First region'],
+        'second' => ['label' => 'Second region'],
+      ],
+    ];
+    $plugin_manager->processDefinition($definition, 'simple_layout');
+    $this->assertEquals('modules/layout_plugin_test', $definition['path']);
+    $this->assertEquals([
+      'first' => 'First region',
+      'second' => 'Second region'
+    ], $definition['region_names']);
+
+    // A more complex definition.
+    $definition = [
+      'label' => 'Complex layout',
+      'category' => 'Test layouts',
+      'template' => 'complex-layout',
+      'library' => 'library_module/library_name',
+      'provider' => 'layout_plugin_test',
+      'path' => 'layout/complex',
+      'icon' => 'complex-layout.png',
+      'regions' => [
+        'first' => ['label' => 'First region'],
+        'second' => ['label' => 'Second region'],
+      ],
+    ];
+    $plugin_manager->processDefinition($definition, 'complex_layout');
+    $this->assertEquals('modules/layout_plugin_test/layout/complex', $definition['path']);
+    $this->assertEquals('modules/layout_plugin_test/layout/complex', $definition['template_path']);
+    $this->assertEquals('modules/layout_plugin_test/layout/complex/complex-layout.png', $definition['icon']);
+    $this->assertEquals('complex_layout', $definition['theme']);
+    $this->assertEquals(['module' => ['library_module']], $definition['dependencies']);
+
+    // A layout with a template path.
+    $definition = [
+      'label' => 'Split layout',
+      'category' => 'Test layouts',
+      'template' => 'templates/split-layout',
+      'provider' => 'layout_plugin_test',
+      'path' => 'layouts',
+      'icon' => 'images/split-layout.png',
+      'regions' => [
+        'first' => ['label' => 'First region'],
+        'second' => ['label' => 'Second region'],
+      ],
+    ];
+    $plugin_manager->processDefinition($definition, 'split_layout');
+    $this->assertEquals('modules/layout_plugin_test/layouts', $definition['path']);
+    $this->assertEquals('modules/layout_plugin_test/layouts/templates', $definition['template_path']);
+    $this->assertEquals('modules/layout_plugin_test/layouts/images/split-layout.png', $definition['icon']);
+    $this->assertEquals('split_layout', $definition['theme']);
+  }
+
+  /**
+   * Test getting layout options.
+   *
+   * @covers ::getLayoutOptions
+   */
+  public function testGetLayoutOptions() {
+    /** @var LayoutPluginManager|\PHPUnit_Framework_MockObject_MockBuilder $layout_manager */
+    $layout_manager = $this->getMockBuilder(LayoutPluginManager::class)
+      ->disableOriginalConstructor()
+      ->setMethods(['getDefinitions'])
+      ->getMock();
+
+    $layout_manager->method('getDefinitions')
+      ->willReturn([
+        'simple_layout' => [
+          'label' => 'Simple layout',
+          'category' => 'Test layouts',
+        ],
+        'complex_layout' => [
+          'label' => 'Complex layout',
+          'category' => 'Test layouts',
+        ],
+      ]);
+
+    $options = $layout_manager->getLayoutOptions();
+    $this->assertEquals([
+      'simple_layout' => 'Simple layout',
+      'complex_layout' => 'Complex layout',
+    ], $options);
+
+    $options = $layout_manager->getLayoutOptions(array('group_by_category' => TRUE));
+    $this->assertEquals([
+      'Test layouts' => [
+        'simple_layout' => 'Simple layout',
+        'complex_layout' => 'Complex layout',
+      ],
+    ], $options);
+  }
+
+  /**
+   * Tests layout theme implementations.
+   *
+   * @covers ::getThemeImplementations
+   */
+  public function testGetThemeImplementations() {
+    /** @var LayoutPluginManager|\PHPUnit_Framework_MockObject_MockBuilder $layout_manager */
+    $layout_manager = $this->getMockBuilder(LayoutPluginManager::class)
+      ->disableOriginalConstructor()
+      ->setMethods(['getDefinitions'])
+      ->getMock();
+
+    $layout_manager->method('getDefinitions')
+      ->willReturn([
+        // Should get template registered automatically.
+        'simple_layout' => [
+          'path' => 'modules/layout_plugin_test',
+          'template_path' => 'modules/layout_plugin_test/templates',
+          'template' => 'simple-layout',
+          'theme' => 'simple_layout',
+        ],
+        // Shouldn't get registered automatically.
+        'complex_layout' => [
+          'path' => 'modules/layout_plugin_test',
+          'theme' => 'complex_layout',
+        ],
+      ]);
+
+    $theme_registry = $layout_manager->getThemeImplementations();
+    $this->assertEquals([
+      'simple_layout' => [
+        'template' => 'simple-layout',
+        'path' => 'modules/layout_plugin_test/templates',
+        'variables' => [
+          'content' => [],
+          'settings' => [],
+          'layout' => [],
+        ],
+      ],
+    ], $theme_registry);
+  }
+
+}
