diff --git a/src/Form/AjaxFormTrait.php b/src/Form/AjaxFormTrait.php
new file mode 100644
index 0000000..0bc4bc7
--- /dev/null
+++ b/src/Form/AjaxFormTrait.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\ctools\Form\AjaxFormTrait.
+ */
+
+namespace Drupal\ctools\Form;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Component\Utility\NestedArray;
+
+/**
+ * Provides helper methods for using an AJAX modal.
+ */
+trait AjaxFormTrait {
+
+  /**
+   * Gets attributes for use with an AJAX modal.
+   *
+   * @return array
+   */
+   public static function getAjaxAttributes() {
+    return [
+      'class' => ['use-ajax'],
+      'data-dialog-type' => 'modal',
+      'data-dialog-options' => Json::encode([
+        'width' => 'auto',
+      ]),
+    ];
+  }
+
+  /**
+   * Gets attributes for use with an add button AJAX modal.
+   *
+   * @return array
+   */
+   public static function getAjaxButtonAttributes() {
+    return NestedArray::mergeDeep(AjaxFormTrait::getAjaxAttributes(), [
+      'class' => [
+        'button',
+        'button--small',
+        'button-action',
+      ],
+    ]);
+  }
+
+}
diff --git a/src/Plugin/BlockPluginCollection.php b/src/Plugin/BlockPluginCollection.php
new file mode 100644
index 0000000..1d3dd0f
--- /dev/null
+++ b/src/Plugin/BlockPluginCollection.php
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\ctools\Plugin\BlockPluginCollection.
+ */
+
+namespace Drupal\ctools\Plugin;
+
+use Drupal\Core\Block\BlockPluginInterface;
+use Drupal\Core\Plugin\DefaultLazyPluginCollection;
+
+/**
+ * Provides a collection of block plugins.
+ */
+class BlockPluginCollection extends DefaultLazyPluginCollection {
+
+  /**
+   * {@inheritdoc}
+   *
+   * @return \Drupal\Core\Block\BlockPluginInterface
+   */
+  public function &get($instance_id) {
+    return parent::get($instance_id);
+  }
+
+  /**
+   * Returns all blocks keyed by their region.
+   *
+   * @return array
+   *   An associative array keyed by region, containing an associative array of
+   *   block plugins.
+   */
+  public function getAllByRegion() {
+    $region_assignments = [];
+    foreach ($this as $block_id => $block) {
+      $configuration = $block->getConfiguration();
+      $region = isset($configuration['region']) ? $configuration['region'] : NULL;
+      $region_assignments[$region][$block_id] = $block;
+    }
+    foreach ($region_assignments as $region => $region_assignment) {
+      // @todo Determine the reason this needs error suppression.
+      @uasort($region_assignment, function (BlockPluginInterface $a, BlockPluginInterface $b) {
+        $a_config = $a->getConfiguration();
+        $a_weight = isset($a_config['weight']) ? $a_config['weight'] : 0;
+        $b_config = $b->getConfiguration();
+        $b_weight = isset($b_config['weight']) ? $b_config['weight'] : 0;
+        if ($a_weight == $b_weight) {
+          return strcmp($a->label(), $b->label());
+        }
+        return $a_weight > $b_weight ? 1 : -1;
+      });
+      $region_assignments[$region] = $region_assignment;
+    }
+    return $region_assignments;
+  }
+
+}
diff --git a/src/Plugin/BlockVariantInterface.php b/src/Plugin/BlockVariantInterface.php
new file mode 100644
index 0000000..bb745ee
--- /dev/null
+++ b/src/Plugin/BlockVariantInterface.php
@@ -0,0 +1,101 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\ctools\Plugin\BlockVariantInterface.
+ */
+
+namespace Drupal\ctools\Plugin;
+
+use Drupal\Core\Display\VariantInterface;
+
+/**
+ * Provides an interface for variant plugins that use block plugins.
+ */
+interface BlockVariantInterface extends VariantInterface {
+
+  /**
+   * Returns the human-readable list of regions keyed by machine name.
+   *
+   * @return array
+   *   An array of human-readable region names keyed by machine name.
+   */
+  public function getRegionNames();
+
+  /**
+   * Returns the human-readable name of a specific region.
+   *
+   * @param string $region
+   *   The machine name of a region.
+   *
+   * @return string
+   *   The human-readable name of a region.
+   */
+  public function getRegionName($region);
+
+  /**
+   * Adds a block to this display variant.
+   *
+   * @param array $configuration
+   *   An array of block configuration.
+   *
+   * @return string
+   *   The block ID.
+   */
+  public function addBlock(array $configuration);
+
+  /**
+   * Returns the region a specific block is assigned to.
+   *
+   * @param string $block_id
+   *   The block ID.
+   *
+   * @return string
+   *   The machine name of the region this block is assigned to.
+   */
+  public function getRegionAssignment($block_id);
+
+  /**
+   * Returns an array of regions and their block plugins.
+   *
+   * @return array
+   *   The array is first keyed by region machine name, with the values
+   *   containing an array keyed by block ID, with block plugin instances as the
+   *   values.
+   */
+  public function getRegionAssignments();
+
+  /**
+   * Returns a specific block plugin.
+   *
+   * @param string $block_id
+   *   The block ID.
+   *
+   * @return \Drupal\Core\Block\BlockPluginInterface
+   *   The block plugin.
+   */
+  public function getBlock($block_id);
+
+  /**
+   * Updates the configuration of a specific block plugin.
+   *
+   * @param string $block_id
+   *   The block ID.
+   * @param array $configuration
+   *   The array of configuration to set.
+   *
+   * @return $this
+   */
+  public function updateBlock($block_id, array $configuration);
+
+  /**
+   * Removes a specific block from this display variant.
+   *
+   * @param string $block_id
+   *   The block ID.
+   *
+   * @return $this
+   */
+  public function removeBlock($block_id);
+
+}
diff --git a/src/Plugin/BlockVariantTrait.php b/src/Plugin/BlockVariantTrait.php
new file mode 100644
index 0000000..bc97d82
--- /dev/null
+++ b/src/Plugin/BlockVariantTrait.php
@@ -0,0 +1,115 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\ctools\Plugin\BlockVariantTrait.
+ */
+
+namespace Drupal\ctools\Plugin;
+
+/**
+ * Provides methods for \Drupal\ctools\Plugin\BlockVariantInterface.
+ */
+trait BlockVariantTrait {
+
+  /**
+   * The plugin collection that holds the block plugins.
+   *
+   * @var \Drupal\ctools\Plugin\BlockPluginCollection
+   */
+  protected $blockPluginCollection;
+
+  /**
+   * @see \Drupal\ctools\Plugin\BlockVariantInterface::getRegionNames()
+   */
+  abstract public function getRegionNames();
+
+  /**
+   * @see \Drupal\ctools\Plugin\BlockVariantInterface::getBlock()
+   */
+  public function getBlock($block_id) {
+    return $this->getBlockCollection()->get($block_id);
+  }
+
+  /**
+   * @see \Drupal\ctools\Plugin\BlockVariantInterface::addBlock()
+   */
+  public function addBlock(array $configuration) {
+    $configuration['uuid'] = $this->uuidGenerator()->generate();
+    $this->getBlockCollection()->addInstanceId($configuration['uuid'], $configuration);
+    return $configuration['uuid'];
+  }
+
+  /**
+   * @see \Drupal\ctools\Plugin\BlockVariantInterface::removeBlock()
+   */
+  public function removeBlock($block_id) {
+    $this->getBlockCollection()->removeInstanceId($block_id);
+    return $this;
+  }
+
+  /**
+   * @see \Drupal\ctools\Plugin\BlockVariantInterface::updateBlock()
+   */
+  public function updateBlock($block_id, array $configuration) {
+    $existing_configuration = $this->getBlock($block_id)->getConfiguration();
+    $this->getBlockCollection()->setInstanceConfiguration($block_id, $configuration + $existing_configuration);
+    return $this;
+  }
+
+  /**
+   * @see \Drupal\ctools\Plugin\BlockVariantInterface::getRegionAssignment()
+   */
+  public function getRegionAssignment($block_id) {
+    $configuration = $this->getBlock($block_id)->getConfiguration();
+    return isset($configuration['region']) ? $configuration['region'] : NULL;
+  }
+
+  /**
+   * @see \Drupal\ctools\Plugin\BlockVariantInterface::getRegionAssignments()
+   */
+  public function getRegionAssignments() {
+    // Build an array of the region names in the right order.
+    $empty = array_fill_keys(array_keys($this->getRegionNames()), []);
+    $full = $this->getBlockCollection()->getAllByRegion();
+    // Merge it with the actual values to maintain the ordering.
+    return array_intersect_key(array_merge($empty, $full), $empty);
+  }
+
+  /**
+   * @see \Drupal\ctools\Plugin\BlockVariantInterface::getRegionName()
+   */
+  public function getRegionName($region) {
+    $regions = $this->getRegionNames();
+    return isset($regions[$region]) ? $regions[$region] : '';
+  }
+
+  /**
+   * Returns the block plugins used for this display variant.
+   *
+   * @return \Drupal\Core\Block\BlockPluginInterface[]|\Drupal\ctools\Plugin\BlockPluginCollection
+   *   An array or collection of configured block plugins.
+   */
+  protected function getBlockCollection() {
+    if (!$this->blockPluginCollection) {
+      $this->blockPluginCollection = new BlockPluginCollection(\Drupal::service('plugin.manager.block'), $this->getBlockConfig());
+    }
+    return $this->blockPluginCollection;
+  }
+
+  /**
+   * Returns the UUID generator.
+   *
+   * @return \Drupal\Component\Uuid\UuidInterface
+   */
+  abstract protected function uuidGenerator();
+
+  /**
+   * Returns the configuration for stored blocks.
+   *
+   * @return array
+   *   An array of block configuration, keyed by the unique block ID.
+   */
+  abstract protected function getBlockConfig();
+
+}
diff --git a/src/Plugin/ConditionVariantInterface.php b/src/Plugin/ConditionVariantInterface.php
new file mode 100644
index 0000000..2d59201
--- /dev/null
+++ b/src/Plugin/ConditionVariantInterface.php
@@ -0,0 +1,92 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\ctools\Plugin\ConditionVariantInterface.
+ */
+
+namespace Drupal\ctools\Plugin;
+
+use Drupal\Core\Display\VariantInterface;
+
+/**
+ * Provides an interface for variant plugins that use condition plugins.
+ */
+interface ConditionVariantInterface extends VariantInterface {
+
+  /**
+   * Gets the values for all defined contexts.
+   *
+   * @return \Drupal\Component\Plugin\Context\ContextInterface[]
+   *   An array of set contexts, keyed by context name.
+   */
+  public function getContexts();
+
+  /**
+   * Returns the conditions used for determining if this variant is selected.
+   *
+   * @return \Drupal\Core\Condition\ConditionInterface[]|\Drupal\Core\Condition\ConditionPluginCollection
+   *   An array of configured condition plugins.
+   */
+  public function getSelectionConditions();
+
+  /**
+   * Removes a specific selection condition.
+   *
+   * @param string $condition_id
+   *   The selection condition ID.
+   *
+   * @return $this
+   */
+  public function removeSelectionCondition($condition_id);
+
+  /**
+   * Adds a new selection condition to the page entity.
+   *
+   * @param array $configuration
+   *   An array of configuration for the new selection condition.
+   *
+   * @return string
+   *   The selection condition ID.
+   */
+  public function addSelectionCondition(array $configuration);
+
+  /**
+   * Returns the logic used to compute selections, either 'and' or 'or'.
+   *
+   * @return string
+   *   The string 'and', or the string 'or'.
+   */
+  public function getSelectionLogic();
+
+  /**
+   * Returns the definition of the plugin implementation.
+   *
+   * @return array
+   *   The plugin definition, as returned by the discovery object used by the
+   *   plugin manager.
+   */
+  public function getPluginDefinition();
+
+  /**
+   * Sets the context values for this display variant.
+   *
+   * @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts
+   *   An array of contexts, keyed by context name.
+   *
+   * @return $this
+   */
+  public function setContexts(array $contexts);
+
+  /**
+   * Retrieves a specific selection condition.
+   *
+   * @param string $condition_id
+   *   The selection condition ID.
+   *
+   * @return \Drupal\Core\Condition\ConditionInterface
+   *   The selection condition object.
+   */
+  public function getSelectionCondition($condition_id);
+
+}
diff --git a/src/Plugin/ConditionVariantTrait.php b/src/Plugin/ConditionVariantTrait.php
new file mode 100644
index 0000000..ba1be95
--- /dev/null
+++ b/src/Plugin/ConditionVariantTrait.php
@@ -0,0 +1,108 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\ctools\Plugin\ConditionVariantTrait.
+ */
+
+namespace Drupal\ctools\Plugin;
+
+use Drupal\Core\Condition\ConditionAccessResolverTrait;
+use Drupal\Core\Condition\ConditionPluginCollection;
+use Drupal\Core\Plugin\ContextAwarePluginInterface;
+
+/**
+ * Provides methods for \Drupal\ctools\Plugin\ConditionVariantInterface.
+ */
+trait ConditionVariantTrait {
+
+  use ConditionAccessResolverTrait;
+
+  /**
+   * The plugin collection that holds the selection condition plugins.
+   *
+   * @var \Drupal\Component\Plugin\LazyPluginCollection
+   */
+  protected $selectionConditionCollection;
+
+  /**
+   * @see \Drupal\ctools\Plugin\ConditionVariantInterface::getSelectionConditions()
+   */
+  public function getSelectionConditions() {
+    if (!$this->selectionConditionCollection) {
+      $this->selectionConditionCollection = new ConditionPluginCollection(\Drupal::service('plugin.manager.condition'), $this->getSelectionConfiguration());
+    }
+    return $this->selectionConditionCollection;
+  }
+
+  /**
+   * @see \Drupal\ctools\Plugin\ConditionVariantInterface::addSelectionCondition()
+   */
+  public function addSelectionCondition(array $configuration) {
+    $configuration['uuid'] = $this->uuidGenerator()->generate();
+    $this->getSelectionConditions()->addInstanceId($configuration['uuid'], $configuration);
+    return $configuration['uuid'];
+  }
+
+  /**
+   * @see \Drupal\ctools\Plugin\ConditionVariantInterface::getSelectionCondition()
+   */
+  public function getSelectionCondition($condition_id) {
+    return $this->getSelectionConditions()->get($condition_id);
+  }
+
+  /**
+   * @see \Drupal\ctools\Plugin\ConditionVariantInterface::removeSelectionCondition()
+   */
+  public function removeSelectionCondition($condition_id) {
+    $this->getSelectionConditions()->removeInstanceId($condition_id);
+    return $this;
+  }
+
+  /**
+   * Determines if the selection conditions will pass given a set of contexts.
+   *
+   * @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts
+   *   An array of set contexts, keyed by context name.
+   *
+   * @return bool
+   *   TRUE if access is granted, FALSE otherwise.
+   */
+  protected function determineSelectionAccess(array $contexts) {
+    $conditions = $this->getSelectionConditions();
+    foreach ($conditions as $condition) {
+      if ($condition instanceof ContextAwarePluginInterface) {
+        $this->contextHandler()->applyContextMapping($condition, $contexts);
+      }
+    }
+    return $this->resolveConditions($conditions, $this->getSelectionLogic());
+  }
+
+  /**
+   * @see \Drupal\ctools\Plugin\ConditionVariantInterface::getSelectionLogic()
+   */
+  abstract public function getSelectionLogic();
+
+  /**
+   * Returns the configuration for stored selection conditions.
+   *
+   * @return array
+   *   An array of condition configuration, keyed by the unique condition ID.
+   */
+  abstract protected function getSelectionConfiguration();
+
+  /**
+   * Returns the UUID generator.
+   *
+   * @return \Drupal\Component\Uuid\UuidInterface
+   */
+  abstract protected function uuidGenerator();
+
+  /**
+   * Returns the context handler.
+   *
+   * @return \Drupal\Core\Plugin\Context\ContextHandlerInterface
+   */
+  abstract protected function contextHandler();
+
+}
diff --git a/src/Plugin/DisplayVariant/BlockDisplayVariant.php b/src/Plugin/DisplayVariant/BlockDisplayVariant.php
new file mode 100644
index 0000000..87ce680
--- /dev/null
+++ b/src/Plugin/DisplayVariant/BlockDisplayVariant.php
@@ -0,0 +1,222 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\ctools\Plugin\DisplayVariant\BlockDisplayVariant.
+ */
+
+namespace Drupal\ctools\Plugin\DisplayVariant;
+
+use Drupal\Component\Uuid\UuidInterface;
+use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
+use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
+use Drupal\Core\Display\VariantBase;
+use Drupal\Core\Display\ContextAwareVariantInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Plugin\Context\ContextHandlerInterface;
+use Drupal\Core\Render\Element;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Utility\Token;
+use Drupal\ctools\Form\AjaxFormTrait;
+use Drupal\ctools\Plugin\BlockVariantInterface;
+use Drupal\ctools\Plugin\BlockVariantTrait;
+use Drupal\ctools\Plugin\ConditionVariantInterface;
+use Drupal\ctools\Plugin\ConditionVariantTrait;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a display variant that simply contains blocks.
+ *
+ * @DisplayVariant(
+ *   id = "block_display",
+ *   admin_label = @Translation("Block page")
+ * )
+ */
+abstract class BlockDisplayVariant extends VariantBase implements ContextAwareVariantInterface, ConditionVariantInterface, ContainerFactoryPluginInterface, BlockVariantInterface, RefinableCacheableDependencyInterface {
+
+  use AjaxFormTrait;
+  use BlockVariantTrait;
+  use ConditionVariantTrait;
+  use RefinableCacheableDependencyTrait;
+
+  /**
+   * The context handler.
+   *
+   * @var \Drupal\Core\Plugin\Context\ContextHandlerInterface
+   */
+  protected $contextHandler;
+
+  /**
+   * The UUID generator.
+   *
+   * @var \Drupal\Component\Uuid\UuidInterface
+   */
+  protected $uuidGenerator;
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $account;
+
+  /**
+   * The token service.
+   *
+   * @var \Drupal\Core\Utility\Token
+   */
+  protected $token;
+
+  /**
+   * An array of collected contexts.
+   *
+   * This is only used on runtime, and is not stored.
+   *
+   * @var \Drupal\Component\Plugin\Context\ContextInterface[]
+   */
+  protected $contexts = [];
+
+  /**
+   * Constructs a new BlockDisplayVariant.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin ID for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Plugin\Context\ContextHandlerInterface $context_handler
+   *   The context handler.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The current user.
+   * @param \Drupal\Component\Uuid\UuidInterface $uuid_generator
+   *   The UUID generator.
+   * @param \Drupal\Core\Utility\Token $token
+   *   The token service.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, ContextHandlerInterface $context_handler, AccountInterface $account, UuidInterface $uuid_generator, Token $token) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    $this->contextHandler = $context_handler;
+    $this->account = $account;
+    $this->uuidGenerator = $uuid_generator;
+    $this->token = $token;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('context.handler'),
+      $container->get('current_user'),
+      $container->get('uuid'),
+      $container->get('token')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access(AccountInterface $account = NULL) {
+    // Delegate to the conditions.
+    return $this->determineSelectionAccess($this->getContexts());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return parent::defaultConfiguration() + [
+      'blocks' => [],
+      'selection_conditions' => [],
+      'selection_logic' => 'and',
+      'page_title' => '',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function calculateDependencies() {
+    foreach ($this->getBlockCollection() as $instance) {
+      $this->calculatePluginDependencies($instance);
+    }
+    foreach ($this->getSelectionConditions() as $instance) {
+      $this->calculatePluginDependencies($instance);
+    }
+    return $this->dependencies;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfiguration() {
+    return [
+      'selection_conditions' => $this->getSelectionConditions()->getConfiguration(),
+      'blocks' => $this->getBlockCollection()->getConfiguration(),
+    ] + parent::getConfiguration();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSelectionLogic() {
+    return $this->configuration['selection_logic'];
+  }
+
+  /**
+   * Gets the contexts.
+   *
+   * @return \Drupal\Component\Plugin\Context\ContextInterface[]
+   *   An array of set contexts, keyed by context name.
+   */
+  public function getContexts() {
+    return $this->contexts;
+  }
+
+  /**
+   * Sets the contexts.
+   *
+   * @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts
+   *   An array of contexts, keyed by context name.
+   *
+   * @return $this
+   */
+  public function setContexts(array $contexts) {
+    $this->contexts = $contexts;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function contextHandler() {
+    return $this->contextHandler;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getSelectionConfiguration() {
+    return $this->configuration['selection_conditions'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getBlockConfig() {
+    return $this->configuration['blocks'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function uuidGenerator() {
+    return $this->uuidGenerator;
+  }
+
+}
diff --git a/tests/src/Unit/BlockDisplayVariantTest.php b/tests/src/Unit/BlockDisplayVariantTest.php
new file mode 100644
index 0000000..282b116
--- /dev/null
+++ b/tests/src/Unit/BlockDisplayVariantTest.php
@@ -0,0 +1,116 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\page_manager\Unit\BlockDisplayVariantTest.
+ */
+
+namespace Drupal\Tests\ctools\Unit;
+
+use Drupal\Component\Uuid\UuidInterface;
+use Drupal\Core\Plugin\Context\ContextHandlerInterface;
+use Drupal\Core\Form\FormState;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Utility\Token;
+use Drupal\ctools\Plugin\DisplayVariant\BlockDisplayVariant;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests the block display variant plugin.
+ *
+ * @coversDefaultClass \Drupal\ctools\Plugin\DisplayVariant\BlockDisplayVariant
+ *
+ * @group PageManager
+ */
+class BlockDisplayVariantTest extends UnitTestCase {
+
+  /**
+   * Tests the access() method.
+   *
+   * @covers ::access
+   */
+  public function testAccess() {
+    $display_variant = $this->getMockBuilder(TestBlockDisplayVariant::class)
+      ->disableOriginalConstructor()
+      ->setMethods(['determineSelectionAccess'])
+      ->getMock();
+    $display_variant->expects($this->once())
+      ->method('determineSelectionAccess')
+      ->willReturn(FALSE);
+    $this->assertSame(FALSE, $display_variant->access());
+
+    $display_variant = $this->getMockBuilder(TestBlockDisplayVariant::class)
+      ->disableOriginalConstructor()
+      ->setMethods(['determineSelectionAccess'])
+      ->getMock();
+    $display_variant->expects($this->once())
+      ->method('determineSelectionAccess')
+      ->willReturn(TRUE);
+    $this->assertSame(TRUE, $display_variant->access());
+  }
+
+  /**
+   * Tests the submitConfigurationForm() method.
+   *
+   * @covers ::submitConfigurationForm
+   *
+   * @dataProvider providerTestSubmitConfigurationForm
+   */
+  public function testSubmitConfigurationForm($values) {
+    $account = $this->prophesize(AccountInterface::class);
+    $context_handler = $this->prophesize(ContextHandlerInterface::class);
+    $uuid_generator = $this->prophesize(UuidInterface::class);
+    $token = $this->prophesize(Token::class);
+
+    $display_variant = new TestBlockDisplayVariant([], '', [], $context_handler->reveal(), $account->reveal(), $uuid_generator->reveal(), $token->reveal());
+
+    $form = [];
+    $form_state = (new FormState())->setValues($values);
+    $display_variant->submitConfigurationForm($form, $form_state);
+    $this->assertSame($values['label'], $display_variant->label());
+  }
+
+  /**
+   * Provides data for testSubmitConfigurationForm().
+   */
+  public function providerTestSubmitConfigurationForm() {
+    $data = [];
+    $data[] = [
+      [
+        'label' => 'test_label1',
+      ],
+    ];
+    $data[] = [
+      [
+        'label' => 'test_label2',
+        'blocks' => ['foo1' => []],
+      ],
+    ];
+    $data[] = [
+      [
+        'label' => 'test_label3',
+        'blocks' => ['foo1' => [], 'foo2' => []],
+      ],
+    ];
+    return $data;
+  }
+
+}
+
+class TestBlockDisplayVariant extends BlockDisplayVariant {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build() {
+    return [];
+  }
+
+  public function getRegionNames() {
+    return [
+      'top' => 'Top',
+      'bottom' => 'Bottom',
+    ];
+  }
+
+}
diff --git a/tests/src/Unit/BlockPluginCollectionTest.php b/tests/src/Unit/BlockPluginCollectionTest.php
new file mode 100644
index 0000000..0a2818b
--- /dev/null
+++ b/tests/src/Unit/BlockPluginCollectionTest.php
@@ -0,0 +1,86 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\page_manager\Unit\BlockPluginCollectionTest.
+ */
+
+namespace Drupal\Tests\ctools\Unit;
+
+use Drupal\Core\Block\BlockManagerInterface;
+use Drupal\Core\Block\BlockPluginInterface;
+use Drupal\ctools\Plugin\BlockPluginCollection;
+use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
+
+/**
+ * Tests the block plugin collection.
+ *
+ * @coversDefaultClass \Drupal\ctools\Plugin\BlockPluginCollection
+ *
+ * @group PageManager
+ */
+class BlockPluginCollectionTest extends UnitTestCase {
+
+  /**
+   * Tests the getAllByRegion() method.
+   *
+   * @covers ::getAllByRegion
+   */
+  public function testGetAllByRegion() {
+    $blocks = [
+      'foo' => [
+        'id' => 'foo',
+        'label' => 'Foo',
+        'plugin' => 'system_powered_by_block',
+        'region' => 'bottom',
+      ],
+      'bar' => [
+        'id' => 'bar',
+        'label' => 'Bar',
+        'plugin' => 'system_powered_by_block',
+        'region' => 'top',
+      ],
+      'bing' => [
+        'id' => 'bing',
+        'label' => 'Bing',
+        'plugin' => 'system_powered_by_block',
+        'region' => 'bottom',
+        'weight' => -10,
+      ],
+      'baz' => [
+        'id' => 'baz',
+        'label' => 'Baz',
+        'plugin' => 'system_powered_by_block',
+        'region' => 'bottom',
+      ],
+    ];
+    $block_manager = $this->prophesize(BlockManagerInterface::class);
+    $plugins = [];
+    foreach ($blocks as $block_id => $block) {
+      $plugin = $this->prophesize(BlockPluginInterface::class);
+      $plugin->label()->willReturn($block['label']);
+      $plugin->getConfiguration()->willReturn($block);
+      $plugins[$block_id] = $plugin->reveal();
+
+      $block_manager->createInstance($block_id, $block)
+        ->willReturn($plugin->reveal())
+        ->shouldBeCalled();
+    }
+
+
+    $block_plugin_collection = new BlockPluginCollection($block_manager->reveal(), $blocks);
+    $expected = [
+      'bottom' => [
+        'bing' => $plugins['bing'],
+        'baz' => $plugins['baz'],
+        'foo' => $plugins['foo'],
+      ],
+      'top' => [
+        'bar' => $plugins['bar'],
+      ],
+    ];
+    $this->assertSame($expected, $block_plugin_collection->getAllByRegion());
+  }
+
+}
diff --git a/tests/src/Unit/BlockVariantTraitTest.php b/tests/src/Unit/BlockVariantTraitTest.php
new file mode 100644
index 0000000..e95ef2c
--- /dev/null
+++ b/tests/src/Unit/BlockVariantTraitTest.php
@@ -0,0 +1,144 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\page_manager\Unit\BlockVariantTraitTest.
+ */
+
+namespace Drupal\Tests\page_manager\Unit;
+
+use Drupal\Component\Uuid\UuidInterface;
+use Drupal\ctools\Plugin\BlockPluginCollection;
+use Drupal\ctools\Plugin\BlockVariantTrait;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests the methods of a block-based variant.
+ *
+ * @coversDefaultClass \Drupal\ctools\Plugin\BlockVariantTrait
+ *
+ * @group PageManager
+ */
+class BlockVariantTraitTest extends UnitTestCase {
+
+  /**
+   * Tests the getRegionAssignments() method.
+   *
+   * @covers ::getRegionAssignments
+   *
+   * @dataProvider providerTestGetRegionAssignments
+   */
+  public function testGetRegionAssignments($expected, $blocks = []) {
+    $block_collection = $this->prophesize(BlockPluginCollection::class);
+    $block_collection->getAllByRegion()
+      ->willReturn($blocks)
+      ->shouldBeCalled();
+
+    $display_variant = new TestBlockVariantTrait();
+    $display_variant->setBlockPluginCollection($block_collection->reveal());
+
+    $this->assertSame($expected, $display_variant->getRegionAssignments());
+  }
+
+  public function providerTestGetRegionAssignments() {
+    return [
+      [
+        [
+          'top' => [],
+          'bottom' => [],
+        ],
+      ],
+      [
+        [
+          'top' => ['foo'],
+          'bottom' => [],
+        ],
+        [
+          'top' => ['foo'],
+        ],
+      ],
+      [
+        [
+          'top' => [],
+          'bottom' => [],
+        ],
+        [
+          'invalid' => ['foo'],
+        ],
+      ],
+      [
+        [
+          'top' => [],
+          'bottom' => ['foo'],
+        ],
+        [
+          'bottom' => ['foo'],
+          'invalid' => ['bar'],
+        ],
+      ],
+    ];
+  }
+
+}
+
+class TestBlockVariantTrait {
+  use BlockVariantTrait;
+
+  /**
+   * @var array
+   */
+  protected $blockConfig = [];
+
+  /**
+   * @var \Drupal\Component\Uuid\UuidInterface
+   */
+  protected $uuidGenerator;
+
+  /**
+   * @param BlockPluginCollection $block_plugin_collection
+   *
+   * @return $this
+   */
+  public function setBlockPluginCollection(BlockPluginCollection $block_plugin_collection) {
+    $this->blockPluginCollection = $block_plugin_collection;
+    return $this;
+  }
+
+  /**
+   * @param \Drupal\Component\Uuid\UuidInterface $uuid_generator
+   *
+   * @return $this
+   */
+  public function setUuidGenerator(UuidInterface $uuid_generator) {
+    $this->uuidGenerator = $uuid_generator;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function uuidGenerator() {
+    return $this->uuidGenerator;
+  }
+
+  /**
+   * Sets the block configuration.
+   *
+   * @param array $config
+   *   The block configuration.
+   *
+   * @return $this
+   */
+  public function setBlockConfig(array $config) {
+    $this->blockConfig = $config;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getBlockConfig() {
+    return $this->blockConfig;
+  }
+
+}
