diff --git a/panels.info.yml b/panels.info.yml
index e8dbd5a..cff6d99 100644
--- a/panels.info.yml
+++ b/panels.info.yml
@@ -6,7 +6,7 @@ package: Panels
 core: 8.x
 configure: panels.admin
 dependencies:
-  - ctools (>= 3.0-alpha21)
+  - ctools
   - layout_plugin
 test_dependencies:
   - page_manager
diff --git a/panels.routing.yml b/panels.routing.yml
new file mode 100644
index 0000000..101a808
--- /dev/null
+++ b/panels.routing.yml
@@ -0,0 +1,28 @@
+panels.select_block:
+  path: '/admin/structure/panels/{tempstore_id}/{machine_name}/select_block'
+  defaults:
+    _controller: '\Drupal\panels\Controller\Panels::selectBlock'
+    _title: 'Select block'
+  requirements:
+    _ctools_access: 'machine_name'
+panels.add_block:
+  path: '/admin/structure/panels/{tempstore_id}/{machine_name}/add/{block_id}'
+  defaults:
+    _form: '\Drupal\panels\Form\PanelsAddBlockForm'
+    _title: 'Add block'
+  requirements:
+    _ctools_access: 'machine_name'
+panels.edit_block:
+  path: '/admin/structure/panels/{tempstore_id}/{machine_name}/edit/{block_id}'
+  defaults:
+    _form: '\Drupal\panels\Form\PanelsEditBlockForm'
+    _title: 'Edit block'
+  requirements:
+    _ctools_access: 'machine_name'
+panels.delete_block:
+  path: '/admin/structure/panels/{tempstore_id}/{machine_name}/delete/{block_id}'
+  defaults:
+    _form: '\Drupal\panels\Form\PanelsDeleteBlockForm'
+    _title: 'Delete block'
+  requirements:
+    _ctools_access: 'machine_name'
diff --git a/panels.services.yml b/panels.services.yml
index dade114..a54a1d7 100644
--- a/panels.services.yml
+++ b/panels.services.yml
@@ -2,6 +2,9 @@ services:
   plugin.manager.panels.display_builder:
     class: Drupal\panels\Plugin\DisplayBuilder\DisplayBuilderManager
     arguments: ['@container.namespaces', '@cache.discovery', '@module_handler']
+  plugin.manager.panels.pattern:
+    class: Drupal\panels\PanelsPatternManager
+    parent: default_plugin_manager
   panels.display_manager:
     class: Drupal\panels\PanelsDisplayManager
     arguments: ['@plugin.manager.display_variant', '@config.typed']
diff --git a/src/Annotation/PanelsPattern.php b/src/Annotation/PanelsPattern.php
new file mode 100644
index 0000000..d2b7143
--- /dev/null
+++ b/src/Annotation/PanelsPattern.php
@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panels\Annotation\PanelsPattern.
+ */
+
+namespace Drupal\panels\Annotation;
+
+use Drupal\Component\Annotation\PluginID;
+
+/**
+ * Defines a PanelsPattern annotation object.
+ *
+ * @Annotation
+ */
+class PanelsPattern extends PluginID {}
diff --git a/src/CachedValuesGetterTrait.php b/src/CachedValuesGetterTrait.php
new file mode 100644
index 0000000..a53177d
--- /dev/null
+++ b/src/CachedValuesGetterTrait.php
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panels\getCachedValuesTrait.
+ */
+
+namespace Drupal\panels;
+
+use Drupal\user\SharedTempStoreFactory;
+
+trait CachedValuesGetterTrait {
+
+  /**
+   * Gets cached values for non-wizard classes that interact with a wizard.
+   *
+   * This method is specifically geared toward the needs of a panels use case
+   * both within and outside of PageManager. To that end, some of the logic in
+   * here is explicitly checking for known PageManager standards and behaving
+   * as necessary to compensate for PageManager's needs. Other Panels
+   * implementations are generally simpler and do not need the same degree of
+   * customization. This trait accounts for both use cases.
+   *
+   * @param \Drupal\user\SharedTempStoreFactory $tempstore
+   *   The tempstore object in use for the desired cached values.
+   * @param string $tempstore_id
+   *   The tempstore identifier.
+   * @param string $machine_name
+   *   The tempstore key.
+   *
+   * @return mixed
+   */
+  protected function getCachedValues(SharedTempStoreFactory $tempstore, $tempstore_id, $machine_name) {
+    $cached_values = $tempstore->get($tempstore_id)->get($machine_name);
+
+    $machine_name = explode('--', $machine_name);
+    // PageManager specific handling. If $machine_name[1] is set, it's the
+    // page variant ID.
+    if (isset($machine_name[1]) && !isset($cached_values['page_variant'])) {
+      /** @var \Drupal\page_manager\PageInterface $page */
+      $page = $cached_values['page'];
+      $cached_values['page_variant'] = $page->getVariant($machine_name[1]);
+    }
+    if (!isset($cached_values['plugin']) && !empty($cached_values['page_variant'])) {
+      $cached_values['plugin'] = $cached_values['page_variant']->getVariantPlugin();
+    }
+    return $cached_values;
+  }
+
+}
diff --git a/src/Controller/Panels.php b/src/Controller/Panels.php
new file mode 100644
index 0000000..4a317db
--- /dev/null
+++ b/src/Controller/Panels.php
@@ -0,0 +1,157 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panels\Controller\Panels.
+ */
+
+namespace Drupal\panels\Controller;
+
+use Drupal\Component\Plugin\PluginManagerInterface;
+use Drupal\Core\Block\BlockManagerInterface;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Plugin\Context\ContextHandlerInterface;
+use Drupal\ctools\Form\AjaxFormTrait;
+use Drupal\panels\CachedValuesGetterTrait;
+use Drupal\user\SharedTempStoreFactory;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Provides route controllers for Panels routes.
+ */
+class Panels extends ControllerBase {
+
+  use AjaxFormTrait;
+  use CachedValuesGetterTrait;
+
+  /**
+   * The block manager.
+   *
+   * @var \Drupal\Core\Block\BlockManagerInterface
+   */
+  protected $blockManager;
+
+  /**
+   * The condition manager.
+   *
+   * @var \Drupal\Core\Plugin\Context\ContextAwarePluginManagerInterface
+   */
+  protected $conditionManager;
+
+  /**
+   * The variant manager.
+   *
+   * @var \Drupal\Component\Plugin\PluginManagerInterface
+   */
+  protected $variantManager;
+
+  /**
+   * The context handler.
+   *
+   * @var \Drupal\Core\Plugin\Context\ContextHandlerInterface
+   */
+  protected $contextHandler;
+
+  /**
+   * Tempstore factory.
+   *
+   * @var \Drupal\user\SharedTempStoreFactory
+   */
+  protected $tempstore;
+
+  /**
+   * Constructs a new VariantPluginEditForm.
+   *
+   * @param \Drupal\Core\Block\BlockManagerInterface $block_manager
+   *   The block manager.
+   * @param \Drupal\Component\Plugin\PluginManagerInterface $condition_manager
+   *   The condition manager.
+   * @param \Drupal\Component\Plugin\PluginManagerInterface $variant_manager
+   *   The variant manager.
+   * @param \Drupal\Core\Plugin\Context\ContextHandlerInterface $context_handler
+   *   The context handler.
+  +   * @param \Drupal\user\SharedTempStoreFactory $tempstore
+  +   *   The tempstore factory.
+   */
+  public function __construct(BlockManagerInterface $block_manager, PluginManagerInterface $condition_manager, PluginManagerInterface $variant_manager, ContextHandlerInterface $context_handler, SharedTempStoreFactory $tempstore) {
+    $this->blockManager = $block_manager;
+    $this->conditionManager = $condition_manager;
+    $this->variantManager = $variant_manager;
+    $this->contextHandler = $context_handler;
+    $this->tempstore = $tempstore;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.block'),
+      $container->get('plugin.manager.condition'),
+      $container->get('plugin.manager.display_variant'),
+      $container->get('context.handler'),
+      $container->get('user.shared_tempstore'),
+      $container->get('plugin.manager.panels.pattern')
+    );
+  }
+
+  /**
+   * Presents a list of blocks to add to the variant.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
+   * @param string $machine_name
+   *   The identifier of the block display variant.
+   * @param string $tempstore_id
+   *   The identifier of the temporary store.
+   *
+   * @return array
+   *   The block selection page.
+   */
+  public function selectBlock(Request $request, $machine_name, $tempstore_id) {
+    $cached_values = $this->getCachedValues($this->tempstore, $tempstore_id, $machine_name);
+    /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $variant_plugin */
+    $variant_plugin = $cached_values['plugin'];
+    /** @var \Drupal\panels\Plugin\PanelsPattern\PanelsPatternInterface $pattern_plugin */
+    $pattern_plugin = $variant_plugin->getPattern();
+
+    $contexts = $pattern_plugin->getDefaultContexts($this->tempstore, $tempstore_id, $machine_name);
+    $variant_plugin->setContexts($contexts);
+
+    // Add a section containing the available blocks to be added to the variant.
+    $build = [
+      '#type' => 'container',
+      '#attached' => [
+        'library' => [
+          'core/drupal.ajax',
+        ],
+      ],
+    ];
+    $available_plugins = $this->blockManager->getDefinitionsForContexts($variant_plugin->getContexts());
+    // Order by category, and then by admin label.
+    $available_plugins = $this->blockManager->getSortedDefinitions($available_plugins);
+    foreach ($available_plugins as $plugin_id => $plugin_definition) {
+      // Make a section for each region.
+      $category = $plugin_definition['category'];
+      $category_key = 'category-' . $category;
+      if (!isset($build[$category_key])) {
+        $build[$category_key] = [
+          '#type' => 'fieldgroup',
+          '#title' => $category,
+          'content' => [
+            '#theme' => 'links',
+          ],
+        ];
+      }
+      // Add a link for each available block within each region.
+      $build[$category_key]['content']['#links'][$plugin_id] = [
+        'title' => $plugin_definition['admin_label'],
+        'url' => $pattern_plugin->getBlockAddUrl($tempstore_id, $machine_name, $plugin_id, $request->query->get('region'), $request->query->get('destination')),
+        'attributes' => $this->getAjaxAttributes(),
+      ];
+    }
+    return $build;
+  }
+
+}
diff --git a/src/Form/LayoutChangeRegions.php b/src/Form/LayoutChangeRegions.php
new file mode 100644
index 0000000..5205b57
--- /dev/null
+++ b/src/Form/LayoutChangeRegions.php
@@ -0,0 +1,237 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panels\Form\LayoutChangeRegions.
+ */
+
+namespace Drupal\panels\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\layout_plugin\Layout;
+use Drupal\layout_plugin\Plugin\Layout\LayoutPluginManagerInterface;
+use Drupal\user\SharedTempStoreFactory;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+class LayoutChangeRegions extends FormBase {
+
+  /**
+   * The layout plugin manager.
+   *
+   * @var \Drupal\layout_plugin\Plugin\Layout\LayoutPluginManagerInterface
+   */
+  protected $manager;
+
+  /**
+   * The tempstore factory.
+   *
+   * @var \Drupal\user\SharedTempStoreFactory
+   */
+  protected $tempstore;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.layout_plugin'),
+      $container->get('user.shared_tempstore')
+    );
+  }
+
+  /**
+   * LayoutChangeRegions constructor.
+   *
+   * @param \Drupal\layout_plugin\Plugin\Layout\LayoutPluginManagerInterface $manager
+   *   The layout plugin manager
+   * @param \Drupal\user\SharedTempStoreFactory $tempstore
+   *   The tempstore factory.
+   */
+  public function __construct(LayoutPluginManagerInterface $manager, SharedTempStoreFactory $tempstore) {
+    $this->manager = $manager;
+    $this->tempstore = $tempstore;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'panels_layout_regions_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $cached_values = $form_state->getTemporaryValue('wizard');
+
+    /* @var $variant_plugin \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant */
+    $variant_plugin = $cached_values['plugin'];
+
+    $form['#attached']['library'][] = 'block/drupal.block';
+
+    $form['old_layout'] = [
+      '#title' => $this->t('Old Layout'),
+      '#type' => 'select',
+      '#options' => Layout::getLayoutOptions(['group_by_category' => TRUE]),
+      '#default_value' => $cached_values['layout_change']['old_layout'],
+      '#disabled' => TRUE,
+    ];
+
+    $form['new_layout'] = [
+      '#title' => $this->t('New Layout'),
+      '#type' => 'select',
+      '#options' => Layout::getLayoutOptions(['group_by_category' => TRUE]),
+      '#default_value' => $cached_values['layout_change']['new_layout'],
+      '#disabled' => TRUE,
+    ];
+
+    $layout_settings = !empty($cached_values['layout_change']['layout_settings']) ? $cached_values['layout_change']['layout_settings'] : [];
+    $old_layout = Layout::layoutPluginManager()->createInstance($cached_values['layout_change']['old_layout'], []);
+    $new_layout = Layout::layoutPluginManager()->createInstance($cached_values['layout_change']['new_layout'], $layout_settings);
+
+    if ($block_assignments = $variant_plugin->getRegionAssignments()) {
+      // Build a table of all blocks used by this variant.
+
+      $form['blocks'] = [
+        '#type' => 'table',
+        '#header' => [
+          $this->t('Label'),
+          $this->t('ID'),
+          $this->t('Region'),
+          $this->t('Weight'),
+        ],
+        '#attributes' => array(
+          'id' => 'blocks',
+        ),
+        '#empty' => $this->t('There are no regions for blocks.'),
+      ];
+
+      // Loop through the blocks per region.
+      $new_regions = $new_layout->getPluginDefinition()['region_names'];
+      $new_regions['__unassigned__'] = $this->t('Unassigned');
+      foreach ($new_regions as $region => $label) {
+
+        // Add a section for each region and allow blocks to be dragged between
+        // them.
+        $form['blocks']['#tabledrag'][] = [
+          'action' => 'match',
+          'relationship' => 'sibling',
+          'group' => 'block-region-select',
+          'subgroup' => 'block-region-' . $region,
+          'hidden' => FALSE,
+        ];
+        $form['blocks']['#tabledrag'][] = [
+          'action' => 'order',
+          'relationship' => 'sibling',
+          'group' => 'block-weight',
+          'subgroup' => 'block-weight-' . $region,
+        ];
+        $form['blocks']['region-' . $region] = [
+          '#attributes' => [
+            'class' => ['region-title', 'region-title-' . $region],
+            'no_striping' => TRUE,
+          ],
+        ];
+        $form['blocks']['region-' . $region]['title'] = [
+          '#markup' => $label,
+          '#wrapper_attributes' => [
+            'colspan' => 4,
+          ],
+        ];
+        $form['blocks']['region-' . $region . '-message'] = [
+          '#attributes' => [
+            'class' => [
+              'region-message',
+              'region-' . $region . '-message',
+              empty($blocks) ? 'region-empty' : 'region-populated',
+            ],
+          ],
+        ];
+        $form['blocks']['region-' . $region . '-message']['message'] = [
+          '#markup' => '<em>' . $this->t('No blocks in this region') . '</em>',
+          '#wrapper_attributes' => [
+            'colspan' => 4,
+          ],
+        ];
+      }
+
+      foreach ($old_layout->getPluginDefinition()['region_names'] as $region => $label) {
+        if (empty($block_assignments[$region])) {
+          continue;
+        }
+        // Prevent region names clashing with new regions.
+        $region_id = 'old_'.$region;
+
+        $row = [
+          '#attributes' => [
+            'class' => ['draggable'],
+          ],
+        ];
+        $row['label']['#markup'] = $label;
+        $row['id']['#markup'] = $region;
+        // Allow the region to be changed for each block.
+        $row['region'] = [
+          '#title' => $this->t('Region'),
+          '#title_display' => 'invisible',
+          '#type' => 'select',
+          '#options' => $new_regions,
+          '#default_value' => isset($new_regions[$region]) ? $region : '__unassigned__',
+          '#attributes' => [
+            'class' => ['block-region-select', 'block-region-' . $region],
+          ],
+        ];
+        // Allow the weight to be changed for each region.
+        $row['weight'] = [
+          '#type' => 'weight',
+          '#default_value' => 0,
+          '#title' => $this->t('Weight for @block block', ['@block' => $label]),
+          '#title_display' => 'invisible',
+          '#attributes' => [
+            'class' => ['block-weight', 'block-weight-' . $region],
+          ],
+        ];
+        $form['blocks'][$region_id] = $row;
+      }
+    }
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $cached_values = $form_state->getTemporaryValue('wizard');
+    /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $plugin */
+    $plugin = $cached_values['plugin'];
+    $blocks = $plugin->getRegionAssignments();
+    /**
+     * @var string $region
+     * @var \Drupal\Core\Block\BlockPluginInterface[] $block_group
+     */
+    foreach ($blocks as $region => $block_group) {
+      foreach ($block_group as $uuid => $block) {
+        $new_region = $form_state->getValue(['blocks', 'old_' . $region, 'region']);
+        $block->setConfiguration(['region' => $new_region] + $block->getConfiguration());
+      }
+    }
+    $layout_id = !empty($cached_values['layout_change']['new_layout']) ? $cached_values['layout_change']['new_layout'] : $plugin->getConfiguration()['layout'];
+    $layout_settings = !empty($cached_values['layout_change']['layout_settings']) ? $cached_values['layout_change']['layout_settings'] : [];
+    $plugin->setLayout($layout_id, $layout_settings);
+    unset($cached_values['layout_change']);
+    $form_state->setTemporaryValue('wizard', $cached_values);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    foreach ($form_state->getValue('blocks') as $old_region => $values) {
+      if ($values['region'] == '__unassigned__') {
+        $form_state->setErrorByName('blocks][' . $old_region, $this->t('You must assign your old regions to an available new region.'));
+      }
+    }
+  }
+
+}
diff --git a/src/Form/LayoutChangeSettings.php b/src/Form/LayoutChangeSettings.php
new file mode 100644
index 0000000..1430f50
--- /dev/null
+++ b/src/Form/LayoutChangeSettings.php
@@ -0,0 +1,186 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panels\Form\LayoutChangeSettings.
+ */
+
+namespace Drupal\panels\Form;
+
+use Drupal\Component\Plugin\ConfigurablePluginInterface;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormState;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\layout_plugin\Layout;
+use Drupal\layout_plugin\Plugin\Layout\LayoutInterface;
+use Drupal\layout_plugin\Plugin\Layout\LayoutPluginManagerInterface;
+use Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant;
+use Drupal\user\SharedTempStoreFactory;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+class LayoutChangeSettings extends FormBase {
+
+  /**
+   * The layout plugin manager.
+   *
+   * @var \Drupal\layout_plugin\Plugin\Layout\LayoutPluginManagerInterface
+   */
+  protected $manager;
+
+  /**
+   * The tempstore factory.
+   *
+   * @var \Drupal\user\SharedTempStoreFactory
+   */
+  protected $tempstore;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.layout_plugin'),
+      $container->get('user.shared_tempstore')
+    );
+  }
+
+  /**
+   * LayoutChangeSettings constructor.
+   *
+   * @param \Drupal\layout_plugin\Plugin\Layout\LayoutPluginManagerInterface $manager
+   *   The layout plugin manager.
+   * @param \Drupal\user\SharedTempStoreFactory $tempstore
+   *   The tempstore factory.
+   */
+  public function __construct(LayoutPluginManagerInterface $manager, SharedTempStoreFactory $tempstore) {
+    $this->manager = $manager;
+    $this->tempstore = $tempstore;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'panels_layout_settings_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $cached_values = $form_state->getTemporaryValue('wizard');
+
+    /* @var $variant_plugin \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant */
+    $variant_plugin = $cached_values['plugin'];
+
+    $form['old_layout'] = [
+      '#title' => $this->t('Old Layout'),
+      '#type' => 'select',
+      '#options' => Layout::getLayoutOptions(['group_by_category' => TRUE]),
+      '#default_value' => $cached_values['layout_change']['old_layout'],
+      '#disabled' => TRUE,
+      '#access' => !empty($cached_values['layout_change']),
+    ];
+
+    $form['new_layout'] = [
+      '#title' => $this->t('New Layout'),
+      '#type' => 'select',
+      '#options' => Layout::getLayoutOptions(['group_by_category' => TRUE]),
+      '#default_value' => $cached_values['layout_change']['new_layout'],
+      '#disabled' => TRUE,
+      '#access' => !empty($cached_values['layout_change']),
+    ];
+
+    // If a layout is already selected, show the layout settings.
+    $form['layout_settings_wrapper'] = [
+      '#type' => 'fieldset',
+      '#title' => $this->t('Layout settings'),
+      '#tree' => TRUE,
+    ];
+
+    $layout_settings = !empty($cached_values['layout_change']['layout_settings']) ? $cached_values['layout_change']['layout_settings'] : [];
+    if (!$layout_settings && $variant_plugin->getLayout() instanceof ConfigurablePluginInterface) {
+      $layout_settings = $variant_plugin->getLayout()->getConfiguration();
+    }
+    $layout_id = !empty($cached_values['layout_change']['new_layout']) ? $cached_values['layout_change']['new_layout'] : $variant_plugin->getConfiguration()['layout'];
+    $layout = Layout::layoutPluginManager()->createInstance($layout_id, $layout_settings);
+    $form['layout_settings_wrapper']['layout_settings'] = $layout->buildConfigurationForm([], $form_state);
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $cached_values = $form_state->getTemporaryValue('wizard');
+    /** @var \Drupal\ctools\Wizard\EntityFormWizardInterface $wizard */
+    $wizard = $form_state->getFormObject();
+    $next_params = $wizard->getNextParameters($cached_values);
+    /* @var $plugin \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant */
+    $plugin = $cached_values['plugin'];
+    $layout_id = !empty($cached_values['layout_change']['new_layout']) ? $cached_values['layout_change']['new_layout'] : $plugin->getConfiguration()['layout'];
+    /** @var \Drupal\layout_plugin\Plugin\Layout\LayoutInterface $layout */
+    $layout = Layout::layoutPluginManager()->createInstance($layout_id, []);
+    // If we're dealing with a form, submit it.
+    if ($layout instanceof PluginFormInterface) {
+      $sub_form_state = new FormState();
+      $plugin_values = $form_state->getValue(['layout_settings_wrapper', 'layout_settings']);
+      // If form values came through the step's submission, handle them.
+      if ($plugin_values) {
+        $sub_form_state->setValues($plugin_values);
+        $layout->submitConfigurationForm($form, $sub_form_state);
+        // If this plugin is configurable, get that configuration and set it in
+        // cached values.
+        if ($layout instanceof ConfigurablePluginInterface) {
+          $cached_values = $this->setCachedValues($next_params['step'], $plugin, $layout, $cached_values, $layout->getConfiguration());
+        }
+      }
+      // If no values came through, set the cached values layout config to
+      // empty array.
+      else {
+        $cached_values = $this->setCachedValues($next_params['step'], $plugin, $layout, $cached_values, []);
+      }
+    }
+    // If we're not dealing with a Layout plugin that implements
+    // PluginFormInterface, handle this unlikely situation.
+    else {
+      $cached_values = $this->setCachedValues($next_params['step'], $plugin, $layout, $cached_values, []);
+    }
+    $form_state->setTemporaryValue('wizard', $cached_values);
+  }
+
+  protected function setCachedValues($next_step, PanelsDisplayVariant $plugin, LayoutInterface $layout, $cached_values, $configuration) {
+    // The step is modified by various wizards but will end in "regions"
+    if (substr($next_step, 0 -7) == 'regions') {
+      $cached_values['layout_change']['layout_settings'] = $configuration;
+    }
+    else {
+      $plugin->setLayout($layout, $configuration);
+      $cached_values['plugin'] = $plugin;
+      unset($cached_values['layout_change']);
+    }
+    return $cached_values;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    $cached_values = $form_state->getTemporaryValue('wizard');
+    /* @var $plugin \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant */
+    $plugin = $cached_values['plugin'];
+    $layout_id = !empty($cached_values['layout_change']['new_layout']) ? $cached_values['layout_change']['new_layout'] : $plugin->getConfiguration()['layout'];
+    $layout = Layout::layoutPluginManager()->createInstance($layout_id, []);
+    if ($layout instanceof PluginFormInterface) {
+      $sub_form_state = new FormState();
+      $plugin_values = $form_state->getValue(['layout_settings_wrapper', 'layout_settings']);
+      if ($plugin_values) {
+        $sub_form_state->setValues($plugin_values);
+        $layout->validateConfigurationForm($form, $sub_form_state);
+      }
+    }
+  }
+
+}
diff --git a/src/Form/LayoutPluginSelector.php b/src/Form/LayoutPluginSelector.php
new file mode 100644
index 0000000..b5e6571
--- /dev/null
+++ b/src/Form/LayoutPluginSelector.php
@@ -0,0 +1,139 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panels\Form\LayoutPluginSelector.
+ */
+
+namespace Drupal\panels\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\layout_plugin\Layout;
+use Drupal\layout_plugin\Plugin\Layout\LayoutPluginManagerInterface;
+use Drupal\user\SharedTempStoreFactory;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+class LayoutPluginSelector extends FormBase {
+
+  /**
+   * The layout plugin manager.
+   *
+   * @var \Drupal\layout_plugin\Plugin\Layout\LayoutPluginManagerInterface
+   */
+  protected $manager;
+
+  /**
+   * The tempstore factory.
+   *
+   * @var \Drupal\user\SharedTempStoreFactory
+   */
+  protected $tempstore;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.layout_plugin'),
+      $container->get('user.shared_tempstore')
+    );
+  }
+
+  /**
+   * LayoutPluginSelector constructor.
+   *
+   * @param \Drupal\layout_plugin\Plugin\Layout\LayoutPluginManagerInterface $manager
+   *   The layout plugin manager.
+   * @param \Drupal\user\SharedTempStoreFactory $tempstore
+   *   The tempstore factory.
+   */
+  public function __construct(LayoutPluginManagerInterface $manager, SharedTempStoreFactory $tempstore) {
+    $this->manager = $manager;
+    $this->tempstore = $tempstore;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'panels_layout_selection_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $cached_values = $form_state->getTemporaryValue('wizard');
+
+    /* @var $variant_plugin \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant */
+    $variant_plugin = $cached_values['plugin'];
+    $form['layout'] = [
+      '#title' => $this->t('Layout'),
+      '#type' => 'select',
+      '#options' => Layout::layoutPluginManager()->getLayoutOptions(['group_by_category' => TRUE]),
+      '#default_value' => $variant_plugin->getConfiguration()['layout'] ?: NULL,
+    ];
+
+    $wizard = $form_state->getFormObject();
+    $form['update_layout'] = [
+      '#type' => 'submit',
+      '#value' => 'Change Layout',
+      '#access' => !empty($variant_plugin->getConfiguration()['layout']),
+      '#validate' => [
+        [$this, 'validateForm'],
+      ],
+      '#submit' => [
+        [$this, 'submitForm'],
+        [$wizard, 'submitForm'],
+      ],
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $cached_values = $form_state->getTemporaryValue('wizard');
+    /* @var $variant_plugin \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant */
+    $variant_plugin = $cached_values['plugin'];
+    // If we're changing the layout, the variant plugin must remain out of date
+    // until the layout is fully configured and regions are remapped.
+    if ($form_state->getValue('op') == $form['update_layout']['#value']) {
+      $cached_values['layout_change'] = [
+        'old_layout' => $variant_plugin->getConfiguration()['layout'],
+        'new_layout' => $form_state->getValue('layout'),
+      ];
+      /** @var \Drupal\ctools\Wizard\EntityFormWizardInterface $wizard */
+      $wizard = $form_state->getFormObject();
+      $next_op = $wizard->getNextOp();
+      $form_state->setValue('op', $next_op);
+    }
+    // When setting the layout for the first time, update the variant plugin.
+    else {
+      $variant_plugin->setLayout($form_state->getValue('layout'), $form_state->getValue('layout_settings') ?: []);
+      $cached_values['plugin'] = $variant_plugin;
+    }
+    $form_state->setTemporaryValue('wizard', $cached_values);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    $cached_values = $form_state->getTemporaryValue('wizard');
+
+    /* @var $variant_plugin \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant */
+    $variant_plugin = $cached_values['plugin'];
+
+    if ((string)$form_state->getValue('op') == $this->t('Change Layout') && $variant_plugin->getConfiguration()['layout'] == $form_state->getValue('layout')) {
+      $form_state->setErrorByName('layout', $this->t('You must select a different layout if you wish to change layouts.'));
+    }
+    if ($variant_plugin->getConfiguration()['layout'] != $form_state->getValue('layout') && (string)$form_state->getValue('op') != $this->t('Change Layout')) {
+      $form_state->setErrorByName('layout', $this->t('To select a different layout, you must click "Change Layout".'));
+    }
+  }
+
+}
diff --git a/src/Form/PanelsAddBlockForm.php b/src/Form/PanelsAddBlockForm.php
new file mode 100644
index 0000000..b5506ba
--- /dev/null
+++ b/src/Form/PanelsAddBlockForm.php
@@ -0,0 +1,83 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panels\Form\PanelsAddBlockForm.
+ */
+
+namespace Drupal\panels\Form;
+
+use Drupal\Component\Plugin\PluginManagerInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\user\SharedTempStoreFactory;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Provides a form for adding a block plugin to a variant.
+ */
+class PanelsAddBlockForm extends PanelsBlockConfigureFormBase   {
+
+  /**
+   * The block plugin manager.
+   *
+   * @var \Drupal\Component\Plugin\PluginManagerInterface
+   */
+  protected $blockManager;
+
+  /**
+   * PanelsAddBlockForm constructor.
+   *
+   * @param \Drupal\user\SharedTempStoreFactory $tempstore
+   *   The tempstore factory.
+   * @param \Drupal\Component\Plugin\PluginManagerInterface $block_manager
+   *   The block plugin manager.
+   */
+  public function __construct(SharedTempStoreFactory $tempstore, PluginManagerInterface $block_manager) {
+    parent::__construct($tempstore);
+    $this->blockManager = $block_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('user.shared_tempstore'),
+      $container->get('plugin.manager.block')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'panels_add_block_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function prepareBlock($plugin_id) {
+    $block = $this->blockManager->createInstance($plugin_id);
+    $block_id = $this->getVariantPlugin()->addBlock($block->getConfiguration());
+    return $this->getVariantPlugin()->getBlock($block_id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, Request $request = NULL, $tempstore_id = NULL, $machine_name = NULL, $block_id = NULL) {
+    $form = parent::buildForm($form, $form_state, $tempstore_id, $machine_name, $block_id);
+    $form['region']['#default_value'] = $request->query->get('region');
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function submitText() {
+    return $this->t('Add block');
+  }
+
+}
diff --git a/src/Form/PanelsBlockConfigureFormBase.php b/src/Form/PanelsBlockConfigureFormBase.php
new file mode 100644
index 0000000..2dabe75
--- /dev/null
+++ b/src/Form/PanelsBlockConfigureFormBase.php
@@ -0,0 +1,203 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panels\Form\PanelsBlockConfigureFormBase.
+ */
+
+namespace Drupal\panels\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormState;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\Context\Context;
+use Drupal\Core\Plugin\Context\ContextDefinition;
+use Drupal\Core\Plugin\ContextAwarePluginAssignmentTrait;
+use Drupal\Core\Plugin\ContextAwarePluginInterface;
+use Drupal\panels\CachedValuesGetterTrait;
+use Drupal\user\SharedTempStoreFactory;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a base form for configuring a block as part of a variant.
+ */
+abstract class PanelsBlockConfigureFormBase extends FormBase {
+
+  use ContextAwarePluginAssignmentTrait;
+  use CachedValuesGetterTrait;
+
+  /**
+   * Tempstore factory.
+   *
+   * @var \Drupal\user\SharedTempStoreFactory
+   */
+  protected $tempstore;
+
+  /**
+   * The tempstore id.
+   *
+   * @var string
+   */
+  protected $tempstore_id;
+
+  /**
+   * The variant plugin.
+   *
+   * @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant
+   */
+  protected $variantPlugin;
+
+  /**
+   * The plugin being configured.
+   *
+   * @var \Drupal\Core\Block\BlockPluginInterface
+   */
+  protected $block;
+
+  /**
+   * Constructs a new VariantPluginConfigureBlockFormBase.
+   *
+   * @param \Drupal\user\SharedTempStoreFactory $tempstore
+   *   The tempstore factory.
+   */
+  public function __construct(SharedTempStoreFactory $tempstore) {
+    $this->tempstore = $tempstore;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('user.shared_tempstore')
+    );
+  }
+
+  /**
+   * Get the tempstore id.
+   *
+   * @return string
+   */
+  protected function getTempstoreId() {
+    return $this->tempstore_id;
+  }
+
+  /**
+   * Get the tempstore.
+   *
+   * @return \Drupal\user\SharedTempStore
+   */
+  protected function getTempstore() {
+    return $this->tempstore->get($this->getTempstoreId());
+  }
+
+  /**
+   * Prepares the block plugin based on the block ID.
+   *
+   * @param string $block_id
+   *   Either a block ID, or the plugin ID used to create a new block.
+   *
+   * @return \Drupal\Core\Block\BlockPluginInterface
+   *   The block plugin.
+   */
+  abstract protected function prepareBlock($block_id);
+
+  /**
+   * Returns the text to use for the submit button.
+   *
+   * @return string
+   *   The submit button text.
+   */
+  abstract protected function submitText();
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $tempstore_id = NULL, $machine_name = NULL, $block_id = NULL) {
+    $this->tempstore_id = $tempstore_id;
+    $cached_values = $this->getCachedValues($this->tempstore, $tempstore_id, $machine_name);
+    $this->variantPlugin = $cached_values['plugin'];
+
+    $contexts = $this->variantPlugin->getPattern()->getDefaultContexts($this->tempstore, $this->getTempstoreId(), $machine_name);
+    $this->variantPlugin->setContexts($contexts);
+    $form_state->setTemporaryValue('gathered_contexts', $contexts);
+
+    $this->block = $this->prepareBlock($block_id);
+    $form_state->set('machine_name', $machine_name);
+    $form_state->set('block_id', $this->block->getConfiguration()['uuid']);
+
+    $form['#tree'] = TRUE;
+    $form['settings'] = $this->block->buildConfigurationForm([], $form_state);
+    $form['settings']['id'] = [
+      '#type' => 'value',
+      '#value' => $this->block->getPluginId(),
+    ];
+    $form['region'] = [
+      '#title' => $this->t('Region'),
+      '#type' => 'select',
+      '#options' => $this->variantPlugin->getRegionNames(),
+      '#default_value' => $this->variantPlugin->getRegionAssignment($this->block->getConfiguration()['uuid']),
+      '#required' => TRUE,
+    ];
+
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->submitText(),
+      '#button_type' => 'primary',
+    ];
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    // The page might have been serialized, resulting in a new variant
+    // collection. Refresh the block object.
+    $this->block = $this->getVariantPlugin()->getBlock($form_state->get('block_id'));
+
+    $settings = (new FormState())->setValues($form_state->getValue('settings'));
+    // Call the plugin validate handler.
+    $this->block->validateConfigurationForm($form, $settings);
+    // Update the original form values.
+    $form_state->setValue('settings', $settings->getValues());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $settings = (new FormState())->setValues($form_state->getValue('settings'));
+
+    // Call the plugin submit handler.
+    $this->block->submitConfigurationForm($form, $settings);
+    // Update the original form values.
+    $form_state->setValue('settings', $settings->getValues());
+
+    if ($this->block instanceof ContextAwarePluginInterface) {
+      $this->block->setContextMapping($settings->getValue('context_mapping', []));
+    }
+
+    $configuration = $this->block->getConfiguration();
+    $configuration['region'] = $form_state->getValue('region');
+    $this->getVariantPlugin()->updateBlock($this->block->getConfiguration()['uuid'], $configuration);
+
+    $cached_values = $this->getCachedValues($this->tempstore, $this->tempstore_id, $form_state->get('machine_name'));
+    $cached_values['plugin'] = $this->getVariantPlugin();
+    // PageManager specific handling.
+    if (isset($cached_values['page_variant'])) {
+      $cached_values['page_variant']->getVariantPlugin()->setConfiguration($cached_values['plugin']->getConfiguration());
+    }
+    $this->getTempstore()->set($cached_values['id'], $cached_values);
+  }
+
+  /**
+   * Gets the variant plugin for this page variant entity.
+   *
+   * @return \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant
+   */
+  protected function getVariantPlugin() {
+    return $this->variantPlugin;
+  }
+
+}
diff --git a/src/Form/PanelsContentForm.php b/src/Form/PanelsContentForm.php
new file mode 100644
index 0000000..fbe2c8b
--- /dev/null
+++ b/src/Form/PanelsContentForm.php
@@ -0,0 +1,255 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panels\Form\PanelsContentForm.
+ */
+
+namespace Drupal\panels\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\ctools\Form\AjaxFormTrait;
+use Drupal\user\SharedTempStoreFactory;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a form for editing a panel variant display's content.
+ */
+class PanelsContentForm extends FormBase {
+
+  use AjaxFormTrait;
+
+  /**
+   * Tempstore factory.
+   *
+   * @var \Drupal\user\SharedTempStoreFactory
+   */
+  protected $tempstore;
+
+  /**
+   * The tempstore ID.
+   *
+   * @var string
+   */
+  protected $tempstore_id;
+
+  /**
+   * Constructs a new VariantPluginContentForm.
+   *
+   * @param \Drupal\user\SharedTempStoreFactory $tempstore
+   *   The tempstore factory.
+   */
+  public function __construct(SharedTempStoreFactory $tempstore) {
+    $this->tempstore = $tempstore;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('user.shared_tempstore')
+    );
+  }
+
+  /**
+   * Get the tempstore ID.
+   *
+   * @return string
+   */
+  protected function getTempstoreId() {
+    return $this->tempstore_id;
+  }
+
+  /**
+   * Get the tempstore.
+   *
+   * @return \Drupal\user\SharedTempStore
+   */
+  protected function getTempstore() {
+    return $this->tempstore->get($this->getTempstoreId());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'panels_block_page_content';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['#attached']['library'][] = 'block/drupal.block';
+    $this->tempstore_id = $form_state->getFormObject()->getTempstoreId();
+    $cached_values = $form_state->getTemporaryValue('wizard');
+    /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $variant_plugin */
+    $variant_plugin = $cached_values['plugin'];
+    // Allow to configure the page title, even when adding a new display.
+    // Default to the page label in that case.
+    $form['page_title'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Page title'),
+      '#description' => $this->t('Configure the page title that will be used for this display.'),
+      '#default_value' => $variant_plugin->getConfiguration()['page_title'] ?: '',
+    ];
+    $pattern_plugin = $variant_plugin->getPattern();
+    $machine_name = $pattern_plugin->getMachineName($cached_values);
+
+    // Set up the attributes used by a modal to prevent duplication later.
+    $attributes = $this->getAjaxAttributes();
+    $add_button_attributes = $this->getAjaxButtonAttributes();
+
+    if ($block_assignments = $variant_plugin->getRegionAssignments()) {
+      // Build a table of all blocks used by this variant.
+      $form['add'] = [
+        '#type' => 'link',
+        '#title' => $this->t('Add new block'),
+        '#url' => $pattern_plugin->getBlockListUrl($this->tempstore_id, $machine_name, NULL, $this->getRequest()->getRequestUri()),
+        '#attributes' => $add_button_attributes,
+        '#attached' => [
+          'library' => [
+            'core/drupal.ajax',
+          ],
+        ],
+      ];
+      $form['blocks'] = [
+        '#type' => 'table',
+        '#header' => [
+          $this->t('Label'),
+          $this->t('Plugin ID'),
+          $this->t('Region'),
+          $this->t('Weight'),
+          $this->t('Operations'),
+        ],
+        '#attributes' => array(
+          'id' => 'blocks',
+        ),
+        '#empty' => $this->t('There are no regions for blocks.'),
+      ];
+      // Loop through the blocks per region.
+      foreach ($block_assignments as $region => $blocks) {
+        // Add a section for each region and allow blocks to be dragged between
+        // them.
+        $form['blocks']['#tabledrag'][] = [
+          'action' => 'match',
+          'relationship' => 'sibling',
+          'group' => 'block-region-select',
+          'subgroup' => 'block-region-' . $region,
+          'hidden' => FALSE,
+        ];
+        $form['blocks']['#tabledrag'][] = [
+          'action' => 'order',
+          'relationship' => 'sibling',
+          'group' => 'block-weight',
+          'subgroup' => 'block-weight-' . $region,
+        ];
+        $form['blocks']['region-' . $region] = [
+          '#attributes' => [
+            'class' => ['region-title', 'region-title-' . $region],
+            'no_striping' => TRUE,
+          ],
+        ];
+        $form['blocks']['region-' . $region]['title'] = [
+          '#markup' => $variant_plugin->getRegionName($region),
+          '#wrapper_attributes' => [
+            'colspan' => 5,
+          ],
+        ];
+        $form['blocks']['region-' . $region . '-message'] = [
+          '#attributes' => [
+            'class' => [
+              'region-message',
+              'region-' . $region . '-message',
+              empty($blocks) ? 'region-empty' : 'region-populated',
+            ],
+          ],
+        ];
+        $form['blocks']['region-' . $region . '-message']['message'] = [
+          '#markup' => '<em>' . $this->t('No blocks in this region') . '</em>',
+          '#wrapper_attributes' => [
+            'colspan' => 5,
+          ],
+        ];
+
+        /** @var \Drupal\Core\Block\BlockPluginInterface[] $blocks */
+        foreach ($blocks as $block_id => $block) {
+          $row = [
+            '#attributes' => [
+              'class' => ['draggable'],
+            ],
+          ];
+          $row['label']['#markup'] = $block->label();
+          $row['id']['#markup'] = $block->getPluginId();
+          // Allow the region to be changed for each block.
+          $row['region'] = [
+            '#title' => $this->t('Region'),
+            '#title_display' => 'invisible',
+            '#type' => 'select',
+            '#options' => $variant_plugin->getRegionNames(),
+            '#default_value' => $variant_plugin->getRegionAssignment($block_id),
+            '#attributes' => [
+              'class' => ['block-region-select', 'block-region-' . $region],
+            ],
+          ];
+          // Allow the weight to be changed for each block.
+          $configuration = $block->getConfiguration();
+          $row['weight'] = [
+            '#type' => 'weight',
+            '#default_value' => isset($configuration['weight']) ? $configuration['weight'] : 0,
+            '#title' => $this->t('Weight for @block block', ['@block' => $block->label()]),
+            '#title_display' => 'invisible',
+            '#attributes' => [
+              'class' => ['block-weight', 'block-weight-' . $region],
+            ],
+          ];
+          // Add the operation links.
+          $operations = [];
+          $operations['edit'] = [
+            'title' => $this->t('Edit'),
+            'url' => $pattern_plugin->getBlockEditUrl($this->tempstore_id, $machine_name, $block_id, $this->getRequest()->getRequestUri()),
+            'attributes' => $attributes,
+          ];
+          $operations['delete'] = [
+            'title' => $this->t('Delete'),
+            'url' => $pattern_plugin->getBlockDeleteUrl($this->tempstore_id, $machine_name, $block_id, $this->getRequest()->getRequestUri()),
+            'attributes' => $attributes,
+          ];
+
+          $row['operations'] = [
+            '#type' => 'operations',
+            '#links' => $operations,
+          ];
+          $form['blocks'][$block_id] = $row;
+        }
+      }
+    }
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $cached_values = $form_state->getTemporaryValue('wizard');
+    /** @var \Drupal\page_manager\Plugin\DisplayVariant\PageBlockDisplayVariant $variant_plugin */
+    $variant_plugin = $cached_values['plugin'];
+
+    // If the blocks were rearranged, update their values.
+    if (!$form_state->isValueEmpty('blocks')) {
+      foreach ($form_state->getValue('blocks') as $block_id => $block_values) {
+        $variant_plugin->updateBlock($block_id, $block_values);
+      }
+    }
+    // Page Variant title handling.
+    if ($form_state->hasValue('page_title')) {
+      $configuration = $variant_plugin->getConfiguration();
+      $configuration['page_title'] = $form_state->getValue('page_title');
+      $variant_plugin->setConfiguration($configuration);
+    }
+  }
+
+}
diff --git a/src/Form/PanelsDeleteBlockForm.php b/src/Form/PanelsDeleteBlockForm.php
new file mode 100644
index 0000000..0edb03c
--- /dev/null
+++ b/src/Form/PanelsDeleteBlockForm.php
@@ -0,0 +1,135 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panels\Form\PanelsDeleteBlockForm.
+ */
+
+namespace Drupal\panels\Form;
+
+use Drupal\Core\Form\ConfirmFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\panels\CachedValuesGetterTrait;
+use Drupal\user\SharedTempStoreFactory;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a form for deleting an access condition.
+ */
+class PanelsDeleteBlockForm extends ConfirmFormBase {
+
+  use CachedValuesGetterTrait;
+
+  /**
+   * The tempstore factory.
+   *
+   * @var \Drupal\user\SharedTempStoreFactory
+   */
+  protected $tempstore;
+
+  /**
+   * The tempstore id.
+   *
+   * @var string
+   */
+  protected $tempstore_id;
+
+  /**
+   * @var \Drupal\ctools\Plugin\BlockVariantInterface
+   */
+  protected $plugin;
+
+  /**
+   * The plugin being configured.
+   *
+   * @var \Drupal\Core\Block\BlockPluginInterface
+   */
+  protected $block;
+
+  /**
+   * PanelsDeleteBlockForm constructor.
+   *
+   * @param \Drupal\user\SharedTempStoreFactory $tempstore
+   *   The tempstore factory.
+   */
+  public function __construct(SharedTempStoreFactory $tempstore) {
+    $this->tempstore = $tempstore;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('user.shared_tempstore')
+    );
+  }
+
+  /**
+   * Get the tempstore id.
+   *
+   * @return string
+   */
+  protected function getTempstoreId() {
+    return $this->tempstore_id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'panels_delete_block_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    return $this->t('Are you sure you want to delete the block %label?', ['%label' => $this->block->label()]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelUrl() {
+    return $this->getRequest()->attributes->get('destination');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return $this->t('Delete');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $tempstore_id = NULL, $machine_name = NULL, $block_id = NULL) {
+    $this->tempstore_id = $tempstore_id;
+    $cached_values = $this->getCachedValues($this->tempstore, $tempstore_id, $machine_name);
+    $this->plugin = $cached_values['plugin'];
+    $this->block = $this->plugin->getBlock($block_id);
+    $form['block_display'] = [
+      '#type' => 'value',
+      '#value' => $machine_name
+    ];
+    return parent::buildForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $this->plugin->removeBlock($this->block->getConfiguration()['uuid']);
+    $cached_values = $this->getCachedValues($this->tempstore, $this->getTempstoreId(), $form_state->getValue('block_display'));
+    $cached_values['plugin'] = $this->plugin;
+    // PageManager specific handling.
+    if (isset($cached_values['page_variant'])) {
+      $cached_values['page_variant']->getVariantPlugin()->setConfiguration($cached_values['plugin']->getConfiguration());
+    }
+    $this->tempstore->get($this->getTempstoreId())->set($cached_values['id'], $cached_values);
+    drupal_set_message($this->t('The block %label has been removed.', ['%label' => $this->block->label()]));
+  }
+
+}
diff --git a/src/Form/PanelsEditBlockForm.php b/src/Form/PanelsEditBlockForm.php
new file mode 100644
index 0000000..c28db14
--- /dev/null
+++ b/src/Form/PanelsEditBlockForm.php
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panels\Form\PanelsEditBlockForm.
+ */
+
+namespace Drupal\panels\Form;
+
+/**
+ * Provides a form for editing a block plugin of a variant.
+ */
+class PanelsEditBlockForm extends PanelsBlockConfigureFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'panels_edit_block_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function prepareBlock($block_id) {
+    return $this->getVariantPlugin()->getBlock($block_id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function submitText() {
+    return $this->t('Update block');
+  }
+
+}
diff --git a/src/PanelsPatternManager.php b/src/PanelsPatternManager.php
new file mode 100644
index 0000000..8122e75
--- /dev/null
+++ b/src/PanelsPatternManager.php
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panels\PanelsPatternManager.
+ */
+
+namespace Drupal\panels;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Plugin\DefaultPluginManager;
+
+class PanelsPatternManager extends DefaultPluginManager {
+
+  /**
+   * PanelsPatternManager constructor.
+   *
+   * @param \Traversable $namespaces
+   *   The namespaces to search for plugins.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   The cache backend.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   */
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
+    $this->alterInfo('panels_pattern_info');
+    $this->setCacheBackend($cache_backend, 'panels_pattern_plugins');
+
+    parent::__construct('Plugin/PanelsPattern', $namespaces, $module_handler, 'Drupal\panels\Plugin\PanelsPattern\PanelsPatternInterface', '\Drupal\panels\Annotation\PanelsPattern');
+  }
+
+}
diff --git a/src/Plugin/DisplayBuilder/DisplayBuilderBase.php b/src/Plugin/DisplayBuilder/DisplayBuilderBase.php
index 08164a5..ec6f02a 100644
--- a/src/Plugin/DisplayBuilder/DisplayBuilderBase.php
+++ b/src/Plugin/DisplayBuilder/DisplayBuilderBase.php
@@ -7,8 +7,7 @@
 
 namespace Drupal\panels\Plugin\DisplayBuilder;
 
-use Drupal\Component\Plugin\PluginBase;
-use Drupal\layout_plugin\Plugin\Layout\LayoutInterface;
+use Drupal\Core\Plugin\PluginBase;
 use Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant;
 
 /**
diff --git a/src/Plugin/DisplayBuilder/StandardDisplayBuilder.php b/src/Plugin/DisplayBuilder/StandardDisplayBuilder.php
index 1e23671..7a068c0 100644
--- a/src/Plugin/DisplayBuilder/StandardDisplayBuilder.php
+++ b/src/Plugin/DisplayBuilder/StandardDisplayBuilder.php
@@ -13,7 +13,12 @@ use Drupal\Core\Plugin\Context\ContextHandlerInterface;
 use Drupal\Core\Plugin\ContextAwarePluginInterface;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Session\AccountInterface;
-use Drupal\layout_plugin\Plugin\Layout\LayoutInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\ctools\Plugin\PluginWizardInterface;
+use Drupal\panels\Form\LayoutChangeRegions;
+use Drupal\panels\Form\LayoutChangeSettings;
+use Drupal\panels\Form\LayoutPluginSelector;
+use Drupal\panels\Form\PanelsContentForm;
 use Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -25,7 +30,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
  *   label = @Translation("Standard")
  * )
  */
-class StandardDisplayBuilder extends DisplayBuilderBase implements ContainerFactoryPluginInterface {
+class StandardDisplayBuilder extends DisplayBuilderBase implements PluginWizardInterface, ContainerFactoryPluginInterface {
 
   /**
    * The context handler.
@@ -153,4 +158,13 @@ class StandardDisplayBuilder extends DisplayBuilderBase implements ContainerFact
     return $regions;
   }
 
+  public function getWizardOperations($cached_values) {
+    $operations = [];
+    $operations['content'] = [
+      'title' => $this->t('Content'),
+      'form' => PanelsContentForm::class
+    ];
+    return $operations;
+  }
+
 }
diff --git a/src/Plugin/DisplayVariant/PanelsDisplayVariant.php b/src/Plugin/DisplayVariant/PanelsDisplayVariant.php
index aa34cf2..afb54fa 100644
--- a/src/Plugin/DisplayVariant/PanelsDisplayVariant.php
+++ b/src/Plugin/DisplayVariant/PanelsDisplayVariant.php
@@ -7,23 +7,27 @@
 
 namespace Drupal\panels\Plugin\DisplayVariant;
 
-use Drupal\Component\Utility\NestedArray;
 use Drupal\Component\Uuid\UuidInterface;
 use Drupal\Core\Block\BlockManager;
 use Drupal\Core\Condition\ConditionManager;
 use Drupal\Core\Extension\ModuleHandlerInterface;
-use Drupal\Core\Form\FormState;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\Context\ContextHandlerInterface;
+use Drupal\Core\Plugin\PluginFormInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Utility\Token;
-use Drupal\ctools\Plugin\BlockPluginCollection;
 use Drupal\ctools\Plugin\DisplayVariant\BlockDisplayVariant;
-use Drupal\layout_plugin\Layout;
+use Drupal\ctools\Plugin\PluginWizardInterface;
 use Drupal\layout_plugin\Plugin\Layout\LayoutInterface;
+use Drupal\layout_plugin\Plugin\Layout\LayoutPluginManager;
 use Drupal\layout_plugin\Plugin\Layout\LayoutPluginManagerInterface;
+use Drupal\panels\Form\LayoutChangeRegions;
+use Drupal\panels\Form\LayoutChangeSettings;
+use Drupal\panels\Form\LayoutPluginSelector;
+use Drupal\panels\Form\LayoutPluginUpdate;
 use Drupal\panels\Plugin\DisplayBuilder\DisplayBuilderInterface;
 use Drupal\panels\Plugin\DisplayBuilder\DisplayBuilderManagerInterface;
+use Drupal\panels\Plugin\PanelsPattern\PanelsPatternInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -34,7 +38,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
  *   admin_label = @Translation("Panels")
  * )
  */
-class PanelsDisplayVariant extends BlockDisplayVariant {
+class PanelsDisplayVariant extends BlockDisplayVariant implements PluginWizardInterface {
 
   /**
    * The module handler.
@@ -135,7 +139,12 @@ class PanelsDisplayVariant extends BlockDisplayVariant {
    */
   public function getBuilder() {
     if (!isset($this->builder)) {
-      $this->builder = $this->builderManager->createInstance($this->configuration['builder'], []);
+      if (empty($this->configuration['builder'])) {
+        $this->builder = $this->builderManager->createInstance('standard', []);
+      }
+      else {
+        $this->builder = $this->builderManager->createInstance($this->configuration['builder'], []);
+      }
     }
     return $this->builder;
   }
@@ -212,6 +221,49 @@ class PanelsDisplayVariant extends BlockDisplayVariant {
   }
 
   /**
+   * Gets the assigned PanelsPattern or falls back to the default pattern.
+   *
+   * @return \Drupal\panels\Plugin\PanelsPattern\PanelsPatternInterface
+   */
+  public function getPattern() {
+    if (!isset($this->pattern)) {
+      if (empty($this->configuration['pattern'])) {
+        $this->pattern = \Drupal::service('plugin.manager.panels.pattern')->createInstance('default');
+      }
+      else {
+        $this->pattern = \Drupal::service('plugin.manager.panels.pattern')->createInstance($this->configuration['pattern']);
+      }
+    }
+    return $this->pattern;
+  }
+
+  /**
+   * Assign the pattern for panels content operations and default contexts.
+   *
+   * @param mixed string|\Drupal\panels\Plugin\PanelsPattern\PanelsPatternInterface $pattern
+   *
+   * @return $this
+   *
+   * @throws \Exception
+   *   If $pattern isn't a string or PanelsPatternInterface object.
+   */
+  public function setPattern($pattern) {
+    if ($pattern instanceof PanelsPatternInterface) {
+      $this->pattern = $pattern;
+      $this->configuration['pattern'] = $pattern->getPluginId();
+    }
+    elseif (is_string($pattern)) {
+      $this->pattern = NULL;
+      $this->configuration['pattern'] = $pattern;
+    }
+    else {
+      throw new \Exception("Pattern must be a string or PanelsPatternInterface object");
+    }
+
+    return $this;
+  }
+
+  /**
    * Configures how this Panel is being stored.
    *
    * @param string $type
@@ -296,167 +348,35 @@ class PanelsDisplayVariant extends BlockDisplayVariant {
     // label on the page variant entity.
     //$form = parent::buildConfigurationForm($form, $form_state);
 
-    // Allow to configure the page title, even when adding a new display.
-    // Default to the page label in that case.
-    $form['page_title'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Page title'),
-      '#description' => $this->t('Configure the page title that will be used for this display.'),
-      '#default_value' => $this->configuration['page_title'] ?: '',
-    ];
-
-    if (empty($this->configuration['builder'])) {
-      $plugins = $this->builderManager->getDefinitions();
-      $options = array();
-      foreach ($plugins as $id => $plugin) {
-        $options[$id] = $plugin['label'];
-      }
-      // Only allow the IPE if the storage information is set.
-      if (!$this->getStorageType()) {
-        unset($options['ipe']);
-      }
-      $form['builder'] = [
-        '#title' => $this->t('Builder'),
-        '#type' => 'select',
-        '#options' => $options,
-        '#default_value' => 'standard',
-      ];
+    $plugins = $this->builderManager->getDefinitions();
+    $options = array();
+    foreach ($plugins as $id => $plugin) {
+      $options[$id] = $plugin['label'];
     }
-
-    $form['layout'] = [
-      '#title' => $this->t('Layout'),
+    // Only allow the IPE if the storage information is set.
+    if (!$this->getStorageType()) {
+      unset($options['ipe']);
+    }
+    $form['builder'] = [
+      '#title' => $this->t('Builder'),
       '#type' => 'select',
-      '#options' => Layout::getLayoutOptions(['group_by_category' => TRUE]),
-      '#default_value' => $this->configuration['layout'] ?: NULL,
+      '#options' => $options,
+      '#default_value' => !empty($this->configuration['builder']) ? $this->configuration['builder'] : 'standard',
     ];
 
-    if (!empty($this->configuration['layout'])) {
-      $form['layout']['#ajax'] = [
-        'callback' => [$this, 'layoutSettingsAjaxCallback'],
-        'wrapper' => 'layout-settings-wrapper',
-        'effect' => 'fade',
-      ];
-
-      // If a layout is already selected, show the layout settings.
-      $form['layout_settings_wrapper'] = [
-        '#type' => 'fieldset',
-        '#title' => $this->t('Layout settings'),
-        '#prefix' => '<div id="layout-settings-wrapper">',
-        '#suffix' => '</div>',
-      ];
-      $form['layout_settings_wrapper']['layout_settings'] = [];
-
-      // Process callback to configure #parents correctly on settings, since
-      // we don't know where in the form hierarchy our settings appear.
-      $form['#process'][] = [$this, 'layoutSettingsProcessCallback'];
-    }
-
     return $form;
   }
 
   /**
-   * Render API callback: builds the layout settings elements.
-   */
-  public function layoutSettingsProcessCallback(array &$element, FormStateInterface $form_state, array &$complete_form) {
-    $parents_base = $element['#parents'];
-    $layout_parent = array_merge($parents_base, ['layout']);
-    $layout_settings_parent = array_merge($parents_base, ['layout_settings']);
-
-    $settings_element =& $element['layout_settings_wrapper']['layout_settings'];
-
-    // Set the #parents on the layout_settings so they end up as a sibling of
-    // layout.
-    $layout_settings_parents = array_merge($element['#parents'], ['layout_settings']);
-    $settings_element['#parents'] = $layout_settings_parents;
-    $settings_element['#tree'] = TRUE;
-
-    // Get the layout name in a way that works regardless of whether we're
-    // getting the value via AJAX or not.
-    $layout_name = NestedArray::getValue($form_state->getUserInput(), $layout_parent) ?: $element['layout']['#default_value'];
-
-    // Place the layout settings on the form if a layout is selected.
-    if ($layout_name) {
-      $layout = Layout::layoutPluginManager()->createInstance($layout_name, $form_state->getValue($layout_settings_parent, $this->configuration['layout_settings'] ?: []));
-      $settings_element = $layout->buildConfigurationForm($settings_element, $form_state);
-    }
-
-    // Store the array parents for our element so that we can use it to pull out
-    // the layout settings in the validate and submit functions.
-    $complete_form['#variant_array_parents'] = $element['#array_parents'];
-
-    return $element;
-  }
-
-  /**
-   * Render API callback: gets the layout settings elements.
-   */
-  public function layoutSettingsAjaxCallback(array $form, FormStateInterface $form_state) {
-    $variant_array_parents = $form['#variant_array_parents'];
-    return NestedArray::getValue($form, array_merge($variant_array_parents, ['layout_settings_wrapper']));
-  }
-
-  /**
-   * Extracts the layout settings form and form state from the full form.
-   *
-   * @param array $form
-   *   Full form array.
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   Full form state.
-   *
-   * @return array
-   *   An array with two values: the new form array and form state object.
-   */
-  protected function getLayoutSettingsForm(array &$form, FormStateInterface $form_state) {
-    $layout_settings_form = NestedArray::getValue($form, array_merge($form['#variant_array_parents'], ['layout_settings_wrapper', 'layout_settings']));
-    $layout_settings_form_state = (new FormState())->setValues($form_state->getValue('layout_settings'));
-    return [$layout_settings_form, $layout_settings_form_state];
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
-    parent::validateConfigurationForm($form, $form_state);
-
-    // Validate layout settings.
-    if ($form_state->hasValue('layout_settings')) {
-      $layout_settings = $this->configuration['layout'] == $form_state->getValue('layout') ? $this->configuration['layout_settings'] : [];
-      $layout = $this->layoutManager->createInstance($form_state->getValue('layout'), $layout_settings);
-      list ($layout_settings_form, $layout_settings_form_state) = $this->getLayoutSettingsForm($form, $form_state);
-      $layout->validateConfigurationForm($layout_settings_form, $layout_settings_form_state);
-
-      // Save the layout plugin for later (so we don't have to instantiate again
-      // on submit.
-      $form_state->set('layout_plugin', $layout);
-    }
-  }
-
-  /**
    * {@inheritdoc}
    */
   public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
     parent::submitConfigurationForm($form, $form_state);
 
-    if ($form_state->hasValue('layout')) {
-      $this->configuration['layout'] = $form_state->getValue('layout');
-    }
-
-    // Submit layout settings.
-    if ($form_state->hasValue('layout_settings')) {
-      $layout_settings = $this->configuration['layout'] == $form_state->getValue('layout') ? $this->configuration['layout_settings'] : [];
-      $layout = $form_state->has('layout_plugin') ? $form_state->get('layout_plugin') : $this->layoutManager->createInstance($form_state->getValue('layout'), $layout_settings);
-      list ($layout_settings_form, $layout_settings_form_state) = $this->getLayoutSettingsForm($form, $form_state);
-      $layout->submitConfigurationForm($layout_settings_form, $layout_settings_form_state);
-      $this->configuration['layout_settings'] = $layout->getConfiguration();
-    }
-
     if ($form_state->hasValue('builder')) {
       $this->configuration['builder'] = $form_state->getValue('builder');
     }
-
-    if ($form_state->hasValue('page_title')) {
-      $this->configuration['page_title'] = $form_state->getValue('page_title');
-    }
+    $this->configuration['page_title'] = $form_state->getValue('page_title');
   }
 
   /**
@@ -488,6 +408,48 @@ class PanelsDisplayVariant extends BlockDisplayVariant {
   /**
    * {@inheritdoc}
    */
+  public function getWizardOperations($cached_values) {
+    $operations = [];
+    $operations['layout'] = [
+      'title' => $this->t('Layout'),
+      'form' => LayoutPluginSelector::class
+    ];
+    if (!empty($this->getConfiguration()['layout']) && $cached_values['plugin']->getLayout() instanceof PluginFormInterface) {
+      /** @var \Drupal\layout_plugin\Plugin\Layout\LayoutInterface $layout */
+      if (empty($cached_values['layout_change']['new_layout'])) {
+        $layout = $cached_values['plugin']->getLayout();
+        $r = new \ReflectionClass(get_class($layout));
+      }
+      else {
+        $layout_definition = \Drupal::service('plugin.manager.layout_plugin')->getDefinition($cached_values['layout_change']['new_layout']);
+        $r = new \ReflectionClass($layout_definition['class']);
+      }
+      $method = $r->getMethod('buildConfigurationForm');
+      if ($method->class != 'Drupal\layout_plugin\Plugin\Layout\LayoutBase') {
+        $operations['settings'] = [
+          'title' => $this->t('Layout Settings'),
+          'form' => LayoutChangeSettings::class,
+        ];
+      }
+    }
+    if (!empty($cached_values['layout_change']['old_layout'])) {
+      $operations['regions'] = [
+        'title' => $this->t('Layout Regions'),
+        'form' => LayoutChangeRegions::class,
+      ];
+    }
+    /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $plugin */
+    $plugin = $cached_values['plugin'];
+    $builder = $plugin->getBuilder();
+    if ($builder instanceof PluginWizardInterface) {
+      $operations = array_merge($operations, $builder->getWizardOperations($cached_values));
+    }
+    return $operations;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function setConfiguration(array $configuration) {
     if (empty($configuration['uuid'])) {
       $configuration['uuid'] = $this->uuidGenerator()->generate();
diff --git a/src/Plugin/PanelsPattern/DefaultPattern.php b/src/Plugin/PanelsPattern/DefaultPattern.php
new file mode 100644
index 0000000..8db47fb
--- /dev/null
+++ b/src/Plugin/PanelsPattern/DefaultPattern.php
@@ -0,0 +1,131 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panels\PanelsPattern\DefaultPattern.
+ */
+
+namespace Drupal\panels\Plugin\PanelsPattern;
+
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Plugin\PluginBase;
+use Drupal\Core\Url;
+use Drupal\ctools\ContextMapperInterface;
+use Drupal\panels\CachedValuesGetterTrait;
+use Drupal\user\SharedTempStoreFactory;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * @PanelsPattern("default")
+ */
+class DefaultPattern extends PluginBase implements PanelsPatternInterface, ContainerFactoryPluginInterface {
+
+  use CachedValuesGetterTrait;
+
+  /**
+   * The context mapper.
+   *
+   * @var \Drupal\ctools\ContextMapperInterface
+   */
+  protected $contextMapper;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static($configuration, $plugin_id, $plugin_definition, $container->get('ctools.context_mapper'));
+  }
+
+  /**
+   * DefaultPattern constructor.
+   *
+   * @param array $configuration
+   *   The plugin's configuration.
+   * @param string $plugin_id
+   *   The plugin id.
+   * @param mixed $plugin_definition
+   *   The plugin definition.
+   * @param \Drupal\ctools\ContextMapperInterface $context_mapper
+   *   The context mapper.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, ContextMapperInterface $context_mapper) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->contextMapper = $context_mapper;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMachineName($cached_values) {
+    // PageManager needs special handling, so lets see if we're dealing with a PM page.
+    if (isset($cached_values['page_variant'])) {
+      return implode('--', [$cached_values['id'], $cached_values['page_variant']->id()]);
+    }
+    return $cached_values['id'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultContexts(SharedTempStoreFactory $tempstore, $tempstore_id, $machine_name) {
+    $cached_values = $this->getCachedValues($tempstore, $tempstore_id, $machine_name);
+    // PageManager specific context loading.
+    if (!empty($cached_values['page_variant'])) {
+      /** @var \Drupal\page_manager\PageVariantInterface $page_variant */
+      $page_variant = $cached_values['page_variant'];
+      return $page_variant->getContexts();
+    }
+    // General handling for contexts.
+    return !empty($cached_values['contexts']) ? $this->contextMapper->getContextValues($cached_values['contexts']) : [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getBlockListUrl($tempstore_id, $machine_name, $region = NULL, $destination = NULL) {
+    return Url::fromRoute('panels.select_block', [
+      'tempstore_id' => $tempstore_id,
+      'machine_name' => $machine_name,
+      'region' => $region,
+      'destination' => $destination,
+    ]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getBlockAddUrl($tempstore_id, $machine_name, $block_id, $region = NULL, $destination = NULL) {
+    return Url::fromRoute('panels.add_block', [
+      'tempstore_id' => $tempstore_id,
+      'machine_name' => $machine_name,
+      'block_id' => $block_id,
+      'region' => $region,
+      'destination' => $destination,
+    ]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getBlockEditUrl($tempstore_id, $machine_name, $block_id, $destination = NULL) {
+    return Url::fromRoute('panels.edit_block', [
+      'tempstore_id' => $tempstore_id,
+      'machine_name' => $machine_name,
+      'block_id' => $block_id,
+      'destination' => $destination,
+    ]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getBlockDeleteUrl($tempstore_id, $machine_name, $block_id, $destination = NULL) {
+    return Url::fromRoute('panels.delete_block', [
+      'tempstore_id' => $tempstore_id,
+      'machine_name' => $machine_name,
+      'block_id' => $block_id,
+      'destination' => $destination,
+    ]);
+  }
+
+}
diff --git a/src/Plugin/PanelsPattern/PanelsPatternInterface.php b/src/Plugin/PanelsPattern/PanelsPatternInterface.php
new file mode 100644
index 0000000..ca69a38
--- /dev/null
+++ b/src/Plugin/PanelsPattern/PanelsPatternInterface.php
@@ -0,0 +1,104 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panels\Plugin\PanelsPattern\PanelsPatternInterface.
+ */
+
+namespace Drupal\panels\Plugin\PanelsPattern;
+
+use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\user\SharedTempStoreFactory;
+
+interface PanelsPatternInterface extends PluginInspectionInterface {
+
+  /**
+   * Gets the tempstore key identifier.
+   *
+   * @param array $cached_values
+   *
+   * @return string
+   */
+  public function getMachineName($cached_values);
+
+  /**
+   * Gets the array of default contexts for this panels pattern.
+   *
+   * @param \Drupal\user\SharedTempStoreFactory $tempstore
+   *   The tempstore factory object.
+   * @param string $tempstore_id
+   *   The tempstore identifier.
+   * @param string $machine_name
+   *   The tempstore key.
+   *
+   * @return \Drupal\Core\Plugin\Context\ContextInterface[]
+   */
+  public function getDefaultContexts(SharedTempStoreFactory $tempstore, $tempstore_id, $machine_name);
+
+  /**
+   * Gets the block list url.
+   *
+   * @param string $tempstore_id
+   *   The tempstore identifier.
+   * @param string $machine_name
+   *   The tempstore key.
+   * @param string $region
+   *   The region in which to place the block after it is created.
+   * @param string $destination
+   *   The destination to which to redirect after submission.
+   *
+   * @return \Drupal\Core\Url
+   */
+  public function getBlockListUrl($tempstore_id, $machine_name, $region = NULL, $destination = NULL);
+
+  /**
+   * Gets the block add url.
+   *
+   * @param string $tempstore_id
+   *   The tempstore identifier.
+   * @param string $machine_name
+   *   The tempstore key.
+   * @param string $block_id
+   *   The id of the block plugin to create.
+   * @param string $region
+   *   The region in which to place the block after it is created.
+   * @param string $destination
+   *   The destination to which to redirect after submission.
+   *
+   * @return \Drupal\Core\Url
+   */
+  public function getBlockAddUrl($tempstore_id, $machine_name, $block_id, $region = NULL, $destination = NULL);
+
+  /**
+   * Gets the block edit url.
+   *
+   * @param string $tempstore_id
+   *   The tempstore identifier.
+   * @param string $machine_name
+   *   The tempstore key.
+   * @param string $block_id
+   *   The unique id of the block in this panel.
+   * @param string $destination
+   *   The destination to which to redirect after submission.
+   *
+   * @return \Drupal\Core\Url
+   */
+  public function getBlockEditUrl($tempstore_id, $machine_name, $block_id, $destination = NULL);
+
+  /**
+   * Gets the block delete url.
+   *
+   * @param string $tempstore_id
+   *   The tempstore identifier.
+   * @param string $machine_name
+   *   The tempstore key.
+   * @param string $block_id
+   *   The unique id of the block in this panel.
+   * @param string $destination
+   *   The destination to which to redirect after submission.
+   *
+   * @return \Drupal\Core\Url
+   */
+  public function getBlockDeleteUrl($tempstore_id, $machine_name, $block_id, $destination = NULL);
+
+}
diff --git a/src/Tests/PageManagerPanelsStorageIntegrationTest.php b/src/Tests/PageManagerPanelsStorageIntegrationTest.php
index a7539e6..a30e100 100644
--- a/src/Tests/PageManagerPanelsStorageIntegrationTest.php
+++ b/src/Tests/PageManagerPanelsStorageIntegrationTest.php
@@ -49,28 +49,39 @@ class PageManagerPanelsStorageIntegrationTest extends WebTestBase {
       'id' => 'foo',
       'label' => 'foo',
       'path' => 'testing',
+      'variant_plugin_id' => 'panels_variant',
     ];
-    $this->drupalPostForm(NULL, $edit, 'Save');
+    $this->drupalPostForm(NULL, $edit, 'Next');
+
+    // Parameters step
+    // @TODO remove once parameters step is contextual.
+    // $this->drupalPostForm(NULL, [], 'Next');
+
 
     // Add a Panels variant which uses the IPE.
-    $this->clickLink('Add new variant');
-    $this->clickLink('Panels');
     $edit = [
-      'id' => 'panels_1',
-      'label' => 'Default',
       // This option won't be present at all if our integration isn't working!
       'variant_settings[builder]' => 'ipe',
     ];
-    $this->drupalPostForm(NULL, $edit, 'Save');
+    $this->drupalPostForm(NULL, $edit, 'Next');
+
+    // Choose a layout.
+    $edit = [
+      'layout' => 'twocol',
+    ];
+    $this->drupalPostForm(NULL, $edit, 'Next');
+
+    // Finish without adding any blocks.
+    $this->drupalPostForm(NULL, [], 'Finish');
 
     /** @var \Drupal\page_manager\PageVariantInterface $page_variant */
-    $page_variant = PageVariant::load('panels_1');
+    $page_variant = PageVariant::load('foo-panels_variant-0');
     /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $panels_display */
     $panels_display = $page_variant->getVariantPlugin();
 
     // Make sure the storage type and id were set to the right value.
     $this->assertEqual($panels_display->getStorageType(), 'page_manager');
-    $this->assertEqual($panels_display->getStorageId(), 'panels_1');
+    $this->assertEqual($panels_display->getStorageId(), 'foo-panels_variant-0');
   }
 
 }
diff --git a/src/Tests/PanelsTest.php b/src/Tests/PanelsTest.php
index b83ab4b..1ac6e8a 100644
--- a/src/Tests/PanelsTest.php
+++ b/src/Tests/PanelsTest.php
@@ -53,18 +53,25 @@ class PanelsTest extends WebTestBase {
       'id' => 'foo',
       'label' => 'foo',
       'path' => 'testing',
+      'variant_plugin_id' => 'panels_variant',
     ];
-    $this->drupalPostForm(NULL, $edit, 'Save');
+    $this->drupalPostForm(NULL, $edit, 'Next');
+
+    // Parameters step
+    // @TODO remove once parameters step is contextual.
+    // $this->drupalPostForm(NULL, [], 'Next');
 
     // Add variant with a layout that has settings.
-    $this->clickLink('Add new variant');
-    $this->clickLink('Panels');
     $edit = [
-      'id' => 'panels_1',
-      'label' => 'Default',
-      'variant_settings[layout]' => 'layout_example_test',
+      'page_variant_label' => 'Default',
+    ];
+    $this->drupalPostForm(NULL, $edit, 'Next');
+
+    // Choose a layout.
+    $edit = [
+      'layout' => 'layout_example_test',
     ];
-    $this->drupalPostForm(NULL, $edit, 'Save');
+    $this->drupalPostForm(NULL, $edit, 'Next');
 
     // Add a block.
     $this->clickLink('Add new block');
@@ -74,22 +81,12 @@ class PanelsTest extends WebTestBase {
     ];
     $this->drupalPostForm(NULL, $edit, 'Add block');
 
-    // Check the default value and change a layout setting.
-    $this->assertText('Blah');
-    $this->assertFieldByName("variant_settings[layout_settings][setting_1]", "Default");
-    $edit = [
-      'variant_settings[layout_settings][setting_1]' => 'Abracadabra',
-    ];
-    $this->drupalPostForm(NULL, $edit, 'Save');
-
-    // Go back to the variant edit form and see that the setting stuck.
-    $this->drupalGet('admin/structure/page_manager/manage/foo/variant/panels_1');
-    $this->assertFieldByName("variant_settings[layout_settings][setting_1]", "Abracadabra");
+    // Finish the page add wizard.
+    $this->drupalPostForm(NULL, [], 'Finish');
 
     // View the page and make sure the setting is present.
     $this->drupalGet('testing');
     $this->assertText('Blah:');
-    $this->assertText('Abracadabra');
     $this->assertText('Powered by Drupal');
   }
 
diff --git a/tests/src/Unit/PanelsDisplayVariantTest.php b/tests/src/Unit/PanelsDisplayVariantTest.php
index cfb3c95..8ef088e 100644
--- a/tests/src/Unit/PanelsDisplayVariantTest.php
+++ b/tests/src/Unit/PanelsDisplayVariantTest.php
@@ -112,9 +112,8 @@ class PanelsDisplayVariantTest extends UnitTestCase {
     $form_state = (new FormState())->setValues($values);
     $this->variant->submitConfigurationForm($form, $form_state);
 
-    $property = new \ReflectionProperty($this->variant, 'configuration');
-    $property->setAccessible(TRUE);
-    $this->assertSame($values['page_title'], $property->getValue($this->variant)['page_title']);
+    $configuration = $this->variant->getConfiguration();
+    $this->assertSame($values['page_title'], $configuration['page_title']);
   }
 
   /**
