diff --git a/css/panelizer.admin.css b/css/panelizer.admin.css
new file mode 100644
index 0000000..1aa3794
--- /dev/null
+++ b/css/panelizer.admin.css
@@ -0,0 +1,113 @@
+/**
+ * @file
+ * Styles for Panelizer wizard admin.
+ */
+
+/* Narrow screens */
+
+.panelizer-wizard-tree,
+.panelizer-wizard-form {
+  box-sizing: border-box;
+}
+
+/**
+ * Wizard actions across the top.
+ */
+.panelizer-wizard-actions {
+  text-align: right; /* LTR */
+}
+.panelizer-wizard-actions ul.inline,
+.panelizer-wizard-actions ul.inline li {
+  display: inline-block;
+  margin: 0;
+}
+.panelizer-wizard-actions ul.inline {
+  border-top: 1px solid black;
+  border-left: 1px solid black;
+}
+.panelizer-wizard-actions ul.inline li {
+  border-right: 1px solid black;
+  padding: .5em;
+}
+
+/**
+ * The tree of wizard steps.
+ */
+.panelizer-wizard-tree ul {
+  margin: 0;
+  padding: 0;
+  list-style: none;
+}
+.panelizer-wizard-tree ul > li > ul {
+  margin-left: 1em;
+}
+.panelizer-wizard-tree > ul {
+  border: 1px solid black;
+  padding-bottom: .5em;
+  margin-bottom: 20px;
+}
+.panelizer-wizard-tree li {
+  border-bottom: 1px solid black;
+  padding: .5em;
+  padding-right: 0;
+}
+.panelizer-wizard-tree li:last-child {
+  border-bottom: 0;
+  padding-bottom: 0;
+}
+
+/**
+ * The wizard form.
+ */
+.panelizer-wizard-form {
+  border: 1px solid black;
+  padding: 1em;
+  margin-bottom: 20px;
+}
+
+/* Wide screens */
+@media
+  screen and (min-width: 780px),
+  (orientation: landscape) and (min-device-height: 780px) {
+
+  /**
+   * Overall layout.
+   */
+  .panelizer-wizard-tree {
+    float: left; /* LTR */
+    width: 20%;
+  }
+  .panelizer-wizard-form {
+    float: left; /* LTR */
+    width: 80%;
+  }
+  .panelizer-wizard-form-actions {
+    margin-left: 20%; /* LTR */
+  }
+
+  /**
+   * Make the borders look nice.
+   */
+  .panelizer-wizard-tree > ul {
+    border-right: 0; /* LTR */
+  }
+  .panelizer-wizard-form {
+    min-height: 400px;
+  }
+
+  /**
+   * Right-to-left support.
+   */
+  [dir="rtl"] .panelizer-wizard-tree,
+  [dir="rtl"] .panelizer-wizard-form {
+    float: right;
+  }
+  [dir="rtl"] .panelizer-wizard-form-actions {
+    margin-left: 0;
+    margin-right: 20%;
+  }
+  [dir="rtl"] .panelizer-wizard-tree > ul {
+    border-right: 1px solid black;
+    border-left: 0;
+  }
+}
diff --git a/panelizer.libraries.yml b/panelizer.libraries.yml
index 6e9a5b2..50b9c76 100644
--- a/panelizer.libraries.yml
+++ b/panelizer.libraries.yml
@@ -17,4 +17,8 @@ panels_ipe:
       css/panels_ipe.css: {}
   dependencies:
     - panels_ipe/panels_ipe
-
+wizard_admin:
+  version: VERSION
+  css:
+    layout:
+      css/panelizer.admin.css: {}
diff --git a/panelizer.module b/panelizer.module
index 2672b1b..97e28f6 100644
--- a/panelizer.module
+++ b/panelizer.module
@@ -4,10 +4,11 @@
  * Hook implementations for the Panelizer module.
  */
 
-use \Drupal\Core\Entity\FieldableEntityInterface;
-use \Drupal\Core\Entity\RevisionableInterface;
-use \Drupal\Core\Form\FormStateInterface;
-use \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Entity\RevisionableInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant;
 
 /**
  * Implements hook_theme().
@@ -17,10 +18,62 @@ function panelizer_theme() {
     'panelizer_view_mode' => [
       'render element' => 'element',
     ],
+    'panelizer_wizard_form' => [
+      'render element' => 'form',
+    ],
+    'panelizer_wizard_tree' => [
+      'variables' => [
+        'wizard' => NULL,
+        'cached_values' => [],
+        'tree' => [],
+        'divider' => ' » ',
+        'step' => NULL,
+      ],
+    ],
   ];
 }
 
 /**
+ * Preprocess function for panelizer-wizard-tree.html.twig.
+ */
+function template_preprocess_panelizer_wizard_tree(&$variables) {
+  /** @var $wizard \Drupal\ctools\Wizard\FormWizardInterface|\Drupal\ctools\Wizard\EntityFormWizardInterface */
+  $wizard = $variables['wizard'];
+  $cached_values = $variables['cached_values'];
+  $tree = $variables['tree'];
+  $variables['step'] = $wizard->getStep($cached_values);
+
+  foreach ($wizard->getOperations($cached_values) as $step => $operation) {
+    $parameters = $wizard->getNextParameters($cached_values);
+    // Override step to be the step we want.
+    $parameters['step'] = $step;
+
+    // Fill in parents if there are breadcrumbs.
+    $parent =& $tree;
+    if (isset($operation['breadcrumbs'])) {
+      foreach ($operation['breadcrumbs'] as $breadcrumb) {
+        $breadcrumb_string = (string) $breadcrumb;
+        if (!isset($parent[$breadcrumb_string])) {
+          $parent[$breadcrumb_string] = [
+            'title' => $breadcrumb,
+            'children' => [],
+          ];
+        }
+        $parent =& $parent[$breadcrumb_string]['children'];
+      }
+    }
+
+    $parent[$step] = [
+      'title' => !empty($operation['title']) ? $operation['title'] : '',
+      'url' => new \Drupal\Core\Url($wizard->getRouteName(), $parameters),
+      'step' => $step,
+    ];
+  }
+
+  $variables['tree'] = $tree;
+}
+
+/**
  * Implements hook_entity_type_alter().
  */
 function panelizer_entity_type_alter(array &$entity_types) {
@@ -162,9 +215,11 @@ function panelizer_form_entity_view_display_edit_form_alter(&$form, FormStateInt
       '#tree' => TRUE,
     ];
     $form['panelizer']['enable'] = [
-      '#type' => 'checkbox',
       '#title' => t('Panelize this view mode'),
-      '#default_value' => !empty($settings['enable']),
+      '#type' => 'link',
+      '#url' => Url::fromRoute('panelizer.wizard.add', [
+        'machine_name' => $display->getTargetEntityTypeId() . '__' . $display->getTargetBundle() . '__' . $display->getMode(),
+      ]),
     ];
     $form['panelizer']['custom'] = [
       '#type' => 'checkbox',
diff --git a/panelizer.routing.yml b/panelizer.routing.yml
index feac328..2bda08e 100644
--- a/panelizer.routing.yml
+++ b/panelizer.routing.yml
@@ -12,3 +12,97 @@ panelizer.panels_ipe.revert_to_default:
     _method: 'POST'
     _permission: 'access panels in-place editing'
     _custom_access: '\Drupal\panelizer\Controller\PanelizerPanelsIPEController::accessRevertToDefault'
+
+# Wizard
+panelizer.wizard.add:
+  path: '/admin/structure/panelizer/add/{machine_name}'
+  defaults:
+    _wizard: '\Drupal\panelizer\Wizard\PanelizerWizard'
+    _title: 'Panelizer Wizard'
+    tempstore_id: 'panelizer.wizard'
+  requirements:
+    _access: 'TRUE'
+
+panelizer.wizard.add.step:
+  path: '/admin/structure/panelizer/add/{machine_name}/{step}'
+  defaults:
+    _wizard: '\Drupal\panelizer\Wizard\PanelizerWizard'
+    _title: 'Panelizer Wizard'
+    tempstore_id: 'panelizer.wizard'
+  requirements:
+    _access: 'TRUE'
+
+panelizer.wizard.edit:
+  path: '/admin/structure/panelizer/edit/{machine_name}/{step}'
+  defaults:
+    _wizard: '\Drupal\panelizer\Wizard\PanelizerEditWizard'
+    _title: 'Panelizer Wizard'
+    tempstore_id: 'panelizer.wizard'
+    step: 'general'
+  requirements:
+    _permission: 'administer pages'
+
+# Contexts
+panelizer.wizard.step.context.add:
+  path: '/admin/panelizer/wizard/{machine_name}/contexts/add/{context_id}'
+  defaults:
+    _form: '\Drupal\panelizer\Form\PanelizerWizardContextConfigure'
+    _title: 'Add custom context'
+    tempstore_id: 'panelizer.wizard'
+  requirements:
+    _permission: 'administer pages'
+
+panelizer.wizard.step.context.edit:
+  path: '/admin/panelizer/wizard/{machine_name}/contexts/edit/{context_id}'
+  defaults:
+    _form: '\Drupal\panelizer\Form\PanelizerWizardContextConfigure'
+    _title: 'Edit context'
+    tempstore_id: 'panelizer.wizard'
+  requirements:
+    _permission: 'administer pages'
+
+panelizer.wizard.step.context.delete:
+  path: '/admin/panelizer/wizard/{machine_name}/context/delete/{context_id}'
+  defaults:
+    _form: '\Drupal\panelizer\Form\PanelizerWizardContextDeleteForm'
+    _title: 'Delete static context'
+    tempstore_id: 'panelizer.wizard'
+  requirements:
+    _permission: 'administer pages'
+
+# Content blocks
+panelizer.wizard.step.content.select_block:
+  path: '/admin/panelizer/wizard/block_display/{block_display}/select'
+  defaults:
+    _controller: '\Drupal\panelizer\Controller\PanelizerWizardController::selectBlock'
+    _title: 'Select block'
+    tempstore_id: 'panelizer.block_display'
+  requirements:
+    _permission: 'administer pages'
+
+panelizer.wizard.step.content.add_block:
+  path: '/admin/panelizer/wizard/block_display/{block_display}/add/{block_id}'
+  defaults:
+    _form: '\Drupal\panelizer\Form\PanelizerWizardAddBlockForm'
+    _title: 'Add block'
+    tempstore_id: 'panelizer.block_display'
+  requirements:
+    _permission: 'administer pages'
+
+panelizer.wizard.step.content.edit_block:
+  path: '/admin/panelizer/wizard/block_display/{block_display}/edit/{block_id}'
+  defaults:
+    _form: '\Drupal\panelizer\Form\PanelizerWizardEditBlockForm'
+    _title: 'Edit block'
+    tempstore_id: 'panelizer.block_display'
+  requirements:
+    _permission: 'administer pages'
+
+panelizer.wizard.step.content.delete_block:
+  path: '/admin/panelizer/wizard/block_display/{block_display}/delete/{block_id}'
+  defaults:
+    _form: '\Drupal\panelizer\Form\PanelizerWizardDeleteBlockForm'
+    _title: 'Delete block'
+    tempstore_id: 'panelizer.block_display'
+  requirements:
+    _permission: 'administer pages'
diff --git a/src/Controller/PanelizerWizardController.php b/src/Controller/PanelizerWizardController.php
new file mode 100644
index 0000000..3f6364d
--- /dev/null
+++ b/src/Controller/PanelizerWizardController.php
@@ -0,0 +1,167 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panelizer\Controller\PanelizerWizardController.
+ */
+
+namespace Drupal\panelizer\Controller;
+
+use Drupal\Component\Plugin\PluginManagerInterface;
+use Drupal\Core\Block\BlockManagerInterface;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Plugin\Context\Context;
+use Drupal\Core\Plugin\Context\ContextHandlerInterface;
+use Drupal\Core\Url;
+use Drupal\ctools\Form\AjaxFormTrait;
+use Drupal\user\SharedTempStoreFactory;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Provides route controllers for the Panelizer wizard.
+ */
+class PanelizerWizardController extends ControllerBase {
+
+  use AjaxFormTrait;
+
+  /**
+   * 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')
+    );
+  }
+
+  /**
+   * Presents a list of blocks to add to the variant.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
+   * @param string $block_display
+   *   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, $block_display, $tempstore_id) {
+    $cached_values = $this->tempstore->get($tempstore_id)->get($block_display);
+    /** @var \Drupal\page_manager\Plugin\DisplayVariant\PageBlockDisplayVariant $variant_plugin */
+    $variant_plugin = $cached_values['plugin'];
+
+    // Rehydrate the contexts on this end.
+    $contexts = [];
+    /**
+     * @var string $context_name
+     * @var \Drupal\Core\Plugin\Context\ContextDefinitionInterface $context_definition
+     */
+    foreach ($cached_values['contexts'] as $context_name => $context_definition) {
+      $contexts[$context_name] = new Context($context_definition);
+    }
+    $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' => Url::fromRoute('panelizer.wizard.step.content.add_block', [
+          'block_display' => $block_display,
+          'block_id' => $plugin_id,
+          'region' => $request->query->get('region'),
+          'destination' => $request->query->get('destination'),
+        ]),
+        'attributes' => $this->getAjaxAttributes(),
+      ];
+    }
+    return $build;
+  }
+
+}
diff --git a/src/Form/PanelizerWizardAddBlockForm.php b/src/Form/PanelizerWizardAddBlockForm.php
new file mode 100644
index 0000000..1d173c7
--- /dev/null
+++ b/src/Form/PanelizerWizardAddBlockForm.php
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panelizer\Form\PanelizerWizardAddBlockForm.
+ */
+
+namespace Drupal\panelizer\Form;
+
+use Drupal\Component\Plugin\PluginManagerInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\page_manager\PageVariantInterface;
+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 PanelizerWizardAddBlockForm extends PanelizerWizardConfigureBlockFormBase   {
+
+  /**
+   * 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 'panelizer_wizard_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, $block_display = NULL, $block_id = NULL) {
+    $form = parent::buildForm($form, $form_state, $block_display, $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/PanelizerWizardConfigureBlockFormBase.php b/src/Form/PanelizerWizardConfigureBlockFormBase.php
new file mode 100644
index 0000000..d442127
--- /dev/null
+++ b/src/Form/PanelizerWizardConfigureBlockFormBase.php
@@ -0,0 +1,199 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panelizer\Form\PanelizerWizardConfigureBlockFormBase.
+ */
+
+namespace Drupal\panelizer\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\ContextAwarePluginAssignmentTrait;
+use Drupal\Core\Plugin\ContextAwarePluginInterface;
+use Drupal\page_manager\PageVariantInterface;
+use Drupal\user\SharedTempStoreFactory;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a base form for configuring a block as part of a variant.
+ */
+abstract class PanelizerWizardConfigureBlockFormBase extends FormBase {
+
+  use ContextAwarePluginAssignmentTrait;
+
+  /**
+   * Tempstore factory.
+   *
+   * @var \Drupal\user\SharedTempStoreFactory
+   */
+  protected $tempstore;
+
+  /**
+   * The variant plugin.
+   *
+   * @var \Drupal\page_manager\Plugin\DisplayVariant\PageBlockDisplayVariant
+   */
+  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 'panelizer.block_display';
+  }
+
+  /**
+   * 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, $block_display = NULL, $block_id = NULL) {
+    $cached_values = $this->tempstore->get($this->getTempstoreId())->get($block_display);
+    /** @var \Drupal\page_manager\Plugin\DisplayVariant\PageBlockDisplayVariant $variant_plugin */
+    $this->variantPlugin = $cached_values['plugin'];
+
+    // Rehydrate the contexts on this end.
+    $contexts = [];
+    /**
+     * @var string $context_name
+     * @var \Drupal\Core\Plugin\Context\ContextDefinitionInterface $context_definition
+     */
+    foreach ($cached_values['contexts'] as $context_name => $context_definition) {
+      $contexts[$context_name] = new Context($context_definition);
+    }
+    $this->variantPlugin->setContexts($contexts);
+
+    $this->block = $this->prepareBlock($block_id);
+    $form_state->set('variant_id', $this->getVariantPlugin()->id());
+    $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,
+    ];
+
+    if ($this->block instanceof ContextAwarePluginInterface) {
+      $form['context_mapping'] = $this->addContextAssignmentElement($this->block, $this->getVariantPlugin()->getContexts());
+    }
+
+    $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($form_state->getValue('context_mapping', []));
+    }
+
+    $this->getVariantPlugin()->updateBlock($this->block->getConfiguration()['uuid'], ['region' => $form_state->getValue('region')]);
+
+    $cached_values = $this->getTempstore()->get($form_state->get('variant_id'));
+    $cached_values['plugin'] = $this->getVariantPlugin();
+    $this->getTempstore()->set($form_state->get('variant_id'), $cached_values);
+  }
+
+  /**
+   * Gets the variant plugin for this page variant entity.
+   *
+   * @return \Drupal\ctools\Plugin\BlockVariantInterface
+   */
+  protected function getVariantPlugin() {
+    return $this->variantPlugin;
+  }
+
+}
diff --git a/src/Form/PanelizerWizardContentForm.php b/src/Form/PanelizerWizardContentForm.php
new file mode 100644
index 0000000..e16c553
--- /dev/null
+++ b/src/Form/PanelizerWizardContentForm.php
@@ -0,0 +1,257 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\panelizer\Form\PanelizerWizardContentForm.
+ */
+
+namespace Drupal\panelizer\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 variant's content.
+ */
+class PanelizerWizardContentForm extends FormBase {
+
+  use AjaxFormTrait;
+
+  /**
+   * Tempstore factory.
+   *
+   * @var \Drupal\user\SharedTempStoreFactory
+   */
+  protected $tempstore;
+
+  /**
+   * 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 'panelizer.block_display';
+  }
+
+  /**
+   * Get the tempstore.
+   *
+   * @return \Drupal\user\SharedTempStore
+   */
+  protected function getTempstore() {
+    return $this->tempstore->get($this->getTempstoreId());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'panelizer_wizard_block_page_content';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(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'];
+
+    // Store the block display plugin so we can get it in our dialogs.
+    if (!empty($this->getTempstore()->get($variant_plugin->id())['plugin'])) {
+      $variant_plugin->setConfiguration($this->getTempstore()->get($variant_plugin->id())['plugin']->getConfiguration());
+      $form_state->setTemporaryValue('wizard', $cached_values);
+    }
+    $context_definitions = [];
+    foreach ($variant_plugin->getContexts() as $context_name => $context) {
+      $context_definitions[$context_name] = $context->getContextDefinition();
+    }
+    $this->getTempstore()->set($variant_plugin->id(), [
+      'plugin' => $variant_plugin,
+      'access' => $cached_values['access'],
+      'contexts' => $context_definitions,
+    ]);
+
+    // 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' => Url::fromRoute('panelizer.wizard.step.content.select_block', [
+          'block_display' => $variant_plugin->id(),
+          'destination' => $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'),
+        ],
+        '#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] = [
+          '#attributes' => [
+            'class' => ['region-title', 'region-title-' . $region],
+            'no_striping' => TRUE,
+          ],
+        ];
+        $form['blocks'][$region]['title'] = [
+          '#markup' => $variant_plugin->getRegionName($region),
+          '#wrapper_attributes' => [
+            'colspan' => 5,
+          ],
+        ];
+        $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' => 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' => Url::fromRoute('panelizer.wizard.step.content.edit_block', [
+              'block_display' => $variant_plugin->id(),
+              'block_id' => $block_id,
+              'destination' => $this->getRequest()->getRequestUri(),
+            ]),
+            'attributes' => $attributes,
+          ];
+          $operations['delete'] = [
+            'title' => $this->t('Delete'),
+            'url' => Url::fromRoute('panelizer.wizard.step.content.delete_block', [
+              'block_display' => $variant_plugin->id(),
+              'block_id' => $block_id,
+              'destination' => $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);
+      }
+    }
+
+    // Remove from the tempstore so we refresh from the database the next time
+    // we come here.
+    $this->getTempstore()->delete($variant_plugin->id());
+  }
+
+}
diff --git a/src/Form/PanelizerWizardContextConfigure.php b/src/Form/PanelizerWizardContextConfigure.php
new file mode 100644
index 0000000..d171fff
--- /dev/null
+++ b/src/Form/PanelizerWizardContextConfigure.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panelizer\Form\PanelizerWizardContextConfigure.
+ */
+
+namespace Drupal\panelizer\Form;
+
+use Drupal\Core\Plugin\Context\ContextInterface;
+use Drupal\ctools\Form\ContextConfigure;
+
+class PanelizerWizardContextConfigure extends ContextConfigure {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getParentRouteInfo($cached_values) {
+    return ['panelizer.wizard.add.step', [
+      'machine_name' => $cached_values['id'],
+      'step' => 'contexts',
+    ]];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getContexts($cached_values) {
+    $static_contexts = $cached_values['page_variant']->getStaticContexts();
+    return $this->getContextMapper()->getContextValues($static_contexts);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function addContext($cached_values, $context_id, ContextInterface $context) {
+    $context_config = [
+      'label' => $context->getContextDefinition()->getLabel(),
+      'type' => $context->getContextDefinition()->getDataType(),
+      'description' => $context->getContextDefinition()->getDescription(),
+      'value' => strpos($context->getContextDefinition()->getDataType(), 'entity:') === 0 ? $context->getContextValue()->uuid() : $context->getContextValue(),
+    ];
+    $cached_values['page_variant']->setStaticContext($context_id, $context_config);
+    return $cached_values;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function contextExists($value, $element, $form_state) {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function disableMachineName($cached_values, $machine_name) {
+    if ($machine_name) {
+      return !empty($cached_values['page_variant']->getStaticContext($machine_name));
+    }
+    return FALSE;
+  }
+
+  /**
+   * Wraps the context mapper.
+   *
+   * @return \Drupal\page_manager\ContextMapperInterface
+   */
+  protected function getContextMapper() {
+    return \Drupal::service('page_manager.context_mapper');
+  }
+
+}
diff --git a/src/Form/PanelizerWizardContextDeleteForm.php b/src/Form/PanelizerWizardContextDeleteForm.php
new file mode 100644
index 0000000..ec4f3b5
--- /dev/null
+++ b/src/Form/PanelizerWizardContextDeleteForm.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panelizer\Form\PanelizerWizardContextDeleteForm.
+ */
+
+namespace Drupal\panelizer\Form;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\ctools\Form\ContextDelete;
+
+/**
+ * Provides a form for deleting a context.
+ */
+class PanelizerWizardContextDeleteForm extends ContextDelete {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'panelizer_wizard_context_delete_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    $cached_values = $this->getTempstore();
+    $context = $cached_values['page_variant']->getStaticContext($this->context_id);
+    return $this->t('Are you sure you want to delete the context @label?', ['@label' => $context['label']]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelUrl() {
+    $cached_values = $this->getTempstore();
+
+    return new Url('panelizer.wizard.add.step', [
+      'machine_name' => $cached_values['id'],
+      'step' => 'contexts',
+    ]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $cached_values = $this->getTempstore();
+    $context = $cached_values['page_variant']->getStaticContext($this->context_id);
+    drupal_set_message($this->t('The static context %label has been removed.', ['%label' => $context['label']]));
+    $cached_values['page_variant']->removeStaticContext($this->context_id);
+    $this->setTempstore($cached_values);
+    parent::submitForm($form, $form_state);
+  }
+
+}
diff --git a/src/Form/PanelizerWizardContextForm.php b/src/Form/PanelizerWizardContextForm.php
new file mode 100644
index 0000000..40857d4
--- /dev/null
+++ b/src/Form/PanelizerWizardContextForm.php
@@ -0,0 +1,127 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panelizer\Form\PanelizerWizardContextForm.
+ */
+
+namespace Drupal\panelizer\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\ctools\Form\ManageContext;
+use Drupal\panelizer\Form\PanelizerWizardContextConfigure;
+
+/**
+ * Simple wizard step form.
+ */
+class PanelizerWizardContextForm extends ManageContext {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $relationships = FALSE;
+
+  /**
+   * Returns a unique string identifying the form.
+   *
+   * @return string
+   *   The unique string identifying the form.
+   */
+  public function getFormId() {
+    return 'panelizer_wizard_context_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getContextClass($cached_values) {
+    return PanelizerWizardContextConfigure::class;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getRelationshipClass($cached_values) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getContextAddRoute($cached_values) {
+    return 'panelizer.wizard.step.context.add';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getRelationshipAddRoute($cached_values) {
+    return 'panelizer.wizard.step.context.add';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getContexts($cached_values) {
+    return $this->getContextMapper()->getContextValues($cached_values['page_variant']->getStaticContexts());
+  }
+
+  /**
+   * Wraps the context mapper.
+   *
+   * @return \Drupal\page_manager\ContextMapperInterface
+   */
+  protected function getContextMapper() {
+    return \Drupal::service('page_manager.context_mapper');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getTempstoreId() {
+    return 'panelizer.wizard';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getContextOperationsRouteInfo($cached_values, $machine_name, $row) {
+    return ['panelizer.wizard.step.context', [
+      'machine_name' => $machine_name,
+      'context_id' => $row,
+    ]];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getRelationshipOperationsRouteInfo($cached_values, $machine_name, $row) {
+    return ['panelizer.wizard.step.context', [
+      'machine_name' => $machine_name,
+      'context_id' => $row,
+    ]];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function isEditableContext($cached_values, $row) {
+    $context = $cached_values['page_variant']->getStaticContext($row);
+    return !empty($context['value']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addContext(array &$form, FormStateInterface $form_state) {
+    $cached_values = $form_state->getTemporaryValue('wizard');
+    $context = $form_state->getValue('context');
+    $content = $this->formBuilder->getForm($this->getContextClass($cached_values), $context, $this->getTempstoreId(), $this->machine_name);
+    $content['#attached']['library'][] = 'core/drupal.dialog.ajax';
+    list(, $route_parameters) = $this->getContextOperationsRouteInfo($cached_values, $this->machine_name, $context);
+    $content['submit']['#attached']['drupalSettings']['ajax'][$content['submit']['#id']]['url'] = $this->url($this->getContextAddRoute($cached_values), $route_parameters, ['query' => [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]]);
+    $response = new AjaxResponse();
+    $response->addCommand(new OpenModalDialogCommand($this->t('Add new context'), $content, array('width' => '700')));
+    return $response;
+  }
+
+}
diff --git a/src/Form/PanelizerWizardDeleteBlockForm.php b/src/Form/PanelizerWizardDeleteBlockForm.php
new file mode 100644
index 0000000..e5bf70d
--- /dev/null
+++ b/src/Form/PanelizerWizardDeleteBlockForm.php
@@ -0,0 +1,101 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panelizer\Form\PanelizerWizardDeleteBlockForm.
+ */
+
+namespace Drupal\panelizer\Form;
+
+use Drupal\Core\Form\ConfirmFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\page_manager\PageVariantInterface;
+
+/**
+ * Provides a form for deleting an access condition.
+ */
+class PanelizerWizardDeleteBlockForm extends ConfirmFormBase {
+
+  /**
+   * @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 'panelizer.block_display';
+  }
+
+  /**
+   * Get the tempstore.
+   *
+   * @return \Drupal\user\SharedTempStore
+   */
+  protected function getTempstore() {
+    return \Drupal::service('user.shared_tempstore')->get($this->getTempstoreId());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'panelizer_wizard_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, $block_display = NULL, $block_id = NULL) {
+    $this->plugin = $this->getTempstore()->get($block_display)['plugin'];
+    $this->block = $this->plugin->getBlock($block_id);
+    $form['block_display'] = [
+      '#type' => 'value',
+      '#value' => $block_display
+    ];
+    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->getTempstore()->get($form_state->getValue('block_display'));
+    $cached_values['plugin'] = $this->plugin;
+    $this->getTempstore()->set($form_state->getValue('block_display'), $cached_values);
+    drupal_set_message($this->t('The block %label has been removed.', ['%label' => $this->block->label()]));
+  }
+
+}
diff --git a/src/Form/PanelizerWizardEditBlockForm.php b/src/Form/PanelizerWizardEditBlockForm.php
new file mode 100644
index 0000000..ddf9366
--- /dev/null
+++ b/src/Form/PanelizerWizardEditBlockForm.php
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panelizer\Form\PanelizerWizardEditBlockForm.
+ */
+
+namespace Drupal\panelizer\Form;
+
+/**
+ * Provides a form for editing a block plugin of a variant.
+ */
+class PanelizerWizardEditBlockForm extends PanelizerWizardConfigureBlockFormBase  {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'panelizer_wizard_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/Form/PanelizerWizardGeneralForm.php b/src/Form/PanelizerWizardGeneralForm.php
new file mode 100644
index 0000000..49ebd59
--- /dev/null
+++ b/src/Form/PanelizerWizardGeneralForm.php
@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panelizer\Form\PanelizerWizardGeneralForm.
+ */
+
+namespace Drupal\panelizer\Form;
+
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * General settings for a panelized bundle.
+ */
+class PanelizerWizardGeneralForm extends FormBase {
+
+  /**
+   * The SharedTempStore key for our current wizard values.
+   *
+   * @var string|NULL
+   */
+  protected $machine_name;
+
+  /**
+   * Returns a unique string identifying the form.
+   *
+   * @return string
+   *   The unique string identifying the form.
+   */
+  public function getFormId() {
+    return 'panelizer_wizard_general_form';
+  }
+
+  /**
+   * Form constructor.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return array
+   *   The form structure.
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $machine_name = NULL) {
+    $cached_values = $form_state->getTemporaryValue('wizard');
+
+    $form['custom'] = [
+      '#type' => 'checkbox',
+      '#title' => t('Allow custom overrides of each entity'),
+      '#default_value' => !empty($cached_values['custom']) ? $cached_values['custom'] : FALSE,
+    ];
+
+    return $form;
+  }
+
+  /**
+   * Form submission handler.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $keys = array(
+      'custom',
+    );
+    $cached_values = $form_state->getTemporaryValue('wizard');
+    foreach ($keys as $key) {
+      $cached_values[$key] = $form_state->getValue($key);
+    }
+    $form_state->setTemporaryValue('wizard', $cached_values);
+  }
+
+}
diff --git a/src/Panelizer.php b/src/Panelizer.php
index 864191d..1a8c964 100644
--- a/src/Panelizer.php
+++ b/src/Panelizer.php
@@ -147,7 +147,7 @@ class Panelizer implements PanelizerInterface {
    * @return \Drupal\Core\Entity\Display\EntityViewDisplayInterface|NULL
    *   The entity view display if one exists; NULL otherwise.
    */
-  protected function getEntityViewDisplay($entity_type_id, $bundle, $view_mode) {
+  public function getEntityViewDisplay($entity_type_id, $bundle, $view_mode) {
     // Check the existence and status of:
     // - the display for the view mode,
     // - the 'default' display.
@@ -564,4 +564,4 @@ class Panelizer implements PanelizerInterface {
     return $this->hasOperationPermission($op, $entity_type_id, $bundle, $account);
   }
 
-}
\ No newline at end of file
+}
diff --git a/src/Wizard/PanelizerEditWizard.php b/src/Wizard/PanelizerEditWizard.php
new file mode 100644
index 0000000..491bc34
--- /dev/null
+++ b/src/Wizard/PanelizerEditWizard.php
@@ -0,0 +1,326 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\panelizer\Wizard\PanelizerEditWizard.
+ */
+
+namespace Drupal\panelizer\Wizard;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Display\ContextAwareVariantInterface;
+use Drupal\Core\Form\FormBuilderInterface;
+use Drupal\Core\Form\FormInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\ctools\Event\WizardEvent;
+use Drupal\ctools\Plugin\PluginWizardInterface;
+use Drupal\ctools\Wizard\FormWizardBase;
+use Drupal\ctools\Wizard\FormWizardInterface;
+use Drupal\page_manager\PageInterface;
+use Drupal\page_manager\PageVariantInterface;
+use Drupal\panelizer\Form\PanelizerWizardContentForm;
+use Drupal\panelizer\Form\PanelizerWizardContextForm;
+use Drupal\panelizer\Form\PanelizerWizardGeneralForm;
+
+class PanelizerEditWizard extends FormWizardBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $cached_values = $form_state->getTemporaryValue('wizard');
+    // Load data in to values to be cached and managed by the
+    // wizard until the user clicks on Save or Cancel.
+    $cached_values['id'] = $this->getMachineName();
+    $cached_values['access'] = [];
+
+    if (empty($cached_values['page_variant'])) {
+      // Load the page variant.
+      $page_variant = \Drupal::entityManager()
+        ->getStorage('page_variant')
+        ->load($cached_values['id'] . '__variant');
+
+      // Add a dummy page entity to the page variant. This is needed so the
+      // variant can be saved.
+      $page = \Drupal::entityManager()
+        ->getStorage('page')
+        ->create([
+          'id' => $cached_values['id'] . '__page',
+        ]);
+      $page_variant->setPageEntity($page);
+      $cached_values['page_variant'] = $page_variant;
+
+      // Load the panels display variant.
+      $panelizer = \Drupal::service('panelizer');
+      list($entity_type, $bundle, $display_id) = explode('__', $this->getMachineName());
+      $variant_plugin = $panelizer->getDefaultPanelsDisplay($display_id, $entity_type, $bundle, $display_id);
+      $cached_values['plugin'] = $variant_plugin;
+
+      // Load general settings.
+      $settings = $panelizer->getPanelizerSettings($entity_type, $bundle, $display_id);
+      $cached_values['custom'] = $settings['custom'];
+    }
+
+    $form_state->setTemporaryValue('wizard', $cached_values);
+    $form = parent::buildForm($form, $form_state);
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getWizardLabel() {
+    return $this->t('Wizard Information');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMachineLabel() {
+    return $this->t('Wizard Test Name');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOperations($cached_values) {
+    $operations = [
+      'general' => [
+        'form' => PanelizerWizardGeneralForm::class,
+        'title' => $this->t('General settings'),
+      ],
+      'contexts' => [
+        'form' => PanelizerWizardContextForm::class,
+        'title' => $this->t('Contexts'),
+      ],
+    ];
+
+    // Add any wizard operations from the plugin itself.
+    if (isset($cached_values['page_variant'])) {
+      $page_variant = $cached_values['page_variant'];
+      $variant_plugin = $page_variant->getVariantPlugin();
+      $variant_plugin->setContexts($this->getContexts($cached_values));
+      foreach ($variant_plugin->getWizardOperations($cached_values) as $name => $operation) {
+        $operation['values']['plugin'] = $variant_plugin;
+        $operation['submit'][] = '::submitVariantStep';
+        $operations[$name] = $operation;
+      }
+
+      // Change the class that manages the Content step.
+      if (isset($operations['content'])) {
+        $operations['content']['form'] = PanelizerWizardContentForm::class;
+      }
+    }
+
+    return $operations;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRouteName() {
+    return 'panelizer.wizard.edit';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function customizeForm(array $form, FormStateInterface $form_state) {
+    $cached_values = $form_state->getTemporaryValue('wizard');
+
+    // The page actions.
+    $form['wizard_actions'] = [
+      '#theme' => 'links',
+      '#links' => [],
+      '#attributes' => [
+        'class' => ['inline'],
+      ]
+    ];
+
+    // The tree of wizard steps.
+    $form['wizard_tree'] = [
+      '#theme' => ['panelizer_wizard_tree'],
+      '#wizard' => $this,
+      '#cached_values' => $form_state->getTemporaryValue('wizard'),
+    ];
+
+    $form['#theme'] = 'panelizer_wizard_form';
+    $form['#attached']['library'][] = 'panelizer/wizard_admin';
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function actions(FormInterface $form_object, FormStateInterface $form_state) {
+    $cached_values = $form_state->getTemporaryValue('wizard');
+    $operation = $this->getOperation($cached_values);
+
+    $actions = [];
+
+    $actions['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Update'),
+      '#validate' => [
+        '::populateCachedValues',
+        [$form_object, 'validateForm'],
+      ],
+      '#submit' => [
+        [$form_object, 'submitForm'],
+      ],
+    ];
+
+    $actions['update_and_save'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Update and save'),
+      '#button_type' => 'primary',
+      '#validate' => [
+        '::populateCachedValues',
+        [$form_object, 'validateForm'],
+      ],
+      '#submit' => [
+        [$form_object, 'submitForm'],
+      ],
+    ];
+
+    $actions['finish'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Save'),
+      '#validate' => [
+        '::populateCachedValues',
+        [$form_object, 'validateForm'],
+      ],
+      '#submit' => [
+        [$form_object, 'submitForm'],
+      ],
+    ];
+
+    $actions['cancel'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Cancel'),
+      '#submit' => [
+        '::clearTempstore'
+      ],
+    ];
+
+    // Add any submit or validate functions for the step and the global ones.
+    foreach (['submit', 'update_and_save', 'finish'] as $button) {
+      if (isset($operation['validate'])) {
+        $actions[$button]['#validate'] = array_merge($actions[$button]['#validate'], $operation['validate']);
+      }
+      $actions[$button]['#validate'][] = '::validateForm';
+      if (isset($operation['submit'])) {
+        $actions[$button]['#submit'] = array_merge($actions[$button]['#submit'], $operation['submit']);
+      }
+      $actions[$button]['#submit'][] = '::submitForm';
+    }
+    $actions['update_and_save']['#submit'][] = '::finish';
+    $actions['finish']['#submit'][] = '::finish';
+
+    if ($form_state->get('ajax')) {
+      $cached_values = $form_state->getTemporaryValue('wizard');
+      $ajax_parameters = $this->getNextParameters($cached_values);
+      $ajax_parameters['step'] = $this->getStep($cached_values);
+      $actions['submit']['#ajax'] = [
+        'callback' => '::ajaxSubmit',
+        'url' => Url::fromRoute($this->getRouteName(), $ajax_parameters),
+        'options' => ['query' => \Drupal::request()->query->all() + [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]],
+      ];
+      $actions['update_and_save']['#ajax'] = [
+        'callback' => '::ajaxFinish',
+        'url' => Url::fromRoute($this->getRouteName(), $ajax_parameters),
+        'options' => ['query' => \Drupal::request()->query->all() + [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]],
+      ];
+      $actions['finish']['#ajax'] = [
+        'callback' => '::ajaxFinish',
+        'url' => Url::fromRoute($this->getRouteName(), $ajax_parameters),
+        'options' => ['query' => \Drupal::request()->query->all() + [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]],
+      ];
+    }
+
+    return $actions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $cached_values = $form_state->getTemporaryValue('wizard');
+    // Normally, the wizard only saves the data when the 'Next' button is
+    // clicked, but we want to save the data always when editing.
+    $this->getTempstore()->set($this->getMachineName(), $cached_values);
+  }
+
+  /**
+   * Submission callback for the variant plugin steps.
+   */
+  public function submitVariantStep(array &$form, FormStateInterface $form_state) {
+    $cached_values = $form_state->getTemporaryValue('wizard');
+    /** @var \Drupal\page_manager\PageVariantInterface $page_variant */
+    $page_variant = $cached_values['page_variant'];
+    /** @var \Drupal\Core\Display\VariantInterface $plugin */
+    $plugin = $cached_values['plugin'];
+
+    // Make sure the variant plugin on the page variant gets the configuration
+    // from the 'plugin' which should have been setup by the variant's steps.
+    if (!empty($plugin) && !empty($page_variant)) {
+      $page_variant->getVariantPlugin()->setConfiguration($plugin->getConfiguration());
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function finish(array &$form, FormStateInterface $form_state) {
+    $cached_values = $form_state->getTemporaryValue('wizard');
+
+    $cached_values['page_variant']->save();
+
+    // Save the panels display mode and its custom settings as third party
+    // data of the display mode for this entity+bundle+display.
+    $panelizer = \Drupal::service('panelizer');
+    $panelizer_entity_manager = \Drupal::service('plugin.manager.panelizer_entity');
+    $panels_display_manager = \Drupal::service('panels.display_manager');
+    list($entity_type, $bundle, $display_id) = explode('__', $cached_values['id']);
+    $display = $panelizer->getEntityViewDisplay($entity_type, $bundle, $display_id);
+    $settings = $panelizer->getPanelizerSettings($entity_type, $bundle, $display_id);
+    $settings['enable'] = TRUE;
+    $settings['custom'] = $cached_values['custom'];
+    /** @var \Drupal\panelizer\Plugin\PanelizerEntityInterface $panelizer_entity_plugin */
+    $panelizer_entity_plugin = $panelizer_entity_manager->createInstance($display->getTargetEntityTypeId(), []);
+    $displays = $display->getThirdPartySetting('panelizer', 'displays', []);
+    $displays[$display_id] = $panels_display_manager->exportDisplay($cached_values['plugin']);
+    $display->setThirdPartySetting('panelizer', 'displays', $displays);
+    $panelizer->setPanelizerSettings($entity_type, $bundle, $display_id, $settings, $display);
+
+    parent::finish($form, $form_state);
+    $form_state->setRedirect('panelizer.wizard.edit', ['machine_name' => $cached_values['id']]);
+  }
+
+  /**
+   * Clears the temporary store.
+   *
+   * @param array $form
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   */
+  public function clearTempstore(array &$form, FormStateInterface $form_state) {
+    $this->getTempstore()->delete($this->getMachineName());
+  }
+
+  /**
+   * Wraps the context mapper.
+   *
+   * @return \Drupal\page_manager\ContextMapperInterface
+   */
+  protected function getContextMapper() {
+    return \Drupal::service('page_manager.context_mapper');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getContexts($cached_values) {
+    return $this->getContextMapper()->getContextValues($cached_values['page_variant']->getStaticContexts());
+  }
+
+}
diff --git a/src/Wizard/PanelizerWizard.php b/src/Wizard/PanelizerWizard.php
new file mode 100644
index 0000000..64bcd04
--- /dev/null
+++ b/src/Wizard/PanelizerWizard.php
@@ -0,0 +1,178 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panelizer\Wizard\PanelizerWizard.
+ */
+
+namespace Drupal\panelizer\Wizard;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\ctools\Wizard\FormWizardBase;
+use Drupal\panelizer\Form\PanelizerWizardContentForm;
+use Drupal\panelizer\Form\PanelizerWizardContextForm;
+use Drupal\panelizer\Form\PanelizerWizardGeneralForm;
+
+class PanelizerWizard extends FormWizardBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getWizardLabel() {
+    return $this->t('Wizard Information');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMachineLabel() {
+    return $this->t('Wizard Test Name');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOperations($cached_values) {
+    $operations = [
+      'general' => [
+        'form' => PanelizerWizardGeneralForm::class,
+        'title' => $this->t('General settings'),
+      ],
+      'contexts' => [
+        'form' => PanelizerWizardContextForm::class,
+        'title' => $this->t('Contexts'),
+      ],
+    ];
+
+    // Add any wizard operations from the plugin itself.
+    if (isset($cached_values['page_variant'])) {
+      $page_variant = $cached_values['page_variant'];
+      $variant_plugin = $page_variant->getVariantPlugin();
+      $variant_plugin->setContexts($this->getContexts($cached_values));
+      foreach ($variant_plugin->getWizardOperations($cached_values) as $name => $operation) {
+        $operation['values']['plugin'] = $variant_plugin;
+        $operation['submit'][] = '::submitVariantStep';
+        $operations[$name] = $operation;
+      }
+
+      // Change the class that manages the Content step.
+      if (isset($operations['content'])) {
+        $operations['content']['form'] = PanelizerWizardContentForm::class;
+      }
+    }
+
+    return $operations;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRouteName() {
+    return 'panelizer.wizard.add.step';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form = parent::buildForm($form, $form_state);
+    $cached_values = $form_state->getTemporaryValue('wizard');
+    $cached_values['id'] = $this->getMachineName();
+    // Some variants like PanelsDisplayVariant need this. Set it to empty.
+    $cached_values['access'] = [];
+    if (empty($cached_values['page_variant'])) {
+      $variant_plugin_id = 'panels_variant';
+      $page_variant = \Drupal::entityManager()
+        ->getStorage('page_variant')
+        ->create([
+          'variant' => $variant_plugin_id,
+          'id' => $cached_values['id'] . '__variant',
+        ]);
+
+      // Add a dummy page entity to the page variant. This is needed so the
+      // variant can be saved.
+      $page = \Drupal::entityManager()
+        ->getStorage('page')
+        ->create([
+          'id' => $cached_values['id'] . '__page',
+        ]);
+      $page_variant->setPageEntity($page);
+
+      // Initialize contexts by adding the current entity context.
+      list($entity_type_id, $bundle, $display_id) = explode('__', $cached_values['id']);
+      $page_variant->setStaticContext('panelizer_context_entity', [
+        'label' => 'Current entity',
+        'type' => 'entity:' . $entity_type_id,
+        'description' => 'The entity being viewed',
+        'value' => NULL,
+      ]);
+
+      $cached_values['page_variant'] = $page_variant;
+    }
+    $form_state->setTemporaryValue('wizard', $cached_values);
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function finish(array &$form, FormStateInterface $form_state) {
+    $cached_values = $form_state->getTemporaryValue('wizard');
+
+    $cached_values['page_variant']->save();
+
+    // Save the panels display mode and its custom settings as third party
+    // data of the display mode for this entity+bundle+display.
+    $panelizer = \Drupal::service('panelizer');
+    $panelizer_entity_manager = \Drupal::service('plugin.manager.panelizer_entity');
+    $panels_display_manager = \Drupal::service('panels.display_manager');
+    list($entity_type, $bundle, $display_id) = explode('__', $cached_values['id']);
+    $display = $panelizer->getEntityViewDisplay($entity_type, $bundle, $display_id);
+    $settings = $panelizer->getPanelizerSettings($entity_type, $bundle, $display_id);
+    $settings['enable'] = TRUE;
+    $settings['custom'] = $cached_values['custom'];
+    /** @var \Drupal\panelizer\Plugin\PanelizerEntityInterface $panelizer_entity_plugin */
+    $panelizer_entity_plugin = $panelizer_entity_manager->createInstance($display->getTargetEntityTypeId(), []);
+    $displays = $display->getThirdPartySetting('panelizer', 'displays', []);
+    $displays[$display_id] = $panels_display_manager->exportDisplay($cached_values['plugin']);
+    $display->setThirdPartySetting('panelizer', 'displays', $displays);
+    $panelizer->setPanelizerSettings($entity_type, $bundle, $display_id, $settings, $display);
+
+    parent::finish($form, $form_state);
+    $form_state->setRedirect('panelizer.wizard.edit', ['machine_name' => $cached_values['id']]);
+  }
+
+  /**
+   * Wraps the context mapper.
+   *
+   * @return \Drupal\page_manager\ContextMapperInterface
+   */
+  protected function getContextMapper() {
+    return \Drupal::service('page_manager.context_mapper');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getContexts($cached_values) {
+    return $this->getContextMapper()->getContextValues($cached_values['page_variant']->getStaticContexts());
+  }
+
+  /**
+   * Submission callback for the variant plugin steps.
+   */
+  public function submitVariantStep(array &$form, FormStateInterface $form_state) {
+    $cached_values = $form_state->getTemporaryValue('wizard');
+    /** @var \Drupal\page_manager\PageVariantInterface $page_variant */
+    $page_variant = $cached_values['page_variant'];
+    /** @var \Drupal\Core\Display\VariantInterface $plugin */
+    $plugin = $cached_values['plugin'];
+
+    // Make sure the variant plugin on the page variant gets the configuration
+    // from the 'plugin' which should have been setup by the variant's steps.
+    if (!empty($plugin) && !empty($page_variant)) {
+      $page_variant->getVariantPlugin()->setConfiguration($plugin->getConfiguration());
+    }
+  }
+
+}
diff --git a/templates/panelizer-wizard-form.html.twig b/templates/panelizer-wizard-form.html.twig
new file mode 100644
index 0000000..a691162
--- /dev/null
+++ b/templates/panelizer-wizard-form.html.twig
@@ -0,0 +1,31 @@
+{#
+/**
+ * @file
+ * Default theme implementation for a 'form' element.
+ *
+ * Available variables
+ * - attributes: A list of HTML attributes for the wrapper element.
+ * - children: The child elements of the form.
+ *
+ * @see template_preprocess_form()
+ *
+ * @ingroup themeable
+ */
+#}
+<div class="panelizer-wizard">
+  <div class="panelizer-wizard-actions">
+    {{ form.wizard_actions }}
+  </div>
+  <div class="panelizer-wizard-main clearfix">
+    <div class="panelizer-wizard-tree">
+      {{ form.wizard_tree }}
+    </div>
+    <div class="panelizer-wizard-form">
+      {{ form|without('wizard_actions', 'wizard_tree', 'actions') }}
+    </div>
+  </div>
+
+  <div class="panelizer-wizard-form-actions">
+    {{ form.actions }}
+  </div>
+</div>
diff --git a/templates/panelizer-wizard-tree.html.twig b/templates/panelizer-wizard-tree.html.twig
new file mode 100644
index 0000000..7d48fcf
--- /dev/null
+++ b/templates/panelizer-wizard-tree.html.twig
@@ -0,0 +1,47 @@
+{#
+/**
+ * @file
+ * Default theme implementation to display wizard tree.
+ *
+ * Available variables:
+ * - step: The current step name.
+ * - tree: A nested list of menu items. Each menu item contains:
+ *   - title: The menu link title.
+ *   - url: The menu link url, instance of \Drupal\Core\Url
+ *   - children: The menu item child items.
+ *   - step: The name of the step.
+ *
+ * @ingroup themeable
+ */
+#}
+{% import _self as panelizer %}
+
+{#
+  We call a macro which calls itself to render the full tree.
+  @see http://twig.sensiolabs.org/doc/tags/macro.html
+#}
+{{ panelizer.wizard_tree(tree, step, 0) }}
+
+{% macro wizard_tree(items, step, menu_level) %}
+  {% import _self as panelizer %}
+  {% if items %}
+    <ul>
+    {% for item in items %}
+      <li>
+        {% if item.url %}
+          {% if step is same as(item.step) %}
+            <strong>{{ link(item.title, item.url) }}</strong>
+          {% else %}
+            {{ link(item.title, item.url) }}
+          {% endif %}
+        {% else %}
+          {{ item.title }}
+        {% endif %}
+        {% if item.children %}
+          {{ panelizer.wizard_tree(item.children, step, menu_level + 1) }}
+        {% endif %}
+      </li>
+    {% endfor %}
+    </ul>
+  {% endif %}
+{% endmacro %}
