diff --git a/core/modules/media_library/css/media_library.theme.css b/core/modules/media_library/css/media_library.theme.css
index 0e066d8887483d71fc5bf9350404f3d0298ed011..baf0704167e3c06b58f000a3f30ee441ae292dca 100644
--- a/core/modules/media_library/css/media_library.theme.css
+++ b/core/modules/media_library/css/media_library.theme.css
@@ -435,6 +435,7 @@
  *   seven theme in https://www.drupal.org/project/drupal/issues/2980769
  */
 .button.media-library-open-button {
+  margin-bottom: 1em;
   margin-left: 0; /* LTR */
 }
 [dir="rtl"] .button.media-library-open-button {
diff --git a/core/modules/media_library/js/media_library.widget.es6.js b/core/modules/media_library/js/media_library.widget.es6.js
index 93878a0f6a34297ae7eba7104e04fb84fae67460..b34a863d15fbaea2f1c30fa98edbd28cd1fb98c8 100644
--- a/core/modules/media_library/js/media_library.widget.es6.js
+++ b/core/modules/media_library/js/media_library.widget.es6.js
@@ -70,4 +70,34 @@
         .hide();
     },
   };
+
+  /**
+   * Disable the open button when the user is not allowed to add more items.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches behavior to disable the media library open button.
+   */
+  Drupal.behaviors.MediaLibraryWidgetDisableButton = {
+    attach(context) {
+      // When the user returns from the modal to the widget, we want to shift
+      // the focus back to the open button. If the user is not allowed to add
+      // more items, the button needs to be disabled. Since we can't shift the
+      // focus to disabled elements, the focus is set back to the open button
+      // via JavaScript by adding the 'data-disabled-focus' attribute.
+      $('.js-media-library-open-button[data-disabled-focus="true"]', context)
+        .once('media-library-disable')
+        .each(function() {
+          $(this).focus();
+
+          // There is a small delay between the focus set by the browser and the
+          // focus of screen readers. We need to give screen readers time to
+          // shift the focus as well before the button is disabled.
+          setTimeout(() => {
+            $(this).attr('disabled', 'disabled');
+          }, 50);
+        });
+    },
+  };
 })(jQuery, Drupal);
diff --git a/core/modules/media_library/js/media_library.widget.js b/core/modules/media_library/js/media_library.widget.js
index f2fcf82b3b0150629a0b835680f85d6b08ec000b..8194fe26595868c22926ef568106d79fde0319fd 100644
--- a/core/modules/media_library/js/media_library.widget.js
+++ b/core/modules/media_library/js/media_library.widget.js
@@ -36,4 +36,18 @@
       $('.js-media-library-item-weight', context).once('media-library-toggle').parent().hide();
     }
   };
+
+  Drupal.behaviors.MediaLibraryWidgetDisableButton = {
+    attach: function attach(context) {
+      $('.js-media-library-open-button[data-disabled-focus="true"]', context).once('media-library-disable').each(function () {
+        var _this = this;
+
+        $(this).focus();
+
+        setTimeout(function () {
+          $(_this).attr('disabled', 'disabled');
+        }, 50);
+      });
+    }
+  };
 })(jQuery, Drupal);
\ No newline at end of file
diff --git a/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
index 54adfa9c802d3d6a599d1805f3798f40ef12848d..031234e2cdbba67ffef7a3a58f96e5852c9b8076 100644
--- a/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
+++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
@@ -5,7 +5,9 @@
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Component\Utility\SortArray;
 use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\InvokeCommand;
 use Drupal\Core\Ajax\OpenModalDialogCommand;
+use Drupal\Core\Ajax\ReplaceCommand;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldItemListInterface;
@@ -383,7 +385,12 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
       else {
         $cardinality_message = $this->t('The maximum number of media items have been selected.');
       }
-      $element['#description'] .= '<br />' . $cardinality_message;
+
+      // Add a line break between the field message and the cardinality message.
+      if (!empty($element['#description'])) {
+        $element['#description'] .= '<br />';
+      }
+      $element['#description'] .= $cardinality_message;
     }
 
     // Create a new media library URL with the correct state parameters.
@@ -421,9 +428,19 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
       '#submit' => [],
       // Allow the media library to be opened even if there are form errors.
       '#limit_validation_errors' => [],
-      '#access' => $cardinality_unlimited || $remaining > 0,
     ];
 
+    // When the user returns from the modal to the widget, we want to shift the
+    // focus back to the open button. If the user is not allowed to add more
+    // items, the button needs to be disabled. Since we can't shift the focus to
+    // disabled elements, the focus is set back to the open button via
+    // JavaScript by adding the 'data-disabled-focus' attribute.
+    // @see Drupal.behaviors.MediaLibraryWidgetDisableButton
+    if (!$cardinality_unlimited && $remaining === 0) {
+      $element['media_library_open_button']['#attributes']['data-disabled-focus'] = 'true';
+      $element['media_library_open_button']['#attributes']['class'][] = 'visually-hidden';
+    }
+
     // This hidden field and button are used to add new items to the widget.
     $element['media_library_selection'] = [
       '#type' => 'hidden',
@@ -482,14 +499,17 @@ public function massageFormValues(array $values, array $form, FormStateInterface
    * @param \Drupal\Core\Form\FormStateInterface $form_state
    *   The form state.
    *
-   * @return array
-   *   An array representing the updated widget.
+   * @return \Drupal\Core\Ajax\AjaxResponse
+   *   An AJAX response to update the selection.
    */
   public static function updateWidget(array $form, FormStateInterface $form_state) {
     $triggering_element = $form_state->getTriggeringElement();
+    $wrapper_id = $triggering_element['#ajax']['wrapper'];
+
     // This callback is either invoked from the remove button or the update
     // button, which have different nesting levels.
-    $length = end($triggering_element['#parents']) === 'remove_button' ? -4 : -1;
+    $remove_button = end($triggering_element['#parents']) === 'remove_button';
+    $length = $remove_button ? -4 : -1;
     if (count($triggering_element['#array_parents']) < abs($length)) {
       throw new \LogicException('The element that triggered the widget update was at an unexpected depth. Triggering element parents were: ' . implode(',', $triggering_element['#array_parents']));
     }
@@ -497,7 +517,30 @@ public static function updateWidget(array $form, FormStateInterface $form_state)
     $element = NestedArray::getValue($form, $parents);
     // Always clear the textfield selection to prevent duplicate additions.
     $element['media_library_selection']['#value'] = '';
-    return $element;
+
+    $response = new AjaxResponse();
+    $response->addCommand(new ReplaceCommand("#$wrapper_id", $element));
+
+    $field_state = static::getFieldState($element, $form_state);
+
+    // When the remove button is clicked, the focus will be kept in the
+    // selection area by default. When the last item is deleted, we no longer
+    // have a selection and shift the focus to the open button.
+    $removed_last = $remove_button && !count($field_state['items']);
+
+    // Shift focus to the open button if the user did not click the remove
+    // button. When the user is not allowed to add more items, the button needs
+    // to be disabled. Since we can't shift the focus to disabled elements, the
+    // focus is set via JavaScript by adding the 'data-disabled-focus' attribute
+    // and we also don't want to set the focus here.
+    // @see Drupal.behaviors.MediaLibraryWidgetDisableButton
+    $select_more = !$remove_button && !isset($element['media_library_open_button']['#attributes']['data-disabled-focus']);
+
+    if ($removed_last || $select_more) {
+      $response->addCommand(new InvokeCommand("#$wrapper_id .js-media-library-open-button", 'focus'));
+    }
+
+    return $response;
   }
 
   /**
diff --git a/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
index c2067660e8231efa790d9c806ee3b46be5afa768..311151b626ebc406be5dba6cbdfe05a433127863 100644
--- a/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
+++ b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
@@ -99,8 +99,17 @@ public function viewsForm(array &$form, FormStateInterface $form_state) {
 
     $form['actions']['submit']['#value'] = $this->t('Select media');
     $form['actions']['submit']['#field_id'] = $selection_field_id;
+    // By default, the AJAX system tries to move the focus back to the element
+    // that triggered the AJAX request. Since the media library is closed after
+    // clicking the select button, the focus can't be moved back. We need to set
+    // the 'data-disable-refocus' attribute to prevent the AJAX system from
+    // moving focus to a random element. The select button triggers an update in
+    // the opener, and the opener should be responsible for moving the focus. An
+    // example of this can be seen in MediaLibraryWidget::updateWidget().
+    // @see \Drupal\media_library\Plugin\Field\FieldWidget\MediaLibraryWidget::updateWidget()
     $form['actions']['submit']['#attributes'] = [
       'class' => ['media-library-select'],
+      'data-disable-refocus' => 'true',
     ];
   }
 
diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
index cbba55dc81e4c4d5e4915e34951e775f56acb968..a2a9b75b9e1a3101ad33cc7060b06c9c09ae2276 100644
--- a/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
+++ b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
@@ -242,6 +242,7 @@ public function testWidget() {
     // Select a media item, assert the hidden selection field contains the ID of
     // the selected item.
     $checkboxes = $page->findAll('css', '.media-library-view .js-click-to-select-checkbox input');
+    $this->assertGreaterThanOrEqual(1, count($checkboxes));
     $checkboxes[0]->click();
     $assert_session->hiddenFieldValueEquals('media-library-modal-selection', '4');
     $assert_session->elementTextContains('css', '.media-library-selected-count', '1 of 1 item selected');
@@ -366,6 +367,22 @@ public function testWidget() {
     $assert_session->elementExists('css', '.media-library-item__remove')->click();
     $assert_session->assertWaitOnAjaxRequest();
 
+    // Assert adding a single media item and removing it.
+    $assert_session->elementExists('css', '.media-library-open-button[name^="field_twin_media"]')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    $checkboxes = $page->findAll('css', '.media-library-view .js-click-to-select-checkbox input');
+    $this->assertGreaterThanOrEqual(1, count($checkboxes));
+    $checkboxes[0]->click();
+    $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Select media');
+    $assert_session->assertWaitOnAjaxRequest();
+    // Assert the focus is set back on the open button of the media field.
+    $this->assertJsCondition('jQuery("#field_twin_media-media-library-wrapper .js-media-library-open-button").is(":focus")');
+    $assert_session->elementAttributeContains('css', '.media-library-item__remove', 'aria-label', 'Remove Dog');
+    $assert_session->elementExists('css', '.media-library-item__remove')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    // Assert the focus is set back on the open button of the media field.
+    $this->assertJsCondition('jQuery("#field_twin_media-media-library-wrapper .js-media-library-open-button").is(":focus")');
+
     // Assert the selection is persistent in the media library modal, and
     // the number of selected items is displayed correctly.
     $assert_session->elementExists('css', '.media-library-open-button[name^="field_twin_media"]')->click();
@@ -377,6 +394,7 @@ public function testWidget() {
     // Select a media item, assert the hidden selection field contains the ID of
     // the selected item.
     $checkboxes = $page->findAll('css', '.media-library-view .js-click-to-select-checkbox input');
+    $this->assertCount(4, $checkboxes);
     $checkboxes[0]->click();
     $assert_session->hiddenFieldValueEquals('media-library-modal-selection', '4');
     // Assert the number of selected items is displayed correctly.
@@ -416,6 +434,7 @@ public function testWidget() {
     $page->clickLink('Type Two');
     $assert_session->assertWaitOnAjaxRequest();
     $checkboxes = $page->findAll('css', '.media-library-view .js-click-to-select-checkbox input');
+    $this->assertCount(4, $checkboxes);
     $checkboxes[0]->click();
     // Assert the selection is updated correctly.
     $assert_session->elementTextContains('css', '.media-library-selected-count', '2 of 2 items selected');
@@ -436,6 +455,11 @@ public function testWidget() {
     // Select the items.
     $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Select media');
     $assert_session->assertWaitOnAjaxRequest();
+    // Assert the open button is disabled.
+    $open_button = $assert_session->elementExists('css', '.media-library-open-button[name^="field_twin_media"]');
+    $this->assertTrue($open_button->hasAttribute('data-disabled-focus'));
+    $this->assertTrue($open_button->hasAttribute('disabled'));
+    $this->assertJsCondition('jQuery("#field_twin_media-media-library-wrapper .media-library-open-button").is(":disabled")');
 
     // Ensure that the selection completed successfully.
     $assert_session->pageTextNotContains('Add or select media');
@@ -448,13 +472,21 @@ public function testWidget() {
     $assert_session->elementAttributeContains('css', '.media-library-item__remove', 'aria-label', 'Remove Cat');
     $assert_session->elementExists('css', '.media-library-item__remove')->click();
     $assert_session->assertWaitOnAjaxRequest();
+    // Assert the focus is set to the remove button of the other selected item.
+    $this->assertJsCondition('jQuery("#field_twin_media-media-library-wrapper .media-library-item__remove").is(":focus")');
     $assert_session->pageTextNotContains('Cat');
     $assert_session->pageTextContains('Turtle');
+    // Assert the open button is no longer disabled.
+    $open_button = $assert_session->elementExists('css', '.media-library-open-button[name^="field_twin_media"]');
+    $this->assertFalse($open_button->hasAttribute('data-disabled-focus'));
+    $this->assertFalse($open_button->hasAttribute('disabled'));
+    $this->assertJsCondition('jQuery("#field_twin_media-media-library-wrapper .media-library-open-button").is(":not(:disabled)")');
 
     // Open the media library again and select another item.
     $assert_session->elementExists('css', '.media-library-open-button[name^="field_twin_media"]')->click();
     $assert_session->assertWaitOnAjaxRequest();
     $checkboxes = $page->findAll('css', '.media-library-view .js-click-to-select-checkbox input');
+    $this->assertGreaterThanOrEqual(1, count($checkboxes));
     $checkboxes[0]->click();
     $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Select media');
     $assert_session->assertWaitOnAjaxRequest();
@@ -462,13 +494,16 @@ public function testWidget() {
     $assert_session->pageTextNotContains('Cat');
     $assert_session->pageTextContains('Turtle');
     $assert_session->pageTextNotContains('Snake');
-
-    // Assert we are not allowed to add more items to the field.
-    $assert_session->elementNotExists('css', '.media-library-open-button[name^="field_twin_media"]');
+    // Assert the open button is disabled.
+    $this->assertTrue($assert_session->elementExists('css', '.media-library-open-button[name^="field_twin_media"]')->hasAttribute('data-disabled-focus'));
+    $this->assertTrue($assert_session->elementExists('css', '.media-library-open-button[name^="field_twin_media"]')->hasAttribute('disabled'));
+    $this->assertJsCondition('jQuery("#field_twin_media-media-library-wrapper .media-library-open-button").is(":disabled")');
 
     // Assert the selection is cleared when the modal is closed.
     $assert_session->elementExists('css', '.media-library-open-button[name^="field_unlimited_media"]')->click();
     $assert_session->assertWaitOnAjaxRequest();
+    $checkboxes = $page->findAll('css', '.media-library-view .js-click-to-select-checkbox input');
+    $this->assertGreaterThanOrEqual(4, count($checkboxes));
     // Nothing is selected yet.
     $this->assertFalse($checkboxes[0]->isChecked());
     $this->assertFalse($checkboxes[1]->isChecked());
@@ -476,7 +511,6 @@ public function testWidget() {
     $this->assertFalse($checkboxes[3]->isChecked());
     $assert_session->elementTextContains('css', '.media-library-selected-count', '0 items selected');
     // Select the first 2 items.
-    $checkboxes = $page->findAll('css', '.media-library-view .js-click-to-select-checkbox input');
     $checkboxes[0]->click();
     $assert_session->elementTextContains('css', '.media-library-selected-count', '1 item selected');
     $checkboxes[1]->click();
@@ -527,6 +561,7 @@ public function testWidget() {
     // Select all media items of type one (should also contain Dog, again).
     $checkbox_selector = '.media-library-view .js-click-to-select-checkbox input';
     $checkboxes = $page->findAll('css', $checkbox_selector);
+    $this->assertGreaterThanOrEqual(4, count($checkboxes));
     $checkboxes[0]->click();
     $checkboxes[1]->click();
     $checkboxes[2]->click();