diff --git a/components/progress-bar-delay/progress-bar-delay.component.yml b/components/progress-bar-delay/progress-bar-delay.component.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5021c0b9d4c456917df214cf2c3fe03f6cbbf4cb
--- /dev/null
+++ b/components/progress-bar-delay/progress-bar-delay.component.yml
@@ -0,0 +1,12 @@
+$schema: https://git.drupalcode.org/project/drupal/-/raw/10.1.x/core/modules/sdc/src/metadata.schema.json
+name: RefreshLess progress bar delay
+description: Wraps the Refreshless progress bar to delay displaying until configured time has elapsed.
+props:
+  type: object
+  additionalProperties: false
+  properties: {}
+libraryOverrides:
+  dependencies:
+    - core/drupal
+  js:
+    progress-bar-delay.js: { attributes: { defer: true }, group: refreshless, preprocess: false }
diff --git a/components/progress-bar-delay/progress-bar-delay.js b/components/progress-bar-delay/progress-bar-delay.js
new file mode 100644
index 0000000000000000000000000000000000000000..374062143523a1751a9681a3235d556c78b08877
--- /dev/null
+++ b/components/progress-bar-delay/progress-bar-delay.js
@@ -0,0 +1,215 @@
+/**
+ * @file
+ * RefreshLess progress bar delay.
+ *
+ * @see https://www.drupal.org/project/refreshless/issues/3488464
+ *   Issue for adapting this out of Turbo.
+ *
+ * @see https://github.com/hotwired/turbo/blob/ea54ae5ad4b6b28cb62ccd62951352641ed08293/src/core/drive/progress_bar.js
+ *   Originally adapted from the Turbo progress bar.
+ */
+((Drupal) => {
+
+  'use strict';
+
+  /**
+   * CSS custom property base name.
+   *
+   * @type {String}
+   */
+  const customPropertyBase = '--refreshless-progress-bar';
+
+  /**
+   * Name of the CSS custom property containing the progress bar delay.
+   *
+   * The value will be a time in ms.
+   *
+   * @type {String}
+   */
+  const delayCustomProperty = `${customPropertyBase}-delay`;
+
+  /**
+   * The <html> element.
+   *
+   * @type {HTMLHtmlElement}
+   */
+  const html = document.documentElement;
+
+  /**
+   * Represents a progress bar delay wrapper.
+   */
+  class ProgressBarDelay {
+
+    /**
+     * Time in milliseconds before the progress bar is shown.
+     *
+     * @type {Number}
+     */
+    #delay = 0;
+
+    /**
+     * The visit timeout ID, if any.
+     *
+     * @type {Number|null}
+     */
+    #visitTimeout = null;
+
+    /**
+     * The form submit timeout ID, if any.
+     *
+     * @type {Number|null}
+     */
+    #formTimeout = null;
+
+    /**
+     * The progress bar instance that we're wrapping.
+     *
+     * @type {ProgressBar}
+     */
+    #progressBar;
+
+    constructor(delay, progressBar) {
+
+      this.#delay = delay;
+
+      this.#progressBar = progressBar;
+
+    }
+
+    /**
+     * Install the progress bar in the document and set various properties.
+     */
+    install() {
+
+      html.style.setProperty(
+        delayCustomProperty, `${this.#delay}ms`,
+      );
+
+    }
+
+    /**
+     * Uninstall the progress bar from the document and remove properties.
+     */
+    uninstall() {
+
+      html.style.removeProperty(delayCustomProperty);
+
+    }
+
+    /**
+     * Start the timeout to show the progress bar.
+     *
+     * @param {Number|null} timeoutId
+     *   An existing timeout ID, if any.
+     *
+     * @return {Number}
+     *   A new timeout ID.
+     */
+    showAfterDelay(timeoutId) {
+
+      if (timeoutId !== null) {
+        window.clearTimeout(timeoutId);
+      }
+
+      this.install();
+
+      return window.setTimeout(async () => {
+
+        await this.#progressBar.setValue(0);
+
+        this.#progressBar.show();
+
+      }, this.#delay);
+
+    }
+
+    /**
+     * Hide the progress bar and cancel an existing timeout, if any.
+     *
+     * @param {Number|null} timeoutId
+     *
+     * @return {null}
+     */
+    hideAndCancelDelay(timeoutId) {
+
+      if (timeoutId === null) {
+        return timeoutId;
+      }
+
+      window.clearTimeout(timeoutId);
+
+      timeoutId = null;
+
+      this.#progressBar.finish().then(() => this.uninstall());
+
+      return timeoutId;
+
+    }
+
+    /**
+     * Show the progress bar after a delay for non-form submission visits.
+     */
+    showVisitAfterDelay() {
+
+      this.#visitTimeout = this.showAfterDelay(this.#visitTimeout);
+
+    }
+
+    /**
+     * Hide the progress bar for non-form submission visits.
+     */
+    hideVisit() {
+
+      this.#visitTimeout = this.hideAndCancelDelay(this.#visitTimeout);
+
+    }
+
+    /**
+     * Show the progress bar after a delay for form submissions.
+     */
+    showFormAfterDelay() {
+
+      // Unlike the visit progress bar, we prefer to not replace an existing
+      // form submission progress bar.
+      //
+      // @todo Is this necessary? Don't browsers by default treat both types the
+      //   same when showing progress?
+      if (this.#formTimeout !== null) {
+        return;
+      }
+
+      this.#formTimeout = this.showAfterDelay(this.#formTimeout);
+
+    }
+
+    /**
+     * Hide the progress bar for form submissions.
+     */
+    hideForm() {
+
+      this.#formTimeout = this.hideAndCancelDelay(this.#formTimeout);
+
+    }
+
+    /**
+     * Get the progress bar instance we're wrapping.
+     *
+     * @return {ProgressBar}
+     */
+    get progressBar() {
+      return this.#progressBar;
+    }
+
+  }
+
+  if (!('RefreshLess' in Drupal)) {
+    Drupal.RefreshLess = {};
+  }
+
+  if (!('classes' in Drupal.RefreshLess)) {
+    Drupal.RefreshLess.classes = {};
+  }
+
+  Drupal.RefreshLess.classes.ProgressBarDelay = ProgressBarDelay;
+
+})(Drupal);
diff --git a/components/progress-bar-delay/progress-bar-delay.twig b/components/progress-bar-delay/progress-bar-delay.twig
new file mode 100644
index 0000000000000000000000000000000000000000..34cb7d028fc3d01a5b4337405f567968b56ad460
--- /dev/null
+++ b/components/progress-bar-delay/progress-bar-delay.twig
@@ -0,0 +1 @@
+{# Required or core will throw a fatal error. #}
diff --git a/components/progress-bar/progress-bar.component.yml b/components/progress-bar/progress-bar.component.yml
new file mode 100644
index 0000000000000000000000000000000000000000..725ffe419305dc757c3dff1f4967ecdbe5b8df6e
--- /dev/null
+++ b/components/progress-bar/progress-bar.component.yml
@@ -0,0 +1,12 @@
+$schema: https://git.drupalcode.org/project/drupal/-/raw/10.1.x/core/modules/sdc/src/metadata.schema.json
+name: RefreshLess progress bar
+description: Displays progress for page loads and form submissions.
+props:
+  type: object
+  additionalProperties: false
+  properties: {}
+libraryOverrides:
+  dependencies:
+    - core/drupal
+  js:
+    progress-bar.js: { attributes: { defer: true }, group: refreshless, preprocess: false }
diff --git a/components/progress-bar/progress-bar.css b/components/progress-bar/progress-bar.css
new file mode 100644
index 0000000000000000000000000000000000000000..785582b9a3f64f18e27910debb04dba3b0c3cdeb
--- /dev/null
+++ b/components/progress-bar/progress-bar.css
@@ -0,0 +1,99 @@
+:root {
+
+  /**
+   * The progress bar colour.
+   *
+   * Defaults to Drupal blue.
+   *
+   * @type {Color}
+   */
+  --refreshless-progress-bar-colour: #0678be;
+
+  /**
+   * The progress bar thickness.
+   *
+   * @type {Number}
+   */
+  --refreshless-progress-bar-thickness: 3px;
+
+  /**
+   * The progress bar z-index.
+   *
+   * Themes should override this to a more sensible value.
+   *
+   * @type {Number}
+   */
+  --refreshless-progress-bar-z-index: 2147483647;
+
+  /**
+   * The progress bar minimum/start value.
+   *
+   * This should be a percentage.
+   *
+   * @type {Number}
+   */
+  --refreshless-progress-bar-start: 10%;
+
+  /**
+   * Progress bar width transition.
+   */
+  --refreshless-progress-bar-width-transition:
+    width var(--refreshless-progress-bar-transition-duration) ease-out;
+
+  /**
+   * Progress bar opacity transition out.
+   */
+  --refreshless-progress-bar-opacity-transition-out:
+    opacity
+    calc(var(--refreshless-progress-bar-transition-duration) / 2)
+    calc(var(--refreshless-progress-bar-transition-duration) / 2)
+    ease-in;
+
+  /**
+   * Progress bar opacity transition in.
+   */
+  --refreshless-progress-bar-opacity-transition-in:
+    opacity
+    calc(var(--refreshless-progress-bar-transition-duration) / 2)
+    ease-in;
+
+}
+
+.refreshless-document-progress-bar {
+
+  position: fixed;
+
+  display: block;
+
+  top:  0;
+  left: 0;
+
+  height: var(--refreshless-progress-bar-thickness);
+
+  width: calc(
+    var(--refreshless-progress-bar-value) * (
+      100% - var(--refreshless-progress-bar-start)
+    ) + var(--refreshless-progress-bar-start)
+  );
+
+  background-color: var(--refreshless-progress-bar-colour);
+
+  z-index: var(--refreshless-progress-bar-z-index);
+
+  transition:
+    var(--refreshless-progress-bar-width-transition),
+    var(--refreshless-progress-bar-opacity-transition-out);
+
+  opacity: 0;
+
+}
+
+.refreshless-document-progress-bar--active {
+
+  opacity: 1;
+
+  transition:
+    var(--refreshless-progress-bar-width-transition),
+    var(--refreshless-progress-bar-opacity-transition-in);
+
+}
diff --git a/components/progress-bar/progress-bar.js b/components/progress-bar/progress-bar.js
new file mode 100644
index 0000000000000000000000000000000000000000..064467feb55e6d83b88d94215ac91b7b1f3abca5
--- /dev/null
+++ b/components/progress-bar/progress-bar.js
@@ -0,0 +1,354 @@
+/**
+ * @file
+ * RefreshLess progress bar.
+ *
+ * @see https://www.drupal.org/project/refreshless/issues/3488464
+ *   Issue for adapting this out of Turbo.
+ *
+ * @see https://github.com/hotwired/turbo/blob/ea54ae5ad4b6b28cb62ccd62951352641ed08293/src/core/drive/progress_bar.js
+ *   Originally adapted from the Turbo progress bar.
+ */
+((Drupal) => {
+
+  'use strict';
+
+  Drupal.theme.refreshlessProgressBar = (
+    elementType   = 'div',
+    elementClass  = 'refreshless-document-progress-bar',
+  ) => {
+
+    const element = document.createElement(elementType);
+
+    element.classList.add(elementClass);
+
+    // This provides the ProgressBar JavaScript class the configured class name
+    // to build modifier classes from.
+    element.refreshlessProgressBarClass = elementClass;
+
+    return element;
+
+  }
+
+  /**
+   * CSS custom property base name.
+   *
+   * @type {String}
+   */
+  const customPropertyBase = '--refreshless-progress-bar';
+
+  /**
+   * Name of the CSS custom property containing the transition duration.
+   *
+   * The value will be a time in ms.
+   *
+   * @type {String}
+   */
+  const durationCustomProperty = `${customPropertyBase}-transition-duration`;
+
+  /**
+   * Name of the CSS custom property containing the current progress bar value.
+   *
+   * The value will be float between 0 and 1, inclusive.
+   *
+   * @type {String}
+   */
+  const valueCustomProperty = `${customPropertyBase}-value`;
+
+  /**
+   * The <html> element.
+   *
+   * @type {HTMLHtmlElement}
+   */
+  const html = document.documentElement;
+
+  /**
+   * Represents a progress bar.
+   */
+  class ProgressBar {
+
+    /**
+     * The progress bar HTML element.
+     *
+     * @type {HTMLElement}
+     */
+    #element;
+
+    /**
+     * Whether the progress bar is currently in the processing of hiding.
+     *
+     * @type {Boolean}
+     */
+    #hiding = false;
+
+    /**
+     * The current value of the progress bar, from 0 to 1, inclusive.
+     *
+     * @type {Number}
+     */
+    #value = 0;
+
+    /**
+     * Whether the progress bar is currently visible.
+     *
+     * @type {Boolean}
+     */
+    #visible = false;
+
+    /**
+     * The trickle interval ID, or null if one is not active.
+     *
+     * @type {Number|null}
+     */
+    #trickleInterval = null;
+
+    /**
+     * The progress bar transition duration, in milliseconds.
+     *
+     * @type {Number}
+     */
+    #transitionDuration = 300;
+
+    constructor() {
+
+      this.#element = Drupal.theme('refreshlessProgressBar');
+
+      this.setValue(0);
+
+    }
+
+    /**
+     * Show the progress bar if not already visible.
+     */
+    show() {
+
+      if (this.#visible === true) {
+        return;
+      }
+
+      this.#visible = true;
+      this.install();
+      this.startTrickling();
+
+    }
+
+    /**
+     * Hide the progress bar if visible and not already hiding.
+     *
+     * @return {Promise}
+     */
+    hide() {
+
+      if (!(this.#visible === true && this.#hiding === false)) {
+        return Promise.resolve();
+      }
+
+      this.#hiding = true;
+
+      this.stopTrickling();
+
+      return new Promise(async (resolve, reject) => {
+
+        this.#setInactive();
+
+        await new Promise((resolve, reject) => {
+
+          window.setTimeout(resolve, this.#transitionDuration * 1.5);
+
+        });
+
+        this.uninstall();
+        this.#visible = false;
+        this.#hiding = false;
+
+        resolve();
+
+      });
+
+    }
+
+    /**
+     * Set the progress bar as active, causing CSS to transition it in.
+     */
+    #setActive() {
+
+      this.#element.classList.add(
+        `${this.#element.refreshlessProgressBarClass}--active`,
+      );
+
+    }
+
+    /**
+     * Set the progress bar as inactive, causing CSS to transition it out.
+     */
+    #setInactive() {
+
+      this.#element.classList.remove(
+        `${this.#element.refreshlessProgressBarClass}--active`,
+      );
+
+    }
+
+    /**
+     * Explicitly set the progress bar to a value.
+     *
+     * @param {Number} value
+     *   A number between 0 and 1, inclusive.
+     *
+     * @throws If value is NaN, or if the value is less than 0 or greater than
+     *   1.
+     */
+    setValue(value) {
+
+      if (Number.isNaN(value)) {
+        throw `Progress bar value must be a number! Got: "${typeof value}"`;
+      }
+
+      if (value < 0 || value > 1) {
+        throw `Progress bar value must be an integer or float between 0 and 1! Got: "${value}"`;
+      }
+
+      this.#value = value;
+
+      return this.refresh();
+
+    }
+
+    /**
+     * Install the progress bar in the document and set various properties.
+     */
+    install() {
+
+      html.style.setProperty(
+        durationCustomProperty, `${this.#transitionDuration}ms`,
+      );
+
+      html.style.setProperty(valueCustomProperty, 0);
+
+      this.#setInactive();
+
+      document.body.insertAdjacentElement('beforebegin', this.#element);
+
+      this.refresh().then(async () => {
+
+        await new Promise(requestAnimationFrame);
+
+        this.#setActive();
+
+      });
+
+    }
+
+    /**
+     * Uninstall the progress bar from the document and remove properties.
+     */
+    uninstall() {
+
+      if (this.#element.parentNode) {
+        this.#element.remove();
+      }
+
+      html.style.removeProperty(durationCustomProperty);
+      html.style.removeProperty(valueCustomProperty);
+
+    }
+
+    /**
+     * Start the trickling animation.
+     */
+    startTrickling() {
+
+      if (this.#trickleInterval !== null) {
+        return;
+      }
+
+      this.#trickleInterval = window.setInterval(
+        this.trickle, this.#transitionDuration,
+      );
+
+    }
+
+    /**
+     * Stop the trickling animation.
+     */
+    stopTrickling() {
+
+      window.clearInterval(this.#trickleInterval);
+
+      this.#trickleInterval = null;
+
+    }
+
+    /**
+     * Trickle animation interval callback.
+     *
+     * This generates a random value to give the trickle the irregular movement.
+     */
+    trickle = () => {
+      this.setValue(Math.min(1, this.#value + Math.random() / 100));
+    }
+
+    /**
+     * Update the progress bar element's value with the current value.
+     */
+    async refresh() {
+
+      // Don't write the custom property if not visible or if currently hiding.
+      if (!(this.#visible === true && this.#hiding === false)) {
+        return;
+      }
+
+      await new Promise(requestAnimationFrame);
+
+      html.style.setProperty(valueCustomProperty, this.#value);
+
+    }
+
+    /**
+     * Finish/complete the progress bar by setting to 100% and start hiding it.
+     *
+     * @return {Promise}
+     */
+    finish() {
+      return this.setValue(1).then(() => this.hide());
+    }
+
+    /**
+     * Get the current value of the progress bar.
+     *
+     * @return {Number}
+     */
+    get value() {
+      return this.#value;
+    }
+
+    /**
+     * Get the progress bar HTML element.
+     *
+     * @return {HTMLElement}
+     */
+    get element() {
+      return this.#element;
+    }
+
+    /**
+     * Get the progress bar transition value.
+     *
+     * @return {Number}
+     */
+    get transitionDuration() {
+      return this.#transitionDuration;
+    }
+
+  }
+
+  if (!('RefreshLess' in Drupal)) {
+    Drupal.RefreshLess = {};
+  }
+
+  if (!('classes' in Drupal.RefreshLess)) {
+    Drupal.RefreshLess.classes = {};
+  }
+
+  Drupal.RefreshLess.classes.ProgressBar = ProgressBar;
+
+})(Drupal);
diff --git a/components/progress-bar/progress-bar.twig b/components/progress-bar/progress-bar.twig
new file mode 100644
index 0000000000000000000000000000000000000000..34cb7d028fc3d01a5b4337405f567968b56ad460
--- /dev/null
+++ b/components/progress-bar/progress-bar.twig
@@ -0,0 +1 @@
+{# Required or core will throw a fatal error. #}
diff --git a/modules/refreshless_turbo/css/scroll.css b/modules/refreshless_turbo/css/scroll.css
new file mode 100644
index 0000000000000000000000000000000000000000..e1dc6fce1513ac32b8fdfd94c0980e1888f18a39
--- /dev/null
+++ b/modules/refreshless_turbo/css/scroll.css
@@ -0,0 +1,5 @@
+:root.refreshless-disable-smooth-scroll {
+
+  scroll-behavior: auto !important;
+
+}
diff --git a/modules/refreshless_turbo/css/turbo.css b/modules/refreshless_turbo/css/turbo.css
deleted file mode 100644
index d05fddaab915ad0f0cc4cd877eced2936308cc15..0000000000000000000000000000000000000000
--- a/modules/refreshless_turbo/css/turbo.css
+++ /dev/null
@@ -1,28 +0,0 @@
-/* @see https://turbo.hotwired.dev/handbook/drive#displaying-progress */
-
-:root {
-
-  /* Default values shipped with Turbo. */
-  --turbo-progress-colour: #0076ff;
-  --turbo-progress-thickness: 3px;
-
-  /* We recommend overriding these as they can apply to other (future)
-     RefreshLess implementations as well. */
-  --refreshless-progress-bar-colour:    var(--turbo-progress-colour);
-  --refreshless-progress-bar-thickness: var(--turbo-progress-thickness);
-
-}
-
-.turbo-progress-bar {
-
-  height: var(--refreshless-progress-bar-thickness);
-
-  background-color: var(--refreshless-progress-bar-colour);
-
-}
-
-:root.refreshless-disable-smooth-scroll {
-
-  scroll-behavior: auto !important;
-
-}
diff --git a/modules/refreshless_turbo/js/progress_bar.js b/modules/refreshless_turbo/js/progress_bar.js
index 7b35d581cf9cbe29d5362d0f24c27051985a4302..deed8be302fc45f3ceb672aa748b5c17931c6f27 100644
--- a/modules/refreshless_turbo/js/progress_bar.js
+++ b/modules/refreshless_turbo/js/progress_bar.js
@@ -1,32 +1,105 @@
-(function (Drupal) {
+/**
+ * @file
+ * Implements our progress bar, replacing Turbo's implementation.
+ *
+ * @see https://www.drupal.org/project/refreshless/issues/3488464
+ *   Issue for adapting this out of Turbo.
+ */
+((ProgressBar, ProgressBarDelay) => { once(
+  'refreshless-turbo-progress-bar', document.documentElement,
+).forEach((html) => {
 
   'use strict';
 
-  let turboPopstateProgressBarTimeoutId;
+  const progressBarDelay = new ProgressBarDelay(
+    Turbo.session.progressBarDelay, new ProgressBar(),
+  );
 
-  // Fixes the progress bar delay not being obeyed with back/forward navigation.
-  //
-  // @see https://github.com/hotwired/turbo/issues/1058#issuecomment-2161270766
-  //
-  // @todo Remove if/when Turbo fixes this.
-  window.addEventListener('popstate', (event) => {
+  html.refreshlessDocumentProgressBar = progressBarDelay;
 
-    if (!event.state.turbo) {
+  // Note that this intentionally listens to the fetch request event rather than
+  // turbo:click so that the progress bar also kicks in during back/forward
+  // history navigation.
+  html.addEventListener('turbo:before-fetch-request', (event) => {
+
+    if (
+      // Ignore prefetch requests.
+      'X-Sec-Purpose' in event.detail.fetchOptions.headers &&
+      event.detail.fetchOptions.headers['X-Sec-Purpose'] === 'prefetch' ||
+      // We want to specifically ignore forms as those are handled separately
+      // via the turbo:submit-start and turbo:submit-end event handlers.
+      event.target.matches('form') === true
+    ) {
       return;
     }
 
-    Turbo.session.adapter.progressBar.progressElement.hidden = true;
+    progressBarDelay.showVisitAfterDelay();
 
-    if (turboPopstateProgressBarTimeoutId) {
-      clearTimeout(turboPopstateProgressBarTimeoutId);
-    }
+    const hideSuccess = () => {
+
+      progressBarDelay.hideVisit();
+
+      html.removeEventListener(
+        'turbo:fetch-request-error', hideFailure, {once: true},
+      )
+
+    };
+
+    const hideFailure = () => {
+
+      progressBarDelay.hideVisit();
+
+      html.removeEventListener(
+        'turbo:load', hideSuccess, {once: true},
+      )
+
+    };
+
+    html.addEventListener(
+      'turbo:load', hideSuccess, {once: true},
+    );
+
+    html.addEventListener(
+      'turbo:fetch-request-error', hideFailure, {once: true},
+    );
+
+  });
+
+  html.addEventListener('turbo:submit-start', (event) => {
+
+    progressBarDelay.showFormAfterDelay();
+
+    const hideSuccess = () => {
+
+      progressBarDelay.hideForm();
+
+      html.removeEventListener(
+        'turbo:fetch-request-error', hideFailure, {once: true},
+      )
+
+    };
+
+    const hideFailure = () => {
+
+      progressBarDelay.hideForm();
+
+      html.removeEventListener(
+        'turbo:submit-end', hideSuccess, {once: true},
+      )
 
-    turboPopstateProgressBarTimeoutId = setTimeout(() => {
+    };
 
-      Turbo.session.adapter.progressBar.progressElement.hidden = false;
+    html.addEventListener(
+      'turbo:submit-end', hideSuccess, {once: true},
+    );
 
-    }, Turbo.session.progressBarDelay);
+    html.addEventListener(
+      'turbo:fetch-request-error', hideFailure, {once: true},
+    );
 
   });
 
-})(Drupal);
+}); })(
+  Drupal.RefreshLess.classes.ProgressBar,
+  Drupal.RefreshLess.classes.ProgressBarDelay,
+);
diff --git a/modules/refreshless_turbo/patches/@hotwired/turbo/05-issue-1058-no-progress-bar-delay-on-restore.patch b/modules/refreshless_turbo/patches/@hotwired/turbo/05-issue-1058-no-progress-bar-delay-on-restore.patch
deleted file mode 100644
index aa59dc7575e8c06c2908a231733aa8e11f9813df..0000000000000000000000000000000000000000
--- a/modules/refreshless_turbo/patches/@hotwired/turbo/05-issue-1058-no-progress-bar-delay-on-restore.patch
+++ /dev/null
@@ -1,17 +0,0 @@
-diff --git a/dist/turbo.es2017-umd.js b/dist/turbo.es2017-umd.js
-index 37058d1..cd2cc8c 100644
---- a/dist/turbo.es2017-umd.js
-+++ b/dist/turbo.es2017-umd.js
-@@ -3744,11 +3744,7 @@ Copyright © 2024 37signals LLC
- 
-     visitRequestStarted(visit) {
-       this.progressBar.setValue(0);
--      if (visit.hasCachedSnapshot() || visit.action != "restore") {
--        this.showVisitProgressBarAfterDelay();
--      } else {
--        this.showProgressBar();
--      }
-+      this.showVisitProgressBarAfterDelay();
-     }
- 
-     visitRequestCompleted(visit) {
diff --git a/modules/refreshless_turbo/patches/@hotwired/turbo/05-remove-progress-bar.patch b/modules/refreshless_turbo/patches/@hotwired/turbo/05-remove-progress-bar.patch
new file mode 100644
index 0000000000000000000000000000000000000000..e3ec8a39cbf5ffd0eb4319078cd145421d5b672c
--- /dev/null
+++ b/modules/refreshless_turbo/patches/@hotwired/turbo/05-remove-progress-bar.patch
@@ -0,0 +1,229 @@
+diff --git a/modules/refreshless_turbo/vendor/@hotwired/turbo/dist/turbo.es2017-umd.js b/modules/refreshless_turbo/vendor/@hotwired/turbo/dist/turbo.es2017-umd.js
+index 37058d1..2117d16 100644
+--- a/modules/refreshless_turbo/vendor/@hotwired/turbo/dist/turbo.es2017-umd.js
++++ b/modules/refreshless_turbo/vendor/@hotwired/turbo/dist/turbo.es2017-umd.js
+@@ -2935,129 +2935,6 @@ Copyright © 2024 37signals LLC
+     }
+   }
+ 
+-  class ProgressBar {
+-    static animationDuration = 300 /*ms*/
+-
+-    static get defaultCSS() {
+-      return unindent`
+-      .turbo-progress-bar {
+-        position: fixed;
+-        display: block;
+-        top: 0;
+-        left: 0;
+-        height: 3px;
+-        background: #0076ff;
+-        z-index: 2147483647;
+-        transition:
+-          width ${ProgressBar.animationDuration}ms ease-out,
+-          opacity ${ProgressBar.animationDuration / 2}ms ${ProgressBar.animationDuration / 2}ms ease-in;
+-        transform: translate3d(0, 0, 0);
+-      }
+-    `
+-    }
+-
+-    hiding = false
+-    value = 0
+-    visible = false
+-
+-    constructor() {
+-      this.stylesheetElement = this.createStylesheetElement();
+-      this.progressElement = this.createProgressElement();
+-      this.installStylesheetElement();
+-      this.setValue(0);
+-    }
+-
+-    show() {
+-      if (!this.visible) {
+-        this.visible = true;
+-        this.installProgressElement();
+-        this.startTrickling();
+-      }
+-    }
+-
+-    hide() {
+-      if (this.visible && !this.hiding) {
+-        this.hiding = true;
+-        this.fadeProgressElement(() => {
+-          this.uninstallProgressElement();
+-          this.stopTrickling();
+-          this.visible = false;
+-          this.hiding = false;
+-        });
+-      }
+-    }
+-
+-    setValue(value) {
+-      this.value = value;
+-      this.refresh();
+-    }
+-
+-    // Private
+-
+-    installStylesheetElement() {
+-      document.head.insertBefore(this.stylesheetElement, document.head.firstChild);
+-    }
+-
+-    installProgressElement() {
+-      this.progressElement.style.width = "0";
+-      this.progressElement.style.opacity = "1";
+-      document.documentElement.insertBefore(this.progressElement, document.body);
+-      this.refresh();
+-    }
+-
+-    fadeProgressElement(callback) {
+-      this.progressElement.style.opacity = "0";
+-      setTimeout(callback, ProgressBar.animationDuration * 1.5);
+-    }
+-
+-    uninstallProgressElement() {
+-      if (this.progressElement.parentNode) {
+-        document.documentElement.removeChild(this.progressElement);
+-      }
+-    }
+-
+-    startTrickling() {
+-      if (!this.trickleInterval) {
+-        this.trickleInterval = window.setInterval(this.trickle, ProgressBar.animationDuration);
+-      }
+-    }
+-
+-    stopTrickling() {
+-      window.clearInterval(this.trickleInterval);
+-      delete this.trickleInterval;
+-    }
+-
+-    trickle = () => {
+-      this.setValue(this.value + Math.random() / 100);
+-    }
+-
+-    refresh() {
+-      requestAnimationFrame(() => {
+-        this.progressElement.style.width = `${10 + this.value * 90}%`;
+-      });
+-    }
+-
+-    createStylesheetElement() {
+-      const element = document.createElement("style");
+-      element.type = "text/css";
+-      element.textContent = ProgressBar.defaultCSS;
+-      if (this.cspNonce) {
+-        element.nonce = this.cspNonce;
+-      }
+-      return element
+-    }
+-
+-    createProgressElement() {
+-      const element = document.createElement("div");
+-      element.className = "turbo-progress-bar";
+-      return element
+-    }
+-
+-    get cspNonce() {
+-      return getMetaContent("csp-nonce")
+-    }
+-  }
+-
+   class HeadSnapshot extends Snapshot {
+     detailsByOuterHTML = this.children
+       .filter((element) => !elementIsNoscript(element))
+@@ -3721,7 +3598,6 @@ Copyright © 2024 37signals LLC
+   }
+ 
+   class BrowserAdapter {
+-    progressBar = new ProgressBar()
+ 
+     constructor(session) {
+       this.session = session;
+@@ -3742,14 +3618,7 @@ Copyright © 2024 37signals LLC
+       visit.goToSamePageAnchor();
+     }
+ 
+-    visitRequestStarted(visit) {
+-      this.progressBar.setValue(0);
+-      if (visit.hasCachedSnapshot() || visit.action != "restore") {
+-        this.showVisitProgressBarAfterDelay();
+-      } else {
+-        this.showProgressBar();
+-      }
+-    }
++    visitRequestStarted(visit) {}
+ 
+     visitRequestCompleted(visit) {
+       visit.loadResponse();
+@@ -3773,66 +3642,24 @@ Copyright © 2024 37signals LLC
+ 
+     visitRequestFinished(_visit) {}
+ 
+-    visitCompleted(_visit) {
+-      this.progressBar.setValue(1);
+-      this.hideVisitProgressBar();
+-    }
++    visitCompleted(_visit) {}
+ 
+     pageInvalidated(reason) {
+       this.reload(reason);
+     }
+ 
+-    visitFailed(_visit) {
+-      this.progressBar.setValue(1);
+-      this.hideVisitProgressBar();
+-    }
++    visitFailed(_visit) {}
+ 
+     visitRendered(_visit) {}
+ 
+     // Form Submission Delegate
+ 
+-    formSubmissionStarted(_formSubmission) {
+-      this.progressBar.setValue(0);
+-      this.showFormProgressBarAfterDelay();
+-    }
++    formSubmissionStarted(_formSubmission) {}
+ 
+-    formSubmissionFinished(_formSubmission) {
+-      this.progressBar.setValue(1);
+-      this.hideFormProgressBar();
+-    }
++    formSubmissionFinished(_formSubmission) {}
+ 
+     // Private
+ 
+-    showVisitProgressBarAfterDelay() {
+-      this.visitProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay);
+-    }
+-
+-    hideVisitProgressBar() {
+-      this.progressBar.hide();
+-      if (this.visitProgressBarTimeout != null) {
+-        window.clearTimeout(this.visitProgressBarTimeout);
+-        delete this.visitProgressBarTimeout;
+-      }
+-    }
+-
+-    showFormProgressBarAfterDelay() {
+-      if (this.formProgressBarTimeout == null) {
+-        this.formProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay);
+-      }
+-    }
+-
+-    hideFormProgressBar() {
+-      this.progressBar.hide();
+-      if (this.formProgressBarTimeout != null) {
+-        window.clearTimeout(this.formProgressBarTimeout);
+-        delete this.formProgressBarTimeout;
+-      }
+-    }
+-
+-    showProgressBar = () => {
+-      this.progressBar.show();
+-    }
+-
+     reload(reason) {
+       dispatch("turbo:reload", { detail: reason });
+ 
diff --git a/modules/refreshless_turbo/refreshless_turbo.info.yml b/modules/refreshless_turbo/refreshless_turbo.info.yml
index 172a3622dc755d35ba2d78ecce94680f932fe474..1e8b8133c884ce8d364bd14bcb2d678cf84f8c85 100644
--- a/modules/refreshless_turbo/refreshless_turbo.info.yml
+++ b/modules/refreshless_turbo/refreshless_turbo.info.yml
@@ -7,3 +7,4 @@ php: 8.1
 
 dependencies:
   - hux:hux
+  - refreshless:refreshless
diff --git a/modules/refreshless_turbo/refreshless_turbo.install b/modules/refreshless_turbo/refreshless_turbo.install
index 5d0730cf0f2499b44ecd2fd4897bc12732713348..eca9a5240f257ae22ad5fb3456235d7e26bd058d 100644
--- a/modules/refreshless_turbo/refreshless_turbo.install
+++ b/modules/refreshless_turbo/refreshless_turbo.install
@@ -309,3 +309,12 @@ function refreshless_turbo_update_10217(): void {
   \Drupal::service('library.discovery')->clearCachedDefinitions();
 
 }
+
+/**
+ * Install the base RefreshLess module if not installed for progress bar.
+ */
+function refreshless_turbo_update_10218(): void {
+
+  \Drupal::service('module_installer')->install(['refreshless']);
+
+}
diff --git a/modules/refreshless_turbo/refreshless_turbo.libraries.yml b/modules/refreshless_turbo/refreshless_turbo.libraries.yml
index 174b7b4fd3b85458b852ca86a4d7fd960a9e1caf..d21c35e6a94d795b24d056a6033b77c9e0898424 100644
--- a/modules/refreshless_turbo/refreshless_turbo.libraries.yml
+++ b/modules/refreshless_turbo/refreshless_turbo.libraries.yml
@@ -1,7 +1,4 @@
 refreshless:
-  css:
-    theme:
-      css/turbo.css: {}
   js:
     # The explicit aggregation group should not be changed or removed as it's
     # used to group our JavaScript into a separate aggregate if/when core
@@ -18,8 +15,6 @@ refreshless:
     #   RefreshLess issue implementing JavaScript aggregation support.
     js/drupal_settings.js: { attributes: { defer: true }, group: refreshless-turbo, preprocess: false }
     js/behaviours.js: { attributes: { defer: true }, group: refreshless-turbo, preprocess: false }
-    # js/progress_bar.js: { attributes: { defer: true }, group: refreshless-turbo, preprocess: false }
-    js/scroll.js: { attributes: { defer: true }, group: refreshless-turbo, preprocess: false }
     js/stylesheet_sorter.js: { attributes: { defer: true }, group: refreshless-turbo, preprocess: false }
     js/refreshless.js: { attributes: { defer: true }, group: refreshless-turbo, preprocess: false }
   header: true
@@ -28,7 +23,9 @@ refreshless:
     - core/drupalSettings
     - core/jquery
     - core/once
+    - refreshless_turbo/progress_bar
     - refreshless_turbo/reload
+    - refreshless_turbo/scroll
     # @see \Drupal\refreshless_turbo\Hooks\Library::alterJsCookie()
     #   Conditionally attaches our js-cookie if core's is no longer present.
     #
@@ -52,6 +49,14 @@ reload:
     js/reload.js: { attributes: { defer: false }, group: refreshless-turbo-reload, header: true, preprocess: false }
   header: true
 
+progress_bar:
+  js:
+    js/progress_bar.js: { attributes: { defer: true }, group: refreshless-turbo, preprocess: false }
+  header: true
+  dependencies:
+    - refreshless/progress_bar
+    - refreshless/progress_bar_delay
+
 ajax:
   js:
     js/ajax.js: { attributes: { defer: true } }
@@ -84,12 +89,20 @@ js-cookie:
   js:
     vendor/js-cookie/dist/js.cookie.js: {}
 
+scroll:
+  css:
+    theme:
+      css/scroll.css: {}
+  js:
+    js/scroll.js: { attributes: { defer: true }, group: refreshless-turbo, preprocess: false }
+  header: true
+
 turbo:
   # The "-patchN" is to force downloading of a patched Turbo despite the
   # official release version remaining the same. The "-patchN" number can be
   # incremented if we apply another patch, or removed if a new Turbo version
   # becomes available; whichever comes first.
-  version: 8.0.10-patch1
+  version: 8.0.10-patch2
   js:
     # Note that at the time of writing, Turbo does not cope well with being
     # aggregated (and/or minified?), so the lack of a group here is intentional
diff --git a/modules/refreshless_turbo/vendor/@hotwired/turbo/dist/turbo.es2017-umd.js b/modules/refreshless_turbo/vendor/@hotwired/turbo/dist/turbo.es2017-umd.js
index cd2cc8cb768b9285b4f5cf38970bc01f493cf0e5..2117d16504090d6013c43cec243239635b866b13 100644
--- a/modules/refreshless_turbo/vendor/@hotwired/turbo/dist/turbo.es2017-umd.js
+++ b/modules/refreshless_turbo/vendor/@hotwired/turbo/dist/turbo.es2017-umd.js
@@ -2935,129 +2935,6 @@ Copyright © 2024 37signals LLC
     }
   }
 
-  class ProgressBar {
-    static animationDuration = 300 /*ms*/
-
-    static get defaultCSS() {
-      return unindent`
-      .turbo-progress-bar {
-        position: fixed;
-        display: block;
-        top: 0;
-        left: 0;
-        height: 3px;
-        background: #0076ff;
-        z-index: 2147483647;
-        transition:
-          width ${ProgressBar.animationDuration}ms ease-out,
-          opacity ${ProgressBar.animationDuration / 2}ms ${ProgressBar.animationDuration / 2}ms ease-in;
-        transform: translate3d(0, 0, 0);
-      }
-    `
-    }
-
-    hiding = false
-    value = 0
-    visible = false
-
-    constructor() {
-      this.stylesheetElement = this.createStylesheetElement();
-      this.progressElement = this.createProgressElement();
-      this.installStylesheetElement();
-      this.setValue(0);
-    }
-
-    show() {
-      if (!this.visible) {
-        this.visible = true;
-        this.installProgressElement();
-        this.startTrickling();
-      }
-    }
-
-    hide() {
-      if (this.visible && !this.hiding) {
-        this.hiding = true;
-        this.fadeProgressElement(() => {
-          this.uninstallProgressElement();
-          this.stopTrickling();
-          this.visible = false;
-          this.hiding = false;
-        });
-      }
-    }
-
-    setValue(value) {
-      this.value = value;
-      this.refresh();
-    }
-
-    // Private
-
-    installStylesheetElement() {
-      document.head.insertBefore(this.stylesheetElement, document.head.firstChild);
-    }
-
-    installProgressElement() {
-      this.progressElement.style.width = "0";
-      this.progressElement.style.opacity = "1";
-      document.documentElement.insertBefore(this.progressElement, document.body);
-      this.refresh();
-    }
-
-    fadeProgressElement(callback) {
-      this.progressElement.style.opacity = "0";
-      setTimeout(callback, ProgressBar.animationDuration * 1.5);
-    }
-
-    uninstallProgressElement() {
-      if (this.progressElement.parentNode) {
-        document.documentElement.removeChild(this.progressElement);
-      }
-    }
-
-    startTrickling() {
-      if (!this.trickleInterval) {
-        this.trickleInterval = window.setInterval(this.trickle, ProgressBar.animationDuration);
-      }
-    }
-
-    stopTrickling() {
-      window.clearInterval(this.trickleInterval);
-      delete this.trickleInterval;
-    }
-
-    trickle = () => {
-      this.setValue(this.value + Math.random() / 100);
-    }
-
-    refresh() {
-      requestAnimationFrame(() => {
-        this.progressElement.style.width = `${10 + this.value * 90}%`;
-      });
-    }
-
-    createStylesheetElement() {
-      const element = document.createElement("style");
-      element.type = "text/css";
-      element.textContent = ProgressBar.defaultCSS;
-      if (this.cspNonce) {
-        element.nonce = this.cspNonce;
-      }
-      return element
-    }
-
-    createProgressElement() {
-      const element = document.createElement("div");
-      element.className = "turbo-progress-bar";
-      return element
-    }
-
-    get cspNonce() {
-      return getMetaContent("csp-nonce")
-    }
-  }
-
   class HeadSnapshot extends Snapshot {
     detailsByOuterHTML = this.children
       .filter((element) => !elementIsNoscript(element))
@@ -3721,7 +3598,6 @@ Copyright © 2024 37signals LLC
   }
 
   class BrowserAdapter {
-    progressBar = new ProgressBar()
 
     constructor(session) {
       this.session = session;
@@ -3742,10 +3618,7 @@ Copyright © 2024 37signals LLC
       visit.goToSamePageAnchor();
     }
 
-    visitRequestStarted(visit) {
-      this.progressBar.setValue(0);
-      this.showVisitProgressBarAfterDelay();
-    }
+    visitRequestStarted(visit) {}
 
     visitRequestCompleted(visit) {
       visit.loadResponse();
@@ -3769,66 +3642,24 @@ Copyright © 2024 37signals LLC
 
     visitRequestFinished(_visit) {}
 
-    visitCompleted(_visit) {
-      this.progressBar.setValue(1);
-      this.hideVisitProgressBar();
-    }
+    visitCompleted(_visit) {}
 
     pageInvalidated(reason) {
       this.reload(reason);
     }
 
-    visitFailed(_visit) {
-      this.progressBar.setValue(1);
-      this.hideVisitProgressBar();
-    }
+    visitFailed(_visit) {}
 
     visitRendered(_visit) {}
 
     // Form Submission Delegate
 
-    formSubmissionStarted(_formSubmission) {
-      this.progressBar.setValue(0);
-      this.showFormProgressBarAfterDelay();
-    }
+    formSubmissionStarted(_formSubmission) {}
 
-    formSubmissionFinished(_formSubmission) {
-      this.progressBar.setValue(1);
-      this.hideFormProgressBar();
-    }
+    formSubmissionFinished(_formSubmission) {}
 
     // Private
 
-    showVisitProgressBarAfterDelay() {
-      this.visitProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay);
-    }
-
-    hideVisitProgressBar() {
-      this.progressBar.hide();
-      if (this.visitProgressBarTimeout != null) {
-        window.clearTimeout(this.visitProgressBarTimeout);
-        delete this.visitProgressBarTimeout;
-      }
-    }
-
-    showFormProgressBarAfterDelay() {
-      if (this.formProgressBarTimeout == null) {
-        this.formProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay);
-      }
-    }
-
-    hideFormProgressBar() {
-      this.progressBar.hide();
-      if (this.formProgressBarTimeout != null) {
-        window.clearTimeout(this.formProgressBarTimeout);
-        delete this.formProgressBarTimeout;
-      }
-    }
-
-    showProgressBar = () => {
-      this.progressBar.show();
-    }
-
     reload(reason) {
       dispatch("turbo:reload", { detail: reason });
 
diff --git a/refreshless.info.yml b/refreshless.info.yml
index 51fc081191539dced752588cc44126eeffed0a37..be5ec8e942ad7bb38d619b55ada1b73428d776e5 100644
--- a/refreshless.info.yml
+++ b/refreshless.info.yml
@@ -4,6 +4,3 @@ description: 'Layers JavaScript-based navigation on top of Drupal''s existing se
 package: User interface
 core_version_requirement: ^10.2 || ^11.0
 php: 8.1
-# Hidden until we either merge Turbo into this root module or we implement a
-# shared framework here that multiple implementations can depend on.
-hidden: true
diff --git a/refreshless.install b/refreshless.install
index c331edf23758d4a0027f31fd0261dfec0af58aed..3d3495e00f2cb751d90dc2c67bd88b7954b5ecaf 100644
--- a/refreshless.install
+++ b/refreshless.install
@@ -10,3 +10,12 @@ function refreshless_update_10201(): void {
   \Drupal::service('kernel')->invalidateContainer();
 
 }
+
+/**
+ * Rebuild libraries for addition of progress bar components.
+ */
+function refreshless_update_10202(): void {
+
+  \Drupal::service('library.discovery')->clearCachedDefinitions();
+
+}
diff --git a/refreshless.libraries.yml b/refreshless.libraries.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2068ee7743d29b7730db60ea2dab85e240b7adf6
--- /dev/null
+++ b/refreshless.libraries.yml
@@ -0,0 +1,18 @@
+# @todo Switch to the real components when we require drupal/core:^10.3 for SDC.
+
+progress_bar:
+  css:
+    theme:
+      components/progress-bar/progress-bar.css: {}
+  js:
+    components/progress-bar/progress-bar.js: { attributes: { defer: true }, group: refreshless, preprocess: false }
+  header: true
+  dependencies:
+    - core/drupal
+
+progress_bar_delay:
+  js:
+    components/progress-bar-delay/progress-bar-delay.js: { attributes: { defer: true }, group: refreshless, preprocess: false }
+  header: true
+  dependencies:
+    - core/drupal