diff --git a/core/.eslintrc.json b/core/.eslintrc.json
index f4b9ff39fb65086fb41566059484603e9e335b96..5a982db5a266c016b3f45da642b731bfc6969905 100644
--- a/core/.eslintrc.json
+++ b/core/.eslintrc.json
@@ -15,6 +15,7 @@
     "drupalTranslations": true,
     "jQuery": true,
     "_": true,
+    "Cookies": true,
     "Backbone": true,
     "Modernizr": true,
     "Popper": true,
diff --git a/core/assets/vendor/jquery.cookie/jquery.cookie.min.js b/core/assets/vendor/jquery.cookie/jquery.cookie.min.js
deleted file mode 100644
index c0f19d8a3b4097a2f5dfbb731c248d9d8bdbded5..0000000000000000000000000000000000000000
--- a/core/assets/vendor/jquery.cookie/jquery.cookie.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-/*! jquery.cookie v1.4.1 | MIT */
-!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof exports?a(require("jquery")):a(jQuery)}(function(a){function b(a){return h.raw?a:encodeURIComponent(a)}function c(a){return h.raw?a:decodeURIComponent(a)}function d(a){return b(h.json?JSON.stringify(a):String(a))}function e(a){0===a.indexOf('"')&&(a=a.slice(1,-1).replace(/\\"/g,'"').replace(/\\\\/g,"\\"));try{return a=decodeURIComponent(a.replace(g," ")),h.json?JSON.parse(a):a}catch(b){}}function f(b,c){var d=h.raw?b:e(b);return a.isFunction(c)?c(d):d}var g=/\+/g,h=a.cookie=function(e,g,i){if(void 0!==g&&!a.isFunction(g)){if(i=a.extend({},h.defaults,i),"number"==typeof i.expires){var j=i.expires,k=i.expires=new Date;k.setTime(+k+864e5*j)}return document.cookie=[b(e),"=",d(g),i.expires?"; expires="+i.expires.toUTCString():"",i.path?"; path="+i.path:"",i.domain?"; domain="+i.domain:"",i.secure?"; secure":""].join("")}for(var l=e?void 0:{},m=document.cookie?document.cookie.split("; "):[],n=0,o=m.length;o>n;n++){var p=m[n].split("="),q=c(p.shift()),r=p.join("=");if(e&&e===q){l=f(r,g);break}e||void 0===(r=f(r))||(l[q]=r)}return l};h.defaults={},a.removeCookie=function(b,c){return void 0===a.cookie(b)?!1:(a.cookie(b,"",a.extend({},c,{expires:-1})),!a.cookie(b))}});
\ No newline at end of file
diff --git a/core/assets/vendor/js-cookie/js.cookie.min.js b/core/assets/vendor/js-cookie/js.cookie.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..f5f4c36c17146c4d1fb7346fca83f196de695e8b
--- /dev/null
+++ b/core/assets/vendor/js-cookie/js.cookie.min.js
@@ -0,0 +1,3 @@
+/*! js-cookie v2.2.1 | MIT */
+
+!function(a){var b;if("function"==typeof define&&define.amd&&(define(a),b=!0),"object"==typeof exports&&(module.exports=a(),b=!0),!b){var c=window.Cookies,d=window.Cookies=a();d.noConflict=function(){return window.Cookies=c,d}}}(function(){function a(){for(var a=0,b={};a<arguments.length;a++){var c=arguments[a];for(var d in c)b[d]=c[d]}return b}function b(a){return a.replace(/(%[0-9A-Z]{2})+/g,decodeURIComponent)}function c(d){function e(){}function f(b,c,f){if("undefined"!=typeof document){f=a({path:"/"},e.defaults,f),"number"==typeof f.expires&&(f.expires=new Date(1*new Date+864e5*f.expires)),f.expires=f.expires?f.expires.toUTCString():"";try{var g=JSON.stringify(c);/^[\{\[]/.test(g)&&(c=g)}catch(j){}c=d.write?d.write(c,b):encodeURIComponent(c+"").replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g,decodeURIComponent),b=encodeURIComponent(b+"").replace(/%(23|24|26|2B|5E|60|7C)/g,decodeURIComponent).replace(/[\(\)]/g,escape);var h="";for(var i in f)f[i]&&(h+="; "+i,!0!==f[i]&&(h+="="+f[i].split(";")[0]));return document.cookie=b+"="+c+h}}function g(a,c){if("undefined"!=typeof document){for(var e={},f=document.cookie?document.cookie.split("; "):[],g=0;g<f.length;g++){var h=f[g].split("="),i=h.slice(1).join("=");c||'"'!==i.charAt(0)||(i=i.slice(1,-1));try{var j=b(h[0]);if(i=(d.read||d)(i,j)||b(i),c)try{i=JSON.parse(i)}catch(k){}if(e[j]=i,a===j)break}catch(k){}}return a?e[a]:e}}return e.set=f,e.get=function(a){return g(a,!1)},e.getJSON=function(a){return g(a,!0)},e.remove=function(b,c){f(b,"",a(c,{expires:-1}))},e.defaults={},e.withConverter=c,e}return c(function(){})});
\ No newline at end of file
diff --git a/core/core.libraries.yml b/core/core.libraries.yml
index b632a426e05a890bcd4f1c67952be24d441718cd..51ac98b7aba9a74990a33e0cf62fbd236873becb 100644
--- a/core/core.libraries.yml
+++ b/core/core.libraries.yml
@@ -210,7 +210,6 @@ drupal.form:
     - core/jquery
     - core/drupal
     - core/drupal.debounce
-    - core/jquery.cookie
     - core/jquery.once
 
 drupal.machine-name:
@@ -275,7 +274,6 @@ drupal.tabledrag:
     - core/drupal
     - core/drupalSettings
     - core/jquery.once
-    - core/jquery.cookie
 
 drupal.tableheader:
   version: VERSION
@@ -351,16 +349,14 @@ jquery:
     assets/vendor/jquery/jquery.min.js: { minified: true, weight: -20 }
 
 jquery.cookie:
-  remote: https://github.com/carhartl/jquery-cookie
-  version: "v1.4.1"
-  license:
-    name: MIT
-    url: https://github.com/carhartl/jquery-cookie/blob/v1.4.1/MIT-LICENSE.txt
-    gpl-compatible: true
+  version: VERSION
   js:
-    assets/vendor/jquery.cookie/jquery.cookie.min.js: { minified: true }
+    misc/jquery.cookie.shim.js: {}
   dependencies:
     - core/jquery
+    - core/drupal
+    - core/js-cookie
+  deprecated: The %library_id% asset library is deprecated in Drupal 9.0.0 and will be removed in Drupal 10.0.0. Use the core/js-cookie library instead. See https://www.drupal.org/node/3104677
 
 jquery.farbtastic:
   remote: https://github.com/mattfarina/farbtastic
@@ -406,7 +402,6 @@ jquery.joyride:
     assets/vendor/jquery-joyride/jquery.joyride-2.1.js: { }
   dependencies:
     - core/jquery
-    - core/jquery.cookie
 
 jquery.once:
   remote: https://github.com/RobLoach/jquery-once
@@ -661,3 +656,15 @@ drupal.dialog.off_canvas:
     - core/drupal.announce
     - core/drupal.dialog
     - core/drupal.dialog.ajax
+
+js-cookie:
+  remote: https://github.com/js-cookie/js-cookie
+  version: "v2.2.1"
+  license:
+    name: MIT
+    url: https://github.com/js-cookie/js-cookie/blob/v2.2.1/MIT-LICENSE.txt
+    gpl-compatible: true
+  js:
+    assets/vendor/js-cookie/js.cookie.min.js: {}
+  dependencies:
+    - core/drupal.object.assign
diff --git a/core/misc/jquery.cookie.shim.es6.js b/core/misc/jquery.cookie.shim.es6.js
new file mode 100644
index 0000000000000000000000000000000000000000..b6b925a10070e06f24115da563a09424a995e33b
--- /dev/null
+++ b/core/misc/jquery.cookie.shim.es6.js
@@ -0,0 +1,207 @@
+/**
+ * @file
+ * Defines a backwards-compatible shim for jquery.cookie.
+ */
+
+/**
+ * The core/js-cookie library object.
+ *
+ * @global
+ *
+ * @var {object} Cookies
+ */
+
+(($, Drupal, cookies) => {
+  const deprecatedMessageSuffix = `is deprecated in Drupal 9.0.0 and will be removed in Drupal 10.0.0. Use the core/js-cookie library instead. See https://www.drupal.org/node/3104677`;
+
+  /**
+   * Determines if an object is a function.
+   *
+   * @param {Object} obj
+   *   The object to check.
+   *
+   * @return {boolean}
+   *   True if the object is a function.
+   */
+  const isFunction = obj =>
+    Object.prototype.toString.call(obj) === '[object Function]';
+
+  /**
+   * Decodes cookie value for compatibility with jquery.cookie.
+   *
+   * @param {string} value
+   *   The cookie value to parse.
+   *
+   * @return {string}
+   *   The cookie value for the reader to return.
+   */
+  const parseCookieValue = value => {
+    if (value.indexOf('"') === 0) {
+      value = value
+        .slice(1, -1)
+        .replace(/\\"/g, '"')
+        .replace(/\\\\/g, '\\');
+    }
+    return decodeURIComponent(value.replace(/\+/g, ' '));
+  };
+
+  /**
+   * Wraps the cookie value to support unsanitized values.
+   *
+   * Decoding strings is the job of the converter when using js-cookie, and
+   * the shim uses the same decode function as that library when the deprecated
+   * raw option is not used.
+   *
+   * @param {string} cookieValue
+   *   The cookie value.
+   * @param {string} cookieName
+   *   The cookie name.
+   * @param {reader~converterCallback} converter
+   *   A function that takes the cookie value for further processing.
+   * @param {boolean} readUnsanitized
+   *   Uses the unsanitized value when set to true.
+   *
+   * @return {string}
+   *   The cookie value that js-cookie will return.
+   */
+  const reader = (cookieValue, cookieName, converter, readUnsanitized) => {
+    const value = readUnsanitized ? cookieValue : parseCookieValue(cookieValue);
+
+    if (converter !== undefined && isFunction(converter)) {
+      return converter(value, cookieName);
+    }
+    return value;
+  };
+
+  /**
+   * Gets or sets a browser cookie.
+   *
+   * @example
+   * // Returns 'myCookie=myCookieValue'.
+   * $.cookie('myCookie', 'myCookieValue');
+   * @example
+   * // Returns 'myCookieValue'.
+   * $.cookie('myCookie');
+   *
+   * @example
+   * // Returns the literal URI-encoded value of {"key": "value"} as the cookie
+   * // value along with the path as in the above example.
+   * $.cookie('myCookie', { key: 'value' });
+   * @example
+   * $.cookie.json = true;
+   * // Returns { key: 'value' }.
+   * $.cookie('myCookie');
+   *
+   * @param {string} key
+   *   The name of the cookie.
+   * @param {string|Object|Function|undefined} value
+   *   A js-cookie converter callback when used as a getter. This callback must
+   *   be a function when using this shim for backwards-compatiblity with
+   *   jquery.cookie. When used as a setter, value is the string or JSON object
+   *   to be used as the cookie value.
+   * @param {Object|undefined} options
+   *   Overrides the default options when used as a setter. See the js-cookie
+   *   library README.md file for details.
+   *
+   * @return {string}
+   *   Returns the cookie name, value, and other properties based on the
+   *   return value of the document.cookie setter.
+   *
+   * @deprecated in Drupal 9.0.0 and is removed from Drupal 10.0.0.
+   *   Use the core/js-cookie library instead.
+   *
+   * @see https://www.drupal.org/node/3104677
+   * @see https://github.com/js-cookie/js-cookie/blob/v2.2.1/README.md
+   */
+  $.cookie = (key, value = undefined, options = undefined) => {
+    Drupal.deprecationError({
+      message: `jQuery.cookie() ${deprecatedMessageSuffix}`,
+    });
+    if (value !== undefined && !isFunction(value)) {
+      // The caller is setting a cookie value and not trying to retrieve the
+      // cookie value using a converter callback.
+      const attributes = Object.assign($.cookie.defaults, options);
+
+      if (!$.cookie.json) {
+        // An object that is passed in must be typecast to a string when the
+        // "json" option is not set because js-cookie will always stringify
+        // JSON cookie values.
+        value = String(value);
+      }
+
+      // If the expires value is a non-empty string, it needs to be converted
+      // to a Date() object before being sent to js-cookie.
+      if (typeof attributes.expires === 'string' && attributes.expires !== '') {
+        attributes.expires = new Date(attributes.expires);
+      }
+
+      const cookieSetter = cookies.withConverter({
+        write: cookieValue => encodeURIComponent(cookieValue),
+      });
+
+      return cookieSetter.set(key, value, attributes);
+    }
+
+    // Use either js-cookie or pass in a converter to get the raw cookie value,
+    // which has security implications, but remains in place for
+    // backwards-compatibility.
+    const userProvidedConverter = value;
+    const cookiesShim = cookies.withConverter((cookieValue, cookieName) =>
+      reader(cookieValue, cookieName, userProvidedConverter, $.cookie.raw),
+    );
+
+    return $.cookie.json === true
+      ? cookiesShim.getJSON(key)
+      : cookiesShim.get(key);
+  };
+
+  /**
+   * @prop {Object} defaults
+   *   The default options when setting a cookie.
+   * @prop {string} defaults.path
+   *   The default path for the cookie is ''.
+   * @prop {undefined} defaults.expires
+   *   There is no default value for the expires option. The default expiration
+   *   is set to an empty string.
+   */
+  $.cookie.defaults = { path: '', ...cookies.defaults };
+
+  /**
+   * @prop {boolean} json
+   *   True if the cookie value should be parsed as JSON.
+   */
+  $.cookie.json = false;
+
+  /**
+   * @prop {boolean} json
+   *   True if the cookie value should be returned as-is without decoding
+   *   URI entities. In jquery.cookie, this also would not encode the cookie
+   *   name, but js-cookie does not allow this.
+   */
+  $.cookie.raw = false;
+
+  /**
+   * Removes a browser cookie.
+   *
+   * @param {string} key
+   *   The name of the cookie.
+   * @param {Object} options
+   *   Optional options. See the js-cookie library README.md for more details.
+   *
+   * @return {boolean}
+   *   Returns true when the cookie is successfully removed.
+   *
+   * @deprecated in Drupal 9.0.0 and is removed from Drupal 10.0.0.
+   *   Use the core/js-cookie library instead.
+   *
+   * @see https://www.drupal.org/node/3104677
+   * @see https://github.com/js-cookie/js-cookie/blob/v2.2.1/README.md
+   */
+  $.removeCookie = (key, options) => {
+    Drupal.deprecationError({
+      message: `jQuery.removeCookie() ${deprecatedMessageSuffix}`,
+    });
+    cookies.remove(key, Object.assign($.cookie.defaults, options));
+    return !cookies.get(key);
+  };
+})(jQuery, Drupal, window.Cookies);
diff --git a/core/misc/jquery.cookie.shim.js b/core/misc/jquery.cookie.shim.js
new file mode 100644
index 0000000000000000000000000000000000000000..29f98bca7944e7a3d3db08db203ab2f07b1d68f7
--- /dev/null
+++ b/core/misc/jquery.cookie.shim.js
@@ -0,0 +1,85 @@
+/**
+* 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 ($, Drupal, cookies) {
+  var deprecatedMessageSuffix = "is deprecated in Drupal 9.0.0 and will be removed in Drupal 10.0.0. Use the core/js-cookie library instead. See https://www.drupal.org/node/3104677";
+
+  var isFunction = function isFunction(obj) {
+    return Object.prototype.toString.call(obj) === '[object Function]';
+  };
+
+  var parseCookieValue = function parseCookieValue(value) {
+    if (value.indexOf('"') === 0) {
+      value = value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
+    }
+
+    return decodeURIComponent(value.replace(/\+/g, ' '));
+  };
+
+  var reader = function reader(cookieValue, cookieName, converter, readUnsanitized) {
+    var value = readUnsanitized ? cookieValue : parseCookieValue(cookieValue);
+
+    if (converter !== undefined && isFunction(converter)) {
+      return converter(value, cookieName);
+    }
+
+    return value;
+  };
+
+  $.cookie = function (key) {
+    var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined;
+    var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : undefined;
+    Drupal.deprecationError({
+      message: "jQuery.cookie() ".concat(deprecatedMessageSuffix)
+    });
+
+    if (value !== undefined && !isFunction(value)) {
+      var attributes = Object.assign($.cookie.defaults, options);
+
+      if (!$.cookie.json) {
+        value = String(value);
+      }
+
+      if (typeof attributes.expires === 'string' && attributes.expires !== '') {
+        attributes.expires = new Date(attributes.expires);
+      }
+
+      var cookieSetter = cookies.withConverter({
+        write: function write(cookieValue) {
+          return encodeURIComponent(cookieValue);
+        }
+      });
+      return cookieSetter.set(key, value, attributes);
+    }
+
+    var userProvidedConverter = value;
+    var cookiesShim = cookies.withConverter(function (cookieValue, cookieName) {
+      return reader(cookieValue, cookieName, userProvidedConverter, $.cookie.raw);
+    });
+    return $.cookie.json === true ? cookiesShim.getJSON(key) : cookiesShim.get(key);
+  };
+
+  $.cookie.defaults = _objectSpread({
+    path: ''
+  }, cookies.defaults);
+  $.cookie.json = false;
+  $.cookie.raw = false;
+
+  $.removeCookie = function (key, options) {
+    Drupal.deprecationError({
+      message: "jQuery.removeCookie() ".concat(deprecatedMessageSuffix)
+    });
+    cookies.remove(key, Object.assign($.cookie.defaults, options));
+    return !cookies.get(key);
+  };
+})(jQuery, Drupal, window.Cookies);
\ No newline at end of file
diff --git a/core/modules/system/tests/modules/js_cookie_test/js/js_cookie_shim_test.es6.js b/core/modules/system/tests/modules/js_cookie_test/js/js_cookie_shim_test.es6.js
new file mode 100644
index 0000000000000000000000000000000000000000..b8f5f373e51a5c3be32d043fd72d3d09721472ca
--- /dev/null
+++ b/core/modules/system/tests/modules/js_cookie_test/js/js_cookie_shim_test.es6.js
@@ -0,0 +1,31 @@
+/**
+ * @file
+ * Tests adding and removing browser cookies using the jquery_cookie shim.
+ */
+(({ behaviors }, $) => {
+  behaviors.jqueryCookie = {
+    attach: () => {
+      if ($('body').once('js_cookie_test-init').length) {
+        $('.js_cookie_test_add_button').on('click', () => {
+          $.cookie('js_cookie_test', 'red panda');
+        });
+        $('.js_cookie_test_add_raw_button').on('click', () => {
+          $.cookie.raw = true;
+          $.cookie('js_cookie_test_raw', 'red panda');
+        });
+        $('.js_cookie_test_add_json_button').on('click', () => {
+          $.cookie.json = true;
+          $.cookie('js_cookie_test_json', { panda: 'red' });
+          $.cookie('js_cookie_test_json_simple', 'red panda');
+        });
+        $('.js_cookie_test_add_json_string_button').on('click', () => {
+          $.cookie.json = false;
+          $.cookie('js_cookie_test_json_string', { panda: 'red' });
+        });
+        $('.js_cookie_test_remove_button').on('click', () => {
+          $.removeCookie('js_cookie_test');
+        });
+      }
+    },
+  };
+})(Drupal, jQuery);
diff --git a/core/modules/system/tests/modules/js_cookie_test/js/js_cookie_shim_test.js b/core/modules/system/tests/modules/js_cookie_test/js/js_cookie_shim_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..a8dfa51c9fa518fdc021e4e10e43b6ae0feb2dd7
--- /dev/null
+++ b/core/modules/system/tests/modules/js_cookie_test/js/js_cookie_shim_test.js
@@ -0,0 +1,39 @@
+/**
+* DO NOT EDIT THIS FILE.
+* See the following change record for more information,
+* https://www.drupal.org/node/2815083
+* @preserve
+**/
+
+(function (_ref, $) {
+  var behaviors = _ref.behaviors;
+  behaviors.jqueryCookie = {
+    attach: function attach() {
+      if ($('body').once('js_cookie_test-init').length) {
+        $('.js_cookie_test_add_button').on('click', function () {
+          $.cookie('js_cookie_test', 'red panda');
+        });
+        $('.js_cookie_test_add_raw_button').on('click', function () {
+          $.cookie.raw = true;
+          $.cookie('js_cookie_test_raw', 'red panda');
+        });
+        $('.js_cookie_test_add_json_button').on('click', function () {
+          $.cookie.json = true;
+          $.cookie('js_cookie_test_json', {
+            panda: 'red'
+          });
+          $.cookie('js_cookie_test_json_simple', 'red panda');
+        });
+        $('.js_cookie_test_add_json_string_button').on('click', function () {
+          $.cookie.json = false;
+          $.cookie('js_cookie_test_json_string', {
+            panda: 'red'
+          });
+        });
+        $('.js_cookie_test_remove_button').on('click', function () {
+          $.removeCookie('js_cookie_test');
+        });
+      }
+    }
+  };
+})(Drupal, jQuery);
\ No newline at end of file
diff --git a/core/modules/system/tests/modules/js_cookie_test/js_cookie_test.info.yml b/core/modules/system/tests/modules/js_cookie_test/js_cookie_test.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d33421d82352db55e1408a01f2c2c65896a95d24
--- /dev/null
+++ b/core/modules/system/tests/modules/js_cookie_test/js_cookie_test.info.yml
@@ -0,0 +1,5 @@
+name: 'JS Cookie Test'
+type: module
+description: 'Module for the jsCookieTest.'
+package: Testing
+version: VERSION
diff --git a/core/modules/system/tests/modules/js_cookie_test/js_cookie_test.libraries.yml b/core/modules/system/tests/modules/js_cookie_test/js_cookie_test.libraries.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0f15de6c686942da13b2f969e83ad83168e186fd
--- /dev/null
+++ b/core/modules/system/tests/modules/js_cookie_test/js_cookie_test.libraries.yml
@@ -0,0 +1,9 @@
+with_shim_test:
+  version: VERSION
+  js:
+    js/js_cookie_shim_test.js: {}
+  dependencies:
+    - core/jquery
+    - core/drupal
+    - core/jquery.cookie
+    - core/jquery.once
diff --git a/core/modules/system/tests/modules/js_cookie_test/js_cookie_test.routing.yml b/core/modules/system/tests/modules/js_cookie_test/js_cookie_test.routing.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5ee89b5496595b446be12ca4803952aa3675acad
--- /dev/null
+++ b/core/modules/system/tests/modules/js_cookie_test/js_cookie_test.routing.yml
@@ -0,0 +1,7 @@
+js_cookie_test.with_shim:
+  path: '/js_cookie_with_shim_test'
+  defaults:
+    _controller: '\Drupal\js_cookie_test\Controller\JsCookieTestController::jqueryCookieShimTest'
+    _title: 'JQueryCookieShimTest'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/system/tests/modules/js_cookie_test/src/Controller/JsCookieTestController.php b/core/modules/system/tests/modules/js_cookie_test/src/Controller/JsCookieTestController.php
new file mode 100644
index 0000000000000000000000000000000000000000..7cce7cbf3bec6cbb9334a6eed9a1abc656b4b5ca
--- /dev/null
+++ b/core/modules/system/tests/modules/js_cookie_test/src/Controller/JsCookieTestController.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Drupal\js_cookie_test\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+
+/**
+ * Test controller to assert js-cookie library integration.
+ */
+class JsCookieTestController extends ControllerBase {
+
+  /**
+   * Provides buttons to add and remove cookies using JavaScript.
+   *
+   * @return array
+   *   The render array.
+   */
+  public function jqueryCookieShimTest() {
+    return [
+      'add' => [
+        '#type' => 'button',
+        '#value' => $this->t('Add cookie'),
+        '#attributes' => [
+          'class' => ['js_cookie_test_add_button'],
+        ],
+      ],
+      'add-raw' => [
+        '#type' => 'button',
+        '#value' => $this->t('Add raw cookie'),
+        '#attributes' => [
+          'class' => ['js_cookie_test_add_raw_button'],
+        ],
+      ],
+      'add-json' => [
+        '#type' => 'button',
+        '#value' => $this->t('Add JSON cookie'),
+        '#attributes' => [
+          'class' => ['js_cookie_test_add_json_button'],
+        ],
+      ],
+      'add-json-string' => [
+        '#type' => 'button',
+        '#value' => $this->t('Add JSON cookie without json option'),
+        '#attributes' => [
+          'class' => ['js_cookie_test_add_json_string_button'],
+        ],
+      ],
+      'remove' => [
+        '#type' => 'button',
+        '#value' => $this->t('Remove cookie'),
+        '#attributes' => [
+          'class' => ['js_cookie_test_remove_button'],
+        ],
+      ],
+      '#attached' => ['library' => ['js_cookie_test/with_shim_test']],
+    ];
+  }
+
+}
diff --git a/core/tests/Drupal/Nightwatch/Tests/jsCookieTest.js b/core/tests/Drupal/Nightwatch/Tests/jsCookieTest.js
new file mode 100644
index 0000000000000000000000000000000000000000..189d8bc15cf4c26d2f1bc497cd693a3a40075f0d
--- /dev/null
+++ b/core/tests/Drupal/Nightwatch/Tests/jsCookieTest.js
@@ -0,0 +1,274 @@
+const deprecatedMessageSuffix = `is deprecated in Drupal 9.0.0 and will be removed in Drupal 10.0.0. Use the core/js-cookie library instead. See https://www.drupal.org/node/3104677`;
+// Nightwatch suggests non-ES6 functions when using the execute method.
+// eslint-disable-next-line func-names, prefer-arrow-callback
+const getJqueryCookie = function(cookieName) {
+  return undefined !== cookieName ? jQuery.cookie(cookieName) : jQuery.cookie();
+};
+// eslint-disable-next-line func-names, prefer-arrow-callback
+const setJqueryCookieWithOptions = function(
+  cookieName,
+  cookieValue,
+  options = {},
+) {
+  return jQuery.cookie(cookieName, cookieValue, options);
+};
+module.exports = {
+  '@tags': ['core'],
+  before(browser) {
+    browser.drupalInstall().drupalLoginAsAdmin(() => {
+      browser
+        .drupalRelativeURL('/admin/modules')
+        .setValue('input[type="search"]', 'JS Cookie Test')
+        .waitForElementVisible(
+          'input[name="modules[js_cookie_test][enable]"]',
+          1000,
+        )
+        .click('input[name="modules[js_cookie_test][enable]"]')
+        .click('input[type="submit"]'); // Submit module form.
+    });
+  },
+  after(browser) {
+    browser.drupalUninstall();
+  },
+  'Test jquery.cookie Shim Simple Value and jquery.removeCookie': browser => {
+    browser
+      .drupalRelativeURL('/js_cookie_with_shim_test')
+      .waitForElementVisible('.js_cookie_test_add_button', 1000)
+      .click('.js_cookie_test_add_button')
+      // prettier-ignore
+      .execute(getJqueryCookie, ['js_cookie_test'], result => {
+        browser.assert.equal(
+          result.value,
+          'red panda',
+          '$.cookie returns cookie value',
+        );
+      })
+      .waitForElementVisible('.js_cookie_test_remove_button', 1000)
+      .click('.js_cookie_test_remove_button')
+      .execute(getJqueryCookie, ['js_cookie_test_remove'], result => {
+        browser.assert.equal(result.value, null, 'cookie removed');
+      })
+      .drupalLogAndEnd({ onlyOnError: false });
+  },
+  'Test jquery.cookie Shim Empty Value': browser => {
+    browser
+      .setCookie({
+        name: 'js_cookie_test_empty',
+        value: '',
+      })
+      // prettier-ignore
+      .execute(getJqueryCookie, ['js_cookie_test_empty'], result => {
+        browser.assert.equal(
+          result.value,
+          '',
+          '$.cookie returns empty cookie value',
+        );
+      })
+      .getCookie('js_cookie_test_empty', result => {
+        browser.assert.equal(result.value, '', 'Cookie value is empty.');
+      })
+      .drupalLogAndEnd({ onlyOnError: false });
+  },
+  'Test jquery.cookie Shim Undefined': browser => {
+    browser
+      .deleteCookie('js_cookie_test_undefined', () => {
+        browser.execute(
+          getJqueryCookie,
+          ['js_cookie_test_undefined'],
+          result => {
+            browser.assert.equal(
+              result.value,
+              undefined,
+              '$.cookie returns undefined cookie value',
+            );
+          },
+        );
+      })
+      .drupalLogAndEnd({ onlyOnError: false });
+  },
+  'Test jquery.cookie Shim Decode': browser => {
+    browser
+      .setCookie({
+        name: encodeURIComponent(' js_cookie_test_encoded'),
+        value: encodeURIComponent(' red panda'),
+      })
+      .execute(getJqueryCookie, [' js_cookie_test_encoded'], result => {
+        browser.assert.equal(
+          result.value,
+          ' red panda',
+          '$.cookie returns decoded cookie value',
+        );
+      })
+      .setCookie({
+        name: 'js_cookie_test_encoded_plus_to_space',
+        value: 'red+panda',
+      })
+      .execute(
+        getJqueryCookie,
+        ['js_cookie_test_encoded_plus_to_space'],
+        result => {
+          browser.assert.equal(
+            result.value,
+            'red panda',
+            '$.cookie returns decoded plus to space in cookie value',
+          );
+        },
+      )
+      .drupalLogAndEnd({ onlyOnError: false });
+  },
+  'Test jquery.cookie Shim With raw': browser => {
+    browser
+      .drupalRelativeURL('/js_cookie_with_shim_test')
+      .waitForElementVisible('.js_cookie_test_add_raw_button', 1000)
+      .click('.js_cookie_test_add_raw_button')
+      .execute(getJqueryCookie, ['js_cookie_test_raw'], result => {
+        browser.assert.equal(
+          result.value,
+          'red%20panda',
+          '$.cookie returns raw cookie value',
+        );
+      })
+      .drupalLogAndEnd({ onlyOnError: false });
+  },
+  'Test jquery.cookie Shim With JSON': browser => {
+    browser
+      .drupalRelativeURL('/js_cookie_with_shim_test')
+      .waitForElementVisible('.js_cookie_test_add_json_button', 1000)
+      .click('.js_cookie_test_add_json_button')
+      .execute(getJqueryCookie, ['js_cookie_test_json'], result => {
+        browser.assert.deepEqual(
+          result.value,
+          { panda: 'red' },
+          'Stringified JSON is returned as JSON.',
+        );
+      })
+      .getCookie('js_cookie_test_json', result => {
+        browser.assert.equal(
+          result.value,
+          '%7B%22panda%22%3A%22red%22%7D',
+          'Cookie value is encoded backwards-compatible with jquery.cookie.',
+        );
+      })
+      .execute(getJqueryCookie, ['js_cookie_test_json_simple'], result => {
+        browser.assert.equal(
+          result.value,
+          'red panda',
+          '$.cookie returns simple cookie value with JSON enabled',
+        );
+      })
+      .waitForElementVisible('.js_cookie_test_add_json_string_button', 1000)
+      .click('.js_cookie_test_add_json_string_button')
+      .execute(getJqueryCookie, ['js_cookie_test_json_string'], result => {
+        browser.assert.deepEqual(
+          result.value,
+          '[object Object]',
+          'JSON used without json option is return as a string.',
+        );
+      })
+      .getCookie('js_cookie_test_json_string', result => {
+        browser.assert.equal(
+          result.value,
+          '%5Bobject%20Object%5D',
+          'Cookie value is encoded backwards-compatible with jquery.cookie.',
+        );
+      })
+      .drupalLogAndEnd({ onlyOnError: false });
+  },
+  'Test jquery.cookie Shim invalid URL encoding': browser => {
+    browser
+      .setCookie({
+        name: 'js_cookie_test_bad',
+        value: 'red panda%',
+      })
+      .execute(getJqueryCookie, ['js_cookie_test_bad'], result => {
+        browser.assert.equal(
+          result.value,
+          undefined,
+          '$.cookie won`t throw exception, returns undefined',
+        );
+      })
+      .drupalLogAndEnd({ onlyOnError: false });
+  },
+  'Test jquery.cookie Shim Read all when there are cookies or return empty object': browser => {
+    browser
+      .getCookie('SIMPLETEST_USER_AGENT', simpletestCookie => {
+        const simpletestCookieValue = simpletestCookie.value;
+        browser
+          .drupalRelativeURL('/js_cookie_with_shim_test')
+          .deleteCookies(() => {
+            browser
+              .execute(getJqueryCookie, [], result => {
+                browser.assert.deepEqual(
+                  result.value,
+                  {},
+                  '$.cookie() returns empty object',
+                );
+              })
+              .setCookie({
+                name: 'js_cookie_test_first',
+                value: 'red panda',
+              })
+              .setCookie({
+                name: 'js_cookie_test_second',
+                value: 'second red panda',
+              })
+              .setCookie({
+                name: 'js_cookie_test_third',
+                value: 'third red panda id bad%',
+              })
+              .execute(getJqueryCookie, [], result => {
+                browser.assert.deepEqual(
+                  result.value,
+                  {
+                    js_cookie_test_first: 'red panda',
+                    js_cookie_test_second: 'second red panda',
+                  },
+                  '$.cookie() returns object containing all cookies',
+                );
+              })
+              .setCookie({
+                name: 'SIMPLETEST_USER_AGENT',
+                value: simpletestCookieValue,
+              });
+          });
+      })
+      .drupalLogAndEnd({ onlyOnError: false });
+  },
+  'Test jquery.cookie Shim $.cookie deprecation message': browser => {
+    browser
+      .drupalRelativeURL('/js_cookie_with_shim_test')
+      .waitForElementVisible('.js_cookie_test_add_button', 1000)
+      .click('.js_cookie_test_add_button')
+      .assert.deprecationErrorExists(
+        `jQuery.cookie() ${deprecatedMessageSuffix}`,
+      )
+      .drupalLogAndEnd({ onlyOnError: false });
+  },
+  'Test jquery.cookie Shim $.removeCookie deprecation message': browser => {
+    browser
+      .drupalRelativeURL('/js_cookie_with_shim_test')
+      .waitForElementVisible('.js_cookie_test_remove_button', 1000)
+      .click('.js_cookie_test_remove_button')
+      .assert.deprecationErrorExists(
+        `jQuery.removeCookie() ${deprecatedMessageSuffix}`,
+      )
+      .drupalLogAndEnd({ onlyOnError: false });
+  },
+  'Test jquery.cookie Shim expires option as Date instance': browser => {
+    const sevenDaysFromNow = new Date();
+    sevenDaysFromNow.setDate(sevenDaysFromNow.getDate() + 7);
+    browser
+      .execute(
+        setJqueryCookieWithOptions,
+        ['c', 'v', { expires: sevenDaysFromNow }],
+        result => {
+          browser.assert.equal(
+            result.value,
+            `c=v; expires=${sevenDaysFromNow.toUTCString()}`,
+            'should write the cookie string with expires',
+          );
+        },
+      )
+      .drupalLogAndEnd({ onlyOnError: false });
+  },
+};