Skip to content

Dworf/x-no-scroll-chrome-extension

Repository files navigation

X No-Scroll — keep your place

A tiny Chrome extension that keeps your scroll position fixed when X (Twitter) loads new posts above your current view — for example when you click the "Show N posts" / "See new posts" pill at the top of the home timeline.

When you click that pill, X loads the new posts and scrolls you up to the newest one (its scrollToNewest), so the post you were reading jumps away and you lose your place. This extension stops that: the post you were looking at stays put, and the new posts appear above it (scroll up to read them) — whether you're working up the feed or scrolling down through it.

It also covers the inline "Show more posts" gap button. It recognizes the load pills by their label ("Show N posts" / "Show more posts" / "See new posts"), so it arms only on those — never on Home, other nav, tweets, or buttons like Like. (Labels are currently English; other locales can be added in src/content.js.)

Screenshots

The pill appears Your place is held
A "Show 3 posts" pill at the top of the home timeline, with the post you're reading below it After the posts load, the same post stays exactly where it was — no jump to the top

When you click "Show 3 posts", the new posts load above and the post you were reading stays put — X's jump-to-top is suppressed (the ↑↓ with the red ✗).

Dismiss the "See new posts" pill (optional)

X floats a "See new posts" pill at the top of the timeline. This extension adds a small to its corner so you can close it without loading the posts — no jump, nothing loaded. Clicking the pill itself is unchanged (it still loads the posts and holds your place, as above).

X's "See new posts" pill with the added ✕ — close it without loading new posts

The ✕ is on by default. You can toggle it from the extension's Options page (right-click the extension icon → Options):

The 'See new posts' pill showing the dismiss ✕ in its top-right corner

Install

The easiest way is from the Chrome Web Store — click Add to Chrome and you're done.

Or load unpacked (from source)

  1. Open chrome://extensions.
  2. Turn on Developer mode (top right).
  3. Click Load unpacked and select this folder (x-no-scroll-chrome-extension).
  4. Open x.com/home. That's it — it's always on, no settings.

Works in normal Chrome windows and in "open in app" / installed-PWA windows.

How it works

Two things make you lose your place when a pill loads posts (both verified by inspecting X live):

  1. X's timeline is a virtualized list — posts are absolutely-positioned cells (transform: translateY(...)) inside a fixed-height container, and off-screen posts are unmounted — which defeats the browser's native scroll anchoring.
  2. More importantly, X deliberately scrolls you to the top: clicking the top "Show N posts" / "See new posts" pill calls X's scrollToNewest()window.scrollTo(0) + window.scrollBy(...).

src/content.js is event-driven and passive by default — it does nothing while you read, scroll, or drag the scrollbar, so it can never cause a stray jump. It acts only in a short window after a click:

  1. When you click a recognized load pill ("Show N posts" / "Show more posts" / "See new posts", matched by its label — so it works whether X renders the pill as a <button> or an <a>), it remembers the anchor — the topmost in-view post (status id + on-screen offset) — and arms for a few seconds. Home, nav, tweets, and other buttons never match, so they're never touched.
  2. If a load arrives while armed, a MutationObserver restores the anchor to exactly where it was (re-pinning across chunked loads), and X's window.scrollTo / window.scrollBy (its scroll-to-newest) are suppressed so X can't undo it. We move via element.scrollTop while X moves via window.scrollTo/scrollBy — a clean seam, so our scroll and X's never get confused. If a big prepend unmounts the anchor, it restores by the height the feed grew (an absolute target, correct even after X has already scrolled the page).
  3. If you scroll yourself while armed, it disarms — you've moved on.

Because it intercepts X's own scroll call, the extension runs in the page's MAIN world (see manifest.json); an isolated content script can't override the window.scrollTo that X's code calls.

Testing

test/harness.html is an offline mock of X's virtualized timeline (window-scroll, cellInnerDiv cells with translateY, off-screen unmounting, and a simulated scrollToNewest). It runs automated assertions for: small/large/slow prepends, anchor-unmount self-heal, below-fold gating, surviving scrollToNewest (including while slightly scrolled), passivity with no click (scrollbar drag must not jump), and disarming when you scroll after a click.

# from this folder
python3 -m http.server 8753
# then open http://localhost:8753/test/harness.html in a FOCUSED window — the panel shows pass/fail.
# (Run it in a foreground tab; background tabs throttle timers and skew the timing-sensitive cases.)

Scope

Home timeline only (x.com/home, twitter.com/home). Always on; the only UI is a single Options toggle (the dismiss ✕, on by default).

Versions

v0.1.5

  • New: a one-click on X's floating "See new posts" pill, so you can dismiss it without loading the posts (clicking the pill itself is unchanged). On by default; toggle it from the new Options page. Adds the storage permission for that one setting — still no browsing data, no analytics, no network requests.

v0.1.4

  • Fix: activate when you reach the home timeline via in-app navigation, not only on a full page load. Previously the extension only switched on if a tab was loaded directly on /home; navigating to Home from elsewhere in X (the single-page app just changes the URL, no reload) left it off until you reloaded. It now starts on any X page and gates its work to the home timeline at runtime.

v0.1.3

  • Fix: keep the post you're reading fixed when a mid-view "Show more posts" (the chained one after "Show N posts") fills the gap with newer posts. Previously the engine could anchor to a post that was mostly scrolled off the top, so the newer posts loading in below it pushed your reading content down. It now anchors to the topmost post that actually starts inside your view.

v0.1.2

  • Fix: arms only on the load pills, matched by label ("Show N posts" / "Show more posts" / "See new posts"), instead of on any non-link click. This is more precise — Home, nav, tweets, and other buttons are never touched — and it works whether X renders a pill as a <button> or an <a> (it A/B-tests this).
  • Fix: hold your post even when a newly-loaded post above it grows after loading (a late image or embed). Previously we only reacted to posts being added/removed, so in-place height growth could let small loads drift your post down (e.g. "Show 1 post" with a media tweet). We now re-pin on a short timer while armed, catching that.

v0.1.1

  • Fix: clicking the Home tab (or any nav link) no longer drags you back to your previous spot. The engine now ignores clicks on links (<a>) and only arms on the load-pill buttons, so Home's scroll-to-top works normally.

v0.1.0 — first release

  • Keeps your place when X loads new posts above you ("Show N posts" / "See new posts" / inline "Show more posts").
  • Event-driven and passive by default: only acts in a short window after a click, so normal reading, scrolling, and scrollbar dragging are never touched.
  • Suppresses X's scrollToNewest and restores your anchored post — precise when the post stays rendered, with a self-heal restore for big/chunked loads that virtualize it away.
  • Runs in the page MAIN world; home timeline only; no UI, no settings, no data collected.
  • Offline test harness (test/harness.html) covering prepends, self-heal, gating, passivity, disarming, and scrollToNewest.

Contributing

Contributions and bug reports are welcome. It's a small, single-file extension with no build step — the whole engine is src/content.js (dependency-free vanilla JS).

  • Before changing behavior, run the offline test harness (test/harness.html) in a focused browser tab and keep it green — see Testing.
  • Found a case where it loses your place or fights your scrolling? Open an issue with the steps: which button you clicked and roughly where you were scrolled.
  • For larger changes, please open an issue to discuss first.

Acknowledgements

  • Built as a Manifest V3 content script for Google Chrome.
  • Operates on the X (formerly Twitter) web app.
  • No third-party libraries — plain vanilla JavaScript.

Not affiliated with, endorsed by, or sponsored by X Corp or Google LLC. "X" and "Twitter" are trademarks of X Corp; "Google Chrome" is a trademark of Google LLC. This is an independent, unofficial project.

Privacy

No browsing or personal data is collected — no analytics, no tracking, no network requests. The only thing stored is your single on/off preference for the dismiss ✕, kept by Chrome's storage (synced to your own account only if you use Chrome Sync). See PRIVACY.md.

License

MIT — see LICENSE.