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.module b/panels.module
index 05e0a2e..20c9d57 100644
--- a/panels.module
+++ b/panels.module
@@ -6,6 +6,7 @@
  * Core functionality for the Panels engine.
  */
 
+use Drupal\Core\Form\FormStateInterface;
 use \Drupal\page_manager\PageVariantInterface;
 
 define('PANELS_REQUIRED_CTOOLS_API', '2.0-alpha');
@@ -14,6 +15,47 @@ define('PANELS_TITLE_FIXED', 0); // Hide title use to be true/false. So false re
 define('PANELS_TITLE_NONE', 1); // And true meant no title.
 define('PANELS_TITLE_PANE', 2); // And this is the new behavior, where the title field will pick from a pane.
 
+function panels_form_alter(&$form, FormStateInterface $form_state, $form_id) {
+  if ($form_id == 'page_manage_variant_configure_form') {
+    $cached_values = $form_state->getTemporaryValue('wizard');
+    /** @var \Drupal\page_manager\PageVariantInterface $page_variant */
+    $page_variant = $cached_values['page_variant'];
+    if ($page_variant->getVariantPluginId() == 'panels_variant') {
+      // We'll set the title on the content page.
+      $form['variant_settings']['page_title']['#access'] = FALSE;
+      // Get renderer options.
+      $options = [];
+      foreach (\Drupal::service('plugin.manager.panels.display_builder')->getDefinitions() as $plugin_id => $definition) {
+        $options[$plugin_id] = $definition['label'];
+      }
+      $form['panel_settings'] = [
+        'builder' => [
+          '#type' => 'select',
+          '#title' => t('Renderer'),
+          '#options' => $options,
+          '#default_value' => $page_variant->getVariantPlugin()->getBuilder()->getPluginId(),
+        ],
+      ];
+      $form['delete']['#weight'] = 100;
+      array_unshift($form['actions']['submit']['#submit'], 'panels_page_manager_general_form_submit_handler');
+      array_unshift($form['actions']['update_and_save']['#submit'], 'panels_page_manager_general_form_submit_handler');
+      array_unshift($form['actions']['finish']['#submit'], 'panels_page_manager_general_form_submit_handler');
+    }
+  }
+}
+
+function panels_page_manager_general_form_submit_handler(&$form, FormStateInterface $form_state) {
+  $builder_id = $form_state->getValue('builder');
+  $cached_values = $form_state->getTemporaryValue('wizard');
+  /** @var \Drupal\page_manager\PageVariantInterface $page_variant */
+  $page_variant = $cached_values['page_variant'];
+  /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $variant */
+  $variant = $page_variant->getVariantPlugin();
+  $variant->setBuilder($builder_id);
+  $cached_values['plugin'] = $variant;
+  $form_state->setTemporaryValue('wizard', $cached_values);
+}
+
 /**
  * Returns the API version of Panels. This didn't exist in 1.
  *
diff --git a/panels.routing.yml b/panels.routing.yml
new file mode 100644
index 0000000..0402329
--- /dev/null
+++ b/panels.routing.yml
@@ -0,0 +1,36 @@
+panels.layout.change_form:
+  path: '/admin/structure/panels/{tempstore_id}/{machine_name}/layout/{step}'
+  defaults:
+    _wizard: '\Drupal\panels\Wizard\LayoutChangeWizard'
+    _title: 'Add page variant'
+    step: 'settings'
+  requirements:
+    _permission: 'administer pages'
+panels.select_block:
+  path: '/admin/structure/panels/{tempstore_id}/{machine_name}/select_block'
+  defaults:
+    _controller: '\Drupal\panels\Controller\Panels::selectBlock'
+    _title: 'Select block'
+  requirements:
+    _permission: 'administer pages'
+panels.add_block:
+  path: '/admin/structure/panels/{tempstore_id}/{machine_name}/add/{block_id}'
+  defaults:
+    _form: '\Drupal\panels\Form\PanelsAddBlockForm'
+    _title: 'Add block'
+  requirements:
+    _permission: 'administer pages'
+panels.edit_block:
+  path: '/admin/structure/panels/{tempstore_id}/{machine_name}/edit/{block_id}'
+  defaults:
+    _form: '\Drupal\panels\Form\PanelsEditBlockForm'
+    _title: 'Edit block'
+  requirements:
+    _permission: 'administer pages'
+panels.delete_block:
+  path: '/admin/structure/panels/{tempstore_id}/{machine_name}/delete/{block_id}'
+  defaults:
+    _form: '\Drupal\panels\Form\PanelsDeleteBlockForm'
+    _title: 'Delete block'
+  requirements:
+    _permission: 'administer pages'
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..34efc9d
--- /dev/null
+++ b/src/Annotation/PanelsPattern.php
@@ -0,0 +1,15 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panels\Annotation\PanelsPattern.
+ */
+
+namespace Drupal\panels\Annotation;
+
+use Drupal\Component\Annotation\PluginID;
+
+/**
+ * @Annotation
+ */
+class PanelsPattern extends PluginID {}
diff --git a/src/CachedValuesGetterTrait.php b/src/CachedValuesGetterTrait.php
new file mode 100644
index 0000000..6b7397a
--- /dev/null
+++ b/src/CachedValuesGetterTrait.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panels\getCachedValuesTrait.
+ */
+
+namespace Drupal\panels;
+
+use Drupal\user\SharedTempStoreFactory;
+
+trait CachedValuesGetterTrait {
+
+  protected function getCachedValues(SharedTempStoreFactory $tempstore, $tempstore_id, $machine_name) {
+    list($machine_name, $variant_id) = explode('--', $machine_name);
+    $cached_values = $tempstore->get($tempstore_id)->get($machine_name);
+    // PageManager specific handling.
+    if ($variant_id && !isset($cached_values['page_variant'])) {
+      /** @var \Drupal\page_manager\PageInterface $page */
+      $page = $cached_values['page'];
+      $cached_values['page_variant'] = $page->getVariant($variant_id);
+    }
+    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..2cba4ab
--- /dev/null
+++ b/src/Controller/Panels.php
@@ -0,0 +1,162 @@
+<?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;
+  }
+
+
+  public function addBlock($tempstore_id, $machine_name, $plugin_id) {
+
+  }
+
+}
diff --git a/src/Form/LayoutChangeRegions.php b/src/Form/LayoutChangeRegions.php
new file mode 100644
index 0000000..90257af
--- /dev/null
+++ b/src/Form/LayoutChangeRegions.php
@@ -0,0 +1,226 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\panels\Form\LayoutChangeRegions.
+ */
+
+namespace Drupal\panels\Form;
+
+
+use Drupal\Component\Utility\NestedArray;
+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 {
+
+  /**
+   * @var \Drupal\layout_plugin\Plugin\Layout\LayoutPluginManagerInterface
+   */
+  protected $manager;
+
+  /**
+   * @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')
+    );
+  }
+
+  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['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,
+    ];
+
+    $old_layout = Layout::layoutPluginManager()->createInstance($cached_values['layout_change']['old_layout'], []);
+    $new_layout = Layout::layoutPluginManager()->createInstance($cached_values['layout_change']['new_layout'], []);
+
+
+
+    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('Region'),
+          $this->t('Weight'),
+        ],
+        '#empty' => $this->t('There are no regions for blocks.'),
+      ];
+
+      // Loop through the blocks per region.
+      foreach ($new_layout->getPluginDefinition()['region_names'] 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] = [
+          '#attributes' => [
+            'class' => ['region-title', 'region-title-' . $region],
+            'no_striping' => TRUE,
+          ],
+        ];
+        $form['blocks'][$region]['title'] = [
+          '#markup' => $label,
+          '#wrapper_attributes' => [
+            'colspan' => 3,
+          ],
+        ];
+        $form['blocks'][$region . '-message'] = [
+          '#attributes' => [
+            'class' => [
+              'region-message',
+              'region-' . $region . '-message',
+              empty($blocks) ? 'region-empty' : 'region-populated',
+            ],
+          ],
+        ];
+        $form['blocks'][$region . '-message']['message'] = [
+          '#markup' => '<em>' . $this->t('No blocks in this region') . '</em>',
+          '#wrapper_attributes' => [
+            'colspan' => 3,
+          ],
+        ];
+      }
+
+
+      /** @var \Drupal\Core\Block\BlockPluginInterface[] $blocks */
+      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;
+        // Allow the region to be changed for each block.
+        $row['region'] = [
+          '#title' => $this->t('Region'),
+          '#title_display' => 'invisible',
+          '#type' => 'select',
+          '#options' => $new_layout->getPluginDefinition()['region_names'],
+          //'#default_value' => $variant_plugin->getRegionAssignment($block_id),
+          '#default_value' => 'left',
+          '#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,
+          '#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;
+  }
+
+  /**
+   * 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']));
+  }
+
+  /**
+   * {@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'];
+    $variant_plugin->setLayout($cached_values['layout_change']['new_layout']);
+    $cached_values['plugin'] = $variant_plugin;
+
+    unset($cached_values['layout_change']);
+  }
+
+  /**
+   * {@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 ($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.'));
+    }
+  }
+
+}
diff --git a/src/Form/LayoutChangeSettings.php b/src/Form/LayoutChangeSettings.php
new file mode 100644
index 0000000..022ce47
--- /dev/null
+++ b/src/Form/LayoutChangeSettings.php
@@ -0,0 +1,116 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\panels\Form\LayoutChangeSettings.
+ */
+
+namespace Drupal\panels\Form;
+
+
+use Drupal\Component\Utility\NestedArray;
+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 LayoutChangeSettings extends FormBase {
+
+  /**
+   * @var \Drupal\layout_plugin\Plugin\Layout\LayoutPluginManagerInterface
+   */
+  protected $manager;
+
+  /**
+   * @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')
+    );
+  }
+
+  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,
+    ];
+
+    $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,
+    ];
+
+    // If a layout is already selected, show the layout settings.
+    $form['layout_settings_wrapper'] = [
+      '#type' => 'fieldset',
+      '#title' => $this->t('Layout settings'),
+    ];
+
+    $layout = Layout::layoutPluginManager()->createInstance($cached_values['layout_change']['new_layout'], []);
+    $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\panels\Plugin\DisplayVariant\PanelsDisplayVariant $plugin */
+    $plugin = $cached_values['plugin'];
+    $layout = $form_state->getValue('new_layout');
+    $settings = $form_state->getValue(['layout_settings_wrapper', 'layout_settings']) ?: [];
+    $plugin->setLayout($layout, $settings);
+    $cached_values['plugin'] = $plugin;
+  }
+
+  /**
+   * {@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 ($variant_plugin->getLayout()->getPluginId() == $form_state->getValue('new_layout') || $form_state->getValue('old_layout') == $form_state->getValue('new_layout')) {
+      $form_state->setErrorByName('layout', $this->t('You must select a different layout if you wish to change layouts.'));
+    }
+  }
+
+}
diff --git a/src/Form/LayoutPluginSelector.php b/src/Form/LayoutPluginSelector.php
new file mode 100644
index 0000000..bac9da9
--- /dev/null
+++ b/src/Form/LayoutPluginSelector.php
@@ -0,0 +1,135 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\panels\Form\LayoutPluginSelector.
+ */
+
+namespace Drupal\panels\Form;
+
+
+use Drupal\Component\Utility\NestedArray;
+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 {
+
+  /**
+   * @var \Drupal\layout_plugin\Plugin\Layout\LayoutPluginManagerInterface
+   */
+  protected $manager;
+
+  /**
+   * @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')
+    );
+  }
+
+  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'];
+    $options = [];
+    foreach ($this->manager->getDefinitions() as $plugin_id => $definition) {
+      $options[$plugin_id] = $definition['label'];
+    }
+    $form['layout'] = [
+      '#title' => $this->t('Layout'),
+      '#type' => 'select',
+      '#options' => Layout::getLayoutOptions(['group_by_category' => TRUE]),
+      '#default_value' => $variant_plugin->getConfiguration()['layout'] ?: NULL,
+    ];
+
+    if (!empty($variant_plugin->getConfiguration()['layout'])) {
+      $form['update_layout'] = [
+        '#type' => 'submit',
+        '#value' => 'Change Layout',
+        '#validate' => [
+          [$this, 'validateForm'],
+        ],
+        '#submit' => [
+          [$this, 'submitForm'],
+        ],
+      ];
+    }
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $cached_values = $form_state->getTemporaryValue('wizard');
+
+    if ((string)$form_state->getValue('op') == $this->t('Change Layout')) {
+      /** @var \Drupal\ctools\Wizard\EntityFormWizardInterface $wizard */
+      $wizard = $form_state->getFormObject();
+      $variant_plugin = $cached_values['plugin'];
+      $cached_values['layout_change'] = [];
+      $cached_values['layout_change']['plugin'] = $variant_plugin;
+      $cached_values['layout_change']['old_layout'] = $variant_plugin->getConfiguration()['layout'];
+      $cached_values['layout_change']['new_layout'] = $form_state->getValue('layout');
+      $cached_values['layout_change']['tempstore_id'] = $wizard->getTempstoreId();
+      $cached_values['layout_change']['destination'] = [
+        $wizard->getRouteName(),
+        ['step' => 'content'] + $wizard->getNextParameters($cached_values),
+      ];
+      $wizard->getTempstore()->set($wizard->getMachineName(), $cached_values);
+
+      /*$form_state->setRedirect('panels.layout.change_form', [
+        'tempstore_id' => $wizard->getTempstoreId(),
+        'machine_name' => $wizard->getMachineName()
+      ]);*/
+    }
+    else {
+      /* @var $variant_plugin \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant */
+      $variant_plugin = $cached_values['plugin'];
+      $variant_plugin->setLayout($form_state->getValue('layout'), $form_state->getValue('layout_settings') ? $form_state->getValue('layout_settings') : []);
+      $cached_values['plugin'] = $variant_plugin;
+    }
+  }
+
+  /**
+   * {@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 ($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.'));
+    }
+  }
+
+}
diff --git a/src/Form/PanelsAddBlockForm.php b/src/Form/PanelsAddBlockForm.php
new file mode 100644
index 0000000..f2382e8
--- /dev/null
+++ b/src/Form/PanelsAddBlockForm.php
@@ -0,0 +1,81 @@
+<?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 manager.
+   *
+   * @var \Drupal\Component\Plugin\PluginManagerInterface
+   */
+  protected $blockManager;
+
+  /**
+   * Constructs a new VariantPluginFormBase.
+   *
+   * @param \Drupal\Component\Plugin\PluginManagerInterface $block_manager
+   *   The block 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..e8bbab1
--- /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->getVariantPlugin()->getRegionNames(),
+      '#default_value' => $this->getVariantPlugin()->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..3903aef
--- /dev/null
+++ b/src/Form/PanelsContentForm.php
@@ -0,0 +1,239 @@
+<?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;
+
+  /**
+   * @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'];
+    $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);
+      }
+    }
+  }
+
+}
diff --git a/src/Form/PanelsDeleteBlockForm.php b/src/Form/PanelsDeleteBlockForm.php
new file mode 100644
index 0000000..dea4dde
--- /dev/null
+++ b/src/Form/PanelsDeleteBlockForm.php
@@ -0,0 +1,116 @@
+<?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;
+
+/**
+ * Provides a form for deleting an access condition.
+ */
+class PanelsDeleteBlockForm extends ConfirmFormBase {
+
+  use CachedValuesGetterTrait;
+
+  /**
+   * 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;
+
+  /**
+   * Get the tempstore id.
+   *
+   * @return string
+   */
+  protected function getTempstoreId() {
+    return $this->tempstore_id;
+  }
+
+  /**
+   * Get the tempstore.
+   *
+   * @return \Drupal\user\SharedTempStoreFactory
+   */
+  protected function getTempstore() {
+    return \Drupal::service('user.shared_tempstore');
+  }
+
+  /**
+   * {@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 \Drupal::request()->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->getTempstore(), $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->getTempstore(), $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->getTempstore()->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..009cee4
--- /dev/null
+++ b/src/PanelsPatternManager.php
@@ -0,0 +1,23 @@
+<?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 {
+
+  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..9cb30cb 100644
--- a/src/Plugin/DisplayVariant/PanelsDisplayVariant.php
+++ b/src/Plugin/DisplayVariant/PanelsDisplayVariant.php
@@ -17,11 +17,15 @@ use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\Context\ContextHandlerInterface;
 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\LayoutPluginManagerInterface;
+use Drupal\panels\Form\LayoutChangeRegions;
+use Drupal\panels\Form\LayoutChangeSettings;
+use Drupal\panels\Form\LayoutPluginSelector;
+use Drupal\panels\Form\LayoutPluginUpdate;
+use Drupal\panels\PanelsPattern\Plugin\PanelsPatternInterface;
 use Drupal\panels\Plugin\DisplayBuilder\DisplayBuilderInterface;
 use Drupal\panels\Plugin\DisplayBuilder\DisplayBuilderManagerInterface;
 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.
@@ -212,6 +216,22 @@ class PanelsDisplayVariant extends BlockDisplayVariant {
   }
 
   /**
+   * @return \Drupal\panels\Plugin\PanelsPattern\PanelsPatternInterface
+   */
+  public function getPattern() {
+    if (!isset($this->pattern)) {
+      $this->pattern = \Drupal::service('plugin.manager.panels.pattern')->createInstance('default');
+    }
+    return $this->pattern;
+  }
+
+  public function setPattern(PanelsPatternInterface $pattern) {
+    $this->pattern = $pattern;
+    $this->configuration['pattern'] = $pattern->getPluginId();
+    return $this;
+  }
+
+  /**
    * Configures how this Panel is being stored.
    *
    * @param string $type
@@ -323,79 +343,10 @@ class PanelsDisplayVariant extends BlockDisplayVariant {
       ];
     }
 
-    $form['layout'] = [
-      '#title' => $this->t('Layout'),
-      '#type' => 'select',
-      '#options' => Layout::getLayoutOptions(['group_by_category' => TRUE]),
-      '#default_value' => $this->configuration['layout'] ?: NULL,
-    ];
-
-    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
@@ -407,7 +358,7 @@ class PanelsDisplayVariant extends BlockDisplayVariant {
    *   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 = NestedArray::getValue($form, ['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];
   }
@@ -488,6 +439,35 @@ class PanelsDisplayVariant extends BlockDisplayVariant {
   /**
    * {@inheritdoc}
    */
+  public function getWizardOperations($cached_values) {
+    $operations = [];
+    $operations['layout'] = [
+      'title' => $this->t('Layout'),
+      'form' => LayoutPluginSelector::class
+    ];
+    if (!empty($cached_values['layout_change'])) {
+      // @todo check to see if the layout even has settings before adding this step.
+      $operations['settings'] = [
+        'title' => $this->t('Layout Settings'),
+        'form' => LayoutChangeSettings::class,
+      ];
+      $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..5b3094a
--- /dev/null
+++ b/src/Plugin/PanelsPattern/DefaultPattern.php
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panels\PanelsPattern\DefaultPattern.
+ */
+
+namespace Drupal\panels\Plugin\PanelsPattern;
+
+use Drupal\Core\Plugin\PluginBase;
+use Drupal\Core\Url;
+use Drupal\panels\CachedValuesGetterTrait;
+use Drupal\user\SharedTempStoreFactory;
+
+/**
+ * @PanelsPattern("default")
+ */
+class DefaultPattern extends PluginBase implements PanelsPatternInterface {
+
+  use CachedValuesGetterTrait;
+
+  public function getMachineName(array $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'];
+  }
+
+  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']) ? \Drupal::service('ctools.context_mapper')->getContextValues($cached_values['contexts']) : [];
+  }
+
+  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,
+    ]);
+  }
+
+  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,
+    ]);
+  }
+
+  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,
+    ]);
+  }
+
+  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..5bf0922
--- /dev/null
+++ b/src/Plugin/PanelsPattern/PanelsPatternInterface.php
@@ -0,0 +1,72 @@
+<?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 {
+
+  /**
+   * @param array $cached_values
+   *
+   * @return mixed
+   */
+  public function getMachineName(array $cached_values);
+
+  /**
+   * @param \Drupal\user\SharedTempStoreFactory $tempstore
+   * @param string $tempstore_id
+   * @param string $machine_name
+   *
+   * @return \Drupal\Core\Plugin\Context\ContextInterface[]
+   */
+  public function getDefaultContexts(SharedTempStoreFactory $tempstore, $tempstore_id, $machine_name);
+
+  /**
+   * @param string $tempstore_id
+   * @param string $machine_name
+   * @param string $region
+   * @param string $destination
+   *
+   * @return \Drupal\Core\Url
+   */
+  public function getBlockListUrl($tempstore_id, $machine_name, $region = NULL, $destination = NULL);
+
+  /**
+   * @param string $tempstore_id
+   * @param string $machine_name
+   * @param string $block_id
+   * @param string $region
+   * @param string $destination
+   *
+   * @return \Drupal\Core\Url
+   */
+  public function getBlockAddUrl($tempstore_id, $machine_name, $block_id, $region = NULL, $destination = NULL);
+
+  /**
+   * @param string $tempstore_id
+   * @param string $machine_name
+   * @param string $block_id
+   * @param string $destination
+   *
+   * @return \Drupal\Core\Url
+   */
+  public function getBlockEditUrl($tempstore_id, $machine_name, $block_id, $destination = NULL);
+
+  /**
+   * @param string $tempstore_id
+   * @param string $machine_name
+   * @param string $block_id
+   * @param string $destination
+   *
+   * @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..e61a887 100644
--- a/src/Tests/PageManagerPanelsStorageIntegrationTest.php
+++ b/src/Tests/PageManagerPanelsStorageIntegrationTest.php
@@ -49,28 +49,40 @@ 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',
+      'page_variant_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..51da42d 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,17 +81,25 @@ class PanelsTest extends WebTestBase {
     ];
     $this->drupalPostForm(NULL, $edit, 'Add block');
 
+    // Finish the page add wizard.
+    $this->drupalPostForm(NULL, [], 'Finish');
+
+    // Go to the layout step for the variant.
+    $this->drupalGet('admin/structure/page_manager/manage/foo/page_variant__foo-panels_variant-0__layout');
+
     // Check the default value and change a layout setting.
     $this->assertText('Blah');
-    $this->assertFieldByName("variant_settings[layout_settings][setting_1]", "Default");
+    $this->assertFieldByName("layout_settings[setting_1]", "Default");
     $edit = [
-      'variant_settings[layout_settings][setting_1]' => 'Abracadabra',
+      'layout_settings[setting_1]' => 'Abracadabra',
     ];
-    $this->drupalPostForm(NULL, $edit, 'Save');
+
+    // Save page form.
+    $this->drupalPostForm(NULL, $edit, 'Update and 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");
+    $this->drupalGet('admin/structure/page_manager/manage/foo/page_variant__foo-panels_variant-0__layout');
+    $this->assertFieldByName("layout_settings[setting_1]", "Abracadabra");
 
     // View the page and make sure the setting is present.
     $this->drupalGet('testing');
diff --git a/src/Wizard/LayoutChangeWizard.php b/src/Wizard/LayoutChangeWizard.php
new file mode 100644
index 0000000..5bfa245
--- /dev/null
+++ b/src/Wizard/LayoutChangeWizard.php
@@ -0,0 +1,113 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\page_manager_ui\Wizard\PageVariantAddWizard.
+ */
+
+namespace Drupal\panels\Wizard;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection;
+use Drupal\Core\Url;
+use Drupal\ctools\Event\WizardEvent;
+use Drupal\ctools\Wizard\FormWizardBase;
+use Drupal\ctools\Wizard\FormWizardInterface;
+use Drupal\panels\Form\LayoutChangeRegions;
+use Drupal\panels\Form\LayoutChangeSettings;
+
+class LayoutChangeWizard extends FormWizardBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRouteName() {
+    return 'panels.layout.change_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function initValues() {
+    $values = $this->getTempstore()->get($this->getMachineName());
+    $event = new WizardEvent($this, $values);
+    $this->dispatcher->dispatch(FormWizardInterface::LOAD_VALUES, $event);
+    return $event->getValues();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOperations($cached_values) {
+    $operations = [];
+    $operations['settings'] = [
+      'title' => $this->t('Settings'),
+      'form' => LayoutChangeSettings::class,
+    ];
+    $operations['regions'] = [
+      'title' => $this->t('Regions'),
+      'form' => LayoutChangeRegions::class,
+    ];
+
+    return $operations;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function customizeForm(array $form, FormStateInterface $form_state) {
+    $form = parent::customizeForm($form, $form_state);
+
+    // We set the variant id as part of form submission.
+    if ($this->step == 'type' && isset($form['name']['id'])) {
+      unset($form['name']['id']);
+    }
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getNextParameters($cached_values) {
+    $parameters = parent::getNextParameters($cached_values);
+
+    // Add the page to the url parameters.
+    $parameters['tempstore_id'] = $cached_values['layout_change']['tempstore_id'];
+    return $parameters;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPreviousParameters($cached_values) {
+    $parameters = parent::getPreviousParameters($cached_values);
+
+    // Add the page to the url parameters.
+    $parameters['tempstore_id'] = $cached_values['layout_change']['tempstore_id'];
+    return $parameters;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function finish(array &$form, FormStateInterface $form_state) {
+    $cached_values = $form_state->getTemporaryValue('wizard');
+
+    // Add the variant to the parent page tempstore.
+    $page_tempstore = $this->tempstore->get('page_manager.page')->get($cached_values['page']->id());
+    $page_tempstore['page']->addVariant($cached_values['page_variant']);
+    $this->tempstore->get('page_manager.page')->set($cached_values['page']->id(), $page_tempstore);
+
+    $variant_plugin = $cached_values['page_variant']->getVariantPlugin();
+    drupal_set_message($this->t('The %label @entity_type has been added to the page, but has not been saved. Please save the page to store changes.', array(
+      '%label' => $cached_values['page_variant']->label(),
+      '@entity_type' => $variant_plugin->adminLabel(),
+    )));
+
+    $form_state->setRedirectUrl(new Url('entity.page.edit_form', [
+      'machine_name' => $cached_values['page']->id(),
+      'step' => 'general',
+    ]));
+  }
+
+}
