diff --git a/panelizer.install b/panelizer.install
index e69de29..b3d9bbc 100644
--- a/panelizer.install
+++ b/panelizer.install
@@ -0,0 +1 @@
+<?php
diff --git a/panelizer.module b/panelizer.module
index dfa804f..5bd1753 100644
--- a/panelizer.module
+++ b/panelizer.module
@@ -207,6 +207,13 @@ function panelizer_panels_ipe_panels_display_presave(PanelsDisplayVariant $panel
   }
 }
 
+function panelizer_form_alter(&$form, FormStateInterface $form_state, $form_id) {
+  // @todo make this work for non-ipe scenario.
+  if ($form_id == 'panels_ipe_block_plugin_form' && !empty($form['plugin_id']['#value']) && strpos($form['plugin_id']['#value'], 'inline_content:') === 0) {
+    $form['flipper']['front']['settings']['label']['#access'] = FALSE;
+  }
+}
+
 /**
  * Implements hook_form_FORM_ID_alter().
  */
diff --git a/src/Entity/BlockContent.php b/src/Entity/BlockContent.php
new file mode 100644
index 0000000..1eb9f17
--- /dev/null
+++ b/src/Entity/BlockContent.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace Drupal\panelizer\Entity;
+
+use Drupal\block_content\Entity\BlockContent as CoreBlockContent;
+
+/**
+ * Defines the custom block entity class.
+ *
+ * @ContentEntityType(
+ *   id = "panelizer_block_content",
+ *   label = @Translation("Custom block"),
+ *   bundle_label = @Translation("Custom block type"),
+ *   handlers = {
+ *     "storage" = "Drupal\panelizer\Entity\Storage\ContentEntityNullStorage",
+ *     "access" = "Drupal\block_content\BlockContentAccessControlHandler",
+ *     "list_builder" = "Drupal\block_content\BlockContentListBuilder",
+ *     "view_builder" = "Drupal\block_content\BlockContentViewBuilder",
+ *     "views_data" = "Drupal\block_content\BlockContentViewsData",
+ *     "form" = {
+ *       "add" = "Drupal\block_content\BlockContentForm",
+ *       "edit" = "Drupal\block_content\BlockContentForm",
+ *       "delete" = "Drupal\block_content\Form\BlockContentDeleteForm",
+ *       "default" = "Drupal\block_content\BlockContentForm"
+ *     },
+ *     "translation" = "Drupal\block_content\BlockContentTranslationHandler"
+ *   },
+ *   admin_permission = "administer blocks",
+ *   base_table = "block_content",
+ *   revision_table = "block_content_revision",
+ *   data_table = "block_content_field_data",
+ *   revision_data_table = "block_content_field_revision",
+ *   links = {
+ *     "canonical" = "/block/{block_content}",
+ *     "delete-form" = "/block/{block_content}/delete",
+ *     "edit-form" = "/block/{block_content}",
+ *     "collection" = "/admin/structure/block/block-content",
+ *   },
+ *   translatable = TRUE,
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "revision" = "revision_id",
+ *     "bundle" = "type",
+ *     "label" = "info",
+ *     "langcode" = "langcode",
+ *     "uuid" = "uuid"
+ *   },
+ *   bundle_entity_type = "block_content_type",
+ *   field_ui_base_route = "entity.block_content_type.edit_form",
+ *   render_cache = FALSE,
+ *   multiversion = FALSE,
+ *   shadow = "block_content",
+ * )
+ */
+class BlockContent extends CoreBlockContent {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(array $values, $entity_type, $bundle = FALSE, array $translations = []) {
+    parent::__construct($values, 'block_content', $bundle, $translations);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save() {
+    return $this->entityTypeManager()
+      ->getStorage('panelizer_block_content')
+      ->save($this);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function delete() {
+    parent::delete();
+
+    $this->entityTypeManager()
+      ->getStorage('panelizer_block_content')
+      ->resetCache((array) $this->id());
+  }
+
+}
diff --git a/src/Entity/Storage/ContentEntityNullStorage.php b/src/Entity/Storage/ContentEntityNullStorage.php
new file mode 100644
index 0000000..201b8aa
--- /dev/null
+++ b/src/Entity/Storage/ContentEntityNullStorage.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Drupal\panelizer\Entity\Storage;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
+use Drupal\Core\Language\LanguageManagerInterface;
+
+/**
+ * A content entity storage handler that does not save to the database.
+ */
+class ContentEntityNullStorage extends SqlContentEntityStorage {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager) {
+    $shadow = $entity_type->get('shadow');
+
+    $real_entity_type = $shadow
+      ? $entity_manager->getDefinition($shadow)
+      : $entity_type;
+
+    parent::__construct($real_entity_type, $database, $entity_manager, $cache, $language_manager);
+
+    if ($shadow) {
+      $this->entityClass = $entity_type->getClass();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function saveToSharedTables(ContentEntityInterface $entity, $table_name = NULL, $new_revision = NULL) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function saveToDedicatedTables(ContentEntityInterface $entity, $update = TRUE, $names = array()) {
+  }
+
+}
diff --git a/src/Plugin/Block/InlineBlock.php b/src/Plugin/Block/InlineBlock.php
new file mode 100644
index 0000000..d8f87e6
--- /dev/null
+++ b/src/Plugin/Block/InlineBlock.php
@@ -0,0 +1,116 @@
+<?php
+
+namespace Drupal\panelizer\Plugin\Block;
+
+use Drupal\block_content\Plugin\Block\BlockContentBlock;
+use Drupal\Core\Block\BlockManagerInterface;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Routing\UrlGeneratorInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\panelizer\Entity\BlockContent;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Defines a generic custom block type.
+ *
+ * @Block(
+ *  id = "inline_content",
+ *  admin_label = @Translation("Inline block"),
+ *  category = @Translation("Custom"),
+ *  deriver = "Drupal\panelizer\Plugin\Derivative\InlineBlock"
+ * )
+ */
+class InlineBlock extends BlockContentBlock {
+
+
+  /**
+   * @var \Drupal\panelizer\Entity\BlockContent
+   */
+  protected $entity;
+
+  /**
+   * An array of all the parents set within the entity form.
+   *
+   * @var array
+   */
+  protected $parents;
+
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, BlockManagerInterface $block_manager, EntityManagerInterface $entity_manager, AccountInterface $account, UrlGeneratorInterface $url_generator) {
+    $entity = !empty($configuration['entity']) ? $configuration['entity'] : NULL;
+    unset($configuration['entity']);
+    parent::__construct($configuration, $plugin_id, $plugin_definition, $block_manager, $entity_manager, $account, $url_generator);
+    if ($entity) {
+      $this->entity = unserialize($entity);
+    }
+    else {
+      $bundle = $this->getDerivativeId();
+      $this->entity = new BlockContent([], 'block_content', $bundle);
+    }
+  }
+
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('plugin.manager.block'),
+      $container->get('entity.manager'),
+      $container->get('current_user'),
+      $container->get('url_generator')
+    );
+
+  }
+
+
+  protected function getEntity() {
+    return $this->entity;
+  }
+
+  public function getConfiguration() {
+    $configuration = parent::getConfiguration();
+    $configuration['entity'] = serialize($this->getEntity());
+    return $configuration;
+  }
+
+  public function blockForm($form, FormStateInterface $form_state) {
+    $options = $this->entityManager->getViewModeOptionsByBundle('panelizer_block_content', $this->getDerivativeId());
+    $form['view_mode'] = array(
+      '#type' => 'select',
+      '#options' => $options,
+      '#title' => $this->t('View mode'),
+      '#description' => $this->t('Output the block in this view mode.'),
+      '#default_value' => $this->configuration['view_mode'],
+      '#access' => (count($options) > 1),
+    );
+    $form['title']['#description'] = $this->t('The title of the block as shown to the user.');
+    $entity = $this->getEntity();
+    if (!$entity->info->getValue()) {
+      $entity = NULL;
+    }
+    $form['entity'] = [
+      '#type' => 'inline_entity_form',
+      '#entity_type' => 'panelizer_block_content',
+      '#bundle' => $this->getDerivativeId(),
+      // If the #default_value is NULL, a new entity will be created.
+      '#default_value' => $entity,
+    ];
+    return $form;
+  }
+
+  public function blockSubmit($form, FormStateInterface $form_state) {
+    parent::blockSubmit($form, $form_state);
+    $entity = $form['entity']['#entity'];
+    $this->configuration['label'] = $entity->label();
+    $langcode = $form_state->getValue('langcode');
+    $this->configuration['langcode'] = $langcode;
+    foreach ($entity->toArray() as $key => $value) {
+      if ($form_state->hasValue(['entity', $key])) {
+        $entity->{$key} = $form_state->getValue(['entity', $key]);
+      }
+    }
+    $this->entity = $entity;
+  }
+
+}
\ No newline at end of file
diff --git a/src/Plugin/Derivative/InlineBlock.php b/src/Plugin/Derivative/InlineBlock.php
new file mode 100644
index 0000000..0e752a9
--- /dev/null
+++ b/src/Plugin/Derivative/InlineBlock.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Drupal\panelizer\Plugin\Derivative;
+
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
+use Drupal\Core\StringTranslation\TranslationManager;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Retrieves bundle types for inline blocks.
+ */
+class InlineBlock extends DeriverBase implements ContainerDeriverInterface {
+
+  /**
+   * The custom block type storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $blockContentTypeStorage;
+
+  /**
+   * @var \Drupal\Core\StringTranslation\TranslationManager
+   */
+  protected $translation;
+
+  /**
+   * Constructs a BlockContentType object.
+   *
+   * @param \Drupal\Core\Entity\EntityStorageInterface $block_content_type_storage
+   *   The custom block storage.
+   */
+  public function __construct(EntityStorageInterface $block_content_type_storage, TranslationManager $translation) {
+    $this->blockContentTypeStorage = $block_content_type_storage;
+    $this->translation = $translation;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, $base_plugin_id) {
+    $entity_manager = $container->get('entity.manager');
+    return new static(
+      $entity_manager->getStorage('block_content_type'),
+      $container->get('string_translation')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDerivativeDefinitions($base_plugin_definition) {
+    $block_types = $this->blockContentTypeStorage->loadMultiple();
+    // Reset the discovered definitions.
+    $this->derivatives = [];
+    /** @var $type \Drupal\block_content\Entity\BlockContent */
+    foreach ($block_types as $type) {
+      $this->derivatives[$type->id()] = $base_plugin_definition;
+      $this->derivatives[$type->id()]['admin_label'] = $this->translation->translate('Inline @type', ['@type' => $type->label()]);
+      $this->derivatives[$type->id()]['config_dependencies']['content'] = array(
+        $type->getConfigDependencyName()
+      );
+    }
+    return $this->derivatives;
+  }
+
+}
diff --git a/tests/src/Kernel/BlockContentTest.php b/tests/src/Kernel/BlockContentTest.php
new file mode 100644
index 0000000..1242f3f
--- /dev/null
+++ b/tests/src/Kernel/BlockContentTest.php
@@ -0,0 +1,214 @@
+<?php
+
+namespace Drupal\Tests\panelizer\Kernel;
+
+use Drupal\block_content\BlockContentInterface;
+use Drupal\block_content\Entity\BlockContent as CoreBlockContent;
+use Drupal\block_content\Entity\BlockContentType;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\panelizer\Entity\BlockContent as ShadowBlockContent;
+
+/**
+ * Tests Panelizer's shadowing of custom blocks.
+ *
+ * @group panelizer
+ */
+class BlockContentTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'block',
+    'block_content',
+    'ctools',
+    'field',
+    'field_ui',
+    'filter',
+    'panelizer',
+    'panels',
+    'text',
+    'user',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->installEntitySchema('block_content');
+
+    // Create a custom block type.
+    BlockContentType::create([
+      'id' => 'test_type',
+      'label' => $this->randomMachineName(),
+    ])->save();
+
+    // Create a text field on the custom block type.
+    $storage = FieldStorageConfig::create([
+      'entity_type' => 'block_content',
+      'type' => 'text',
+      'field_name' => 'body',
+    ]);
+    $storage->save();
+
+    FieldConfig::create([
+      'bundle' => 'test_type',
+      'field_storage' => $storage,
+    ])->save();
+  }
+
+  /**
+   * Tests that shadow blocks can transparently load real blocks.
+   */
+  public function testLoadBlockContentByShadow() {
+    $original = CoreBlockContent::create([
+      'type' => 'test_type',
+      'info' => $this->randomMachineName(),
+      'body' => 'Pasta ipsum dolor sit amet tripoline farfalle croxetti mezzelune spirali manicotti ditalini spaghetti calamarata ricciolini ciriole spirali cencioni.',
+    ]);
+    $original->save();
+
+    $id = $original->id();
+
+    // Test static loading.
+    $this->assertShadow($original, ShadowBlockContent::load($id));
+
+    // Test container-ey loading.
+    $shadow = \Drupal::entityTypeManager()
+      ->getStorage('panelizer_block_content')
+      ->load($id);
+    $this->assertShadow($original, $shadow);
+
+    // Test static multiple loading.
+    $shadows = ShadowBlockContent::loadMultiple([$id]);
+    $this->assertShadow($original, reset($shadows));
+
+    // Test container-ey multiple loading.
+    $shadows = \Drupal::entityTypeManager()
+      ->getStorage('panelizer_block_content')
+      ->loadMultiple([$id]);
+    $this->assertShadow($original, reset($shadows));
+  }
+
+  /**
+   * Tests that shadow blocks can be created like normal custom blocks.
+   */
+  public function testCreateShadow() {
+    $values = [
+      'type' => 'test_type',
+      'info' => $this->randomMachineName(),
+    ];
+
+    // Test static creation.
+    $block = ShadowBlockContent::create($values);
+    $this->assertInstanceOf(ShadowBlockContent::class, $block);
+    $this->assertSame('block_content', $block->getEntityTypeId());
+
+    // Test container-ey creation.
+    $block = \Drupal::entityTypeManager()
+      ->getStorage('panelizer_block_content')
+      ->create($values);
+    $this->assertInstanceOf(ShadowBlockContent::class, $block);
+    $this->assertSame('block_content', $block->getEntityTypeId());
+  }
+
+  /**
+   * @depends testCreateShadow
+   */
+  public function testNullSave() {
+    $block = ShadowBlockContent::create([
+      'type' => 'test_type',
+      'info' => $this->randomMachineName(),
+      'body' => 'Creste di galli spaghettoni trennette pennette trennette penne zita linguettine orecchiette pappardelle gramigna casarecce.',
+    ]);
+    $block->save();
+    // Shadow blocks don't write anything to the database, so there should be
+    // no ID. They are always considered new (and therefore cannot be deleted).
+    $this->assertEmpty($block->id());
+    $this->assertTrue($block->isNew());
+
+    // Ensure that no custom blocks were actually created.
+    $count = \Drupal::entityQuery('block_content')
+      ->count()
+      ->execute();
+    $this->assertNumeric($count);
+    $this->assertEmpty($count);
+
+    // Nothing should be written to the field tables.
+    $count = \Drupal::database()
+      ->select('block_content__body')
+      ->countQuery()
+      ->execute()
+      ->fetchField();
+    $this->assertNumeric($count);
+    $this->assertEmpty($count);
+
+    $count = \Drupal::database()
+      ->select('block_content_revision__body')
+      ->countQuery()
+      ->execute()
+      ->fetchField();
+    $this->assertNumeric($count);
+    $this->assertEmpty($count);
+
+    // No shadow blocks should be written into the database.
+    $count = \Drupal::entityQuery('panelizer_block_content')
+      ->count()
+      ->execute();
+    $this->assertNumeric($count);
+    $this->assertEmpty($count);
+  }
+
+  /**
+   * @depends testLoadBlockContentByShadow
+   */
+  public function testDeleteBlockContentByShadow() {
+    $original = CoreBlockContent::create([
+      'type' => 'test_type',
+      'info' => $this->randomMachineName(),
+      'body' => 'Pasta ipsum dolor sit amet tripoline farfalle croxetti mezzelune spirali manicotti ditalini spaghetti calamarata ricciolini ciriole spirali cencioni.',
+    ]);
+    $original->save();
+
+    $id = $original->id();
+    ShadowBlockContent::load($id)->delete();
+
+    $this->assertNull(CoreBlockContent::load($id));
+    $this->assertNull(ShadowBlockContent::load($id));
+  }
+
+  /**
+   * Asserts that a value is numeric.
+   *
+   * @param mixed $value
+   *   The value to check.
+   */
+  protected function assertNumeric($value) {
+    $this->assertTrue(is_numeric($value));
+  }
+
+  /**
+   * Asserts that a shadow block and standard custom block are identical.
+   *
+   * @param \Drupal\block_content\BlockContentInterface $original
+   *   The original standard custom block.
+   * @param \Drupal\block_content\BlockContentInterface $shadow
+   *   The shadow block.
+   */
+  protected function assertShadow(BlockContentInterface $original, BlockContentInterface $shadow) {
+    $this->assertInstanceOf(ShadowBlockContent::class, $shadow);
+    // Shadow blocks should masquerade as normal custom blocks.
+    $this->assertSame('block_content', $shadow->getEntityTypeId());
+    $this->assertSame($original->isNew(), $shadow->isNew());
+    $this->assertSame($original->id(), $shadow->id());
+    $this->assertSame($original->label(), $shadow->label());
+
+    // Shadow blocks should have all custom block fields and load their values.
+    $this->assertTrue($shadow->hasField('body'));
+    $this->assertSame($original->body->value, $shadow->body->value);
+  }
+
+}
