diff --git a/core/modules/editor/editor.module b/core/modules/editor/editor.module
index f3bb1b5314..d704dd06a6 100644
--- a/core/modules/editor/editor.module
+++ b/core/modules/editor/editor.module
@@ -6,6 +6,7 @@
  */
 
 use Drupal\Component\Utility\Html;
+use Drupal\Core\Entity\Entity\EntityFormMode;
 use Drupal\editor\Entity\Editor;
 use Drupal\Core\Entity\FieldableEntityInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
@@ -16,6 +17,8 @@
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\filter\FilterFormatInterface;
 use Drupal\filter\Plugin\FilterInterface;
+use Drupal\media\Entity\Media;
+use Drupal\media\Entity\MediaType;
 
 /**
  * Implements hook_help().
@@ -252,6 +255,56 @@ function editor_form_filter_admin_format_submit($form, FormStateInterface $form_
   }
 }
 
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function editor_form_media_type_add_form_alter(&$form, &$form_state, $form_id) {
+  // Add this submit function to the submit _button_ rather than the form's
+  // '#submit' array, because the latter approach loses our submit function when
+  // the form is refreshed via ajax (e.g. when you have to choose a source field
+  // for your media type).
+  $form['actions']['submit']['#submit'][] = '_editor_add_media_embed_display';
+}
+
+/**
+ * A submit function added to the form that adds a media type. This will create
+ * a form display, containing only this media type's required fields, to be used
+ * when a user embeds media in the editor.
+ *
+ * @param array $form
+ *  The media-type-addition form.
+ *
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ *  That form's state.
+ *
+ * @internal
+ */
+function _editor_add_media_embed_display(array &$form, FormStateInterface $form_state) {
+  $entity_type = 'media';
+  $bundle = $form_state->getValue('id');
+  $form_mode = 'editor_embed';
+
+  // Create and enable the form display for this new media type, using the
+  // editor_embed form mode created during Editor module install.
+  $form_display = entity_get_form_display($entity_type, $bundle, $form_mode);
+
+  // Show only the required fields; hide all others.
+  /** @var \Drupal\Core\Field\BaseFieldDefinition $field_definition */
+  foreach (\Drupal::service('entity_field.manager')->getFieldDefinitions($entity_type, $bundle) as $field_definition) {
+    $field_name = $field_definition->getName();
+    if ($field_definition->isRequired()) {
+      $form_display->setComponent($field_name, []);
+    }
+    else {
+      $form_display->removeComponent($field_name);
+    }
+  }
+
+  // Enable and save the cleaned-up form display.
+  $form_display->set('status', TRUE);
+  $form_display->save();
+}
+
 /**
  * Loads an individual configured text editor based on text format ID.
  *
diff --git a/core/modules/media/config/install/core.entity_form_mode.media.editor_embed.yml b/core/modules/media/config/install/core.entity_form_mode.media.editor_embed.yml
new file mode 100644
index 0000000000..96d67e2790
--- /dev/null
+++ b/core/modules/media/config/install/core.entity_form_mode.media.editor_embed.yml
@@ -0,0 +1,10 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - editor
+    - media
+id: media.editor_embed
+label: Editor embed
+targetEntityType: media
+cache: true
diff --git a/core/modules/media/js/plugins/mediaembed/icons/hidpi/mediaembed.png b/core/modules/media/js/plugins/mediaembed/icons/hidpi/mediaembed.png
new file mode 100644
index 0000000000..19f663a7b9
--- /dev/null
+++ b/core/modules/media/js/plugins/mediaembed/icons/hidpi/mediaembed.png
@@ -0,0 +1,8 @@
+PNG
+
+   IHDR           szz  NIDATx^WQL[e>m--k딍VN愕	[ ηafK|ك,1fی:a.[fⓏA1YFm{=ퟛ4HVowwu|Oب#u+ (*)L^czNE*JdfÙZMz"PvOWf ^3gȱgt	OȚ2v:Jgʑm؁,kk~w98>2R*9'ѩESp<l6ܾ-JBөSv"zGWkY)P ۩5 f+KX1pzZ \"癙SS8|=+vۮC5@tQRT*E,,\;mw fVDؙXɎ*x,
+ L$X__t:wjdـ;lV[(<T̐kY+	M!#L>?qY<cm*rKmh@ak2ZOU
+Lq#b\~y4)ݎREs)
+}蓓HYVTT6Rn+XU#P8L;'hD$g5P 6/4l-siטCG"Fojbohztx8nxycV7}zG.O.( %ƙus#za~bQG
+` FLeB׶a7%.EQ	3@9M&<γKKK` q<8gG,71J5wkPhŢYJe y<lȾ}ON/,@{;=ϤmPDOO/^MB^OѧÐ_OxX:Wxo''D<hmxn@^q p$( ՖV,X`rHo\3.9g+U(%\$rAwy.ʑ"+!o(j *e幗NlKPcG{c	4)v^GGG-HHlPPpbs]ʁ9ىgT+aF$=N']@`"*Pf<%İ6Q|Roww\_##'Jg W7^,kן56p׆\<lܻuʳF 8M7o܍2	no6	nn6'k
+z.+B^c'yH}    IENDB`
\ No newline at end of file
diff --git a/core/modules/media/js/plugins/mediaembed/icons/mediaembed.png b/core/modules/media/js/plugins/mediaembed/icons/mediaembed.png
new file mode 100644
index 0000000000..07ddc2e0f3
--- /dev/null
+++ b/core/modules/media/js/plugins/mediaembed/icons/mediaembed.png
@@ -0,0 +1,7 @@
+PNG
+
+   IHDR         (-S   PLTE                                          ZZZ```
+
+
+      			&&&   """###%%%)))***---666888999;;;<<<>>>@@@AAABBBDDDGGGIIIJJJKKKLLLNNNOOOPPPQQQRRRUUUYYYw    tRNS 
+!*LPU`tv~=K   IDATW` W*2KVG]yO HjZ e3 bOxt@+&0RՊX{]Ofbpٻyt<og۸u۶WC@C׾=t )7OSw:z    IENDB`
\ No newline at end of file
diff --git a/core/modules/media/js/plugins/mediaembed/plugin.js b/core/modules/media/js/plugins/mediaembed/plugin.js
new file mode 100644
index 0000000000..ac72b94aab
--- /dev/null
+++ b/core/modules/media/js/plugins/mediaembed/plugin.js
@@ -0,0 +1,226 @@
+/**
+ * DO NOT EDIT THIS FILE.
+ * See the following change record for more information,
+ * https://www.drupal.org/node/2815083
+ * @preserve
+ **/
+
+(function ($, Drupal, CKEDITOR) {
+
+  "use strict";
+
+  CKEDITOR.plugins.add('mediaembed', {
+    // This plugin requires the Widgets System defined in the 'widget' plugin.
+    requires: 'widget',
+
+    // The plugin initialization logic goes inside this method.
+    beforeInit: function (editor) {
+      // Configure CKEditor DTD for custom drupal-entity element.
+      // @see https://www.drupal.org/node/2448449#comment-9717735
+      var dtd = CKEDITOR.dtd, tagName;
+      dtd['drupal-entity'] = {'#': 1};
+      // Register drupal-entity element as allowed child, in each tag that can
+      // contain a div element.
+      for (tagName in dtd) {
+        if (dtd[tagName].div) {
+          dtd[tagName]['drupal-entity'] = 1;
+        }
+      }
+
+      // Generic command for adding/editing entities of all types.
+      editor.addCommand('editdrupalentity', {
+        allowedContent: 'drupal-entity[data-embed-button,data-entity-type,data-entity-uuid,data-entity-embed-display,data-entity-embed-display-settings,data-align,data-caption]',
+        requiredContent: 'drupal-entity[data-embed-button,data-entity-type,data-entity-uuid,data-entity-embed-display,data-entity-embed-display-settings,data-align,data-caption]',
+        modes: { wysiwyg : 1 },
+        canUndo: true,
+        exec: function (editor, data) {
+          data = data || {};
+
+          var existingElement = getSelectedEmbeddedEntity(editor);
+
+          var existingValues = {};
+          if (existingElement && existingElement.$ && existingElement.$.firstChild) {
+            var embedDOMElement = existingElement.$.firstChild;
+            // Populate array with the entity's current attributes.
+            var attribute = null, attributeName;
+            for (var key = 0; key < embedDOMElement.attributes.length; key++) {
+              attribute = embedDOMElement.attributes.item(key);
+              attributeName = attribute.nodeName.toLowerCase();
+              if (attributeName.substring(0, 15) === 'data-cke-saved-') {
+                continue;
+              }
+              existingValues[attributeName] = existingElement.data('cke-saved-' + attributeName) || attribute.nodeValue;
+            }
+          }
+
+          var embed_button_id = data.id ? data.id : existingValues['data-embed-button'];
+
+          var dialogSettings = {
+            dialogClass: 'entity-select-dialog',
+            resizable: false
+          };
+
+          var saveCallback = function (values) {
+            var entityElement = editor.document.createElement('drupal-entity');
+            var attributes = values.attributes;
+            for (var key in attributes) {
+              entityElement.setAttribute(key, attributes[key]);
+            }
+
+            editor.insertHtml(entityElement.getOuterHtml());
+            if (existingElement) {
+              // Detach the behaviors that were attached when the entity content
+              // was inserted.
+              Drupal.runEmbedBehaviors('detach', existingElement.$);
+              existingElement.remove();
+            }
+          };
+
+          // Open the entity embed dialog for corresponding EmbedButton.
+          Drupal.ckeditor.openDialog(editor, Drupal.url('media/dialog/' + editor.config.drupal.format + '/' + embed_button_id), existingValues, saveCallback, dialogSettings);
+        }
+      });
+
+      // Register the entity embed widget.
+      editor.widgets.add('drupalentity', {
+        // Minimum HTML which is required by this widget to work.
+        allowedContent: 'drupal-entity[data-entity-type,data-entity-uuid,data-entity-embed-display,data-entity-embed-display-settings,data-align,data-caption]',
+        requiredContent: 'drupal-entity[data-entity-type,data-entity-uuid,data-entity-embed-display,data-entity-embed-display-settings,data-align,data-caption]',
+
+        // Simply recognize the element as our own. The inner markup if fetched
+        // and inserted the init() callback, since it requires the actual DOM
+        // element.
+        upcast: function (element) {
+          var attributes = element.attributes;
+          if (attributes['data-entity-type'] === undefined || (attributes['data-entity-id'] === undefined && attributes['data-entity-uuid'] === undefined) || (attributes['data-view-mode'] === undefined && attributes['data-entity-embed-display'] === undefined)) {
+            return;
+          }
+          // Generate an ID for the element, so that we can use the Ajax
+          // framework.
+          element.attributes.id = generateEmbedId();
+          return element;
+        },
+
+        // Fetch the rendered entity.
+        init: function () {
+          /** @type {CKEDITOR.dom.element} */
+          var element = this.element;
+          // Use the Ajax framework to fetch the HTML, so that we can retrieve
+          // out-of-band assets (JS, CSS...).
+          var entityEmbedPreview = Drupal.ajax({
+            base: element.getId(),
+            element: element.$,
+            url: Drupal.url('embed/preview/' + editor.config.drupal.format + '?' + $.param({
+              value: element.getOuterHtml()
+            })),
+            progress: {type: 'none'},
+            // Use a custom event to trigger the call.
+            event: 'entity_embed_dummy_event'
+          });
+          entityEmbedPreview.execute();
+        },
+
+        // Downcast the element.
+        downcast: function (element) {
+          // Only keep the wrapping element.
+          element.setHtml('');
+          // Remove the auto-generated ID.
+          delete element.attributes.id;
+          return element;
+        }
+      });
+
+      // Register the toolbar buttons.
+      if (editor.ui.addButton) {
+        for (var key in editor.config.MediaEmbed_buttons) {
+          var button = editor.config.MediaEmbed_buttons[key];
+          editor.ui.addButton(button.id, {
+            label: button.label,
+            data: button,
+            allowedContent: 'drupal-entity[!data-entity-type,!data-entity-uuid,!data-entity-embed-display,!data-entity-embed-display-settings,!data-align,!data-caption,!data-embed-button]',
+            click: function(editor) {
+              editor.execCommand('editdrupalentity', this.data);
+            },
+            icon: button.image
+          });
+        }
+      }
+
+      // Register context menu option for editing widget.
+      if (editor.contextMenu) {
+        editor.addMenuGroup('drupalentity');
+        editor.addMenuItem('drupalentity', {
+          label: Drupal.t('Edit Entity'),
+          icon: this.path + 'entity.png',
+          command: 'editdrupalentity',
+          group: 'drupalentity'
+        });
+
+        editor.contextMenu.addListener(function(element) {
+          if (isEditableEntityWidget(editor, element)) {
+            return { drupalentity: CKEDITOR.TRISTATE_OFF };
+          }
+        });
+      }
+
+      // Execute widget editing action on double click.
+      editor.on('doubleclick', function (evt) {
+        var element = getSelectedEmbeddedEntity(editor) || evt.data.element;
+
+        if (isEditableEntityWidget(editor, element)) {
+          editor.execCommand('editdrupalentity');
+        }
+      });
+    }
+  });
+
+  /**
+   * Get the surrounding drupalentity widget element.
+   *
+   * @param {CKEDITOR.editor} editor
+   */
+  function getSelectedEmbeddedEntity(editor) {
+    var selection = editor.getSelection();
+    var selectedElement = selection.getSelectedElement();
+    if (isEditableEntityWidget(editor, selectedElement)) {
+      return selectedElement;
+    }
+
+    return null;
+  }
+
+  /**
+   * Checks if the given element is an editable drupalentity widget.
+   *
+   * @param {CKEDITOR.editor} editor
+   * @param {CKEDITOR.htmlParser.element} element
+   */
+  function isEditableEntityWidget (editor, element) {
+    var widget = editor.widgets.getByElement(element, true);
+    if (!widget || widget.name !== 'drupalentity') {
+      return false;
+    }
+
+    var button = $(element.$.firstChild).attr('data-embed-button');
+    if (!button) {
+      // If there was no data-embed-button attribute, not editable.
+      return false;
+    }
+
+    // The button itself must be valid.
+    return editor.config.DrupalEntity_buttons.hasOwnProperty(button);
+  }
+
+  /**
+   * Generates unique HTML IDs for the widgets.
+   *
+   * @returns {string}
+   */
+  function generateEmbedId() {
+    if (typeof generateEmbedId.counter == 'undefined') {
+      generateEmbedId.counter = 0;
+    }
+    return 'entity-embed-' + generateEmbedId.counter++;
+  }
+
+})(jQuery, Drupal, CKEDITOR);
diff --git a/core/modules/media/media.module b/core/modules/media/media.module
index 5df059c284..b8fd8ac845 100644
--- a/core/modules/media/media.module
+++ b/core/modules/media/media.module
@@ -44,6 +44,9 @@ function media_theme() {
     'media' => [
       'render element' => 'elements',
     ],
+    'media_embed' => [
+      'render element' => 'element',
+    ],
   ];
 }
 
@@ -115,3 +118,19 @@ function media_field_ui_preconfigured_options_alter(array &$options, $field_type
     $options['media']['entity_view_display']['type'] = 'entity_reference_entity_view';
   }
 }
+
+/**
+ * Prepares variables for the media embed template.
+ *
+ * Default template: media-embed.html.twig.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #attributes, #children.
+ */
+ function template_preprocess_media_embed(&$variables) {
+   $variables['element'] += ['#attributes' => []];
+   $variables['attributes'] = $variables['element']['#attributes'];
+   $variables['children'] = $variables['element']['#children'];
+ }
diff --git a/core/modules/media/media.routing.yml b/core/modules/media/media.routing.yml
index 9fbadeff29..b09d07a6e7 100644
--- a/core/modules/media/media.routing.yml
+++ b/core/modules/media/media.routing.yml
@@ -19,3 +19,13 @@ entity.media.revision:
   requirements:
     _access_media_revision: 'view'
     media: \d+
+
+media.embed_dialog:
+  path: '/media/dialog/{editor}/{media_type}'
+  defaults:
+    _controller: '\Drupal\media\Controller\MediaEmbedDialog::form'
+    _title: 'Embed entity'
+  requirements:
+    _permission: 'update any media'
+  options:
+    _theme: ajax_base_page
diff --git a/core/modules/media/src/Controller/MediaEmbedDialog.php b/core/modules/media/src/Controller/MediaEmbedDialog.php
new file mode 100644
index 0000000000..8e1f558ec5
--- /dev/null
+++ b/core/modules/media/src/Controller/MediaEmbedDialog.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Drupal\media\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Routing\RequestContext;
+use Drupal\media\Entity\Media;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+class MediaEmbedDialog extends ControllerBase {
+
+  public function form($editor, $media_type) {
+    $entity = Media::create(['bundle' => $media_type, 'uid' => $this->currentUser()->id()]);
+    $form = $this->entityFormBuilder()->getForm($entity, 'editor_embed');
+    return $form;
+  }
+
+}
diff --git a/core/modules/media/src/Entity/Media.php b/core/modules/media/src/Entity/Media.php
index dc8e0dcf0f..6313e0e0d0 100644
--- a/core/modules/media/src/Entity/Media.php
+++ b/core/modules/media/src/Entity/Media.php
@@ -38,6 +38,7 @@
  *       "add" = "Drupal\media\MediaForm",
  *       "edit" = "Drupal\media\MediaForm",
  *       "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm",
+ *       "editor_embed" =  "Drupal\media\Form\MediaFormEmbed",
  *     },
  *     "translation" = "Drupal\content_translation\ContentTranslationHandler",
  *     "views_data" = "Drupal\media\MediaViewsData",
diff --git a/core/modules/media/src/Exception/MediaNotFoundException.php b/core/modules/media/src/Exception/MediaNotFoundException.php
new file mode 100644
index 0000000000..201397b572
--- /dev/null
+++ b/core/modules/media/src/Exception/MediaNotFoundException.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace Drupal\media\Exception;
+
+/**
+ * Exception thrown when the embedded entity cannot be loaded.
+ */
+class MediaNotFoundException extends \Exception {}
diff --git a/core/modules/media/src/Exception/RecursiveRenderingException.php b/core/modules/media/src/Exception/RecursiveRenderingException.php
new file mode 100644
index 0000000000..e30ed0c2d4
--- /dev/null
+++ b/core/modules/media/src/Exception/RecursiveRenderingException.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace Drupal\media\Exception;
+
+/**
+ * Exception thrown when the embedded entity causes recursion when rendering.
+ */
+class RecursiveRenderingException extends \Exception {}
diff --git a/core/modules/media/src/Form/MediaFormEmbed.php b/core/modules/media/src/Form/MediaFormEmbed.php
new file mode 100644
index 0000000000..89b471d08d
--- /dev/null
+++ b/core/modules/media/src/Form/MediaFormEmbed.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace Drupal\media\Form;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\CloseModalDialogCommand;
+use Drupal\Core\Ajax\HtmlCommand;
+use Drupal\Core\Ajax\SetDialogTitleCommand;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\editor\Ajax\EditorDialogSave;
+use Drupal\media\MediaForm;
+
+/**
+ * Form controller for the media embed add/edit forms.
+ *
+ * @internal
+ */
+class MediaFormEmbed extends MediaForm {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form = parent::form($form, $form_state);
+    $form['#tree'] = TRUE;
+    $form['#attached']['library'][] = 'editor/drupal.editor.dialog';
+    $form['#ajax'] = [
+      'callback' => '::ajaxFormRebuild',
+    ];
+    $form['#prefix'] = '<div id="media-embed-dialog-form">';
+    $form['#suffix'] = '</div>';
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function ajaxFormRebuild($form, FormStateInterface $form_state) {
+
+    $response = new AjaxResponse();
+
+    // Display errors in form, if any.
+    if ($form_state->hasAnyErrors()) {
+      unset($form['#prefix'], $form['#suffix']);
+      $form['status_messages'] = array(
+        '#type' => 'status_messages',
+        '#weight' => -10,
+      );
+      $response->addCommand(new HtmlCommand('#media-embed-dialog-form', $form));
+    }
+    else {
+      // Embed the entity element in the editor and close the dialog.
+      $values = $form_state->getValues();
+      $values['attributes'] = [
+        'data-embed-button' => 'media',
+        'data-entity-embed-display' => 'view_mode:media.full',
+        'data-entity-type' => 'media',
+        'data-entity-uuid' => $this->getEntity()->uuid(),
+      ];
+
+      // Filter out empty attributes.
+      $values['attributes'] = array_filter($values['attributes'], function($value) {
+        return (bool) Unicode::strlen((string) $value);
+      });
+
+      $response->addCommand(new EditorDialogSave($values));
+      $response->addCommand(new CloseModalDialogCommand());
+    }
+
+    return $response;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function actionsElement(array $form, FormStateInterface $form_state) {
+    $element = parent::actionsElement($form, $form_state);
+    $element['submit']['#ajax'] = [
+      'callback' => '::ajaxFormRebuild',
+    ];
+    return $element;
+  }
+
+}
diff --git a/core/modules/media/src/Plugin/CKEditorPlugin/MediaEmbed.php b/core/modules/media/src/Plugin/CKEditorPlugin/MediaEmbed.php
new file mode 100644
index 0000000000..d78519b440
--- /dev/null
+++ b/core/modules/media/src/Plugin/CKEditorPlugin/MediaEmbed.php
@@ -0,0 +1,100 @@
+<?php
+
+namespace Drupal\media\Plugin\CKEditorPlugin;
+
+use Drupal\ckeditor\CKEditorPluginBase;
+use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
+use Drupal\editor\Entity\Editor;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+
+/**
+ * Defines the "mediabutton" plugin.
+ *
+ * @CKEditorPlugin(
+ *   id = "mediaembed",
+ *   label = @Translation("Media Embed"),
+ *   module = "media",
+ * )
+ */
+class MediaEmbed extends CKEditorPluginBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The entity type bundle info service.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
+   */
+  protected $entityTypeBundleInfo;
+
+  /**
+   * The collected media buttons.
+   *
+   * @var array
+   */
+  protected $buttons;
+
+  /**
+   * MediaEmbed constructor.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
+   *   The entity type bundle info service.
+   * @param array $configuration
+   * @param $plugin_id
+   * @param $plugin_definition
+   */
+  public function __construct(EntityTypeBundleInfoInterface $entity_type_bundle_info, array $configuration, $plugin_id, $plugin_definition) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->entityTypeBundleInfo = $entity_type_bundle_info;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $container->get('entity_type.bundle.info'),
+      $configuration,
+      $plugin_id,
+      $plugin_definition
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfig(Editor $editor) {
+    return [
+      'MediaEmbed_dialogTitleAdd' => $this->t("Add Media"),
+      'MediaEmbed_dialogTitleEdit' => $this->t("Edit Media"),
+      'MediaEmbed_buttons' => $this->getButtons(),
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFile() {
+    return drupal_get_path('module', 'media') . '/js/plugins/mediaembed/plugin.js';
+  }
+
+  /**
+   * (@inheritdoc}
+   */
+  public function getButtons() {
+    if ($this->buttons) {
+      return $this->buttons;
+    }
+
+    $buttons = [];
+    foreach ($this->entityTypeBundleInfo->getBundleInfo('media') as $machine_name => $media_type) {
+      $buttons[$machine_name] = [
+        'label' => $media_type['label'],
+        'id' => $machine_name,
+        'image' => base_path() . drupal_get_path('module', 'media') . '/js/plugins/mediaembed/icons/mediaembed.png',
+      ];
+    }
+    $this->buttons = $buttons;
+    return $buttons;
+  }
+
+}
diff --git a/core/modules/media/src/Plugin/Filter/MediaEmbedFilter.php b/core/modules/media/src/Plugin/Filter/MediaEmbedFilter.php
new file mode 100644
index 0000000000..41e3715c18
--- /dev/null
+++ b/core/modules/media/src/Plugin/Filter/MediaEmbedFilter.php
@@ -0,0 +1,291 @@
+<?php
+
+namespace Drupal\media\Plugin\Filter;
+
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Render\RenderContext;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\filter\FilterProcessResult;
+use Drupal\filter\Plugin\FilterBase;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\media\Exception\MediaNotFoundException;
+use Drupal\media\Exception\RecursiveRenderingException;
+
+/**
+ * Provides a filter to display embedded entities based on data attributes.
+ *
+ * @Filter(
+ *   id = "media_embed",
+ *   title = @Translation("Display embedded media entities"),
+ *   description = @Translation("Embeds media entities using data attributes: data-entity-type, data-entity-uuid, and data-view-mode."),
+ *   type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE
+ * )
+ */
+class MediaEmbedFilter extends FilterBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The renderer service.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a MediaEmbedFilter object.
+   *
+   * @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\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->entityTypeManager = $entity_type_manager;
+    $this->renderer = $renderer;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('entity_type.manager'),
+      $container->get('renderer')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function process($text, $langcode) {
+    $result = new FilterProcessResult($text);
+
+    if (strpos($text, 'data-entity-type') !== FALSE && (strpos($text, 'data-entity-embed-display') !== FALSE || strpos($text, 'data-view-mode') !== FALSE)) {
+      $dom = Html::load($text);
+      $xpath = new \DOMXPath($dom);
+
+      foreach ($xpath->query('//drupal-entity[@data-entity-type and (@data-entity-uuid or @data-entity-id) and (@data-entity-embed-display or @data-view-mode)]') as $node) {
+        /** @var \DOMElement $node */
+        $entity_type = $node->getAttribute('data-entity-type');
+        $entity = NULL;
+        $entity_output = '';
+
+        // data-entity-embed-settings is deprecated, make sure we convert it to
+        // data-entity-embed-display-settings.
+        if (($settings = $node->getAttribute('data-entity-embed-settings')) && !$node->hasAttribute('data-entity-embed-display-settings')) {
+          $node->setAttribute('data-entity-embed-display-settings', $settings);
+          $node->removeAttribute('data-entity-embed-settings');
+        }
+
+        try {
+          // Load the entity either by UUID (preferred) or ID.
+          $id = NULL;
+          $entity = NULL;
+          if ($id = $node->getAttribute('data-entity-uuid')) {
+            $entity = $this->entityTypeManager->getStorage($entity_type)
+              ->loadByProperties(['uuid' => $id]);
+            $entity = current($entity);
+          }
+          else {
+            $id = $node->getAttribute('data-entity-id');
+            $entity = $this->entityTypeManager->getStorage($entity_type)->load($id);
+          }
+
+          if ($entity) {
+            // Protect ourselves from recursive rendering.
+            static $depth = 0;
+            $depth++;
+            if ($depth > 20) {
+              throw new RecursiveRenderingException(sprintf('Recursive rendering detected when rendering embedded %s entity %s.', $entity_type, $entity->id()));
+            }
+
+            // If a UUID was not used, but is available, add it to the HTML.
+            if (!$node->getAttribute('data-entity-uuid') && $uuid = $entity->uuid()) {
+              $node->setAttribute('data-entity-uuid', $uuid);
+            }
+
+            $context = $this->getNodeAttributesAsArray($node);
+            $context += array('data-langcode' => $langcode);
+            $build = $this->buildRenderArray($entity, $context);
+            // We need to render the embedded entity:
+            // - without replacing placeholders, so that the placeholders are
+            //   only replaced at the last possible moment. Hence we cannot use
+            //   either renderPlain() or renderRoot(), so we must use render().
+            // - without bubbling beyond this filter, because filters must
+            //   ensure that the bubbleable metadata for the changes they make
+            //   when filtering text makes it onto the FilterProcessResult
+            //   object that they return ($result). To prevent that bubbling, we
+            //   must wrap the call to render() in a render context.
+            $entity_output = $this->renderer->executeInRenderContext(new RenderContext(), function () use (&$build) {
+              return $this->renderer->render($build);
+            });
+            $result = $result->merge(BubbleableMetadata::createFromRenderArray($build));
+
+            $depth--;
+          }
+          else {
+            throw new MediaNotFoundException(sprintf('Unable to load embedded %s media entity %s.', $entity_type, $id));
+          }
+        }
+        catch (\Exception $e) {
+          watchdog_exception('media_embed', $e);
+        }
+
+        $this->replaceNodeContent($node, $entity_output);
+      }
+
+      $result->setProcessedText(Html::serialize($dom));
+    }
+
+    return $result;
+  }
+
+  /**
+   * Build a render array for the media entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity to be rendered.
+   * @param array $context
+   *   (optional) Array of context values, corresponding to the attributes on
+   *   the embed HTML tag.
+   *
+   * @return array
+   *   A render array.
+   */
+  public function buildRenderArray(EntityInterface $entity, array $context = []) {
+    // Merge in default attributes.
+    $context += [
+      'data-entity-type' => $entity->getEntityTypeId(),
+      'data-entity-uuid' => $entity->uuid(),
+      'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view',
+      'data-entity-embed-display-settings' => [],
+    ];
+
+    // The caption text is double-encoded, so decode it here.
+    if (isset($context['data-caption'])) {
+      $context['data-caption'] = Html::decodeEntities($context['data-caption']);
+    }
+
+    // Build and render the Entity Embed Display plugin, allowing modules to
+    // alter the result before rendering.
+    $build = [
+      '#theme_wrappers' => ['media_embed'],
+      '#attributes' => ['class' => ['embedded-media']],
+      '#entity' => $entity,
+      '#context' => $context,
+    ];
+    $build += $this->entityTypeManager->getViewBuilder($entity->getEntityTypeId())->view($entity, 'default');
+    // We're providing embed-specific theming, so don't use media's standard.
+    unset($build['#theme']);
+
+    // Maintain data-align if it is there.
+    if (isset($context['data-align'])) {
+      $build['#attributes']['data-align'] = $context['data-align'];
+    }
+    elseif ((isset($context['class']))) {
+      $build['#attributes']['class'][] = $context['class'];
+    }
+
+    // Maintain data-caption if it is there.
+    if (isset($context['data-caption'])) {
+      $build['#attributes']['data-caption'] = $context['data-caption'];
+    }
+
+    // Make sure that access to the entity is respected.
+    $build['#access'] = $entity->access('view', NULL, TRUE);
+
+    return $build;
+  }
+
+  /**
+   * Replace the contents of a DOMNode.
+   *
+   * @param \DOMNode $node
+   *   A DOMNode object.
+   * @param string $content
+   *   The text or HTML that will replace the contents of $node.
+   */
+  protected function replaceNodeContent(\DOMNode &$node, $content) {
+    if (strlen($content)) {
+      // Load the content into a new DOMDocument and retrieve the DOM nodes.
+      $replacement_nodes = Html::load($content)->getElementsByTagName('body')
+        ->item(0)
+        ->childNodes;
+    }
+    else {
+      $replacement_nodes = [$node->ownerDocument->createTextNode('')];
+    }
+
+    foreach ($replacement_nodes as $replacement_node) {
+      // Import the replacement node from the new DOMDocument into the original
+      // one, importing also the child nodes of the replacement node.
+      $replacement_node = $node->ownerDocument->importNode($replacement_node, TRUE);
+      $node->parentNode->insertBefore($replacement_node, $node);
+    }
+    $node->parentNode->removeChild($node);
+  }
+
+  /**
+   * Convert the attributes on a DOMNode object to an array.
+   *
+   * This will also un-serialize any attribute values stored as JSON.
+   *
+   * @param \DOMNode $node
+   *   A DOMNode object.
+   *
+   * @return array
+   *   The attributes as an associative array, keyed by the attribute names.
+   */
+  public function getNodeAttributesAsArray(\DOMNode $node) {
+    $return = [];
+
+    // Convert the data attributes to the context array.
+    foreach ($node->attributes as $attribute) {
+      $key = $attribute->nodeName;
+      $return[$key] = $attribute->nodeValue;
+
+      // Check for JSON-encoded attributes.
+      $data = json_decode($return[$key], TRUE, 10);
+      if ($data !== NULL && json_last_error() === JSON_ERROR_NONE) {
+        $return[$key] = $data;
+      }
+    }
+
+    return $return;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function tips($long = FALSE) {
+    if ($long) {
+      return $this->t('
+        <p>You can embed entities. Additional properties can be added to the embed tag like data-caption and data-align if supported. Example:</p>
+        <code>&lt;drupal-entity data-entity-type="node" data-entity-uuid="07bf3a2e-1941-4a44-9b02-2d1d7a41ec0e" data-view-mode="teaser" /&gt;</code>');
+    }
+    else {
+      return $this->t('You can embed entities.');
+    }
+  }
+
+}
diff --git a/core/modules/media/templates/media-embed.html.twig b/core/modules/media/templates/media-embed.html.twig
new file mode 100644
index 0000000000..284d2d116c
--- /dev/null
+++ b/core/modules/media/templates/media-embed.html.twig
@@ -0,0 +1,15 @@
+{#
+/**
+ * @file
+ * Default theme implementation of a container used to wrap embedded entities.
+ *
+ * Available variables:
+ * - attributes: HTML attributes for the containing element.
+ * - children: The rendered child elements of the container.
+ *
+ * @see template_preprocess_media_embed()
+ *
+ * @ingroup themeable
+ */
+#}
+<article{{ attributes }}>{{ children }}</article>
diff --git a/core/modules/media/tests/src/Kernel/MediaEmbedFilterTest.php b/core/modules/media/tests/src/Kernel/MediaEmbedFilterTest.php
new file mode 100644
index 0000000000..a3c5d2779c
--- /dev/null
+++ b/core/modules/media/tests/src/Kernel/MediaEmbedFilterTest.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Drupal\Tests\media\Kernel;
+
+use Drupal\filter\FilterPluginCollection;
+use Drupal\media\Entity\Media;
+use Drupal\Component\Utility\Html;
+
+/**
+ * Tests the media content input filter.
+ *
+ * @group media
+ */
+class MediaEmbedFilterTest extends MediaKernelTestBase {
+
+  /**
+   * @var \Drupal\filter\Plugin\FilterInterface[]
+   */
+  protected $filters;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['filter'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $manager = $this->container->get('plugin.manager.filter');
+    $bag = new FilterPluginCollection($manager, []);
+    $this->filters = $bag->getAll();
+  }
+
+  /**
+   * Tests the media embed filter.
+   */
+  public function testMediaEmbedFilter() {
+    // Get media embed filter.
+    $filter = $this->filters['media_embed'];
+
+    // Create test function.
+    $test = function ($input) use ($filter) {
+      // Run $input through $filter.
+      $filtered_media = $filter->process($input, 'und')->getProcessedText();
+      // Extract <img /> tag.
+      $dom = Html::load($filtered_media);
+      $img = $dom->getElementsByTagName('img')[0];
+      $filtered_media_img = $dom->saveHTML($img);
+      return $filtered_media_img;
+    };
+
+    // Create media entity.
+    $media = Media::create(['bundle' => $this->testMediaType->id()]);
+    $media->save();
+    $media_uuid = $media->uuid();
+
+    // Render entity.
+    /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $view_builder */
+    $view_builder = \Drupal::entityTypeManager();
+    $build = $view_builder->getViewBuilder($media->getEntityTypeId())->view($media);
+    /** @var \Drupal\Core\Render\RendererInterface $renderer */
+    $renderer = \Drupal::service('renderer');
+    $rendered_media = $renderer->renderPlain($build)->__toString();
+
+    // Extract <img /> tag.
+    $dom = Html::load($rendered_media);
+    $img = $dom->getElementsByTagName('img')[0];
+    $rendered_media_img = $dom->saveHTML($img);
+
+    // Test filter using data-entity-embed-display attribute.
+    $input = '<drupal-entity data-entity-type="media" data-entity-embed-display="view_mode:image.full" data-entity-uuid="' . $media_uuid . '"></drupal-entity>';
+    $expected = $rendered_media_img;
+    $this->assertSame($expected, $test($input));
+
+    // Test filter using data-view-mode attribute.
+    $input = '<drupal-entity data-entity-type="media" data-view-mode="full" data-entity-uuid="' . $media_uuid . '"></drupal-entity>';
+    $expected = $rendered_media_img;
+    $this->assertSame($expected, $test($input));
+  }
+
+}
