diff --git a/app/assets/javascripts/discourse/app/components/composer-body.js b/app/assets/javascripts/discourse/app/components/composer-body.js
index 24e560b67d0..7891a218f53 100644
--- a/app/assets/javascripts/discourse/app/components/composer-body.js
+++ b/app/assets/javascripts/discourse/app/components/composer-body.js
@@ -8,7 +8,7 @@ import Composer from "discourse/models/composer";
 import KeyEnterEscape from "discourse/mixins/key-enter-escape";
 import afterTransition from "discourse/lib/after-transition";
 import discourseDebounce from "discourse-common/lib/debounce";
-import { headerOffset } from "discourse/components/site-header";
+import { headerOffset } from "discourse/lib/offset-calculator";
 import positioningWorkaround from "discourse/lib/safari-hacks";
 
 const START_DRAG_EVENTS = ["touchstart", "mousedown"];
diff --git a/app/assets/javascripts/discourse/app/components/site-header.js b/app/assets/javascripts/discourse/app/components/site-header.js
index febba56a795..6035a5b5830 100644
--- a/app/assets/javascripts/discourse/app/components/site-header.js
+++ b/app/assets/javascripts/discourse/app/components/site-header.js
@@ -7,6 +7,7 @@ import Docking from "discourse/mixins/docking";
 import MountWidget from "discourse/components/mount-widget";
 import ItsATrap from "@discourse/itsatrap";
 import RerenderOnDoNotDisturbChange from "discourse/mixins/rerender-on-do-not-disturb-change";
+import { headerOffset } from "discourse/lib/offset-calculator";
 import { observes } from "discourse-common/utils/decorators";
 import { topicTitleDecorators } from "discourse/components/topic-title";
 
@@ -438,15 +439,6 @@ export default SiteHeaderComponent.extend({
   classNames: ["d-header-wrap"],
 });
 
-export function headerOffset() {
-  return (
-    parseInt(
-      document.documentElement.style.getPropertyValue("--header-offset"),
-      10
-    ) || 0
-  );
-}
-
 export function headerTop() {
   const header = document.querySelector("header.d-header");
   const headerOffsetTop = header.offsetTop ? header.offsetTop : 0;
diff --git a/app/assets/javascripts/discourse/app/components/topic-navigation.js b/app/assets/javascripts/discourse/app/components/topic-navigation.js
index b6a15b6e1f3..f40882cf87e 100644
--- a/app/assets/javascripts/discourse/app/components/topic-navigation.js
+++ b/app/assets/javascripts/discourse/app/components/topic-navigation.js
@@ -5,7 +5,7 @@ import PanEvents, {
 import Component from "@ember/component";
 import EmberObject from "@ember/object";
 import discourseDebounce from "discourse-common/lib/debounce";
-import { headerOffset } from "discourse/components/site-header";
+import { headerOffset } from "discourse/lib/offset-calculator";
 import { later, next } from "@ember/runloop";
 import { observes } from "discourse-common/utils/decorators";
 import showModal from "discourse/lib/show-modal";
diff --git a/app/assets/javascripts/discourse/app/components/topic-timeline.js b/app/assets/javascripts/discourse/app/components/topic-timeline.js
index 5a60bc9be27..d37ced63530 100644
--- a/app/assets/javascripts/discourse/app/components/topic-timeline.js
+++ b/app/assets/javascripts/discourse/app/components/topic-timeline.js
@@ -1,6 +1,6 @@
 import Docking from "discourse/mixins/docking";
 import MountWidget from "discourse/components/mount-widget";
-import { headerOffset } from "discourse/components/site-header";
+import { headerOffset } from "discourse/lib/offset-calculator";
 import { next } from "@ember/runloop";
 import { observes } from "discourse-common/utils/decorators";
 import optionalService from "discourse/lib/optional-service";
diff --git a/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js b/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js
index 42e3a0ad1f3..16f7f9a5b7b 100644
--- a/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js
+++ b/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js
@@ -1,3 +1,5 @@
+import { bind } from "discourse-common/utils/decorators";
+import discourseDebounce from "discourse-common/lib/debounce";
 import { isAppWebview } from "discourse/lib/utilities";
 import { later, run, schedule, throttle } from "@ember/runloop";
 import {
@@ -6,9 +8,10 @@ import {
 } from "discourse/lib/topic-list-tracker";
 import Composer from "discourse/models/composer";
 import DiscourseURL from "discourse/lib/url";
+import domUtils from "discourse-common/utils/dom-utils";
 import { INPUT_DELAY } from "discourse-common/config/environment";
 import { ajax } from "discourse/lib/ajax";
-import { minimumOffset } from "discourse/lib/offset-calculator";
+import { headerOffset } from "discourse/lib/offset-calculator";
 
 const DEFAULT_BINDINGS = {
   "!": { postAction: "showFlags" },
@@ -608,7 +611,7 @@ export default {
     // Discard selection if it is not in viewport, so users can combine
     // keyboard shortcuts with mouse scrolling.
     if ($selected.length !== 0 && !fast) {
-      const offset = minimumOffset();
+      const offset = headerOffset();
       const beginScreen = $(window).scrollTop() - offset;
       const endScreen = beginScreen + window.innerHeight + offset;
       const beginArticle = $selected.offset().top;
@@ -621,7 +624,7 @@ export default {
     // If still nothing is selected, select the first post that is
     // visible and cancel move operation.
     if (!$selected || $selected.length === 0) {
-      const offset = minimumOffset();
+      const offset = headerOffset();
       $selected = $articles
         .toArray()
         .find((article) =>
@@ -636,32 +639,32 @@ export default {
     }
 
     const index = $articles.index($selected);
-    let $article = $articles.eq(index);
+    let article = $articles.eq(index)[0];
 
     // Try doing a page scroll in the context of current post.
-    if (!fast && direction !== 0 && $article.length > 0) {
+    if (!fast && direction !== 0 && article) {
       // The beginning of first article is the beginning of the page.
       const beginArticle =
-        $article.is(".topic-post") && $article.find("#post_1").length
+        article.classList.contains("topic-post") &&
+        article.querySelector("#post_1")
           ? 0
-          : $article.offset().top;
-      const endArticle =
-        $article.offset().top + $article[0].getBoundingClientRect().height;
+          : domUtils.offset(article).top;
+      const endArticle = domUtils.offset(article).top + article.offsetHeight;
 
-      const beginScreen = $(window).scrollTop();
+      const beginScreen = window.scrollY;
       const endScreen = beginScreen + window.innerHeight;
 
       if (direction < 0 && beginScreen > beginArticle) {
         return this._scrollTo(
           Math.max(
-            beginScreen - window.innerHeight + 3 * minimumOffset(), // page up
-            beginArticle - minimumOffset() // beginning of article
+            beginScreen - window.innerHeight + 3 * headerOffset(), // page up
+            beginArticle - headerOffset() // beginning of article
           )
         );
-      } else if (direction > 0 && endScreen < endArticle - minimumOffset()) {
+      } else if (direction > 0 && endScreen < endArticle - headerOffset()) {
         return this._scrollTo(
           Math.min(
-            endScreen - 3 * minimumOffset(), // page down
+            endScreen - 3 * headerOffset(), // page down
             endArticle - window.innerHeight // end of article
           )
         );
@@ -678,68 +681,59 @@ export default {
       }
     }
 
-    $article = $articles.eq(index + direction);
-    if ($article.length > 0) {
-      $articles.removeClass("selected");
-      $article.addClass("selected");
-      this.appEvents.trigger("keyboard:move-selection", {
-        articles: $articles.get(),
-        selectedArticle: $article.get(0),
-      });
-
-      const articleRect = $article[0].getBoundingClientRect();
-      if (!fast && direction < 0 && articleRect.height > window.innerHeight) {
-        // Scrolling to the last "page" of the previous post if post has multiple
-        // "pages" (if its height does not fit in the screen).
-        return this._scrollTo(
-          $article.offset().top + articleRect.height - window.innerHeight
-        );
-      } else if ($article.is(".topic-post")) {
-        return this._scrollTo(
-          $article.find("#post_1").length > 0
-            ? 0
-            : $article.offset().top - minimumOffset(),
-          () => $("a.tabLoc", $article).focus()
-        );
-      }
-
-      // Otherwise scroll through the suggested topic list.
-      this._scrollList($article, direction);
+    article = $articles.eq(index + direction)[0];
+    if (!article) {
+      return;
     }
+
+    $articles.removeClass("selected");
+    article.classList.add("selected");
+
+    this.appEvents.trigger("keyboard:move-selection", {
+      articles: $articles.get(),
+      selectedArticle: article,
+    });
+
+    const articleTop = domUtils.offset(article).top;
+    if (!fast && direction < 0 && article.offsetHeight > window.innerHeight) {
+      // Scrolling to the last "page" of the previous post if post has multiple
+      // "pages" (if its height does not fit in the screen).
+      return this._scrollTo(
+        articleTop + article.offsetHeight - window.innerHeight
+      );
+    } else if (article.classList.contains("topic-post")) {
+      return this._scrollTo(
+        article.querySelector("#post_1") ? 0 : articleTop - headerOffset(),
+        { focusTabLoc: true }
+      );
+    }
+
+    // Otherwise scroll through the suggested topic list.
+    article.scrollIntoView({
+      behavior: "smooth",
+      block: "center",
+    });
   },
 
-  _scrollTo(scrollTop) {
+  _scrollTo(scrollTop, opts = {}) {
     window.scrollTo({
       top: scrollTop,
       behavior: "smooth",
     });
+
+    if (opts.focusTabLoc) {
+      window.addEventListener("scroll", this._onScrollEnds, { passive: true });
+    }
   },
 
-  _scrollList($article) {
-    // Try to keep the article on screen
-    const pos = $article.offset();
-    const height = $article.height();
-    const headerHeight = $("header.d-header").height();
-    const scrollTop = $(window).scrollTop();
-    const windowHeight = $(window).height();
+  @bind
+  _onScrollEnds() {
+    window.removeEventListener("scroll", this._onScrollEnds, { passive: true });
+    discourseDebounce(this, this._onScrollEndsCallback, animationDuration);
+  },
 
-    // skip if completely on screen
-    if (
-      pos.top - headerHeight > scrollTop &&
-      pos.top + height < scrollTop + windowHeight
-    ) {
-      return;
-    }
-
-    let scrollPos = pos.top + height / 2 - windowHeight * 0.5;
-    if (height > windowHeight - headerHeight) {
-      scrollPos = pos.top - headerHeight;
-    }
-    if (scrollPos < 0) {
-      scrollPos = 0;
-    }
-
-    this._scrollTo(scrollPos);
+  _onScrollEndsCallback() {
+    document.querySelector(".topic-post.selected a.tabLoc")?.focus();
   },
 
   categoriesTopicsList() {
diff --git a/app/assets/javascripts/discourse/app/lib/lock-on.js b/app/assets/javascripts/discourse/app/lib/lock-on.js
index 32a92b342bd..9c77de68d02 100644
--- a/app/assets/javascripts/discourse/app/lib/lock-on.js
+++ b/app/assets/javascripts/discourse/app/lib/lock-on.js
@@ -1,5 +1,5 @@
 import { bind } from "discourse-common/utils/decorators";
-import { minimumOffset } from "discourse/lib/offset-calculator";
+import { headerOffset } from "discourse/lib/offset-calculator";
 
 // Dear traveller, you are entering a zone where we are at war with the browser.
 // The browser is insisting on positioning scrollTop per the location it was in
@@ -50,7 +50,7 @@ export default class LockOn {
       }
     }
 
-    return offset - minimumOffset();
+    return offset - headerOffset();
   }
 
   clearLock() {
diff --git a/app/assets/javascripts/discourse/app/lib/offset-calculator.js b/app/assets/javascripts/discourse/app/lib/offset-calculator.js
index 9ce8c234e24..976ac56b78a 100644
--- a/app/assets/javascripts/discourse/app/lib/offset-calculator.js
+++ b/app/assets/javascripts/discourse/app/lib/offset-calculator.js
@@ -1,8 +1,18 @@
+import deprecated from "discourse-common/lib/deprecated";
+
 export function scrollTopFor(y) {
   return y - offsetCalculator();
 }
 
 export function minimumOffset() {
+  deprecated(
+    "The minimumOffset() helper is deprecated, please use headerOffset() instead.",
+    {
+      since: "2.8.0.beta10",
+      dropFrom: "2.9.0.beta2",
+    }
+  );
+
   const header = document.querySelector("header.d-header"),
     iPadNav = document.querySelector(".footer-nav-ipad .footer-nav"),
     iPadNavHeight = iPadNav ? iPadNav.offsetHeight : 0;
@@ -17,8 +27,17 @@ export function minimumOffset() {
     : 0;
 }
 
+export function headerOffset() {
+  return (
+    parseInt(
+      document.documentElement.style.getPropertyValue("--header-offset"),
+      10
+    ) || 0
+  );
+}
+
 export default function offsetCalculator() {
-  const min = minimumOffset();
+  const min = headerOffset();
 
   // on mobile, just use the header
   if (document.querySelector("html").classList.contains("mobile-view")) {
diff --git a/app/assets/javascripts/discourse/app/lib/sticky-avatars.js b/app/assets/javascripts/discourse/app/lib/sticky-avatars.js
index 0ab533b97a9..65083ae6176 100644
--- a/app/assets/javascripts/discourse/app/lib/sticky-avatars.js
+++ b/app/assets/javascripts/discourse/app/lib/sticky-avatars.js
@@ -1,7 +1,7 @@
 import { addWidgetCleanCallback } from "discourse/components/mount-widget";
 import Site from "discourse/models/site";
 import { bind } from "discourse-common/utils/decorators";
-import { headerOffset } from "discourse/components/site-header";
+import { headerOffset } from "discourse/lib/offset-calculator";
 import { schedule } from "@ember/runloop";
 
 export default class StickyAvatars {
@@ -79,6 +79,9 @@ export default class StickyAvatars {
   @bind
   _initIntersectionObserver() {
     schedule("afterRender", () => {
+      const headerOffsetInPx =
+        headerOffset() <= 0 ? "0px" : `-${headerOffset()}px`;
+
       this.intersectionObserver = new IntersectionObserver(
         (entries) => {
           entries.forEach((entry) => {
@@ -99,7 +102,7 @@ export default class StickyAvatars {
         },
         {
           threshold: [0.0, 1.0],
-          rootMargin: `-${headerOffset()}px 0px 0px 0px`,
+          rootMargin: `${headerOffsetInPx} 0px 0px 0px`,
         }
       );
     });
diff --git a/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js b/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js
index 272d4930c3b..2eb5f26f269 100644
--- a/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js
+++ b/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js
@@ -1,6 +1,6 @@
 import { ajax } from "discourse/lib/ajax";
 import discourseDebounce from "discourse-common/lib/debounce";
-import { headerOffset } from "discourse/components/site-header";
+import { headerOffset } from "discourse/lib/offset-calculator";
 import isElementInViewport from "discourse/lib/is-element-in-viewport";
 import { withPluginApi } from "discourse/lib/plugin-api";