diff --git a/core/modules/media/js/plugins/drupalmedia/plugin.es6.js b/core/modules/media/js/plugins/drupalmedia/plugin.es6.js
index 31bfe22d3b..a6b96894d9 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);
@@ -459,6 +467,9 @@
           const dataToHash = CKEDITOR.tools.clone(data);
           // The caption does not need rendering.
           delete dataToHash.attributes['data-caption'];
+          // The media entity's label is server-side data and cannot be
+          // modified by the content author.
+          delete dataToHash.label;
           // Changed link destinations do not affect the visual preview.
           if (dataToHash.link) {
             delete dataToHash.link.href;
@@ -485,8 +496,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 f95d963d7f..57726031d5 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);
@@ -299,6 +306,8 @@
 
           delete dataToHash.attributes['data-caption'];
 
+          delete dataToHash.label;
+
           if (dataToHash.link) {
             delete dataToHash.link.href;
           }
@@ -313,8 +322,9 @@
               text: this.downcast().getOuterHtml()
             },
             dataType: 'html',
-            success: function success(previewHtml) {
+            success: function success(previewHtml, textStatus, jqXhr) {
               _this3.element.setHtml(previewHtml);
+              _this3.setData('label', jqXhr.getResponseHeader('Drupal-Media-Label'));
               callback(_this3);
             },
             error: function error() {
diff --git a/core/modules/media/src/Controller/MediaFilterController.php b/core/modules/media/src/Controller/MediaFilterController.php
index 3350b32e78..a2061a8c50 100644
--- a/core/modules/media/src/Controller/MediaFilterController.php
+++ b/core/modules/media/src/Controller/MediaFilterController.php
@@ -2,10 +2,14 @@
 
 namespace Drupal\media\Controller;
 
+use Drupal\Component\Utility\Html;
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\ContentEntityStorageInterface;
+use Drupal\Core\Entity\EntityRepositoryInterface;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\filter\FilterFormatInterface;
+use Drupal\media\MediaInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
@@ -28,14 +32,34 @@ class MediaFilterController implements ContainerInjectionInterface {
    */
   protected $renderer;
 
+  /**
+   * The media storage.
+   *
+   * @var \Drupal\Core\Entity\ContentEntityStorageInterface
+   */
+  protected $mediaStorage;
+
+  /**
+   * The entity repository.
+   *
+   * @var \Drupal\Core\Entity\EntityRepositoryInterface
+   */
+  protected $entityRepository;
+
   /**
    * Constructs an MediaFilterController instance.
    *
    * @param \Drupal\Core\Render\RendererInterface $renderer
    *   The renderer service.
+   * @param \Drupal\Core\Entity\ContentEntityStorageInterface
+   *   The media storage.
+   * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
+   *   The entity repository.
    */
-  public function __construct(RendererInterface $renderer) {
+  public function __construct(RendererInterface $renderer, ContentEntityStorageInterface $media_storage, EntityRepositoryInterface $entity_repository) {
     $this->renderer = $renderer;
+    $this->mediaStorage = $media_storage;
+    $this->entityRepository = $entity_repository;
   }
 
   /**
@@ -43,7 +67,9 @@ 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'),
+      $container->get('entity.repository')
     );
   }
 
@@ -81,12 +107,16 @@ public function preview(Request $request, FilterFormatInterface $filter_format)
     ];
     $html = $this->renderer->renderPlain($build);
 
+    if ($label = $this->getAriaLabel($text)) {
+      $headers['Drupal-Media-Label'] = $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.
@@ -108,4 +138,37 @@ public static function formatUsesMediaEmbedFilter(FilterFormatInterface $filter_
       ->addCacheableDependency($filter_format);
   }
 
+  /**
+   * Get the translated media label.
+   *
+   * @param string $text
+   *   The text from which to extract the media label.
+   *
+   * @return string|null
+   *   The translated media label or NULL if unable to load media.
+   */
+  public function getAriaLabel($text) {
+    if (stristr($text, '<drupal-media') === FALSE) {
+      return NULL;
+    }
+
+    $dom = Html::load($text);
+    $xpath = new \DOMXPath($dom);
+
+    foreach ($xpath->query('//drupal-media[@data-entity-type="media" and normalize-space(@data-entity-uuid)!=""]') as $node) {
+      /** @var \DOMElement $node */
+      $uuid = $node->getAttribute('data-entity-uuid');
+      $media = $this->entityRepository->loadEntityByUuid('media', $uuid);
+      assert($media === NULL || $media instanceof MediaInterface);
+      if (!$media) {
+        return NULL;
+      }
+      else {
+        return $this->entityRepository->getTranslationFromContext($media)->label();
+      }
+    }
+
+    return NULL;
+  }
+
 }
diff --git a/core/modules/media/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php b/core/modules/media/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php
index 1e651dbc98..efc7b7b8de 100644
--- a/core/modules/media/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php
+++ b/core/modules/media/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php
@@ -243,7 +243,6 @@ 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,
@@ -276,6 +275,8 @@ public function testEditableCaption() {
     $this->assignNameToCkeditorIframe();
     $this->getSession()->switchToIFrame('ckeditor');
     $this->assertNotEmpty($assert_session->waitForButton('Edit media'));
+    // Test `aria-label` attribute appears on the widget wrapper.
+    $assert_session->elementExists('css', '.cke_widget_drupalmedia[aria-label="Screaming hairy armadillo"]');
     $assert_session->elementContains('css', 'figcaption', '');
     $assert_session->elementAttributeContains('css', 'figcaption', 'data-placeholder', 'Enter caption here');
     // Test if you leave the caption blank, but change another attribute,
@@ -465,6 +466,8 @@ public function testDialogAccess() {
     $this->assignNameToCkeditorIframe();
     $this->getSession()->switchToIFrame('ckeditor');
     $this->assertNotEmpty($assert_session->waitForElementVisible('css', 'drupal-media', 2000));
+    // Test `aria-label` attribute appears on the widget wrapper.
+    $assert_session->elementExists('css', '.cke_widget_drupalmedia[aria-label="Screaming hairy armadillo"]');
     $page->pressButton('Edit media');
     $this->waitForMetadataDialog();
     $assert_session->fieldNotExists('attributes[alt]');
@@ -625,6 +628,8 @@ public function testAlt() {
     // Assert that the img within the media embed within the CKEditor contains
     // the overridden alt text set in the dialog.
     $this->assertNotEmpty($assert_session->waitForElementVisible('css', 'drupal-media img[alt*="' . $who_is_zartan . '"]'));
+    // Test `aria-label` attribute appears on the widget wrapper.
+    $assert_session->elementExists('css', '.cke_widget_drupalmedia[aria-label="Screaming hairy armadillo"]');
 
     // Test that the downcast drupal-media element now has the alt attribute
     // entered in the dialog.
@@ -759,6 +764,8 @@ public function testTranslationAlt() {
 
     // Test that the default alt attribute displays without an override.
     $this->assertNotEmpty($assert_session->waitForElementVisible('xpath', '//img[contains(@alt, "texte alternatif par défaut")]'));
+    // Test `aria-label` attribute appears on the widget wrapper.
+    $assert_session->elementExists('css', '.cke_widget_drupalmedia[aria-label="Tatou poilu hurlant"]');
     $page->pressButton('Edit media');
     $this->waitForMetadataDialog();
     // Assert that the placeholder is set to the value of the media field's
@@ -1135,6 +1142,7 @@ public function testViewMode() {
     $this->assignNameToCkeditorIframe();
     $this->getSession()->switchToIFrame('ckeditor');
     $this->assertNotEmpty($assert_session->waitForElementVisible('css', 'drupal-media'));
+    $assert_session->elementExists('css', '.cke_widget_drupalmedia[aria-label="Screaming hairy armadillo"]');
     $page->pressButton('Edit media');
     $this->waitForMetadataDialog();
     $assert_session->optionExists('attributes[data-view-mode]', 'view_mode_1');
