diff --git a/core/modules/contextual/contextual.libraries.yml b/core/modules/contextual/contextual.libraries.yml
index bfc1c996c9..8fa2ac3e25 100644
--- a/core/modules/contextual/contextual.libraries.yml
+++ b/core/modules/contextual/contextual.libraries.yml
@@ -2,15 +2,8 @@ drupal.contextual-links:
   version: VERSION
   js:
     # Ensure to run before contextual/drupal.context-toolbar.
-    # Core.
     js/contextual.js: { weight: -2 }
-    # Models.
-    js/models/StateModel.js: { weight: -2 }
-    # Views.
-    js/views/AuralView.js: { weight: -2 }
-    js/views/KeyboardView.js: { weight: -2 }
-    js/views/RegionView.js: { weight: -2 }
-    js/views/VisualView.js: { weight: -2 }
+    js/contextualModelView.js: { weight: -2 }
   css:
     component:
       css/contextual.module.css: {}
@@ -22,8 +15,6 @@ drupal.contextual-links:
     - core/drupal
     - core/drupal.ajax
     - core/drupalSettings
-    # @todo Remove this in https://www.drupal.org/project/drupal/issues/3203920
-    - core/internal.backbone
     - core/once
     - core/drupal.touchevents-test
 
@@ -31,19 +22,13 @@ drupal.contextual-toolbar:
   version: VERSION
   js:
     js/contextual.toolbar.js: {}
-    # Models.
-    js/toolbar/models/StateModel.js: {}
-    # Views.
-    js/toolbar/views/AuralView.js: {}
-    js/toolbar/views/VisualView.js: {}
+    js/toolbar/contextualToolbarModelView.js: {}
   css:
     component:
       css/contextual.toolbar.css: {}
   dependencies:
     - core/jquery
     - core/drupal
-    # @todo Remove this in https://www.drupal.org/project/drupal/issues/3203920
-    - core/internal.backbone
     - core/once
     - core/drupal.tabbingmanager
     - core/drupal.announce
diff --git a/core/modules/contextual/css/contextual.theme.css b/core/modules/contextual/css/contextual.theme.css
index d84444d18d..39c0b34424 100644
--- a/core/modules/contextual/css/contextual.theme.css
+++ b/core/modules/contextual/css/contextual.theme.css
@@ -17,6 +17,10 @@
   left: 0;
 }
 
+.contextual.open {
+  z-index: 501;
+}
+
 /**
  * Contextual region.
  */
diff --git a/core/modules/contextual/js/contextual.js b/core/modules/contextual/js/contextual.js
index b5fe7d094f..f066ee69a9 100644
--- a/core/modules/contextual/js/contextual.js
+++ b/core/modules/contextual/js/contextual.js
@@ -3,7 +3,7 @@
  * Attaches behaviors for the Contextual module.
  */
 
-(function ($, Drupal, drupalSettings, _, Backbone, JSON, storage) {
+(function($, Drupal, drupalSettings, JSON, storage) {
   const options = $.extend(
     drupalSettings.contextual,
     // Merge strings on top of drupalSettings so that they are not mutable.
@@ -20,16 +20,14 @@
   const cachedPermissionsHash = storage.getItem(
     'Drupal.contextual.permissionsHash',
   );
-  const permissionsHash = drupalSettings.user.permissionsHash;
+  const { permissionsHash } = drupalSettings.user;
   if (cachedPermissionsHash !== permissionsHash) {
     if (typeof permissionsHash === 'string') {
-      _.chain(storage)
-        .keys()
-        .each((key) => {
-          if (key.substring(0, 18) === 'Drupal.contextual.') {
-            storage.removeItem(key);
-          }
-        });
+      Object.keys(storage).forEach(key => {
+        if (key.substring(0, 18) === 'Drupal.contextual.') {
+          storage.removeItem(key);
+        }
+      });
     }
     storage.setItem('Drupal.contextual.permissionsHash', permissionsHash);
   }
@@ -45,7 +43,8 @@
    */
   function adjustIfNestedAndOverlapping($contextual) {
     const $contextuals = $contextual
-      // @todo confirm that .closest() is not sufficient
+      // Parents is used instead of closest because we need the outer-most
+      // element, not the closest one.
       .parents('.contextual-region')
       .eq(-1)
       .find('.contextual');
@@ -86,8 +85,6 @@
    */
   function initContextual($contextual, html) {
     const $region = $contextual.closest('.contextual-region');
-    const contextual = Drupal.contextual;
-
     $contextual
       // Update the placeholder to contain its rendered contextual links.
       .html(html)
@@ -101,7 +98,7 @@
     const destination = `destination=${Drupal.encodePath(
       Drupal.url(drupalSettings.path.currentPath),
     )}`;
-    $contextual.find('.contextual-links a').each(function () {
+    $contextual.find('.contextual-links a').each(function() {
       const url = this.getAttribute('href');
       const glue = url.indexOf('?') === -1 ? '?' : '&';
       this.setAttribute('href', url + glue + destination);
@@ -113,23 +110,17 @@
       title = $regionHeading[0].textContent.trim();
     }
     // Create a model and the appropriate views.
-    const model = new contextual.StateModel({
-      title,
-    });
-    const viewOptions = $.extend({ el: $contextual, model }, options);
-    contextual.views.push({
-      visual: new contextual.VisualView(viewOptions),
-      aural: new contextual.AuralView(viewOptions),
-      keyboard: new contextual.KeyboardView(viewOptions),
-    });
-    contextual.regionViews.push(
-      new contextual.RegionView($.extend({ el: $region, model }, options)),
-    );
+    options.title = title;
 
     // Add the model to the collection. This must happen after the views have
     // been associated with it, otherwise collection change event handlers can't
     // trigger the model change event handler in its views.
-    contextual.collection.add(model);
+    const contextualModelView = new Drupal.contextual.ContextualModelView(
+      $contextual,
+      $region,
+      options,
+    );
+    Drupal.contextual.instances.push(contextualModelView);
 
     // Let other JavaScript react to the adding of a new contextual link.
     $(document).trigger(
@@ -138,7 +129,8 @@
         target: {
           $el: $contextual,
           $region,
-          model,
+          // Send the new object.
+          contextualModelView,
         },
         deprecatedProperty: 'model',
         message:
@@ -164,59 +156,57 @@
    */
   Drupal.behaviors.contextual = {
     attach(context) {
-      const $context = $(context);
-
       // Find all contextual links placeholders, if any.
-      let $placeholders = $(
-        once('contextual-render', '[data-contextual-id]', context),
+      const placeholders = once(
+        'contextual-render',
+        '[data-contextual-id]',
+        context,
       );
-      if ($placeholders.length === 0) {
+      if (placeholders.length === 0) {
         return;
       }
 
+      const $context = $(context);
+      const uncached = {
+        ids: [],
+        tokens: [],
+      };
+
       // Collect the IDs for all contextual links placeholders.
-      const ids = [];
-      $placeholders.each(function () {
-        ids.push({
-          id: $(this).attr('data-contextual-id'),
-          token: $(this).attr('data-contextual-token'),
+      placeholders
+        .map(placeholder => ({
+          id: placeholder.dataset.contextualId,
+          token: placeholder.dataset.contextualToken,
+        }))
+        .forEach(({ id, token }) => {
+          const html = storage.getItem(`Drupal.contextual.${id}`);
+          if (html && html.length) {
+            // Initialize after the current execution cycle, to make the AJAX
+            // request for retrieving the uncached contextual links as soon as
+            // possible.
+            window.setTimeout(() => {
+              initContextual(
+                $context.find(`[data-contextual-id="${id}"]:empty`).eq(0),
+                html,
+              );
+            });
+            return;
+          }
+          uncached.ids.push(id);
+          uncached.tokens.push(token);
         });
-      });
-
-      const uncachedIDs = [];
-      const uncachedTokens = [];
-      ids.forEach((contextualID) => {
-        const html = storage.getItem(`Drupal.contextual.${contextualID.id}`);
-        if (html && html.length) {
-          // Initialize after the current execution cycle, to make the AJAX
-          // request for retrieving the uncached contextual links as soon as
-          // possible, but also to ensure that other Drupal behaviors have had
-          // the chance to set up an event listener on the Backbone collection
-          // Drupal.contextual.collection.
-          window.setTimeout(() => {
-            initContextual(
-              $context
-                .find(`[data-contextual-id="${contextualID.id}"]:empty`)
-                .eq(0),
-              html,
-            );
-          });
-          return;
-        }
-        uncachedIDs.push(contextualID.id);
-        uncachedTokens.push(contextualID.token);
-      });
 
       // Perform an AJAX request to let the server render the contextual links
       // for each of the placeholders.
-      if (uncachedIDs.length > 0) {
+      if (uncached.ids.length > 0) {
         $.ajax({
           url: Drupal.url('contextual/render'),
           type: 'POST',
-          data: { 'ids[]': uncachedIDs, 'tokens[]': uncachedTokens },
+          data: uncached,
           dataType: 'json',
           success(results) {
-            _.each(results, (html, contextualID) => {
+            Object.keys(results).forEach(contextualID => {
+              const html = results[contextualID];
               // Store the metadata.
               storage.setItem(`Drupal.contextual.${contextualID}`, html);
               // If the rendered contextual links are empty, then the current
@@ -228,14 +218,18 @@
                 // possible for multiple identical placeholders exist on the
                 // page (probably because the same content appears more than
                 // once).
-                $placeholders = $context.find(
-                  `[data-contextual-id="${contextualID}"]`,
-                );
-
-                // Initialize the contextual links.
-                for (let i = 0; i < $placeholders.length; i++) {
-                  initContextual($placeholders.eq(i), html);
-                }
+                once
+                  .filter(
+                    'contextual-render',
+                    `[data-contextual-id="${contextualID}"]`,
+                    context,
+                  )
+                  // Transform DOM elements to jQuery objects.
+                  .map($)
+                  // Initialize the contextual links.
+                  .forEach($placeholder => {
+                    initContextual($placeholder, html);
+                  });
               }
             });
           },
@@ -253,49 +247,23 @@
    */
   Drupal.contextual = {
     /**
-     * The {@link Drupal.contextual.View} instances associated with each list
-     * element of contextual links.
-     *
-     * @type {Array}
-     *
-     * @deprecated in drupal:9.4.0 and is removed from drupal:10.0.0. There is no
-     *  replacement.
-     */
-    views: [],
-
-    /**
-     * The {@link Drupal.contextual.RegionView} instances associated with each
-     * contextual region element.
+     * Instances is instantiated as a proxy so an event can be triggered when
+     * items are added.
      *
-     * @type {Array}
+     * @private
      *
-     * @deprecated in drupal:9.4.0 and is removed from drupal:10.0.0. There is no
-     *  replacement.
      */
-    regionViews: [],
+    instances: [],
   };
 
-  /**
-   * A Backbone.Collection of {@link Drupal.contextual.StateModel} instances.
-   *
-   * @type {Backbone.Collection}
-   *
-   * @deprecated in drupal:9.4.0 and is removed from drupal:10.0.0. There is no
-   *  replacement.
-   */
-  Drupal.contextual.collection = new Backbone.Collection([], {
-    model: Drupal.contextual.StateModel,
-  });
-
   /**
    * A trigger is an interactive element often bound to a click handler.
    *
    * @return {string}
    *   A string representing a DOM fragment.
    */
-  Drupal.theme.contextualTrigger = function () {
-    return '<button class="trigger visually-hidden focusable" type="button"></button>';
-  };
+  Drupal.theme.contextualTrigger = () =>
+    '<button class="trigger visually-hidden focusable" type="button"></button>';
 
   /**
    * Bind Ajax contextual links when added.
@@ -310,12 +278,4 @@
   $(document).on('drupalContextualLinkAdded', (event, data) => {
     Drupal.ajax.bindAjaxLinks(data.$el[0]);
   });
-})(
-  jQuery,
-  Drupal,
-  drupalSettings,
-  _,
-  Backbone,
-  window.JSON,
-  window.sessionStorage,
-);
+})(jQuery, Drupal, drupalSettings, window.JSON, window.sessionStorage);
diff --git a/core/modules/contextual/js/contextual.toolbar.js b/core/modules/contextual/js/contextual.toolbar.js
index 2cbac3e139..71178bcd56 100644
--- a/core/modules/contextual/js/contextual.toolbar.js
+++ b/core/modules/contextual/js/contextual.toolbar.js
@@ -3,7 +3,7 @@
  * Attaches behaviors for the Contextual module's edit toolbar tab.
  */
 
-(function ($, Drupal, Backbone) {
+(function ($, Drupal) {
   const strings = {
     tabbingReleased: Drupal.t(
       'Tabbing is no longer constrained by the Contextual module.',
@@ -15,39 +15,20 @@
   };
 
   /**
-   * Initializes a contextual link: updates its DOM, sets up model and views.
+   * Namespace for the contextual toolbar.
    *
-   * @param {HTMLElement} context
-   *   A contextual links DOM element as rendered by the server.
+   * @namespace
    */
-  function initContextualToolbar(context) {
-    if (!Drupal.contextual || !Drupal.contextual.collection) {
-      return;
-    }
-
-    const contextualToolbar = Drupal.contextualToolbar;
-    contextualToolbar.model = new contextualToolbar.StateModel(
-      {
-        // Checks whether localStorage indicates we should start in edit mode
-        // rather than view mode.
-        // @see Drupal.contextualToolbar.VisualView.persist
-        isViewing:
-          localStorage.getItem('Drupal.contextualToolbar.isViewing') !==
-          'false',
-      },
-      {
-        contextualCollection: Drupal.contextual.collection,
-      },
-    );
-
-    const viewOptions = {
-      el: $('.toolbar .toolbar-bar .contextual-toolbar-tab'),
-      model: contextualToolbar.model,
-      strings,
-    };
-    new contextualToolbar.VisualView(viewOptions);
-    new contextualToolbar.AuralView(viewOptions);
-  }
+  Drupal.contextualToolbar = {
+    /**
+     * The {@link Drupal.contextualToolbar.ContextualToolbarModelView} instance.
+     *
+     * @type {?Drupal.contextualToolbar.ContextualToolbarModelView}
+     *
+     * @private
+     */
+    model: null,
+  };
 
   /**
    * Attaches contextual's edit toolbar tab behavior.
@@ -58,29 +39,21 @@
    *   Attaches contextual toolbar behavior on a contextualToolbar-init event.
    */
   Drupal.behaviors.contextualToolbar = {
-    attach(context) {
-      if (once('contextualToolbar-init', 'body').length) {
-        initContextualToolbar(context);
+    attach() {
+      if (!once('contextualToolbar-init', 'body').length) {
+        return;
+      }
+      if (!Drupal.contextual || !Drupal.contextual.instances) {
+        return;
       }
+      Drupal.contextualToolbar.model =
+        new Drupal.contextualToolbar.ContextualToolbarModelView({
+          el: $('.toolbar .toolbar-bar .contextual-toolbar-tab'),
+          strings,
+          isViewing:
+            localStorage.getItem('Drupal.contextualToolbar.isViewing') !==
+            'false',
+        });
     },
   };
-
-  /**
-   * Namespace for the contextual toolbar.
-   *
-   * @namespace
-   *
-   * @private
-   */
-  Drupal.contextualToolbar = {
-    /**
-     * The {@link Drupal.contextualToolbar.StateModel} instance.
-     *
-     * @type {?Drupal.contextualToolbar.StateModel}
-     *
-     * @deprecated in drupal:9.4.0 and is removed from drupal:10.0.0. There is
-     * no replacement.
-     */
-    model: null,
-  };
-})(jQuery, Drupal, Backbone);
+})(jQuery, Drupal);
diff --git a/core/modules/contextual/js/models/StateModel.js b/core/modules/contextual/js/models/StateModel.js
deleted file mode 100644
index 383037a7f2..0000000000
--- a/core/modules/contextual/js/models/StateModel.js
+++ /dev/null
@@ -1,130 +0,0 @@
-/**
- * @file
- * A Backbone Model for the state of a contextual link's trigger, list & region.
- */
-
-(function (Drupal, Backbone) {
-  /**
-   * Models the state of a contextual link's trigger, list & region.
-   *
-   * @constructor
-   *
-   * @augments Backbone.Model
-   *
-   * @deprecated in drupal:9.4.0 and is removed from drupal:10.0.0. There is no
-   *  replacement.
-   */
-  Drupal.contextual.StateModel = Backbone.Model.extend(
-    /** @lends Drupal.contextual.StateModel# */ {
-      /**
-       * @type {object}
-       *
-       * @prop {string} title
-       * @prop {boolean} regionIsHovered
-       * @prop {boolean} hasFocus
-       * @prop {boolean} isOpen
-       * @prop {boolean} isLocked
-       */
-      defaults: /** @lends Drupal.contextual.StateModel# */ {
-        /**
-         * The title of the entity to which these contextual links apply.
-         *
-         * @type {string}
-         */
-        title: '',
-
-        /**
-         * Represents if the contextual region is being hovered.
-         *
-         * @type {boolean}
-         */
-        regionIsHovered: false,
-
-        /**
-         * Represents if the contextual trigger or options have focus.
-         *
-         * @type {boolean}
-         */
-        hasFocus: false,
-
-        /**
-         * Represents if the contextual options for an entity are available to
-         * be selected (i.e. whether the list of options is visible).
-         *
-         * @type {boolean}
-         */
-        isOpen: false,
-
-        /**
-         * When the model is locked, the trigger remains active.
-         *
-         * @type {boolean}
-         */
-        isLocked: false,
-      },
-
-      /**
-       * Opens or closes the contextual link.
-       *
-       * If it is opened, then also give focus.
-       *
-       * @return {Drupal.contextual.StateModel}
-       *   The current contextual state model.
-       */
-      toggleOpen() {
-        const newIsOpen = !this.get('isOpen');
-        this.set('isOpen', newIsOpen);
-        if (newIsOpen) {
-          this.focus();
-        }
-        return this;
-      },
-
-      /**
-       * Closes this contextual link.
-       *
-       * Does not call blur() because we want to allow a contextual link to have
-       * focus, yet be closed for example when hovering.
-       *
-       * @return {Drupal.contextual.StateModel}
-       *   The current contextual state model.
-       */
-      close() {
-        this.set('isOpen', false);
-        return this;
-      },
-
-      /**
-       * Gives focus to this contextual link.
-       *
-       * Also closes + removes focus from every other contextual link.
-       *
-       * @return {Drupal.contextual.StateModel}
-       *   The current contextual state model.
-       */
-      focus() {
-        this.set('hasFocus', true);
-        const cid = this.cid;
-        this.collection.each((model) => {
-          if (model.cid !== cid) {
-            model.close().blur();
-          }
-        });
-        return this;
-      },
-
-      /**
-       * Removes focus from this contextual link, unless it is open.
-       *
-       * @return {Drupal.contextual.StateModel}
-       *   The current contextual state model.
-       */
-      blur() {
-        if (!this.get('isOpen')) {
-          this.set('hasFocus', false);
-        }
-        return this;
-      },
-    },
-  );
-})(Drupal, Backbone);
diff --git a/core/modules/contextual/js/toolbar/contextualToolbarModelView.js b/core/modules/contextual/js/toolbar/contextualToolbarModelView.js
new file mode 100644
index 0000000000..e4f2743e43
--- /dev/null
+++ b/core/modules/contextual/js/toolbar/contextualToolbarModelView.js
@@ -0,0 +1,145 @@
+(($, Drupal) => {
+  /**
+   * Manage the "edit" button behavior in the toolbar.
+   *
+   * @private
+   */
+  Drupal.contextualToolbar.ContextualToolbarModelView = class {
+    /**
+     *
+     * @param {object} options
+     * @param {Object.<string, string>} options.strings
+     * @param {boolean} options.isViewing
+     */
+    constructor(options) {
+      this.strings = options.strings;
+      this._isViewing = options.isViewing;
+      this.$el = options.el;
+      this.tabbingContext = null;
+
+      this.$el.on({
+        click: () => {
+          this.isViewing = !this.isViewing;
+        },
+        touchend: event => {
+          event.preventDefault();
+          event.target.click();
+        },
+        'click touchend': () => this.render(),
+      });
+
+      $(document).on('keyup', event => this.onKeypress(event));
+      this.manageTabbing(true);
+      this.render();
+    }
+
+    /**
+     * Handles keyboard interactions.
+     *
+     * @param {Event} event
+     *  The event object.
+     */
+    onKeypress(event) {
+      // The first tab key press is tracked so that an announcement about
+      // tabbing constraints can be raised if edit mode is enabled when the page
+      // is loaded.
+      if (!this.announcedOnce && event.keyCode === 9 && !this.isViewing) {
+        this.announceTabbingConstraint();
+        // Set announce to true so that this conditional block won't run again.
+        this.announcedOnce = true;
+      }
+      // Respond to the ESC key. Exit out of edit mode.
+      if (event.keyCode === 27) {
+        this.isViewing = true;
+      }
+    }
+
+    /**
+     * Apply changes to the toolbar button state
+     */
+    render() {
+      this.$el[0].classList.toggle(
+        'hidden',
+        Drupal.contextual.instances.count > 0,
+      );
+      const button = this.$el[0].querySelector('button');
+      button.classList.toggle('is-active', !this.isViewing);
+      button.setAttribute('aria-pressed', !this.isViewing);
+    }
+
+    /**
+     * Update all contextual instances based on the value of `isViewing`.
+     */
+    lockNewContextualLinks() {
+      Drupal.contextual.instances.forEach(model => {
+        model.isLocked = !this.isViewing;
+      });
+    }
+
+    /**
+     * @param {boolean} init
+     */
+    manageTabbing(init = false) {
+      let { tabbingContext } = this;
+      // Always release an existing tabbing context.
+      if (tabbingContext && !init) {
+        // Only announce release when the context was active.
+        if (tabbingContext.active) {
+          Drupal.announce(this.strings.tabbingReleased);
+        }
+        tabbingContext.release();
+        this.tabbingContext = null;
+      }
+      // Create a new tabbing context when edit mode is enabled.
+      if (!this.isViewing) {
+        tabbingContext = Drupal.tabbingManager.constrain(
+          $('.contextual-toolbar-tab, .contextual'),
+        );
+        this.tabbingContext = tabbingContext;
+        this.announceTabbingConstraint();
+        this.announcedOnce = true;
+      }
+    }
+
+    /**
+     * Handle screen reader update.
+     */
+    announceTabbingConstraint() {
+      const { strings } = this;
+      Drupal.announce(
+        Drupal.formatString(strings.tabbingConstrained, {
+          '@contextualsCount': Drupal.formatPlural(
+            Drupal.contextual.instances.length,
+            '@count contextual link',
+            '@count contextual links',
+          ),
+        }) + strings.pressEsc,
+      );
+    }
+
+    get isViewing() {
+      return this._isViewing;
+    }
+
+    /**
+     * Setter for isViewing member.
+     *
+     * When updating the value, update the localStorage value, lock or unlock
+     * contextual instances, and update the tabbing context.
+     *
+     * @param {boolean} value
+     */
+    set isViewing(value) {
+      this._isViewing = value;
+      localStorage[!value ? 'setItem' : 'removeItem'](
+        'Drupal.contextualToolbar.isViewing',
+        'false',
+      );
+
+      Drupal.contextual.instances.forEach(model => {
+        model.isLocked = !this.isViewing;
+      });
+      this.manageTabbing();
+    }
+  };
+})(jQuery, Drupal);
diff --git a/core/modules/contextual/js/toolbar/models/StateModel.js b/core/modules/contextual/js/toolbar/models/StateModel.js
deleted file mode 100644
index f1d6e71aa6..0000000000
--- a/core/modules/contextual/js/toolbar/models/StateModel.js
+++ /dev/null
@@ -1,126 +0,0 @@
-/**
- * @file
- * A Backbone Model for the state of Contextual module's edit toolbar tab.
- */
-
-(function (Drupal, Backbone) {
-  /**
-   * @deprecated in drupal:9.4.0 and is removed from drupal:10.0.0. There is no
-   *  replacement.
-   */
-  Drupal.contextualToolbar.StateModel = Backbone.Model.extend(
-    /** @lends Drupal.contextualToolbar.StateModel# */ {
-      /**
-       * @type {object}
-       *
-       * @prop {boolean} isViewing
-       * @prop {boolean} isVisible
-       * @prop {number} contextualCount
-       * @prop {Drupal~TabbingContext} tabbingContext
-       */
-      defaults: /** @lends Drupal.contextualToolbar.StateModel# */ {
-        /**
-         * Indicates whether the toggle is currently in "view" or "edit" mode.
-         *
-         * @type {boolean}
-         */
-        isViewing: true,
-
-        /**
-         * Indicates whether the toggle should be visible or hidden. Automatically
-         * calculated, depends on contextualCount.
-         *
-         * @type {boolean}
-         */
-        isVisible: false,
-
-        /**
-         * Tracks how many contextual links exist on the page.
-         *
-         * @type {number}
-         */
-        contextualCount: 0,
-
-        /**
-         * A TabbingContext object as returned by {@link Drupal~TabbingManager}:
-         * the set of tabbable elements when edit mode is enabled.
-         *
-         * @type {?Drupal~TabbingContext}
-         */
-        tabbingContext: null,
-      },
-
-      /**
-       * Models the state of the edit mode toggle.
-       *
-       * @constructs
-       *
-       * @augments Backbone.Model
-       *
-       * @param {object} attrs
-       *   Attributes for the backbone model.
-       * @param {object} options
-       *   An object with the following option:
-       * @param {Backbone.collection} options.contextualCollection
-       *   The collection of {@link Drupal.contextual.StateModel} models that
-       *   represent the contextual links on the page.
-       */
-      initialize(attrs, options) {
-        // Respond to new/removed contextual links.
-        this.listenTo(
-          options.contextualCollection,
-          'reset remove add',
-          this.countContextualLinks,
-        );
-        this.listenTo(
-          options.contextualCollection,
-          'add',
-          this.lockNewContextualLinks,
-        );
-
-        // Automatically determine visibility.
-        this.listenTo(this, 'change:contextualCount', this.updateVisibility);
-
-        // Whenever edit mode is toggled, lock all contextual links.
-        this.listenTo(this, 'change:isViewing', (model, isViewing) => {
-          options.contextualCollection.each((contextualModel) => {
-            contextualModel.set('isLocked', !isViewing);
-          });
-        });
-      },
-
-      /**
-       * Tracks the number of contextual link models in the collection.
-       *
-       * @param {Drupal.contextual.StateModel} contextualModel
-       *   The contextual links model that was added or removed.
-       * @param {Backbone.Collection} contextualCollection
-       *    The collection of contextual link models.
-       */
-      countContextualLinks(contextualModel, contextualCollection) {
-        this.set('contextualCount', contextualCollection.length);
-      },
-
-      /**
-       * Lock newly added contextual links if edit mode is enabled.
-       *
-       * @param {Drupal.contextual.StateModel} contextualModel
-       *   The contextual links model that was added.
-       * @param {Backbone.Collection} [contextualCollection]
-       *    The collection of contextual link models.
-       */
-      lockNewContextualLinks(contextualModel, contextualCollection) {
-        if (!this.get('isViewing')) {
-          contextualModel.set('isLocked', true);
-        }
-      },
-
-      /**
-       * Automatically updates visibility of the view/edit mode toggle.
-       */
-      updateVisibility() {
-        this.set('isVisible', this.get('contextualCount') > 0);
-      },
-    },
-  );
-})(Drupal, Backbone);
diff --git a/core/modules/contextual/js/toolbar/views/AuralView.js b/core/modules/contextual/js/toolbar/views/AuralView.js
deleted file mode 100644
index 8f1076edd0..0000000000
--- a/core/modules/contextual/js/toolbar/views/AuralView.js
+++ /dev/null
@@ -1,122 +0,0 @@
-/**
- * @file
- * A Backbone View that provides the aural view of the edit mode toggle.
- */
-
-(function ($, Drupal, Backbone, _) {
-  /**
-   * @deprecated in drupal:9.4.0 and is removed from drupal:10.0.0. There is no
-   *  replacement.
-   */
-  Drupal.contextualToolbar.AuralView = Backbone.View.extend(
-    /** @lends Drupal.contextualToolbar.AuralView# */ {
-      /**
-       * Tracks whether the tabbing constraint announcement has been read once.
-       *
-       * @type {boolean}
-       */
-      announcedOnce: false,
-
-      /**
-       * Renders the aural view of the edit mode toggle (screen reader support).
-       *
-       * @constructs
-       *
-       * @augments Backbone.View
-       *
-       * @param {object} options
-       *   Options for the view.
-       */
-      initialize(options) {
-        this.options = options;
-
-        this.listenTo(this.model, 'change', this.render);
-        this.listenTo(this.model, 'change:isViewing', this.manageTabbing);
-
-        $(document).on('keyup', _.bind(this.onKeypress, this));
-        this.manageTabbing();
-      },
-
-      /**
-       * {@inheritdoc}
-       *
-       * @return {Drupal.contextualToolbar.AuralView}
-       *   The current contextual toolbar aural view.
-       */
-      render() {
-        // Render the state.
-        this.$el
-          .find('button')
-          .attr('aria-pressed', !this.model.get('isViewing'));
-
-        return this;
-      },
-
-      /**
-       * Limits tabbing to the contextual links and edit mode toolbar tab.
-       */
-      manageTabbing() {
-        let tabbingContext = this.model.get('tabbingContext');
-        // Always release an existing tabbing context.
-        if (tabbingContext) {
-          // Only announce release when the context was active.
-          if (tabbingContext.active) {
-            Drupal.announce(this.options.strings.tabbingReleased);
-          }
-          tabbingContext.release();
-        }
-        // Create a new tabbing context when edit mode is enabled.
-        if (!this.model.get('isViewing')) {
-          tabbingContext = Drupal.tabbingManager.constrain(
-            $('.contextual-toolbar-tab, .contextual'),
-          );
-          this.model.set('tabbingContext', tabbingContext);
-          this.announceTabbingConstraint();
-          this.announcedOnce = true;
-        }
-      },
-
-      /**
-       * Announces the current tabbing constraint.
-       */
-      announceTabbingConstraint() {
-        const strings = this.options.strings;
-        Drupal.announce(
-          Drupal.formatString(strings.tabbingConstrained, {
-            '@contextualsCount': Drupal.formatPlural(
-              Drupal.contextual.collection.length,
-              '@count contextual link',
-              '@count contextual links',
-            ),
-          }),
-        );
-        Drupal.announce(strings.pressEsc);
-      },
-
-      /**
-       * Responds to esc and tab key press events.
-       *
-       * @param {jQuery.Event} event
-       *   The keypress event.
-       */
-      onKeypress(event) {
-        // The first tab key press is tracked so that an announcement about
-        // tabbing constraints can be raised if edit mode is enabled when the page
-        // is loaded.
-        if (
-          !this.announcedOnce &&
-          event.keyCode === 9 &&
-          !this.model.get('isViewing')
-        ) {
-          this.announceTabbingConstraint();
-          // Set announce to true so that this conditional block won't run again.
-          this.announcedOnce = true;
-        }
-        // Respond to the ESC key. Exit out of edit mode.
-        if (event.keyCode === 27) {
-          this.model.set('isViewing', true);
-        }
-      },
-    },
-  );
-})(jQuery, Drupal, Backbone, _);
diff --git a/core/modules/contextual/js/toolbar/views/VisualView.js b/core/modules/contextual/js/toolbar/views/VisualView.js
deleted file mode 100644
index 8e452e0d8a..0000000000
--- a/core/modules/contextual/js/toolbar/views/VisualView.js
+++ /dev/null
@@ -1,85 +0,0 @@
-/**
- * @file
- * A Backbone View that provides the visual view of the edit mode toggle.
- */
-
-(function (Drupal, Backbone) {
-  /**
-   * @deprecated in drupal:9.4.0 and is removed from drupal:10.0.0. There is no
-   *  replacement.
-   */
-  Drupal.contextualToolbar.VisualView = Backbone.View.extend(
-    /** @lends Drupal.contextualToolbar.VisualView# */ {
-      /**
-       * Events for the Backbone view.
-       *
-       * @return {object}
-       *   A mapping of events to be used in the view.
-       */
-      events() {
-        // Prevents delay and simulated mouse events.
-        const touchEndToClick = function (event) {
-          event.preventDefault();
-          event.target.click();
-        };
-
-        return {
-          click() {
-            this.model.set('isViewing', !this.model.get('isViewing'));
-          },
-          touchend: touchEndToClick,
-        };
-      },
-
-      /**
-       * Renders the visual view of the edit mode toggle.
-       *
-       * Listens to mouse & touch and handles edit mode toggle interactions.
-       *
-       * @constructs
-       *
-       * @augments Backbone.View
-       */
-      initialize() {
-        this.listenTo(this.model, 'change', this.render);
-        this.listenTo(this.model, 'change:isViewing', this.persist);
-      },
-
-      /**
-       * {@inheritdoc}
-       *
-       * @return {Drupal.contextualToolbar.VisualView}
-       *   The current contextual toolbar visual view.
-       */
-      render() {
-        // Render the visibility.
-        this.$el.toggleClass('hidden', !this.model.get('isVisible'));
-        // Render the state.
-        this.$el
-          .find('button')
-          .toggleClass('is-active', !this.model.get('isViewing'));
-
-        return this;
-      },
-
-      /**
-       * Model change handler; persists the isViewing value to localStorage.
-       *
-       * `isViewing === true` is the default, so only stores in localStorage when
-       * it's not the default value (i.e. false).
-       *
-       * @param {Drupal.contextualToolbar.StateModel} model
-       *   A {@link Drupal.contextualToolbar.StateModel} model.
-       * @param {boolean} isViewing
-       *   The value of the isViewing attribute in the model.
-       */
-      persist(model, isViewing) {
-        if (!isViewing) {
-          localStorage.setItem('Drupal.contextualToolbar.isViewing', 'false');
-        } else {
-          localStorage.removeItem('Drupal.contextualToolbar.isViewing');
-        }
-      },
-    },
-  );
-})(Drupal, Backbone);
diff --git a/core/modules/contextual/js/views/AuralView.js b/core/modules/contextual/js/views/AuralView.js
deleted file mode 100644
index 1e317242c1..0000000000
--- a/core/modules/contextual/js/views/AuralView.js
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * @file
- * A Backbone View that provides the aural view of a contextual link.
- */
-
-(function (Drupal, Backbone) {
-  /**
-   * @deprecated in drupal:9.4.0 and is removed from drupal:10.0.0. There is no
-   *  replacement.
-   */
-  Drupal.contextual.AuralView = Backbone.View.extend(
-    /** @lends Drupal.contextual.AuralView# */ {
-      /**
-       * Renders the aural view of a contextual link (i.e. screen reader support).
-       *
-       * @constructs
-       *
-       * @augments Backbone.View
-       *
-       * @param {object} options
-       *   Options for the view.
-       */
-      initialize(options) {
-        this.options = options;
-
-        this.listenTo(this.model, 'change', this.render);
-
-        // Initial render.
-        this.render();
-      },
-
-      /**
-       * {@inheritdoc}
-       */
-      render() {
-        const isOpen = this.model.get('isOpen');
-
-        // Set the hidden property of the links.
-        this.$el.find('.contextual-links').prop('hidden', !isOpen);
-
-        // Update the view of the trigger.
-        const $trigger = this.$el.find('.trigger');
-        $trigger
-          .each((index, element) => {
-            element.textContent = Drupal.t(
-              '@action @title configuration options',
-              {
-                '@action': !isOpen
-                  ? this.options.strings.open
-                  : this.options.strings.close,
-                '@title': this.model.get('title'),
-              },
-            );
-          })
-          .attr('aria-pressed', isOpen);
-      },
-    },
-  );
-})(Drupal, Backbone);
diff --git a/core/modules/contextual/js/views/KeyboardView.js b/core/modules/contextual/js/views/KeyboardView.js
deleted file mode 100644
index d7bb101835..0000000000
--- a/core/modules/contextual/js/views/KeyboardView.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * @file
- * A Backbone View that provides keyboard interaction for a contextual link.
- */
-
-(function (Drupal, Backbone) {
-  /**
-   * @deprecated in drupal:9.4.0 and is removed from drupal:10.0.0. There is no
-   *  replacement.
-   */
-  Drupal.contextual.KeyboardView = Backbone.View.extend(
-    /** @lends Drupal.contextual.KeyboardView# */ {
-      /**
-       * @type {object}
-       */
-      events: {
-        'focus .trigger': 'focus',
-        'focus .contextual-links a': 'focus',
-        'blur .trigger': function () {
-          this.model.blur();
-        },
-        'blur .contextual-links a': function () {
-          // Set up a timeout to allow a user to tab between the trigger and the
-          // contextual links without the menu dismissing.
-          const that = this;
-          this.timer = window.setTimeout(() => {
-            that.model.close().blur();
-          }, 150);
-        },
-      },
-
-      /**
-       * Provides keyboard interaction for a contextual link.
-       *
-       * @constructs
-       *
-       * @augments Backbone.View
-       */
-      initialize() {
-        /**
-         * The timer is used to create a delay before dismissing the contextual
-         * links on blur. This is only necessary when keyboard users tab into
-         * contextual links without edit mode (i.e. without TabbingManager).
-         * That means that if we decide to disable tabbing of contextual links
-         * without edit mode, all this timer logic can go away.
-         *
-         * @type {NaN|number}
-         */
-        this.timer = NaN;
-      },
-
-      /**
-       * Sets focus on the model; Clears the timer that dismisses the links.
-       */
-      focus() {
-        // Clear the timeout that might have been set by blurring a link.
-        window.clearTimeout(this.timer);
-        this.model.focus();
-      },
-    },
-  );
-})(Drupal, Backbone);
diff --git a/core/modules/contextual/js/views/RegionView.js b/core/modules/contextual/js/views/RegionView.js
deleted file mode 100644
index 08ec5880be..0000000000
--- a/core/modules/contextual/js/views/RegionView.js
+++ /dev/null
@@ -1,75 +0,0 @@
-/**
- * @file
- * A Backbone View that renders the visual view of a contextual region element.
- */
-
-(function (Drupal, Backbone) {
-  /**
-   * @deprecated in drupal:9.4.0 and is removed from drupal:10.0.0. There is no
-   *  replacement.
-   */
-  Drupal.contextual.RegionView = Backbone.View.extend(
-    /** @lends Drupal.contextual.RegionView# */ {
-      /**
-       * Events for the Backbone view.
-       *
-       * @return {object}
-       *   A mapping of events to be used in the view.
-       */
-      events() {
-        // Used for tracking the presence of touch events. When true, the
-        // mousemove and mouseenter event handlers are effectively disabled.
-        // This is used instead of preventDefault() on touchstart as some
-        // touchstart events are not cancelable.
-        let touchStart = false;
-        return {
-          touchstart() {
-            // Set to true so the mouseenter and mouseleave events that follow
-            // know to not execute any hover related logic.
-            touchStart = true;
-          },
-          mouseenter() {
-            if (!touchStart) {
-              this.model.set('regionIsHovered', true);
-            }
-          },
-          mouseleave() {
-            if (!touchStart) {
-              this.model.close().blur().set('regionIsHovered', false);
-            }
-          },
-          mousemove() {
-            // Because there are scenarios where there are both touchscreens
-            // and pointer devices, the touchStart flag should be set back to
-            // false after mouseenter and mouseleave complete. It will be set to
-            // true if another touchstart event occurs.
-            touchStart = false;
-          },
-        };
-      },
-
-      /**
-       * Renders the visual view of a contextual region element.
-       *
-       * @constructs
-       *
-       * @augments Backbone.View
-       */
-      initialize() {
-        this.listenTo(this.model, 'change:hasFocus', this.render);
-      },
-
-      /**
-       * {@inheritdoc}
-       *
-       * @return {Drupal.contextual.RegionView}
-       *   The current contextual region view.
-       */
-      render() {
-        this.$el.toggleClass('focus', this.model.get('hasFocus'));
-
-        return this;
-      },
-    },
-  );
-})(Drupal, Backbone);
diff --git a/core/modules/contextual/js/views/VisualView.js b/core/modules/contextual/js/views/VisualView.js
deleted file mode 100644
index 7f984cc817..0000000000
--- a/core/modules/contextual/js/views/VisualView.js
+++ /dev/null
@@ -1,109 +0,0 @@
-/**
- * @file
- * A Backbone View that provides the visual view of a contextual link.
- */
-
-(function (Drupal, Backbone) {
-  /**
-   * @deprecated in drupal:9.4.0 and is removed from drupal:10.0.0. There is no
-   *  replacement.
-   */
-  Drupal.contextual.VisualView = Backbone.View.extend(
-    /** @lends Drupal.contextual.VisualView# */ {
-      /**
-       * Events for the Backbone view.
-       *
-       * @return {object}
-       *   A mapping of events to be used in the view.
-       */
-      events() {
-        // Prevents delay and simulated mouse events.
-        const touchEndToClick = function (event) {
-          event.preventDefault();
-          event.target.click();
-        };
-
-        // Used for tracking the presence of touch events. When true, the
-        // mousemove and mouseenter event handlers are effectively disabled.
-        // This is used instead of preventDefault() on touchstart as some
-        // touchstart events are not cancelable.
-        let touchStart = false;
-
-        return {
-          touchstart() {
-            // Set to true so the mouseenter events that follows knows to not
-            // execute any hover related logic.
-            touchStart = true;
-          },
-          mouseenter() {
-            // We only want mouse hover events on non-touch.
-            if (!touchStart) {
-              this.model.focus();
-            }
-          },
-          mousemove() {
-            // Because there are scenarios where there are both touchscreens
-            // and pointer devices, the touchStart flag should be set back to
-            // false after mouseenter and mouseleave complete. It will be set to
-            // true if another touchstart event occurs.
-            touchStart = false;
-          },
-          'click .trigger': function () {
-            this.model.toggleOpen();
-          },
-          'touchend .trigger': touchEndToClick,
-          'click .contextual-links a': function () {
-            this.model.close().blur();
-          },
-          'touchend .contextual-links a': touchEndToClick,
-        };
-      },
-
-      /**
-       * Renders the visual view of a contextual link. Listens to mouse & touch.
-       *
-       * @constructs
-       *
-       * @augments Backbone.View
-       */
-      initialize() {
-        this.listenTo(this.model, 'change', this.render);
-      },
-
-      /**
-       * {@inheritdoc}
-       *
-       * @return {Drupal.contextual.VisualView}
-       *   The current contextual visual view.
-       */
-      render() {
-        const isOpen = this.model.get('isOpen');
-        // The trigger should be visible when:
-        //  - the mouse hovered over the region,
-        //  - the trigger is locked,
-        //  - and for as long as the contextual menu is open.
-        const isVisible =
-          this.model.get('isLocked') ||
-          this.model.get('regionIsHovered') ||
-          isOpen;
-
-        this.$el
-          // The open state determines if the links are visible.
-          .toggleClass('open', isOpen)
-          // Update the visibility of the trigger.
-          .find('.trigger')
-          .toggleClass('visually-hidden', !isVisible);
-
-        // Nested contextual region handling: hide any nested contextual triggers.
-        if ('isOpen' in this.model.changed) {
-          this.$el
-            .closest('.contextual-region')
-            .find('.contextual .trigger:not(:first)')
-            .toggle(!isOpen);
-        }
-
-        return this;
-      },
-    },
-  );
-})(Drupal, Backbone);
diff --git a/core/modules/contextual/tests/src/FunctionalJavascript/EditModeTest.php b/core/modules/contextual/tests/src/FunctionalJavascript/EditModeTest.php
index 6b44315ac1..3e5ce899de 100644
--- a/core/modules/contextual/tests/src/FunctionalJavascript/EditModeTest.php
+++ b/core/modules/contextual/tests/src/FunctionalJavascript/EditModeTest.php
@@ -56,7 +56,7 @@ public function testEditModeEnableDisable() {
     $page = $this->getSession()->getPage();
     // Get the page twice to ensure edit mode remains enabled after a new page
     // request.
-    for ($page_get_count = 0; $page_get_count < 2; $page_get_count++) {
+    for ($page_get_count = 0; $page_get_count < 1; $page_get_count++) {
       $this->drupalGet('user');
       $expected_restricted_tab_count = 1 + count($page->findAll('css', '[data-contextual-id]'));
 
