diff --git a/core/src/main/resources/hudson/model/Run/console.jelly b/core/src/main/resources/hudson/model/Run/console.jelly
index 34fa42286a8f..5e396d2b76f0 100644
--- a/core/src/main/resources/hudson/model/Run/console.jelly
+++ b/core/src/main/resources/hudson/model/Run/console.jelly
@@ -47,5 +47,7 @@ THE SOFTWARE.
+
+
diff --git a/core/src/main/resources/lib/layout/backToTop.jelly b/core/src/main/resources/lib/layout/backToTop.jelly
new file mode 100644
index 000000000000..bf13a65c4b6c
--- /dev/null
+++ b/core/src/main/resources/lib/layout/backToTop.jelly
@@ -0,0 +1,9 @@
+
+
+
+ Adds a "back to top" floating button that appears when the user scrolls down.
+
+
+
diff --git a/core/src/main/resources/lib/layout/backToTop.properties b/core/src/main/resources/lib/layout/backToTop.properties
new file mode 100644
index 000000000000..f86b1f96695b
--- /dev/null
+++ b/core/src/main/resources/lib/layout/backToTop.properties
@@ -0,0 +1 @@
+Back\ to\ top=Back to top
diff --git a/src/main/js/app.js b/src/main/js/app.js
index d1ac7b26ca92..0d5020247987 100644
--- a/src/main/js/app.js
+++ b/src/main/js/app.js
@@ -7,6 +7,7 @@ import StopButtonLink from "@/components/stop-button-link";
import ConfirmationLink from "@/components/confirmation-link";
import Dialogs from "@/components/dialogs";
import Defer from "@/components/defer";
+import BackToTop from "@/components/back-to-top";
Dropdowns.init();
CommandPalette.init();
@@ -17,3 +18,4 @@ Tooltips.init();
StopButtonLink.init();
ConfirmationLink.init();
Dialogs.init();
+BackToTop.init();
diff --git a/src/main/js/components/back-to-top/index.js b/src/main/js/components/back-to-top/index.js
new file mode 100644
index 000000000000..93654ff858e4
--- /dev/null
+++ b/src/main/js/components/back-to-top/index.js
@@ -0,0 +1,39 @@
+function init() {
+ const backToTopButton = document.querySelector(".jenkins-back-to-top");
+
+ if (!backToTopButton) {
+ return;
+ }
+
+ const threshold = 300;
+ let ticking = false;
+
+ const updateVisibility = () => {
+ if (window.scrollY > threshold) {
+ backToTopButton.classList.add("jenkins-back-to-top--visible");
+ } else {
+ backToTopButton.classList.remove("jenkins-back-to-top--visible");
+ }
+ ticking = false;
+ };
+
+ window.addEventListener(
+ "scroll",
+ () => {
+ if (!ticking) {
+ window.requestAnimationFrame(updateVisibility);
+ ticking = true;
+ }
+ },
+ { passive: true },
+ );
+
+ backToTopButton.addEventListener("click", () => {
+ window.scrollTo({
+ top: 0,
+ behavior: "smooth",
+ });
+ });
+}
+
+export default { init };
diff --git a/src/main/scss/components/_back-to-top.scss b/src/main/scss/components/_back-to-top.scss
new file mode 100644
index 000000000000..992ffe1ef1b4
--- /dev/null
+++ b/src/main/scss/components/_back-to-top.scss
@@ -0,0 +1,43 @@
+@use "../abstracts/mixins";
+
+.jenkins-back-to-top {
+ position: fixed;
+ bottom: 1.5rem;
+ right: 1.5rem;
+ z-index: 900;
+ width: 2.75rem;
+ height: 2.75rem;
+ border-radius: 50%;
+ padding: 0;
+ box-shadow: var(--dropdown-box-shadow);
+ opacity: 0;
+ visibility: hidden;
+ transition:
+ opacity var(--standard-transition),
+ visibility var(--standard-transition),
+ transform var(--standard-transition),
+ background-color var(--standard-transition);
+ transform: translateY(0.625rem);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &--visible {
+ opacity: 1;
+ visibility: visible;
+ transform: translateY(0);
+ }
+
+ &:hover {
+ background-color: var(--button-background--hover);
+ }
+
+ &:active {
+ background-color: var(--button-background--active);
+ }
+
+ svg {
+ width: 1.25rem;
+ height: 1.25rem;
+ }
+}
diff --git a/src/main/scss/components/_index.scss b/src/main/scss/components/_index.scss
index 03bd4c03cecf..7b427d77aeda 100644
--- a/src/main/scss/components/_index.scss
+++ b/src/main/scss/components/_index.scss
@@ -4,6 +4,7 @@
@use "badges";
@use "breadcrumbs";
@use "buttons";
+@use "back-to-top";
@use "cards";
@use "command-palette";
@use "content-blocks";