diff --git a/core/modules/media/js/plugins/drupalmedia/plugin.es6.js b/core/modules/media/js/plugins/drupalmedia/plugin.es6.js
index ba7f78735a..f1b61febca 100644
--- a/core/modules/media/js/plugins/drupalmedia/plugin.es6.js
+++ b/core/modules/media/js/plugins/drupalmedia/plugin.es6.js
@@ -163,6 +163,13 @@
           },
         },
 
+        getLabel() {
+          if (this.data.label) {
+            return this.data.label;
+          }
+          return Drupal.t('Embedded media');
+        },
+
         upcast(element, data) {
           const { attributes } = element;
           // This matches the behavior of the corresponding server-side text filter plugin.
@@ -181,6 +188,7 @@
           if (data.hasCaption && data.attributes['data-caption'] === '') {
             data.attributes['data-caption'] = ' ';
           }
+          data.label = null;
           data.link = null;
           if (element.parent.name === 'a') {
             data.link = CKEDITOR.tools.copy(element.parent.attributes);
@@ -496,8 +504,9 @@
               text: this.downcast().getOuterHtml(),
             },
             dataType: 'html',
-            success: previewHtml => {
+            success: (previewHtml, textStatus, jqXhr) => {
               this.element.setHtml(previewHtml);
+              this.setData('label', jqXhr.getResponseHeader('Drupal-Media-Label'));
               callback(this);
             },
             error: () => {
diff --git a/core/modules/media/js/plugins/drupalmedia/plugin.js b/core/modules/media/js/plugins/drupalmedia/plugin.js
index 4b875df35e..c3cafdd58d 100644
--- a/core/modules/media/js/plugins/drupalmedia/plugin.js
+++ b/core/modules/media/js/plugins/drupalmedia/plugin.js
@@ -124,6 +124,12 @@
           }
         },
 
+        getLabel: function getLabel() {
+          if (this.data.label) {
+            return this.data.label;
+          }
+          return Drupal.t('Embedded media');
+        },
         upcast: function upcast(element, data) {
           var attributes = element.attributes;
 
@@ -136,6 +142,7 @@
           if (data.hasCaption && data.attributes['data-caption'] === '') {
             data.attributes['data-caption'] = ' ';
           }
+          data.label = null;
           data.link = null;
           if (element.parent.name === 'a') {
             data.link = CKEDITOR.tools.copy(element.parent.attributes);
@@ -315,8 +322,9 @@
               text: this.downcast().getOuterHtml()
             },
             dataType: 'html',
-            success: function success(previewHtml) {
+            success: function success(previewHtml, textStatus, jqXhr) {
               _this2.element.setHtml(previewHtml);
+              _this2.setData('label', jqXhr.getResponseHeader('Drupal-Media-Label'));
               callback(_this2);
             },
             error: function error() {
diff --git a/core/modules/media/src/Controller/MediaFilterController.php b/core/modules/media/src/Controller/MediaFilterController.php
index 3350b32e78..59c263e1bc 100644
--- a/core/modules/media/src/Controller/MediaFilterController.php
+++ b/core/modules/media/src/Controller/MediaFilterController.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\ContentEntityStorageInterface;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\filter\FilterFormatInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -28,14 +29,24 @@ class MediaFilterController implements ContainerInjectionInterface {
    */
   protected $renderer;
 
+  /**
+   * The media storage.
+   *
+   * @var \Drupal\Core\Entity\ContentEntityStorageInterface
+   */
+  protected $mediaStorage;
+
   /**
    * Constructs an MediaFilterController instance.
    *
    * @param \Drupal\Core\Render\RendererInterface $renderer
    *   The renderer service.
+   * @param \Drupal\Core\Entity\ContentEntityStorageInterface
+   *   The media storage.
    */
-  public function __construct(RendererInterface $renderer) {
+  public function __construct(RendererInterface $renderer, ContentEntityStorageInterface $media_storage) {
     $this->renderer = $renderer;
+    $this->mediaStorage = $media_storage;
   }
 
   /**
@@ -43,7 +54,8 @@ public function __construct(RendererInterface $renderer) {
    */
   public static function create(ContainerInterface $container) {
     return new static(
-      $container->get('renderer')
+      $container->get('renderer'),
+      $container->get('entity_type.manager')->getStorage('media')
     );
   }
 
@@ -81,12 +93,21 @@ public function preview(Request $request, FilterFormatInterface $filter_format)
     ];
     $html = $this->renderer->renderPlain($build);
 
+    // Use the bubbled cache tags to determine which media was rendered (if any)
+    // so we can embed the label in the response, for use in an ARIA label.
+    $headers = [];
+    $media_cache_tags = array_filter($build['#cache']['tags'], function ($t) { return strpos($t, 'media:') === 0;});
+    if (!empty($media_cache_tags)) {
+      $embedded_media_id = substr(reset($media_cache_tags), 6);
+      $headers['Drupal-Media-Label'] = $this->mediaStorage->load($embedded_media_id)->label();
+    }
+
     // Note that we intentionally do not use:
     // - \Drupal\Core\Cache\CacheableResponse because caching it on the server
     //   side is wasteful, hence there is no need for cacheability metadata.
     // - \Drupal\Core\Render\HtmlResponse because there is no need for
     //   attachments nor cacheability metadata.
-    return (new Response($html))
+    return (new Response($html, 200, $headers))
       // Do not allow any intermediary to cache the response, only the end user.
       ->setPrivate()
       // Allow the end user to cache it for up to 5 minutes.
diff --git a/core/modules/media/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php b/core/modules/media/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php
index 071b4121c8..5bd2990e40 100644
--- a/core/modules/media/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php
+++ b/core/modules/media/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php
@@ -243,14 +243,13 @@ public function testPreviewUsesDefaultThemeAndIsClientCacheable() {
     $this->assertNotEmpty($assert_session->waitForElementVisible('css', 'img[src*="image-test.png"]'));
     $element = $assert_session->elementExists('css', '[data-media-embed-test-active-theme]');
     $this->assertSame('stable', $element->getAttribute('data-media-embed-test-active-theme'));
-
     // Assert that the first preview request transferred >500 B over the wire.
     // Then toggle source mode on and off. This causes the CKEditor widget to be
     // destroyed and then reconstructed. Assert that during this reconstruction,
     // a second request is sent. This second request should have transferred 0
     // bytes: the browser should have cached the response, thus resulting in a
     // much better user experience.
-    $this->assertGreaterThan(500, $this->getLastPreviewRequestTransferSize());
+    $this->assertGreaterThan(500, $this->getInitialPreviewRequestTransferSize());
     $this->pressEditorButton('source');
     $this->assertNotEmpty($assert_session->waitForElement('css', 'textarea.cke_source'));
     $this->pressEditorButton('source');
@@ -1352,6 +1351,26 @@ protected function closeDialog() {
     $this->assertTrue($result);
   }
 
+  /**
+   * Gets the transfer size of the initial preview request.
+   *
+   * @return int
+   *   The size of the bytes transferred.
+   */
+  protected function getInitialPreviewRequestTransferSize() {
+    $this->getSession()->switchToIFrame();
+    $javascript = <<<JS
+(function(){
+  return window.performance
+    .getEntries()
+    .filter(function (entry) {
+      return entry.initiatorType == 'xmlhttprequest' && entry.name.indexOf('/media/test_format/preview') !== -1 && entry.nextHopProtocol == 'http/1.1';
+    })[0].transferSize;
+})()
+JS;
+    return $this->getSession()->evaluateScript($javascript);
+  }
+
   /**
    * Gets the transfer size of the last preview request.
    *
