diff --git a/core/core.libraries.yml b/core/core.libraries.yml
index 22362b28aed74c00b2ed7c0e3495bde047e6abbd..885019a857b699c34a4992c1976464f136108063 100644
--- a/core/core.libraries.yml
+++ b/core/core.libraries.yml
@@ -258,6 +258,7 @@ drupal.dialog.ajax:
     - core/drupalSettings
     - core/drupal.ajax
     - core/drupal.dialog
+    - core/tabbable
 
 drupal.displace:
   version: VERSION
@@ -366,7 +367,6 @@ drupal.tabbingmanager:
     - core/jquery
     - core/drupal
     - core/tabbable
-    - core/tabbable.jquery.shim
 
 drupal.tabledrag:
   version: VERSION
@@ -791,6 +791,7 @@ tabbable.jquery.shim:
   js:
     misc/jquery.tabbable.shim.js: {}
   dependencies:
+    - core/drupal
     - core/tabbable
     - core/jquery
 
diff --git a/core/misc/dialog/dialog.jquery-ui.es6.js b/core/misc/dialog/dialog.jquery-ui.es6.js
index e3bde509a902d873bf89861b38f9216b6e0664de..783797d1a14895d31d37bb6499174ea5e183306c 100644
--- a/core/misc/dialog/dialog.jquery-ui.es6.js
+++ b/core/misc/dialog/dialog.jquery-ui.es6.js
@@ -3,7 +3,7 @@
  * Adds default classes to buttons for styling purposes.
  */
 
-(function ($) {
+(function ($, { tabbable, isTabbable }) {
   $.widget('ui.dialog', $.ui.dialog, {
     options: {
       buttonClass: 'button',
@@ -30,5 +30,46 @@
         $buttons.eq(index).addClass(opts.buttonPrimaryClass);
       }
     },
+    // Override jQuery UI's `_focusTabbable()` so finding tabbable elements uses
+    // the core/tabbable library instead of jQuery UI's `:tabbable` selector.
+    _focusTabbable() {
+      // Set focus to the first match:
+
+      // 1. An element that was focused previously.
+      let hasFocus = this._focusedElement ? this._focusedElement.get(0) : null;
+
+      // 2. First element inside the dialog matching [autofocus].
+      if (!hasFocus) {
+        hasFocus = this.element.find('[autofocus]').get(0);
+      }
+
+      // 3. Tabbable element inside the content element.
+      // 4. Tabbable element inside the buttonpane.
+      if (!hasFocus) {
+        const $elements = [this.element, this.uiDialogButtonPane];
+        for (let i = 0; i < $elements.length; i++) {
+          const element = $elements[i].get(0);
+          if (element) {
+            const elementTabbable = tabbable(element);
+            hasFocus = elementTabbable.length ? elementTabbable[0] : null;
+          }
+          if (hasFocus) {
+            break;
+          }
+        }
+      }
+
+      // 5. The close button.
+      if (!hasFocus) {
+        const closeBtn = this.uiDialogTitlebarClose.get(0);
+        hasFocus = closeBtn && isTabbable(closeBtn) ? closeBtn : null;
+      }
+
+      // 6. The dialog itself.
+      if (!hasFocus) {
+        hasFocus = this.uiDialog.get(0);
+      }
+      $(hasFocus).eq(0).trigger('focus');
+    },
   });
-})(jQuery);
+})(jQuery, window.tabbable);
diff --git a/core/misc/dialog/dialog.jquery-ui.js b/core/misc/dialog/dialog.jquery-ui.js
index 2077e23ed7829b493a0ca552449f61278662ddc0..86f9455d99817eeae6a79f2f0cbc5faf5d01edb0 100644
--- a/core/misc/dialog/dialog.jquery-ui.js
+++ b/core/misc/dialog/dialog.jquery-ui.js
@@ -5,7 +5,9 @@
 * @preserve
 **/
 
-(function ($) {
+(function ($, _ref) {
+  var tabbable = _ref.tabbable,
+      isTabbable = _ref.isTabbable;
   $.widget('ui.dialog', $.ui.dialog, {
     options: {
       buttonClass: 'button',
@@ -32,6 +34,41 @@
       if (typeof primaryIndex !== 'undefined') {
         $buttons.eq(index).addClass(opts.buttonPrimaryClass);
       }
+    },
+    _focusTabbable: function _focusTabbable() {
+      var hasFocus = this._focusedElement ? this._focusedElement.get(0) : null;
+
+      if (!hasFocus) {
+        hasFocus = this.element.find('[autofocus]').get(0);
+      }
+
+      if (!hasFocus) {
+        var $elements = [this.element, this.uiDialogButtonPane];
+
+        for (var i = 0; i < $elements.length; i++) {
+          var element = $elements[i].get(0);
+
+          if (element) {
+            var elementTabbable = tabbable(element);
+            hasFocus = elementTabbable.length ? elementTabbable[0] : null;
+          }
+
+          if (hasFocus) {
+            break;
+          }
+        }
+      }
+
+      if (!hasFocus) {
+        var closeBtn = this.uiDialogTitlebarClose.get(0);
+        hasFocus = closeBtn && isTabbable(closeBtn) ? closeBtn : null;
+      }
+
+      if (!hasFocus) {
+        hasFocus = this.uiDialog.get(0);
+      }
+
+      $(hasFocus).eq(0).trigger('focus');
     }
   });
-})(jQuery);
\ No newline at end of file
+})(jQuery, window.tabbable);
\ No newline at end of file
diff --git a/core/misc/jquery.tabbable.shim.es6.js b/core/misc/jquery.tabbable.shim.es6.js
index 381355f2d6653975bee61363471c1b4cbd41b919..de8d043e7423a4df5da312049a4d49de11694eac 100644
--- a/core/misc/jquery.tabbable.shim.es6.js
+++ b/core/misc/jquery.tabbable.shim.es6.js
@@ -6,6 +6,11 @@
 (($, Drupal, { isTabbable }) => {
   $.extend($.expr[':'], {
     tabbable(element) {
+      Drupal.deprecationError({
+        message:
+          'The :tabbable selector is deprecated in Drupal 9.2.0 and will be removed in Drupal 10.0.0. Use the core/tabbable library instead. See https://www.drupal.org/node/3183730',
+      });
+
       // The tabbable library considers the summary element tabbable, and also
       // considers a details element without a summary tabbable. The jQuery UI
       // :tabbable selector does not. This is due to those element types being
diff --git a/core/misc/jquery.tabbable.shim.js b/core/misc/jquery.tabbable.shim.js
index dd95d9bf1bc0d5860982e1cc20d2219e36b3f1be..d5862998470884e89065571ecd94ec475d77d3ad 100644
--- a/core/misc/jquery.tabbable.shim.js
+++ b/core/misc/jquery.tabbable.shim.js
@@ -9,6 +9,10 @@
   var isTabbable = _ref.isTabbable;
   $.extend($.expr[':'], {
     tabbable: function tabbable(element) {
+      Drupal.deprecationError({
+        message: 'The :tabbable selector is deprecated in Drupal 9.2.0 and will be removed in Drupal 10.0.0. Use the core/tabbable library instead. See https://www.drupal.org/node/3183730'
+      });
+
       if (element.tagName === 'SUMMARY' || element.tagName === 'DETAILS') {
         var tabIndex = element.getAttribute('tabIndex');
 
diff --git a/core/misc/tabbingmanager.es6.js b/core/misc/tabbingmanager.es6.js
index ea4628491ae56295760da7b8aba006493265a178..d7b19800879523d3581643f5cb9749ee15acc8cc 100644
--- a/core/misc/tabbingmanager.es6.js
+++ b/core/misc/tabbingmanager.es6.js
@@ -27,7 +27,7 @@
  * @event drupalTabbingContextDeactivated
  */
 
-(function ($, Drupal) {
+(function ($, Drupal, { tabbable, isTabbable }) {
   /**
    * Provides an API for managing page tabbing order modifications.
    *
@@ -116,9 +116,9 @@
        * Makes elements outside of the specified set of elements unreachable via
        * the tab key.
        *
-       * @param {jQuery} elements
+       * @param {jQuery|Selector|Element|ElementArray|object|selection} elements
        *   The set of elements to which tabbing should be constrained. Can also
-       *   be a jQuery-compatible selector string.
+       *   be any jQuery-compatible argument.
        *
        * @return {Drupal~TabbingContext}
        *   The TabbingContext instance.
@@ -136,13 +136,19 @@
 
         // The "active tabbing set" are the elements tabbing should be constrained
         // to.
-        const $elements = $(elements).find(':tabbable').addBack(':tabbable');
+        let tabbableElements = [];
+        $(elements).each((index, rootElement) => {
+          tabbableElements = [...tabbableElements, ...tabbable(rootElement)];
+          if (isTabbable(rootElement)) {
+            tabbableElements = [...tabbableElements, rootElement];
+          }
+        });
 
         const tabbingContext = new TabbingContext({
           // The level is the current height of the stack before this new
           // tabbingContext is pushed on top of the stack.
           level: this.stack.length,
-          $tabbableElements: $elements,
+          $tabbableElements: $(tabbableElements),
         });
 
         this.stack.push(tabbingContext);
@@ -196,7 +202,7 @@
         const $set = tabbingContext.$tabbableElements;
         const level = tabbingContext.level;
         // Determine which elements are reachable via tabbing by default.
-        const $disabledSet = $(':tabbable')
+        const $disabledSet = $(tabbable(document.body))
           // Exclude elements of the active tabbing set.
           .not($set);
         // Set the disabled set on the tabbingContext.
@@ -364,4 +370,4 @@
    * @type {Drupal~TabbingManager}
    */
   Drupal.tabbingManager = new TabbingManager();
-})(jQuery, Drupal);
+})(jQuery, Drupal, window.tabbable);
diff --git a/core/misc/tabbingmanager.js b/core/misc/tabbingmanager.js
index 5db64002af382629d849c66ae29d9600761a133b..65e583a067d81c512d54ba783e2e83ff6f9c824f 100644
--- a/core/misc/tabbingmanager.js
+++ b/core/misc/tabbingmanager.js
@@ -5,7 +5,22 @@
 * @preserve
 **/
 
-(function ($, Drupal) {
+function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }
+
+function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
+
+function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
+
+function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); }
+
+function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }
+
+function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
+
+(function ($, Drupal, _ref) {
+  var tabbable = _ref.tabbable,
+      isTabbable = _ref.isTabbable;
+
   function TabbingManager() {
     this.stack = [];
   }
@@ -28,10 +43,17 @@
         this.stack[i].deactivate();
       }
 
-      var $elements = $(elements).find(':tabbable').addBack(':tabbable');
+      var tabbableElements = [];
+      $(elements).each(function (index, rootElement) {
+        tabbableElements = [].concat(_toConsumableArray(tabbableElements), _toConsumableArray(tabbable(rootElement)));
+
+        if (isTabbable(rootElement)) {
+          tabbableElements = [].concat(_toConsumableArray(tabbableElements), [rootElement]);
+        }
+      });
       var tabbingContext = new TabbingContext({
         level: this.stack.length,
-        $tabbableElements: $elements
+        $tabbableElements: $(tabbableElements)
       });
       this.stack.push(tabbingContext);
       tabbingContext.activate();
@@ -54,7 +76,7 @@
     activate: function activate(tabbingContext) {
       var $set = tabbingContext.$tabbableElements;
       var level = tabbingContext.level;
-      var $disabledSet = $(':tabbable').not($set);
+      var $disabledSet = $(tabbable(document.body)).not($set);
       tabbingContext.$disabledElements = $disabledSet;
       var il = $disabledSet.length;
 
@@ -149,4 +171,4 @@
   }
 
   Drupal.tabbingManager = new TabbingManager();
-})(jQuery, Drupal);
\ No newline at end of file
+})(jQuery, Drupal, window.tabbable);
\ No newline at end of file
diff --git a/core/modules/contextual/tests/src/FunctionalJavascript/EditModeTest.php b/core/modules/contextual/tests/src/FunctionalJavascript/EditModeTest.php
index 9d4e35d96264588f19189ec01c6c52e67a863f18..219b30dc76b9c6532421e86a071f48d66c9f25eb 100644
--- a/core/modules/contextual/tests/src/FunctionalJavascript/EditModeTest.php
+++ b/core/modules/contextual/tests/src/FunctionalJavascript/EditModeTest.php
@@ -131,7 +131,7 @@ protected function assertAnnounceLeaveEditMode() {
    */
   protected function getTabbableElementsCount() {
     // Mark all tabbable elements.
-    $this->getSession()->executeScript("jQuery(':tabbable').attr('data-marked', '');");
+    $this->getSession()->executeScript("jQuery(window.tabbable.tabbable(document.body)).attr('data-marked', '');");
     // Count all marked elements.
     $count = count($this->getSession()->getPage()->findAll('css', "[data-marked]"));
     // Remove set attributes.
diff --git a/core/modules/media_library/js/media_library.ui.es6.js b/core/modules/media_library/js/media_library.ui.es6.js
index 4a7397c38f3848bdcef8020ca9ca606b955ebbdd..c2b47be9372db398d2319d07fa49251c861d5cb7 100644
--- a/core/modules/media_library/js/media_library.ui.es6.js
+++ b/core/modules/media_library/js/media_library.ui.es6.js
@@ -1,7 +1,7 @@
 /**
  * @file media_library.ui.es6.js
  */
-(($, Drupal, window) => {
+(($, Drupal, window, { tabbable }) => {
   /**
    * Wrapper object for the current state of the media library.
    */
@@ -103,7 +103,15 @@
 
             // Set focus to the first tabbable element in the media library
             // content.
-            $('#media-library-content :tabbable:first').focus();
+            const mediaLibraryContent = document.getElementById(
+              'media-library-content',
+            );
+            if (mediaLibraryContent) {
+              const tabbableContent = tabbable(mediaLibraryContent);
+              if (tabbableContent.length) {
+                tabbableContent[0].focus();
+              }
+            }
 
             // Remove any response-specific settings so they don't get used on
             // the next call by mistake.
@@ -420,4 +428,4 @@
   Drupal.theme.mediaLibrarySelectionCount = function () {
     return `<div class="media-library-selected-count js-media-library-selected-count" role="status" aria-live="polite" aria-atomic="true"></div>`;
   };
-})(jQuery, Drupal, window);
+})(jQuery, Drupal, window, window.tabbable);
diff --git a/core/modules/media_library/js/media_library.ui.js b/core/modules/media_library/js/media_library.ui.js
index 73d48cdbc23df764233a9aa12bc6942b6e4a6506..b24ee5db9e2a3ef322d0545aca857c7d0cd8b0ea 100644
--- a/core/modules/media_library/js/media_library.ui.js
+++ b/core/modules/media_library/js/media_library.ui.js
@@ -5,7 +5,8 @@
 * @preserve
 **/
 
-(function ($, Drupal, window) {
+(function ($, Drupal, window, _ref) {
+  var tabbable = _ref.tabbable;
   Drupal.MediaLibrary = {
     currentSelection: []
   };
@@ -55,7 +56,16 @@
               _this.commands[response[i].command](_this, response[i], status);
             }
           });
-          $('#media-library-content :tabbable:first').focus();
+          var mediaLibraryContent = document.getElementById('media-library-content');
+
+          if (mediaLibraryContent) {
+            var tabbableContent = tabbable(mediaLibraryContent);
+
+            if (tabbableContent.length) {
+              tabbableContent[0].focus();
+            }
+          }
+
           this.settings = null;
         };
 
@@ -204,4 +214,4 @@
   Drupal.theme.mediaLibrarySelectionCount = function () {
     return "<div class=\"media-library-selected-count js-media-library-selected-count\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\"></div>";
   };
-})(jQuery, Drupal, window);
\ No newline at end of file
+})(jQuery, Drupal, window, window.tabbable);
\ No newline at end of file
diff --git a/core/modules/media_library/media_library.libraries.yml b/core/modules/media_library/media_library.libraries.yml
index 13fb94f7f08f55883bf3d86f13029a362206da1b..9a3d256ee7995ff7ebff3c5365a3155f301f1588 100644
--- a/core/modules/media_library/media_library.libraries.yml
+++ b/core/modules/media_library/media_library.libraries.yml
@@ -34,3 +34,4 @@ ui:
     - core/drupal.announce
     - core/jquery.once
     - media_library/view
+    - core/tabbable
diff --git a/core/modules/media_library/src/Form/AddFormBase.php b/core/modules/media_library/src/Form/AddFormBase.php
index 317f0842a1a9817ba02abd7790380b5558d8169a..a63bc7dc875f547db71f22b863690464cc092e8c 100644
--- a/core/modules/media_library/src/Form/AddFormBase.php
+++ b/core/modules/media_library/src/Form/AddFormBase.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\Ajax\AjaxResponse;
 use Drupal\Core\Ajax\CloseDialogCommand;
+use Drupal\Core\Ajax\FocusFirstCommand;
 use Drupal\Core\Ajax\InvokeCommand;
 use Drupal\Core\Ajax\ReplaceCommand;
 use Drupal\Core\Entity\Entity\EntityFormDisplay;
@@ -608,7 +609,7 @@ public function updateFormCallback(array &$form, FormStateInterface $form_state)
       // source field).
       if (empty($added_media)) {
         $response->addCommand(new ReplaceCommand('#media-library-add-form-wrapper', $this->buildMediaLibraryUi($form_state)));
-        $response->addCommand(new InvokeCommand('#media-library-add-form-wrapper :tabbable', 'focus'));
+        $response->addCommand(new FocusFirstCommand('#media-library-add-form-wrapper'));
       }
       // When there are still more items, update the form and shift the focus to
       // the next media item. If the last list item is removed, shift focus to
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 31d14f4d7df9081f58fef4929e3e3ee268368116..84b9b3c8926cfb77e997d5368243860c7dc9a52b 100644
--- a/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
+++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
@@ -492,7 +492,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
           'type' => 'throbber',
           'message' => $this->t('Opening media library.'),
         ],
-        // The AJAX system automatically moves focus to the first :tabbable
+        // The AJAX system automatically moves focus to the first tabbable
         // element of the modal, so we need to disable refocus on the button.
         'disable-refocus' => TRUE,
       ],
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 bc506f202a1804853841a88815224daf62b0a7e9..ce273305c604e49080bcb17ce45033dc778446ec 100644
--- a/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
+++ b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
@@ -90,7 +90,7 @@ public function viewsForm(array &$form, FormStateInterface $form_state) {
         'query' => $query,
       ],
       'callback' => [static::class, 'updateWidget'],
-      // The AJAX system automatically moves focus to the first :tabbable
+      // The AJAX system automatically moves focus to the first tabbable
       // element of the modal, so we need to disable refocus on the button.
       'disable-refocus' => TRUE,
     ];
diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/EntityReferenceWidgetTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/EntityReferenceWidgetTest.php
index 28db60805c135c6b3a6aedb3edd2dc5a3263fe39..8f27d364f8956446d64a2949414c33f9c7f0ac61 100644
--- a/core/modules/media_library/tests/src/FunctionalJavascript/EntityReferenceWidgetTest.php
+++ b/core/modules/media_library/tests/src/FunctionalJavascript/EntityReferenceWidgetTest.php
@@ -113,7 +113,7 @@ public function testWidget() {
     $this->assertTrue($menu->hasLink('Show Type Three media (selected)'));
     // Assert the focus is set to the first tabbable element when a vertical tab
     // is clicked.
-    $this->assertJsCondition('jQuery("#media-library-content :tabbable:first").is(":focus")');
+    $this->assertJsCondition('jQuery(tabbable.tabbable(document.getElementById("media-library-content"))[0]).is(":focus")');
     $assert_session->elementExists('css', '.ui-dialog-titlebar-close')->click();
 
     // Assert that there are no links in the media library view.
diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTestBase.php b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTestBase.php
index 71df893d1ae1587053129409996c8c94783a6264..8ec4b3b5a9cb754760de0718a632aeca78497c64 100644
--- a/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTestBase.php
+++ b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTestBase.php
@@ -342,7 +342,7 @@ protected function assertMediaAdded($index = 0) {
   protected function assertNoMediaAdded() {
     // Assert the focus is shifted to the first tabbable element of the add
     // form, which should be the source field.
-    $this->assertJsCondition('jQuery("#media-library-add-form-wrapper :tabbable").is(":focus")');
+    $this->assertJsCondition('jQuery(tabbable.tabbable(document.getElementById("media-library-add-form-wrapper"))[0]).is(":focus")');
 
     $this->assertSession()
       ->elementNotExists('css', '[data-drupal-selector="edit-media-0-fields"]');
diff --git a/core/modules/system/tests/modules/dialog_renderer_test/css/dialog-test.css b/core/modules/system/tests/modules/dialog_renderer_test/css/dialog-test.css
new file mode 100644
index 0000000000000000000000000000000000000000..64fb16406cadcea7124369ea657fa11d3425d3b5
--- /dev/null
+++ b/core/modules/system/tests/modules/dialog_renderer_test/css/dialog-test.css
@@ -0,0 +1,4 @@
+.no-close .ui-dialog-titlebar-close {
+  display: none;
+  visibility: hidden;
+}
diff --git a/core/modules/system/tests/modules/dialog_renderer_test/dialog_renderer_test.libraries.yml b/core/modules/system/tests/modules/dialog_renderer_test/dialog_renderer_test.libraries.yml
new file mode 100644
index 0000000000000000000000000000000000000000..64c78bd175fc61d90eb46de43c0774d2349a5935
--- /dev/null
+++ b/core/modules/system/tests/modules/dialog_renderer_test/dialog_renderer_test.libraries.yml
@@ -0,0 +1,4 @@
+dialog_test:
+  css:
+    theme:
+      css/dialog-test.css: {}
diff --git a/core/modules/system/tests/modules/dialog_renderer_test/dialog_renderer_test.routing.yml b/core/modules/system/tests/modules/dialog_renderer_test/dialog_renderer_test.routing.yml
index 3cfbde0d7efc0b9d5291cd313a29c3123fa24e0d..c135824810b5f5653c5ccb0e3acd34af2d9cbc74 100644
--- a/core/modules/system/tests/modules/dialog_renderer_test/dialog_renderer_test.routing.yml
+++ b/core/modules/system/tests/modules/dialog_renderer_test/dialog_renderer_test.routing.yml
@@ -13,3 +13,19 @@ dialog_renderer_test.modal_content:
     _title: 'Thing 1'
   requirements:
     _access: 'TRUE'
+
+dialog_renderer_test.modal_content_link:
+  path: '/dialog_renderer-content-link'
+  defaults:
+    _controller: '\Drupal\dialog_renderer_test\Controller\TestController::modalContentLink'
+    _title: 'Thing 2'
+  requirements:
+    _access: 'TRUE'
+
+dialog_renderer_test.modal_content_input:
+  path: '/dialog_renderer-content-input'
+  defaults:
+    _controller: '\Drupal\dialog_renderer_test\Controller\TestController::modalContentInput'
+    _title: 'Thing 3'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/system/tests/modules/dialog_renderer_test/src/Controller/TestController.php b/core/modules/system/tests/modules/dialog_renderer_test/src/Controller/TestController.php
index 853c620dc23c83488144d17cd93ae7d9e2a63444..469e35a187de48c248a0ac3f1e0516e9411b0ee8 100644
--- a/core/modules/system/tests/modules/dialog_renderer_test/src/Controller/TestController.php
+++ b/core/modules/system/tests/modules/dialog_renderer_test/src/Controller/TestController.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\dialog_renderer_test\Controller;
 
+use Drupal\Component\Serialization\Json;
 use Drupal\Core\Url;
 
 /**
@@ -22,6 +23,49 @@ public function modalContent() {
     ];
   }
 
+  /**
+   * Return modal content with link.
+   *
+   * @return array
+   *   Render array for display in modal.
+   */
+  public function modalContentLink() {
+    return [
+      '#type' => 'container',
+      'text' => [
+        '#type' => 'markup',
+        '#markup' => 'Look at me in a modal!<br><a href="#">And a link!</a>',
+      ],
+      'input' => [
+        '#type' => 'textfield',
+        '#size' => 60,
+      ],
+    ];
+  }
+
+  /**
+   * Return modal content with autofocus input.
+   *
+   * @return array
+   *   Render array for display in modal.
+   */
+  public function modalContentInput() {
+    return [
+      '#type' => 'container',
+      'text' => [
+        '#type' => 'markup',
+        '#markup' => 'Look at me in a modal!<br><a href="#">And a link!</a>',
+      ],
+      'input' => [
+        '#type' => 'textfield',
+        '#size' => 60,
+        '#attributes' => [
+          'autofocus' => TRUE,
+        ],
+      ],
+    ];
+  }
+
   /**
    * Displays test links that will open in the modal dialog.
    *
@@ -74,6 +118,74 @@ public function linksDisplay() {
           ],
         ],
       ],
+      'no_close_modal' => [
+        '#title' => 'Hidden close button modal!',
+        '#type' => 'link',
+        '#url' => Url::fromRoute('dialog_renderer_test.modal_content'),
+        '#attributes' => [
+          'class' => ['use-ajax'],
+          'data-dialog-type' => 'modal',
+          'data-dialog-options' => Json::encode([
+            'dialogClass' => 'no-close',
+          ]),
+        ],
+        '#attached' => [
+          'library' => [
+            'core/drupal.ajax',
+            'dialog_renderer_test/dialog_test',
+          ],
+        ],
+      ],
+      'button_pane_modal' => [
+        '#title' => 'Button pane modal!',
+        '#type' => 'link',
+        '#url' => Url::fromRoute('dialog_renderer_test.modal_content'),
+        '#attributes' => [
+          'class' => ['use-ajax'],
+          'data-dialog-type' => 'modal',
+          'data-dialog-options' => Json::encode([
+            'buttons' => [
+              [
+                'text' => 'OK',
+                'click' => '() => {}',
+              ],
+            ],
+          ]),
+        ],
+        '#attached' => [
+          'library' => [
+            'core/drupal.ajax',
+          ],
+        ],
+      ],
+      'content_link_modal' => [
+        '#title' => 'Content link modal!',
+        '#type' => 'link',
+        '#url' => Url::fromRoute('dialog_renderer_test.modal_content_link'),
+        '#attributes' => [
+          'class' => ['use-ajax'],
+          'data-dialog-type' => 'modal',
+        ],
+        '#attached' => [
+          'library' => [
+            'core/drupal.ajax',
+          ],
+        ],
+      ],
+      'auto_focus_modal' => [
+        '#title' => 'Auto focus modal!',
+        '#type' => 'link',
+        '#url' => Url::fromRoute('dialog_renderer_test.modal_content_input'),
+        '#attributes' => [
+          'class' => ['use-ajax'],
+          'data-dialog-type' => 'modal',
+        ],
+        '#attached' => [
+          'library' => [
+            'core/drupal.ajax',
+          ],
+        ],
+      ],
     ];
   }
 
diff --git a/core/modules/system/tests/modules/tabbable_shim_test/src/Controller/TabbableShimTestController.php b/core/modules/system/tests/modules/tabbable_shim_test/src/Controller/TabbableShimTestController.php
index 6ef837e55a19cc85c38b19483b14204f5b2c7da9..a8ed16c47a25fb7f31b386e2cc5ba4895ea41ebc 100644
--- a/core/modules/system/tests/modules/tabbable_shim_test/src/Controller/TabbableShimTestController.php
+++ b/core/modules/system/tests/modules/tabbable_shim_test/src/Controller/TabbableShimTestController.php
@@ -23,7 +23,7 @@ public function build() {
           'id' => 'tabbable-test-container',
         ],
       ],
-      '#attached' => ['library' => ['core/drupal.tabbingmanager']],
+      '#attached' => ['library' => ['core/jquery.ui']],
     ];
   }
 
diff --git a/core/modules/system/tests/src/FunctionalJavascript/ModalRendererTest.php b/core/modules/system/tests/src/FunctionalJavascript/ModalRendererTest.php
index ac8da5027645cd29d58aab09898df001ddd35577..0760f7042b6da48ab7a28c5956e4561d08aae9b5 100644
--- a/core/modules/system/tests/src/FunctionalJavascript/ModalRendererTest.php
+++ b/core/modules/system/tests/src/FunctionalJavascript/ModalRendererTest.php
@@ -28,16 +28,52 @@ public function testModalRenderer() {
     $session_assert = $this->assertSession();
     $this->drupalGet('/dialog_renderer-test-links');
     $this->clickLink('Normal Modal!');
+
     // Neither of the wide modals should have been used.
     $style = $session_assert->waitForElementVisible('css', '.ui-dialog')->getAttribute('style');
     $this->assertStringNotContainsString('700px', $style);
     $this->assertStringNotContainsString('1000px', $style);
+
+    // Tabbable should focus the close button when it is the only tabbable item.
+    $this->assertJsCondition('document.activeElement === document.querySelector(".ui-dialog .ui-dialog-titlebar-close")');
     $this->drupalGet('/dialog_renderer-test-links');
     $this->clickLink('Wide Modal!');
     $this->assertNotEmpty($session_assert->waitForElementVisible('css', '.ui-dialog[style*="width: 700px;"]'));
     $this->drupalGet('/dialog_renderer-test-links');
     $this->clickLink('Extra Wide Modal!');
     $this->assertNotEmpty($session_assert->waitForElementVisible('css', '.ui-dialog[style*="width: 1000px;"]'));
+
+    $this->drupalGet('/dialog_renderer-test-links');
+    $this->clickLink('Hidden close button modal!');
+    $session_assert->waitForElementVisible('css', '.ui-dialog');
+
+    // Tabbable should focus the dialog itself when there is no other item.
+    $this->assertJsCondition('document.activeElement === document.querySelector(".ui-dialog")');
+
+    $this->drupalGet('/dialog_renderer-test-links');
+    $this->clickLink('Button pane modal!');
+    $session_assert->waitForElementVisible('css', '.ui-dialog');
+    $session_assert->assertVisibleInViewport('css', '.ui-dialog .ui-dialog-buttonpane');
+
+    // Tabbable should focus the first tabbable item inside button pane.
+    $this->assertJsCondition('document.activeElement === tabbable.tabbable(document.querySelector(".ui-dialog .ui-dialog-buttonpane"))[0]');
+
+    $this->drupalGet('/dialog_renderer-test-links');
+    $this->clickLink('Content link modal!');
+    $session_assert->waitForElementVisible('css', '.ui-dialog');
+    $session_assert->assertVisibleInViewport('css', '.ui-dialog .ui-dialog-content');
+
+    // Tabbable should focus the first tabbable item inside modal content.
+    $this->assertJsCondition('document.activeElement === tabbable.tabbable(document.querySelector(".ui-dialog .ui-dialog-content"))[0]');
+
+    $this->drupalGet('/dialog_renderer-test-links');
+    $this->clickLink('Auto focus modal!');
+    $session_assert->waitForElementVisible('css', '.ui-dialog');
+    $session_assert->assertVisibleInViewport('css', '.ui-dialog .ui-dialog-content');
+
+    // Tabbable should focus the item with autofocus inside button pane.
+    $this->assertJsCondition('document.activeElement === tabbable.tabbable(document.querySelector(".ui-dialog .ui-dialog-content"))[1]');
+    $this->assertJsCondition('document.activeElement === document.querySelector(".ui-dialog .form-text")');
   }
 
 }
diff --git a/core/tests/Drupal/Nightwatch/Tests/tabbableShimTest.js b/core/tests/Drupal/Nightwatch/Tests/tabbableShimTest.js
index 056e9464c55e1999888ca02bb8da2864dd5e8477..cbcd97aa5a4c97c0106f9a3908765c13fa45e6a9 100644
--- a/core/tests/Drupal/Nightwatch/Tests/tabbableShimTest.js
+++ b/core/tests/Drupal/Nightwatch/Tests/tabbableShimTest.js
@@ -294,6 +294,8 @@ module.exports = {
         },
         [iteration],
         (result) => {
+          browser.assert.ok(typeof result.value.actual === 'number');
+          browser.assert.ok(typeof result.value.expected === 'number');
           browser.assert.equal(
             result.value.actual,
             result.value.expected,
@@ -302,6 +304,9 @@ module.exports = {
         },
       );
     });
+    browser.assert.deprecationErrorExists(
+      'The :tabbable selector is deprecated in Drupal 9.2.0 and will be removed in Drupal 10.0.0. Use the core/tabbable library instead. See https://www.drupal.org/node/3183730',
+    );
     browser.drupalLogAndEnd({ onlyOnError: false });
   },
   'test tabbable dialog integration': (browser) => {
@@ -332,7 +337,9 @@ module.exports = {
         },
       );
     });
-
+    browser.assert.deprecationErrorExists(
+      'The :tabbable selector is deprecated in Drupal 9.2.0 and will be removed in Drupal 10.0.0. Use the core/tabbable library instead. See https://www.drupal.org/node/3183730',
+    );
     browser.drupalLogAndEnd({ onlyOnError: false });
   },
 };