diff --git a/core/composer.json b/core/composer.json
index 7302f7e066..c5d2f2349a 100644
--- a/core/composer.json
+++ b/core/composer.json
@@ -54,6 +54,7 @@
         "drupal/action": "self.version",
         "drupal/aggregator": "self.version",
         "drupal/automated_cron": "self.version",
+        "drupal/d9_autocomplete": "self.version",
         "drupal/bartik": "self.version",
         "drupal/ban": "self.version",
         "drupal/basic_auth": "self.version",
diff --git a/core/core.libraries.yml b/core/core.libraries.yml
index 4f41165953..bcc0db0553 100644
--- a/core/core.libraries.yml
+++ b/core/core.libraries.yml
@@ -465,6 +465,7 @@ jquery.ui.autocomplete:
     - core/jquery.ui.widget
     - core/jquery.ui.position
     - core/jquery.ui.menu
+  deprecated: The "%library_id%" asset library is deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. See https://www.drupal.org/project/drupal/issues/3076171
 
 jquery.ui.button:
   version: *jquery_ui_version
diff --git a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php
index 96fbfb8471..3ccc1fbcc4 100644
--- a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php
+++ b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php
@@ -188,6 +188,21 @@ public static function processEntityAutocomplete(array &$element, FormStateInter
       'selection_settings_key' => $selection_settings_key,
     ];
 
+    // Create attribute that will be used for client-side enforcement of field
+    // cardinality.
+    $name = explode('[', $element['#name'])[0];
+    if (!empty($complete_form[$name]['widget']['#theme']) && $complete_form[$name]['widget']['#theme'] === 'field_multiple_value_form') {
+      $cardinality = 1;
+    }
+    elseif (!empty($complete_form[$name]['widget']['#cardinality'])) {
+      $cardinality = $complete_form[$name]['widget']['#cardinality'];
+    }
+    else {
+      $cardinality = -1;
+    }
+
+    $complete_form[$name]['#attributes']['data-autocomplete-cardinality'] = $cardinality;
+
     return $element;
   }
 
diff --git a/core/lib/Drupal/Core/Form/FormState.php b/core/lib/Drupal/Core/Form/FormState.php
index 8264371c83..d2f8cafb4a 100644
--- a/core/lib/Drupal/Core/Form/FormState.php
+++ b/core/lib/Drupal/Core/Form/FormState.php
@@ -1109,11 +1109,13 @@ public function clearErrors() {
   public function getError(array $element) {
     if ($errors = $this->getErrors()) {
       $parents = [];
-      foreach ($element['#parents'] as $parent) {
-        $parents[] = $parent;
-        $key = implode('][', $parents);
-        if (isset($errors[$key])) {
-          return $errors[$key];
+      if (!empty($element['#parents'])) {
+        foreach ($element['#parents'] as $parent) {
+          $parents[] = $parent;
+          $key = implode('][', $parents);
+          if (isset($errors[$key])) {
+            return $errors[$key];
+          }
         }
       }
     }
diff --git a/core/modules/d9_autocomplete/css/d9_autocomplete.css b/core/modules/d9_autocomplete/css/d9_autocomplete.css
new file mode 100644
index 0000000000..24441afe8d
--- /dev/null
+++ b/core/modules/d9_autocomplete/css/d9_autocomplete.css
@@ -0,0 +1,23 @@
+[data-drupal-autocomplete-list] {
+  z-index: 1;
+  margin: 0;
+  padding: 0;
+  list-style: none;
+}
+
+[data-drupal-autocomplete-list][hidden],
+[data-drupal-autocomplete-list]:empty {
+  display: none;
+}
+
+[data-drupal-autocomplete-list] li {
+  padding: 3px 1em 3px 0.4em;
+}
+
+[data-drupal-autocomplete-list] li a {
+  display: block;
+}
+
+[data-drupal-autocomplete-list] li a:hover {
+  cursor: pointer;
+}
diff --git a/core/modules/d9_autocomplete/d9_autocomplete.info.yml b/core/modules/d9_autocomplete/d9_autocomplete.info.yml
new file mode 100644
index 0000000000..26a8c4c5a7
--- /dev/null
+++ b/core/modules/d9_autocomplete/d9_autocomplete.info.yml
@@ -0,0 +1,4 @@
+name: 'Drupal 9 Autocomplete'
+type: module
+description: 'Provides a replacement for jQuery UI Autocomplete. jQuery UI Autocomplete is deprecated in Drupal 8.8.0 and will be removed in Drupal 9.0.'
+package: 'Core (Experimental)'
diff --git a/core/modules/d9_autocomplete/d9_autocomplete.libraries.yml b/core/modules/d9_autocomplete/d9_autocomplete.libraries.yml
new file mode 100644
index 0000000000..1295942fd5
--- /dev/null
+++ b/core/modules/d9_autocomplete/d9_autocomplete.libraries.yml
@@ -0,0 +1,16 @@
+autocomplete:
+  version: VERSION
+  js:
+    js/autocomplete.js: {}
+  css:
+    component:
+      css/d9_autocomplete.css: {}
+  dependencies:
+    - core/drupal
+    - core/drupalSettings
+    - core/drupal.ajax
+    - core/drupal.announce
+    - core/popperjs
+    # Needed for once() and event listeners.
+    - core/jquery
+    - core/jquery.once
diff --git a/core/modules/d9_autocomplete/d9_autocomplete.module b/core/modules/d9_autocomplete/d9_autocomplete.module
new file mode 100644
index 0000000000..baa3d6ce62
--- /dev/null
+++ b/core/modules/d9_autocomplete/d9_autocomplete.module
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @file
+ * Provides a replacement for jQuery UI Autocomplete.
+ */
+
+use Drupal\Core\Routing\RouteMatchInterface;
+
+/**
+ * Implements hook_help().
+ */
+function d9_autocomplete_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    case 'help.page.d9_autocomplete':
+      $output = '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('The Drupal 9 Autocomplete module implements a replacement for jQuery UI Autocomplete. The asset library <code>core/jquery.ui.autocomplete</code> is deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Enabling this module overrides the current core asset library to evaluate Awesomplete and to assist with making any necessary style or API changes to themes or modules.') . '</p>';
+      $output .= '<p>' . t('For more information, see the <a href=":d9_autocomplete">online documentation for the Drupal 9 Autocomplete module.</a>.', [':d9_autocomplete' => 'https://www.drupal.org/docs/']) . '</p>';
+      return $output;
+  }
+}
+
+/**
+ * Implements hook_library_info_alter().
+ */
+function d9_autocomplete_library_info_alter(&$libraries, $extension) {
+  if ($extension == 'core' && isset($libraries['drupal.autocomplete'])) {
+    // Remove core js.
+    $libraries['drupal.autocomplete']['js'] = [];
+    // Replace dependency.
+    if ($index = array_search('core/jquery.ui.autocomplete', $libraries['drupal.autocomplete']['dependencies'])) {
+      $libraries['drupal.autocomplete']['dependencies'][$index] = 'd9_autocomplete/autocomplete';
+    }
+  }
+}
diff --git a/core/modules/d9_autocomplete/js/autocomplete.es6.js b/core/modules/d9_autocomplete/js/autocomplete.es6.js
new file mode 100644
index 0000000000..5c40d5d4d7
--- /dev/null
+++ b/core/modules/d9_autocomplete/js/autocomplete.es6.js
@@ -0,0 +1,492 @@
+/**
+ * @file
+ * Standalone autocomplete.
+ */
+
+(($, Drupal, drupalSettings) => {
+  Drupal.Autocomplete = class {
+    constructor(input, options = {}) {
+      this.keyCode = Object.freeze({
+        TAB: 9,
+        RETURN: 13,
+        ESC: 27,
+        SPACE: 32,
+        PAGEUP: 33,
+        PAGEDOWN: 34,
+        END: 35,
+        HOME: 36,
+        LEFT: 37,
+        UP: 38,
+        RIGHT: 39,
+        DOWN: 40,
+      });
+
+      this.count = document.querySelectorAll(
+        '[data-drupal-autocomplete-initialized]',
+      ).length;
+      const listboxId = `autocomplete-listbox-${this.count}`;
+
+      const defaultOptions = {
+        firstCharacterDenylist: ',',
+        minChars: 1,
+        maxItems: 10,
+        sort: false,
+      };
+      this.options = { ...defaultOptions, ...options };
+      this.preventClose = false;
+      this.isOpened = false;
+      this.cache = [];
+      this.suggestionItems = [];
+      this.hasAnnouncedOnce = false;
+      this.input = input;
+      this.input.setAttribute('data-drupal-autocomplete-initialized', '');
+      this.input.setAttribute('aria-autocomplete', 'list');
+      this.input.setAttribute('autocomplete', 'off');
+      this.input.setAttribute('data-drupal-autocomplete-input', '');
+      this.input.setAttribute('aria-owns', listboxId);
+      this.input.setAttribute('role', 'combobox');
+      this.input.setAttribute('aria-expanded', 'false');
+      this.ul = document.createElement('ul');
+      this.ul.setAttribute('role', 'listbox');
+      this.ul.setAttribute('data-drupal-autocomplete-list', '');
+      this.ul.setAttribute('id', listboxId);
+      this.ul.setAttribute('hidden', '');
+      this.input.parentNode.appendChild(this.ul);
+
+      // Add classes that were previously provided by jQuery UI.
+      this.input.classList.add('ui-autocomplete-input');
+      this.ul.classList.add('ui-autocomplete');
+
+      $(this.input).on('input', () => this.inputListener());
+      $(this.input).on('blur', e => this.blurHandler(e));
+      $(this.input).on('keydown', e => this.inputKeyDown(e));
+      $(this.ul).on('mousedown', e => e.preventDefault());
+      $(this.ul).on('click', e => this.itemClick(e));
+      $(this.ul).on('keydown', e => this.listKeyDown(e));
+      $(this.ul).on('blur', e => this.blurHandler(e));
+    }
+
+    /**
+     * Handles blur events.
+     *
+     * @param {Event} e
+     *   The blur event.
+     */
+    blurHandler(e) {
+      if (this.preventClose) {
+        this.preventClose = false;
+        e.preventDefault();
+      } else {
+        this.close();
+      }
+    }
+
+    listKeyDown(e) {
+      if (
+        !this.ul.contains(document.activeElement) ||
+        e.ctrlKey ||
+        e.altKey ||
+        e.metaKey ||
+        e.keyCode === this.keyCode.TAB
+      ) {
+        return;
+      }
+
+      switch (e.keyCode) {
+        case this.keyCode.SPACE:
+        case this.keyCode.RETURN:
+          this.replaceInputValue(document.activeElement.textContent);
+          this.close();
+          this.input.focus();
+          break;
+
+        case this.keyCode.ESC:
+        case this.keyCode.TAB:
+          this.input.focus();
+          this.close();
+          break;
+
+        case this.keyCode.UP:
+          this.focusPrev();
+          break;
+
+        case this.keyCode.DOWN:
+          this.focusNext();
+          break;
+
+        default:
+          break;
+      }
+
+      e.stopPropagation();
+      e.preventDefault();
+    }
+
+    focusPrev() {
+      this.preventClose = true;
+      const currentItem = document.activeElement.getAttribute(
+        'data-drupal-autocomplete-item',
+      );
+      const prevIndex = parseInt(currentItem, 10) - 1;
+      const previousItem = this.ul.querySelector(
+        `[data-drupal-autocomplete-item="${prevIndex}"]`,
+      );
+      if (previousItem) {
+        previousItem.focus();
+      }
+    }
+
+    focusNext() {
+      this.preventClose = true;
+      const currentItem = document.activeElement.getAttribute(
+        'data-drupal-autocomplete-item',
+      );
+      const nextIndex = parseInt(currentItem, 10) + 1;
+      const nextItem = this.ul.querySelector(
+        `[data-drupal-autocomplete-item="${nextIndex}"]`,
+      );
+      if (nextItem) {
+        nextItem.focus();
+      }
+    }
+
+    inputKeyDown(e) {
+      const { keyCode } = e;
+      if (this.isOpened) {
+        if (keyCode === this.keyCode.ESC) {
+          this.close();
+        }
+        if (keyCode === this.keyCode.DOWN) {
+          e.preventDefault();
+          this.preventClose = true;
+          this.ul.querySelector('li').focus();
+        }
+      }
+    }
+
+    itemClick(e) {
+      const li = e.target;
+      if (li && e.button === 0) {
+        this.replaceInputValue(li.textContent);
+        e.preventDefault();
+        this.close();
+      }
+    }
+
+    /**
+     * Replaces the value of an input field when a new value is chosen.
+     *
+     * @param {string} item
+     *   The item being added to the field.
+     */
+    replaceInputValue(item) {
+      const cardinality = this.getCardinality();
+      const numItems = this.splitValues().length;
+
+      // Add a comma separator if the field allows additional items.
+      const separator =
+        numItems < cardinality || parseInt(cardinality, 10) === 0 ? ',' : '';
+      const before = this.input.value.match(/^.+,\s*|/)[0];
+      this.input.value = `${before}${item}${separator}`;
+    }
+
+    inputListener() {
+      const inputId = this.input.getAttribute('id');
+      const searchTerm = this.extractLastEntityReference();
+
+      if (!(inputId in this.cache)) {
+        this.cache[inputId] = {};
+      }
+
+      if (searchTerm && searchTerm.length > 0) {
+        if (this.cache[inputId].hasOwnProperty(searchTerm)) {
+          this.suggestionItems = this.cache[inputId][searchTerm];
+          this.displayResults();
+          this.announceSuggestionCount(this.ul.children.length);
+        } else {
+          const apiUrl = this.input.getAttribute('data-autocomplete-path');
+          this.input.classList.add('ui-autocomplete-loading');
+          const xhr = new XMLHttpRequest();
+          xhr.open('GET', `${apiUrl}?q=${searchTerm}`);
+          xhr.onload = () => {
+            this.input.classList.remove('ui-autocomplete-loading');
+            if (xhr.status === 200) {
+              const results = JSON.parse(xhr.response);
+              this.suggestionItems = results;
+              this.displayResults();
+
+              this.cache[inputId][searchTerm] = results;
+              this.announceSuggestionCount(results.length);
+            }
+          };
+          xhr.send();
+        }
+      }
+    }
+
+    displayResults() {
+      const { announce } = Drupal;
+      const typed = this.extractLastEntityReference();
+      if (
+        typed.length >= this.options.minChars &&
+        this.suggestionItems.length > 0
+      ) {
+        this.ul.innerHTML = '';
+        this.suggestions = this.suggestionItems.filter(item =>
+          this.filterResults(item, typed),
+        );
+
+        if (this.sort !== false) {
+          this.sortSuggestions();
+        }
+
+        this.suggestions = this.suggestions.slice(0, this.options.maxItems);
+
+        this.suggestions.forEach((suggestion, index) => {
+          this.ul.appendChild(this.suggestionItem(suggestion, index));
+        });
+
+        if (this.ul.children.length === 0) {
+          this.close();
+          announce(Drupal.t('No results found'));
+        } else {
+          this.open();
+          this.announceSuggestionCount(this.ul.children.length);
+        }
+      } else {
+        this.close();
+        announce(Drupal.t('No results found'));
+      }
+    }
+
+    /**
+     * Sorts the array of suggestions.
+     */
+    sortSuggestions() {
+      this.suggestions.sort((prior, current) =>
+        prior.label.toUpperCase() > current.label.toUpperCase() ? 1 : -1,
+      );
+    }
+
+    /**
+     * Creates a list item that displays the suggestion.
+     *
+     * @param {object} suggestion
+     *   A suggestion based on user input. It is an object with label and value
+     *   properties.
+     * @param {number} itemIndex
+     *   The index of the item.
+     *
+     * @return {HTMLElement}
+     *   A list item with the suggestion.
+     */
+    suggestionItem(suggestion, itemIndex) {
+      const li = document.createElement('li');
+      // Wrapped in an `<a>` for backwards compatibility.
+      li.innerHTML = `<a tabindex="-1">${suggestion.value.trim()}</a>`;
+      li.setAttribute('role', 'option');
+      li.setAttribute('tabindex', '-1');
+      li.setAttribute('id', `suggestion-${this.count}-${itemIndex}`);
+      li.setAttribute('data-drupal-autocomplete-item', itemIndex);
+      li.setAttribute('aria-posinset', itemIndex + 1);
+      li.setAttribute('aria-selected', 'false');
+      li.onblur = e => this.blurHandler(e);
+
+      return li;
+    }
+
+    /**
+     * Opens the suggestion list.
+     */
+    open() {
+      this.input.setAttribute('aria-expanded', 'true');
+      this.ul.removeAttribute('hidden');
+      this.isOpened = true;
+      this.ul.style.minWidth = `${this.input.offsetWidth - 4}px`;
+      if (!this.hasOwnProperty('popper')) {
+        this.initPopper();
+      } else {
+        this.popper.forceUpdate();
+      }
+    }
+
+    /**
+     * Closes the suggestion list.
+     */
+    close() {
+      this.input.setAttribute('aria-expanded', 'false');
+      this.ul.setAttribute('hidden', '');
+      this.isOpened = false;
+    }
+
+    /**
+     * Returns the last value of an multi-value textfield.
+     *
+     * @return {string}
+     *   The last value of the input field.
+     */
+    extractLastEntityReference() {
+      return this.splitValues().pop();
+    }
+
+    /**
+     * Helper splitting selections from the autocomplete value.
+     *
+     * @return {Array}
+     *   Array of values, split by comma.
+     */
+    splitValues() {
+      const { value } = this.input;
+      const result = [];
+      let quote = false;
+      let current = '';
+      const valueLength = value.length;
+      for (let i = 0; i < valueLength; i++) {
+        const character = value.charAt(i);
+        if (character === '"') {
+          current += character;
+          quote = !quote;
+        } else if (character === ',' && !quote) {
+          result.push(current.trim());
+          current = '';
+        } else {
+          current += character;
+        }
+      }
+      if (value.length > 0) {
+        result.push(current.trim());
+      }
+      return result;
+    }
+
+    /**
+     * Provides information on suggestion count to screenreaders.
+     *
+     * @param {number} count
+     *   The number of results present.
+     */
+    announceSuggestionCount(count) {
+      const { announce, formatPlural } = Drupal;
+      const { maxItems } = this.options;
+
+      // If the number of suggestions provided equals the maximum allowed,
+      // provide a different message so users are aware there may be additional
+      // suggestions that match their criteria.
+      const pluralMessage =
+        maxItems === count
+          ? 'There are at least @count results available. Type additional characters to refine your search.'
+          : 'There are @count results available.';
+      const message = formatPlural(
+        count,
+        'There is one result available.',
+        pluralMessage,
+      );
+
+      announce(Drupal.t(message), 'assertive');
+    }
+
+    /**
+     * Determines if a suggestion should be an available option.
+     *
+     * @param {object} suggestion
+     *   A suggestion based on user input. It is an object with label and value
+     *   properties.
+     * @param {string} typed
+     *   The text entered in the input field.
+     *
+     * @return {boolean}
+     *   If the suggestion should be displayed in the results.
+     */
+    filterResults(suggestion, typed) {
+      const { firstCharacterDenylist } = this.options;
+      const cardinality = this.getCardinality();
+      const suggestionValue = suggestion.value;
+      const currentValues = this.splitValues();
+
+      const inputSpecificFirstCharDenylist = this.input.hasAttribute(
+        'data-autocomplete-first-character-denylist',
+      )
+        ? this.input.getAttribute('data-autocomplete-first-character-denylist')
+        : '';
+
+      // Prevent suggestions if the first input character is in the denylist, if
+      // the suggestion has already been added to the field, or if the maximum
+      // number of items have been reached.
+      if (
+        firstCharacterDenylist.indexOf(typed[0]) !== -1 ||
+        inputSpecificFirstCharDenylist.indexOf(typed[0]) !== -1 ||
+        currentValues.indexOf(suggestionValue) !== -1 ||
+        (cardinality > 0 && currentValues.length > cardinality)
+      ) {
+        return false;
+      }
+
+      return RegExp(
+        this.extractLastEntityReference()
+          .trim()
+          .replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&'),
+        'i',
+      ).test(suggestionValue);
+    }
+
+    /**
+     * Returns the configured cardinality of the field, when available.
+     *
+     * @return {number}
+     *   The cardinality of the field. Returns -1 if it is not explicitly
+     *   configured, which is interpreted as unlimited.
+     */
+    getCardinality() {
+      const wrapper = this.input.closest('[data-autocomplete-cardinality]');
+      return wrapper
+        ? wrapper.getAttribute('data-autocomplete-cardinality')
+        : -1;
+    }
+
+    /**
+     * Initialize positioning of items with PopperJS.
+     */
+    initPopper() {
+      this.popper = Popper.createPopper(this.input, this.ul, {
+        placement: 'bottom-start',
+        modifiers: [
+          {
+            name: 'flip',
+            options: {
+              fallbackPlacements: [],
+            },
+          },
+        ],
+      });
+    }
+  };
+
+  Drupal.Autocomplete.instances = [];
+
+  /**
+   * Attaches the autocomplete behavior to all required fields.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches the autocomplete behaviors.
+   */
+  Drupal.behaviors.autocomplete = {
+    attach(context) {
+      const options = drupalSettings.autocompleteOptions;
+
+      const $autoCompleteInputs = $(context)
+        .find('input.form-autocomplete')
+        .once('autocomplete-init');
+
+      Drupal.Autocomplete.instances = Drupal.Autocomplete.instances.concat(
+        $autoCompleteInputs
+          .map(
+            (index, autocompleteInput) =>
+              new Drupal.Autocomplete(autocompleteInput, options),
+          )
+          .get(),
+      );
+    },
+  };
+})(jQuery, Drupal, drupalSettings, Popper);
diff --git a/core/modules/d9_autocomplete/js/autocomplete.js b/core/modules/d9_autocomplete/js/autocomplete.js
new file mode 100644
index 0000000000..cd73418ec2
--- /dev/null
+++ b/core/modules/d9_autocomplete/js/autocomplete.js
@@ -0,0 +1,421 @@
+/**
+* DO NOT EDIT THIS FILE.
+* See the following change record for more information,
+* https://www.drupal.org/node/2815083
+* @preserve
+**/
+
+function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
+
+function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
+
+function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+(function ($, Drupal, drupalSettings) {
+  Drupal.Autocomplete = function () {
+    function _class(input) {
+      var _this = this;
+
+      var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+
+      _classCallCheck(this, _class);
+
+      this.keyCode = Object.freeze({
+        TAB: 9,
+        RETURN: 13,
+        ESC: 27,
+        SPACE: 32,
+        PAGEUP: 33,
+        PAGEDOWN: 34,
+        END: 35,
+        HOME: 36,
+        LEFT: 37,
+        UP: 38,
+        RIGHT: 39,
+        DOWN: 40
+      });
+      this.count = document.querySelectorAll('[data-drupal-autocomplete-initialized]').length;
+      var listboxId = "autocomplete-listbox-".concat(this.count);
+      var defaultOptions = {
+        firstCharacterDenylist: ',',
+        minChars: 1,
+        maxItems: 10,
+        sort: false
+      };
+      this.options = _objectSpread(_objectSpread({}, defaultOptions), options);
+      this.preventClose = false;
+      this.isOpened = false;
+      this.cache = [];
+      this.suggestionItems = [];
+      this.hasAnnouncedOnce = false;
+      this.input = input;
+      this.input.setAttribute('data-drupal-autocomplete-initialized', '');
+      this.input.setAttribute('aria-autocomplete', 'list');
+      this.input.setAttribute('autocomplete', 'off');
+      this.input.setAttribute('data-drupal-autocomplete-input', '');
+      this.input.setAttribute('aria-owns', listboxId);
+      this.input.setAttribute('role', 'combobox');
+      this.input.setAttribute('aria-expanded', 'false');
+      this.ul = document.createElement('ul');
+      this.ul.setAttribute('role', 'listbox');
+      this.ul.setAttribute('data-drupal-autocomplete-list', '');
+      this.ul.setAttribute('id', listboxId);
+      this.ul.setAttribute('hidden', '');
+      this.input.parentNode.appendChild(this.ul);
+      this.input.classList.add('ui-autocomplete-input');
+      this.ul.classList.add('ui-autocomplete');
+      $(this.input).on('input', function () {
+        return _this.inputListener();
+      });
+      $(this.input).on('blur', function (e) {
+        return _this.blurHandler(e);
+      });
+      $(this.input).on('keydown', function (e) {
+        return _this.inputKeyDown(e);
+      });
+      $(this.ul).on('mousedown', function (e) {
+        return e.preventDefault();
+      });
+      $(this.ul).on('click', function (e) {
+        return _this.itemClick(e);
+      });
+      $(this.ul).on('keydown', function (e) {
+        return _this.listKeyDown(e);
+      });
+      $(this.ul).on('blur', function (e) {
+        return _this.blurHandler(e);
+      });
+    }
+
+    _createClass(_class, [{
+      key: "blurHandler",
+      value: function blurHandler(e) {
+        if (this.preventClose) {
+          this.preventClose = false;
+          e.preventDefault();
+        } else {
+          this.close();
+        }
+      }
+    }, {
+      key: "listKeyDown",
+      value: function listKeyDown(e) {
+        if (!this.ul.contains(document.activeElement) || e.ctrlKey || e.altKey || e.metaKey || e.keyCode === this.keyCode.TAB) {
+          return;
+        }
+
+        switch (e.keyCode) {
+          case this.keyCode.SPACE:
+          case this.keyCode.RETURN:
+            this.replaceInputValue(document.activeElement.textContent);
+            this.close();
+            this.input.focus();
+            break;
+
+          case this.keyCode.ESC:
+          case this.keyCode.TAB:
+            this.input.focus();
+            this.close();
+            break;
+
+          case this.keyCode.UP:
+            this.focusPrev();
+            break;
+
+          case this.keyCode.DOWN:
+            this.focusNext();
+            break;
+
+          default:
+            break;
+        }
+
+        e.stopPropagation();
+        e.preventDefault();
+      }
+    }, {
+      key: "focusPrev",
+      value: function focusPrev() {
+        this.preventClose = true;
+        var currentItem = document.activeElement.getAttribute('data-drupal-autocomplete-item');
+        var prevIndex = parseInt(currentItem, 10) - 1;
+        var previousItem = this.ul.querySelector("[data-drupal-autocomplete-item=\"".concat(prevIndex, "\"]"));
+
+        if (previousItem) {
+          previousItem.focus();
+        }
+      }
+    }, {
+      key: "focusNext",
+      value: function focusNext() {
+        this.preventClose = true;
+        var currentItem = document.activeElement.getAttribute('data-drupal-autocomplete-item');
+        var nextIndex = parseInt(currentItem, 10) + 1;
+        var nextItem = this.ul.querySelector("[data-drupal-autocomplete-item=\"".concat(nextIndex, "\"]"));
+
+        if (nextItem) {
+          nextItem.focus();
+        }
+      }
+    }, {
+      key: "inputKeyDown",
+      value: function inputKeyDown(e) {
+        var keyCode = e.keyCode;
+
+        if (this.isOpened) {
+          if (keyCode === this.keyCode.ESC) {
+            this.close();
+          }
+
+          if (keyCode === this.keyCode.DOWN) {
+            e.preventDefault();
+            this.preventClose = true;
+            this.ul.querySelector('li').focus();
+          }
+        }
+      }
+    }, {
+      key: "itemClick",
+      value: function itemClick(e) {
+        var li = e.target;
+
+        if (li && e.button === 0) {
+          this.replaceInputValue(li.textContent);
+          e.preventDefault();
+          this.close();
+        }
+      }
+    }, {
+      key: "replaceInputValue",
+      value: function replaceInputValue(item) {
+        var cardinality = this.getCardinality();
+        var numItems = this.splitValues().length;
+        var separator = numItems < cardinality || parseInt(cardinality, 10) === 0 ? ',' : '';
+        var before = this.input.value.match(/^.+,\s*|/)[0];
+        this.input.value = "".concat(before).concat(item).concat(separator);
+      }
+    }, {
+      key: "inputListener",
+      value: function inputListener() {
+        var _this2 = this;
+
+        var inputId = this.input.getAttribute('id');
+        var searchTerm = this.extractLastEntityReference();
+
+        if (!(inputId in this.cache)) {
+          this.cache[inputId] = {};
+        }
+
+        if (searchTerm && searchTerm.length > 0) {
+          if (this.cache[inputId].hasOwnProperty(searchTerm)) {
+            this.suggestionItems = this.cache[inputId][searchTerm];
+            this.displayResults();
+            this.announceSuggestionCount(this.ul.children.length);
+          } else {
+            var apiUrl = this.input.getAttribute('data-autocomplete-path');
+            this.input.classList.add('ui-autocomplete-loading');
+            var xhr = new XMLHttpRequest();
+            xhr.open('GET', "".concat(apiUrl, "?q=").concat(searchTerm));
+
+            xhr.onload = function () {
+              _this2.input.classList.remove('ui-autocomplete-loading');
+
+              if (xhr.status === 200) {
+                var results = JSON.parse(xhr.response);
+                _this2.suggestionItems = results;
+
+                _this2.displayResults();
+
+                _this2.cache[inputId][searchTerm] = results;
+
+                _this2.announceSuggestionCount(results.length);
+              }
+            };
+
+            xhr.send();
+          }
+        }
+      }
+    }, {
+      key: "displayResults",
+      value: function displayResults() {
+        var _this3 = this;
+
+        var announce = Drupal.announce;
+        var typed = this.extractLastEntityReference();
+
+        if (typed.length >= this.options.minChars && this.suggestionItems.length > 0) {
+          this.ul.innerHTML = '';
+          this.suggestions = this.suggestionItems.filter(function (item) {
+            return _this3.filterResults(item, typed);
+          });
+
+          if (this.sort !== false) {
+            this.sortSuggestions();
+          }
+
+          this.suggestions = this.suggestions.slice(0, this.options.maxItems);
+          this.suggestions.forEach(function (suggestion, index) {
+            _this3.ul.appendChild(_this3.suggestionItem(suggestion, index));
+          });
+
+          if (this.ul.children.length === 0) {
+            this.close();
+            announce(Drupal.t('No results found'));
+          } else {
+            this.open();
+            this.announceSuggestionCount(this.ul.children.length);
+          }
+        } else {
+          this.close();
+          announce(Drupal.t('No results found'));
+        }
+      }
+    }, {
+      key: "sortSuggestions",
+      value: function sortSuggestions() {
+        this.suggestions.sort(function (prior, current) {
+          return prior.label.toUpperCase() > current.label.toUpperCase() ? 1 : -1;
+        });
+      }
+    }, {
+      key: "suggestionItem",
+      value: function suggestionItem(suggestion, itemIndex) {
+        var _this4 = this;
+
+        var li = document.createElement('li');
+        li.innerHTML = "<a tabindex=\"-1\">".concat(suggestion.value.trim(), "</a>");
+        li.setAttribute('role', 'option');
+        li.setAttribute('tabindex', '-1');
+        li.setAttribute('id', "suggestion-".concat(this.count, "-").concat(itemIndex));
+        li.setAttribute('data-drupal-autocomplete-item', itemIndex);
+        li.setAttribute('aria-posinset', itemIndex + 1);
+        li.setAttribute('aria-selected', 'false');
+
+        li.onblur = function (e) {
+          return _this4.blurHandler(e);
+        };
+
+        return li;
+      }
+    }, {
+      key: "open",
+      value: function open() {
+        this.input.setAttribute('aria-expanded', 'true');
+        this.ul.removeAttribute('hidden');
+        this.isOpened = true;
+        this.ul.style.minWidth = "".concat(this.input.offsetWidth - 4, "px");
+
+        if (!this.hasOwnProperty('popper')) {
+          this.initPopper();
+        } else {
+          this.popper.forceUpdate();
+        }
+      }
+    }, {
+      key: "close",
+      value: function close() {
+        this.input.setAttribute('aria-expanded', 'false');
+        this.ul.setAttribute('hidden', '');
+        this.isOpened = false;
+      }
+    }, {
+      key: "extractLastEntityReference",
+      value: function extractLastEntityReference() {
+        return this.splitValues().pop();
+      }
+    }, {
+      key: "splitValues",
+      value: function splitValues() {
+        var value = this.input.value;
+        var result = [];
+        var quote = false;
+        var current = '';
+        var valueLength = value.length;
+
+        for (var i = 0; i < valueLength; i++) {
+          var character = value.charAt(i);
+
+          if (character === '"') {
+            current += character;
+            quote = !quote;
+          } else if (character === ',' && !quote) {
+            result.push(current.trim());
+            current = '';
+          } else {
+            current += character;
+          }
+        }
+
+        if (value.length > 0) {
+          result.push(current.trim());
+        }
+
+        return result;
+      }
+    }, {
+      key: "announceSuggestionCount",
+      value: function announceSuggestionCount(count) {
+        var announce = Drupal.announce,
+            formatPlural = Drupal.formatPlural;
+        var maxItems = this.options.maxItems;
+        var pluralMessage = maxItems === count ? 'There are at least @count results available. Type additional characters to refine your search.' : 'There are @count results available.';
+        var message = formatPlural(count, 'There is one result available.', pluralMessage);
+        announce(Drupal.t(message), 'assertive');
+      }
+    }, {
+      key: "filterResults",
+      value: function filterResults(suggestion, typed) {
+        var firstCharacterDenylist = this.options.firstCharacterDenylist;
+        var cardinality = this.getCardinality();
+        var suggestionValue = suggestion.value;
+        var currentValues = this.splitValues();
+        var inputSpecificFirstCharDenylist = this.input.hasAttribute('data-autocomplete-first-character-denylist') ? this.input.getAttribute('data-autocomplete-first-character-denylist') : '';
+
+        if (firstCharacterDenylist.indexOf(typed[0]) !== -1 || inputSpecificFirstCharDenylist.indexOf(typed[0]) !== -1 || currentValues.indexOf(suggestionValue) !== -1 || cardinality > 0 && currentValues.length > cardinality) {
+          return false;
+        }
+
+        return RegExp(this.extractLastEntityReference().trim().replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&'), 'i').test(suggestionValue);
+      }
+    }, {
+      key: "getCardinality",
+      value: function getCardinality() {
+        var wrapper = this.input.closest('[data-autocomplete-cardinality]');
+        return wrapper ? wrapper.getAttribute('data-autocomplete-cardinality') : -1;
+      }
+    }, {
+      key: "initPopper",
+      value: function initPopper() {
+        this.popper = Popper.createPopper(this.input, this.ul, {
+          placement: 'bottom-start',
+          modifiers: [{
+            name: 'flip',
+            options: {
+              fallbackPlacements: []
+            }
+          }]
+        });
+      }
+    }]);
+
+    return _class;
+  }();
+
+  Drupal.Autocomplete.instances = [];
+  Drupal.behaviors.autocomplete = {
+    attach: function attach(context) {
+      var options = drupalSettings.autocompleteOptions;
+      var $autoCompleteInputs = $(context).find('input.form-autocomplete').once('autocomplete-init');
+      Drupal.Autocomplete.instances = Drupal.Autocomplete.instances.concat($autoCompleteInputs.map(function (index, autocompleteInput) {
+        return new Drupal.Autocomplete(autocompleteInput, options);
+      }).get());
+    }
+  };
+})(jQuery, Drupal, drupalSettings, Popper);
\ No newline at end of file
diff --git a/core/modules/d9_autocomplete/tests/src/FunctionalJavascript/D9EntityReferenceAutocompleteWidgetTest.php b/core/modules/d9_autocomplete/tests/src/FunctionalJavascript/D9EntityReferenceAutocompleteWidgetTest.php
new file mode 100644
index 0000000000..5163128b61
--- /dev/null
+++ b/core/modules/d9_autocomplete/tests/src/FunctionalJavascript/D9EntityReferenceAutocompleteWidgetTest.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace Drupal\Tests\d9_autocomplete\FunctionalJavascript;
+
+use Drupal\FunctionalJavascriptTests\EntityReference\EntityReferenceAutocompleteWidgetTest;
+
+/**
+ * Runs entity reference autocomplete widget tests with the D9 replacement.
+ *
+ * @group d9_autocomplete
+ */
+class D9EntityReferenceAutocompleteWidgetTest extends EntityReferenceAutocompleteWidgetTest {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['node', 'field_ui', 'taxonomy', 'd9_autocomplete'];
+}
\ No newline at end of file
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/EntityReference/EntityReferenceAutocompleteWidgetTest.php b/core/tests/Drupal/FunctionalJavascriptTests/EntityReference/EntityReferenceAutocompleteWidgetTest.php
index d827c2b15d..ddc5bda509 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/EntityReference/EntityReferenceAutocompleteWidgetTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/EntityReference/EntityReferenceAutocompleteWidgetTest.php
@@ -2,10 +2,12 @@
 
 namespace Drupal\FunctionalJavascriptTests\EntityReference;
 
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
 use Drupal\Tests\field\Traits\EntityReferenceTestTrait;
 use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
 use Drupal\Tests\node\Traits\NodeCreationTrait;
+use Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait;
 
 /**
  * Tests the output of entity reference autocomplete widgets.
@@ -17,11 +19,19 @@ class EntityReferenceAutocompleteWidgetTest extends WebDriverTestBase {
   use ContentTypeCreationTrait;
   use EntityReferenceTestTrait;
   use NodeCreationTrait;
+  use TaxonomyTestTrait;
 
   /**
    * {@inheritdoc}
    */
-  protected static $modules = ['node', 'field_ui'];
+  protected static $modules = ['node', 'taxonomy', 'field_ui'];
+
+  /**
+   * The test vocabulary.
+   *
+   * @var \Drupal\taxonomy\VocabularyInterface
+   */
+  protected $vocabulary;
 
   /**
    * {@inheritdoc}
@@ -38,6 +48,7 @@ protected function setUp(): void {
     $this->createContentType(['type' => 'page']);
     $this->createNode(['title' => 'Test page']);
     $this->createNode(['title' => 'Page test']);
+    $this->createNode(['title' => 'Guess me']);
 
     $user = $this->drupalCreateUser([
       'access content',
@@ -56,7 +67,14 @@ public function testEntityReferenceAutocompleteWidget() {
     // Create an entity reference field and use the default 'CONTAINS' match
     // operator.
     $field_name = 'field_test';
-    $this->createEntityReferenceField('node', 'page', $field_name, $field_name, 'node', 'default', ['target_bundles' => ['page'], 'sort' => ['field' => 'title', 'direction' => 'DESC']]);
+    $selection_handler_settings = [
+      'target_bundles' => ['page'],
+      'sort' => [
+        'field' => 'title',
+        'direction' => 'DESC',
+      ],
+    ];
+    $this->createEntityReferenceField('node', 'page', $field_name, $field_name, 'node', 'default', $selection_handler_settings);
     $form_display = $display_repository->getFormDisplay('node', 'page');
     $form_display->setComponent($field_name, [
       'type' => 'entity_reference_autocomplete',
@@ -64,6 +82,14 @@ public function testEntityReferenceAutocompleteWidget() {
         'match_operator' => 'CONTAINS',
       ],
     ]);
+
+    $display_repository->getViewDisplay('node', 'page')
+      ->setComponent($field_name, [
+          'type' => 'entity_reference_label',
+          'weight' => 10,
+        ])
+      ->save();
+
     // To satisfy config schema, the size setting must be an integer, not just
     // a numeric value. See https://www.drupal.org/node/2885441.
     $this->assertIsInt($form_display->getComponent($field_name)['settings']['size']);
@@ -79,7 +105,6 @@ public function testEntityReferenceAutocompleteWidget() {
     $autocomplete_field->setValue('Test');
     $this->getSession()->getDriver()->keyDown($autocomplete_field->getXpath(), ' ');
     $assert_session->waitOnAutocomplete();
-
     $results = $page->findAll('css', '.ui-autocomplete li');
 
     $this->assertCount(2, $results);
@@ -127,13 +152,12 @@ public function testEntityReferenceAutocompleteWidget() {
 
     // Change the size of the result set via the UI.
     $this->drupalLogin($this->createUser([
-        'access content',
-        'administer content types',
-        'administer node fields',
-        'administer node form display',
-        'create page content',
-      ]
-    ));
+      'access content',
+      'administer content types',
+      'administer node fields',
+      'administer node form display',
+      'create page content',
+    ]));
     $this->drupalGet('/admin/structure/types/manage/page/form-display');
     $assert_session->pageTextContains('Autocomplete suggestion list size: 1');
     // Click on the widget settings button to open the widget settings form.
@@ -148,6 +172,15 @@ public function testEntityReferenceAutocompleteWidget() {
 
     $this->doAutocomplete($field_name);
     $this->assertCount(2, $page->findAll('css', '.ui-autocomplete li'));
+
+    // Test that an entity reference is saved by providing just the title,
+    // without the addition of the entity ID in parentheses.
+    $this->drupalGet('node/add/page');
+    $page->fillField('Title', 'Testing that the autocomplete field does not require the entity id');
+    $autocomplete_field = $assert_session->waitForElement('css', '[name="' . $field_name . '[0][target_id]"].ui-autocomplete-input');
+    $autocomplete_field->setValue('Guess me');
+    $page->pressButton('Save');
+    $assert_session->elementExists('css', '[href$="/node/3"]:contains("Guess me")');
   }
 
   /**
@@ -163,4 +196,71 @@ protected function doAutocomplete($field_name) {
     $this->assertSession()->waitOnAutocomplete();
   }
 
+  /**
+   * Test that spaces and commas work properly with autocomplete fields.
+   */
+  public function testSeparators() {
+    $this->createTagsFieldOnPage();
+
+    $term_commas = 'I,love,commas';
+    $term_spaces = 'Just a fan of spaces';
+    $term_commas_spaces = 'I dig both commas and spaces, apparently';
+
+    $this->createTerm($this->vocabulary, ['name' => $term_commas]);
+    $this->createTerm($this->vocabulary, ['name' => $term_spaces]);
+    $this->createTerm($this->vocabulary, ['name' => $term_commas_spaces]);
+
+    $this->drupalGet('node/add/page');
+    $page = $this->getSession()->getPage();
+    $assert_session = $this->assertSession();
+
+    $autocomplete_field = $assert_session->waitForElement('css', '[name="taxonomy_reference[target_id]"]');
+    $autocomplete_field->setValue('a');
+    $this->getSession()->getDriver()->keyDown($autocomplete_field->getXpath(), ' ');
+    $assert_session->waitOnAutocomplete();
+
+    $results = $page->findAll('css', '.ui-autocomplete li');
+    $this->assertCount(3, $results);
+
+    $assert_session->elementExists('css', '.ui-autocomplete li:contains("' . $term_commas_spaces . '")')->click();
+    $assert_session->pageTextNotContains($term_commas);
+    $assert_session->pageTextNotContains($term_spaces);
+    $current_value = $autocomplete_field->getValue();
+    $this->assertStringContainsString($term_commas_spaces, $current_value);
+  }
+
+  /**
+   * Create a tags field on the Page content type.
+   */
+  public function createTagsFieldOnPage() {
+    /** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
+    $display_repository = \Drupal::service('entity_display.repository');
+
+    $field_name = 'taxonomy_reference';
+    $vocabulary = $this->createVocabulary();
+    $this->vocabulary = $vocabulary;
+
+    $handler_settings = [
+      'target_bundles' => [
+        $vocabulary->id() => $vocabulary->id(),
+      ],
+      'auto_create' => TRUE,
+    ];
+    $this->createEntityReferenceField('node', 'page', $field_name, 'Tags', 'taxonomy_term', 'default', $handler_settings, FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
+    $display_repository->getFormDisplay('node', 'page')
+      ->setComponent($field_name, [
+        'type' => 'entity_reference_autocomplete_tags',
+        'settings' => [
+          'match_operator' => 'CONTAINS',
+        ],
+      ])
+      ->save();
+    $display_repository->getViewDisplay('node', 'page')
+      ->setComponent($field_name, [
+        'type' => 'entity_reference_label',
+        'weight' => 10,
+      ])
+      ->save();
+  }
+
 }
diff --git a/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php b/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php
index 6ef3a4fd8f..5e6492e995 100644
--- a/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php
+++ b/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php
@@ -143,6 +143,8 @@ public static function getSkippedDeprecations() {
       'assertDirectoryNotIsWritable() is deprecated and will be removed in PHPUnit 10. Refactor your code to use assertDirectoryIsNotWritable() instead.',
       'assertFileNotIsWritable() is deprecated and will be removed in PHPUnit 10. Refactor your code to use assertFileIsNotWritable() instead.',
       'The at() matcher has been deprecated. It will be removed in PHPUnit 10. Please refactor your test to not rely on the order in which methods are invoked.',
+      // The query.ui.autocomplete library is deprecated but still in use as the replacement is currently an optional module.
+      'The "core/jquery.ui.autocomplete" asset library is deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. See https://www.drupal.org/project/drupal/issues/3076171'
     ];
   }
 
